// Package confmgr implements the configuration manager: versioned, git-style
// configuration Sets (schemas of parameters bound to target signals) and
// configuration Instances (concrete values for a set), persisted as versioned
// JSON files. Applying an instance writes each value to its target signal.
package confmgr

import (
	"fmt"
	"math"
	"slices"
	"strconv"
	"strings"
)

// ParamType enumerates the value kinds a parameter may hold.
type ParamType string

const (
	TypeFloat      ParamType = "float64"
	TypeInt        ParamType = "int64"
	TypeBool       ParamType = "bool"
	TypeString     ParamType = "string"
	TypeEnum       ParamType = "enum"
	TypeFloatArray ParamType = "float64[]" // waveform; value is a list of numbers
)

func (t ParamType) valid() bool {
	switch t {
	case TypeFloat, TypeInt, TypeBool, TypeString, TypeEnum, TypeFloatArray:
		return true
	}
	return false
}

// Parameter is one entry in a configuration set's schema. It names a target
// signal (DS + Signal), a value type, an optional default, and validation
// metadata. Parameters may be grouped for presentation via Group/Subgroup.
type Parameter struct {
	Key         string    `json:"key"`             // unique within the set
	Label       string    `json:"label,omitempty"` // human-friendly name
	Group       string    `json:"group,omitempty"` // top-level grouping
	Subgroup    string    `json:"subgroup,omitempty"`
	DS          string    `json:"ds"`     // target data source
	Signal      string    `json:"signal"` // target signal name
	Type        ParamType `json:"type"`   // value kind
	Default     any       `json:"default,omitempty"`
	Mandatory   bool      `json:"mandatory,omitempty"`
	Min         *float64  `json:"min,omitempty"`        // numeric lower bound
	Max         *float64  `json:"max,omitempty"`        // numeric upper bound
	EnumValues  []string  `json:"enumValues,omitempty"` // allowed values for enum
	Unit        string    `json:"unit,omitempty"`
	Description string    `json:"description,omitempty"`
}

// ConfigSet is the schema half of the two-tier model: an ordered list of
// parameters bound to target signals. It is versioned git-style.
type ConfigSet struct {
	ID          string      `json:"id"`
	Name        string      `json:"name"`
	Description string      `json:"description,omitempty"`
	Version     int         `json:"version"`
	Tag         string      `json:"tag,omitempty"`
	Owner       string      `json:"owner,omitempty"`
	Scope       string      `json:"scope,omitempty"`  // access.Scope* visibility token
	Groups      []string    `json:"groups,omitempty"` // groups for ScopeGroup visibility
	Parameters  []Parameter `json:"parameters"`
}

// ConfigInstance is the value half: concrete values for a set's parameters,
// keyed by parameter key. SetID pins the schema it belongs to; SetVersion pins
// the schema revision (0 means "track current"). It is versioned git-style.
type ConfigInstance struct {
	ID         string         `json:"id"`
	Name       string         `json:"name"`
	SetID      string         `json:"setId"`
	SetVersion int            `json:"setVersion,omitempty"` // 0 = current set version
	Version    int            `json:"version"`
	Tag        string         `json:"tag,omitempty"`
	Owner      string         `json:"owner,omitempty"`
	Scope      string         `json:"scope,omitempty"`  // access.Scope* visibility token
	Groups     []string       `json:"groups,omitempty"` // groups for ScopeGroup visibility
	Values     map[string]any `json:"values"`
}

// Validate checks structural invariants of a config set: non-empty name,
// unique non-empty parameter keys, valid types, a target signal per parameter,
// and well-formed enum/default metadata.
func (s ConfigSet) Validate() error {
	if strings.TrimSpace(s.Name) == "" {
		return fmt.Errorf("config set name must not be empty")
	}
	seen := make(map[string]bool, len(s.Parameters))
	for i, p := range s.Parameters {
		if strings.TrimSpace(p.Key) == "" {
			return fmt.Errorf("parameter %d: key must not be empty", i)
		}
		if seen[p.Key] {
			return fmt.Errorf("duplicate parameter key %q", p.Key)
		}
		seen[p.Key] = true
		if !p.Type.valid() {
			return fmt.Errorf("parameter %q: invalid type %q", p.Key, p.Type)
		}
		if strings.TrimSpace(p.DS) == "" || strings.TrimSpace(p.Signal) == "" {
			return fmt.Errorf("parameter %q: target ds and signal are required", p.Key)
		}
		if p.Type == TypeEnum && len(p.EnumValues) == 0 {
			return fmt.Errorf("parameter %q: enum type requires enumValues", p.Key)
		}
		if p.Min != nil && p.Max != nil && *p.Min > *p.Max {
			return fmt.Errorf("parameter %q: min %v greater than max %v", p.Key, *p.Min, *p.Max)
		}
		if p.Default != nil {
			if err := p.checkValue(p.Default); err != nil {
				return fmt.Errorf("parameter %q default: %w", p.Key, err)
			}
		}
	}
	return nil
}

