// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

// Package prefs contains types and functions to work with arbitrary
// preference hierarchies.
//
// Specifically, the package provides [Item], [List], [Map], [StructList] and [StructMap]
// types which represent individual preferences in a user-defined prefs struct.
// A valid prefs struct must contain one or more exported fields of the preference types,
// either directly or within nested structs, but not pointers to these types.
// Additionally to preferences, a prefs struct may contain any number of
// non-preference fields that will be marshalled and unmarshalled but are
// otherwise ignored by the prefs package.
//
// The preference types are compatible with the [tailscale.com/cmd/viewer] and
// [tailscale.com/cmd/cloner] utilities. It is recommended to generate a read-only view
// of the user-defined prefs structure and use it in place of prefs whenever the prefs
// should not be modified.
package prefs

import (
	"errors"

	jsonv2 "github.com/go-json-experiment/json"
	"github.com/go-json-experiment/json/jsontext"
	"tailscale.com/types/opt"
)

var (
	// ErrManaged is the error returned when attempting to modify a managed preference.
	ErrManaged = errors.New("cannot modify a managed preference")
	// ErrReadOnly is the error returned when attempting to modify a read-only preference.
	ErrReadOnly = errors.New("cannot modify a read-only preference")
)

// metadata holds type-agnostic preference metadata.
type metadata struct {
	// Managed indicates whether the preference is managed via MDM, Group Policy, or other means.
	Managed bool `json:",omitzero"`

	// ReadOnly indicates whether the preference is read-only due to any other reasons,
	// such as user's access rights.
	ReadOnly bool `json:",omitzero"`
}

// serializable is a JSON-serializable preference data.
type serializable[T any] struct {
	// Value is an optional preference value that is set when the preference is
	// configured by the user or managed by an admin.
	Value opt.Value[T] `json:",omitzero"`
	// Default is the default preference value to be used
	// when the preference has not been configured.
	Default T `json:",omitzero"`
	// Metadata is any additional type-agnostic preference metadata to be serialized.
	Metadata metadata `json:",inline"`
}

// preference is an embeddable type that provides a common implementation for
// concrete preference types, such as [Item], [List], [Map], [StructList] and [StructMap].
type preference[T any] struct {
	s serializable[T]
}

// preferenceOf returns a preference with the specified value and/or [Options].
func preferenceOf[T any](v opt.Value[T], opts ...Options) preference[T] {
	var m metadata
	for _, o := range opts {
		o(&m)
	}
	return preference[T]{serializable[T]{Value: v, Metadata: m}}
}

// IsSet reports whether p has a value set.
func (p preference[T]) IsSet() bool {
	return p.s.Value.IsSet()
}

// Value returns the value of p if the preference has a value set.
// Otherwise, it returns its default value.
func (p preference[T]) Value() T {
	val, _ := p.ValueOk()
	return val
}

// ValueOk returns the value of p and true if the preference has a value set.
// Otherwise, it returns its default value and false.
func (p preference[T]) ValueOk() (val T, ok bool) {
	if val, ok = p.s.Value.GetOk(); ok {
		return val, true
	}
	return p.DefaultValue(), false
}

// SetValue configures the preference with the specified value.
// It fails and returns [ErrManaged] if p is a managed preference,
// and [ErrReadOnly] if p is a read-only preference.
func (p *preference[T]) SetValue(val T) error {
	switch {
	case p.s.Metadata.Managed:
		return ErrManaged
	case p.s.Metadata.ReadOnly:
		return ErrReadOnly
	default:
		p.s.Value.Set(val)
		return nil
	}
}

// ClearValue resets the preference to an unconfigured state.
// It fails and returns [ErrManaged] if p is a managed preference,
// and [ErrReadOnly] if p is a read-only preference.
func (p *preference[T]) ClearValue() error {
	switch {
	case p.s.Metadata.Managed:
		return ErrManaged
	case p.s.Metadata.ReadOnly:
		return ErrReadOnly
	default:
		p.s.Value.Clear()
		return nil
	}
}

// DefaultValue returns the default value of p.
func (p preference[T]) DefaultValue() T {
	return p.s.Default
}

// SetDefaultValue sets the default value of p.
func (p *preference[T]) SetDefaultValue(def T) {
	p.s.Default = def
}

// IsManaged reports whether p is managed via MDM, Group Policy, or similar means.
func (p preference[T]) IsManaged() bool {
	return p.s.Metadata.Managed
}

// SetManagedValue configures the preference with the specified value
// and marks the preference as managed.
func (p *preference[T]) SetManagedValue(val T) {
	p.s.Value.Set(val)
	p.s.Metadata.Managed = true
}

// ClearManaged clears the managed flag of the preference without altering its value.
func (p *preference[T]) ClearManaged() {
	p.s.Metadata.Managed = false
}

// IsReadOnly reports whether p is read-only and cannot be changed by user.
func (p preference[T]) IsReadOnly() bool {
	return p.s.Metadata.ReadOnly || p.s.Metadata.Managed
}

// SetReadOnly sets the read-only status of p, preventing changes by a user if set to true.
func (p *preference[T]) SetReadOnly(readonly bool) {
	p.s.Metadata.ReadOnly = readonly
}

var (
	_ jsonv2.MarshalerTo     = (*preference[struct{}])(nil)
	_ jsonv2.UnmarshalerFrom = (*preference[struct{}])(nil)
)

// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (p preference[T]) MarshalJSONTo(out *jsontext.Encoder) error {
	return jsonv2.MarshalEncode(out, &p.s)
}

// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (p *preference[T]) UnmarshalJSONFrom(in *jsontext.Decoder) error {
	return jsonv2.UnmarshalDecode(in, &p.s)
}

// MarshalJSON implements [json.Marshaler].
func (p preference[T]) MarshalJSON() ([]byte, error) {
	return jsonv2.Marshal(p) // uses MarshalJSONTo
}

// UnmarshalJSON implements [json.Unmarshaler].
func (p *preference[T]) UnmarshalJSON(b []byte) error {
	return jsonv2.Unmarshal(b, p) // uses UnmarshalJSONFrom
}