// param returns the parameter with the given key, or false.
func (s ConfigSet) param(key string) (Parameter, bool) {
	for _, p := range s.Parameters {
		if p.Key == key {
			return p, true
		}
	}
	return Parameter{}, false
}

// ValidateAgainst checks an instance against its set: every value references a
// known parameter and is type/range valid; every mandatory parameter without a
// default has a value.
func (inst ConfigInstance) ValidateAgainst(set ConfigSet) error {
	for key, v := range inst.Values {
		p, ok := set.param(key)
		if !ok {
			return fmt.Errorf("value for unknown parameter %q", key)
		}
		if err := p.checkValue(v); err != nil {
			return fmt.Errorf("parameter %q: %w", key, err)
		}
	}
	for _, p := range set.Parameters {
		if !p.Mandatory {
			continue
		}
		if _, ok := inst.Values[p.Key]; ok {
			continue
		}
		if p.Default == nil {
			return fmt.Errorf("mandatory parameter %q has no value", p.Key)
		}
	}
	return nil
}

// Resolve returns the effective value for a parameter: the instance value when
// present, otherwise the parameter default. The second result is false when no
// value is available (optional parameter, no default).
func (inst ConfigInstance) Resolve(p Parameter) (any, bool) {
	if v, ok := inst.Values[p.Key]; ok {
		return v, true
	}
	if p.Default != nil {
		return p.Default, true
	}
	return nil, false
}

// checkValue verifies a value matches the parameter's type and constraints.
func (p Parameter) checkValue(v any) error {
	switch p.Type {
	case TypeFloat, TypeInt:
		f, err := toFloat(v)
		if err != nil {
			return err
		}
		if p.Type == TypeInt && f != math.Trunc(f) {
			return fmt.Errorf("value %v is not an integer", f)
		}
		if p.Min != nil && f < *p.Min {
			return fmt.Errorf("value %v below minimum %v", f, *p.Min)
		}
		if p.Max != nil && f > *p.Max {
			return fmt.Errorf("value %v above maximum %v", f, *p.Max)
		}
	case TypeBool:
		if _, ok := v.(bool); !ok {
			return fmt.Errorf("value %v is not a bool", v)
		}
	case TypeString:
		if _, ok := v.(string); !ok {
			return fmt.Errorf("value %v is not a string", v)
		}
	case TypeEnum:
		s, ok := v.(string)
		if !ok {
			return fmt.Errorf("enum value %v is not a string", v)
		}
		if !slices.Contains(p.EnumValues, s) {
			return fmt.Errorf("value %q is not an allowed enum value", s)
		}
	case TypeFloatArray:
		arr, err := toFloatArray(v)
		if err != nil {
			return err
		}
		for i, f := range arr {
			if p.Min != nil && f < *p.Min {
				return fmt.Errorf("element %d value %v below minimum %v", i, f, *p.Min)
			}
			if p.Max != nil && f > *p.Max {
				return fmt.Errorf("element %d value %v above maximum %v", i, f, *p.Max)
			}
		}
	}
	return nil
}

// normalize coerces a JSON-decoded value into the canonical Go type the
// datasource write path expects. Array parameters become []float64 (JSON yields
// []any); scalar values are returned unchanged.
func (p Parameter) normalize(v any) any {
	if p.Type == TypeFloatArray {
		if arr, err := toFloatArray(v); err == nil {
			return arr
		}
	}
	return v
}

// toFloatArray coerces a JSON-decoded array value to []float64. JSON
// unmarshalling yields []any of float64, but a native []float64 is also
// accepted.
func toFloatArray(v any) ([]float64, error) {
	switch a := v.(type) {
	case []float64:
		return a, nil
	case []any:
		out := make([]float64, len(a))
		for i, e := range a {
			f, err := toFloat(e)
			if err != nil {
				return nil, fmt.Errorf("element %d: %w", i, err)
			}
			out[i] = f
		}
		return out, nil
	default:
		return nil, fmt.Errorf("value %v is not an array", v)
	}
}

// toFloat coerces a JSON-decoded numeric value to float64. JSON unmarshalling
// yields float64 for numbers, but values may also arrive as int or string.
func toFloat(v any) (float64, error) {
	switch n := v.(type) {
	case float64:
		return n, nil
	case float32:
		return float64(n), nil
	case int:
		return float64(n), nil
	case int64:
		return float64(n), nil
	case string:
		f, err := strconv.ParseFloat(n, 64)
		if err != nil {
			return 0, fmt.Errorf("value %q is not numeric", n)
		}
		return f, nil
	default:
		return 0, fmt.Errorf("value %v is not numeric", v)
	}
}
