package confmgr

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

// ReadFunc reads the current raw value of a target signal. The API/engine layer
// supplies a closure backed by the broker (ReadNow) so confmgr stays decoupled
// from the transport. The returned value mirrors datasource.Value.Data
// (float64 | []float64 | string | int64 | bool; enums arrive as an int64 index).
type ReadFunc func(ds, signal string) (any, error)

// SnapshotEntry records the outcome of reading one parameter's target signal.
type SnapshotEntry struct {
	Key    string `json:"key"`
	DS     string `json:"ds"`
	Signal string `json:"signal"`
	Value  any    `json:"value,omitempty"`
	OK     bool   `json:"ok"`
	Error  string `json:"error,omitempty"`
}

// SnapshotResult summarises a snapshot run. Values holds the captured,
// type-coerced values ready to populate a new ConfigInstance.
type SnapshotResult struct {
	SetID    string          `json:"setId"`
	Entries  []SnapshotEntry `json:"entries"`
	Captured int             `json:"captured"`
	Failed   int             `json:"failed"`
	Values   map[string]any  `json:"-"`
}

// Snapshot reads the current value of every parameter's target signal via read
// and builds a value map for a new instance. Read failures and values that
// cannot be coerced to the parameter's type are recorded per-entry rather than
// aborting the whole snapshot, so a partial capture is reported faithfully.
func Snapshot(set ConfigSet, read ReadFunc) SnapshotResult {
	res := SnapshotResult{
		SetID:   set.ID,
		Entries: make([]SnapshotEntry, 0, len(set.Parameters)),
		Values:  make(map[string]any, len(set.Parameters)),
	}
	for _, p := range set.Parameters {
		e := SnapshotEntry{Key: p.Key, DS: p.DS, Signal: p.Signal}
		raw, err := read(p.DS, p.Signal)
		if err != nil {
			e.Error = err.Error()
			res.Failed++
			res.Entries = append(res.Entries, e)
			continue
		}
		v, err := p.coerceSnapshot(raw)
		if err != nil {
			e.Error = err.Error()
			res.Failed++
			res.Entries = append(res.Entries, e)
			continue
		}
		e.Value = v
		e.OK = true
		res.Values[p.Key] = v
		res.Captured++
		res.Entries = append(res.Entries, e)
	}
	return res
}

// coerceSnapshot converts a raw datasource value into the canonical Go value the
// parameter's type expects, so the result passes checkValue and serialises
// cleanly. Enum signals arrive as an int64 index and are mapped to their string.
func (p Parameter) coerceSnapshot(raw any) (any, error) {
	switch p.Type {
	case TypeFloat:
		return toFloat(raw)
	case TypeInt:
		f, err := toFloat(raw)
		if err != nil {
			return nil, err
		}
		return int64(math.Round(f)), nil
	case TypeBool:
		switch b := raw.(type) {
		case bool:
			return b, nil
		case string:
			pb, err := strconv.ParseBool(b)
			if err != nil {
				return nil, fmt.Errorf("value %q is not a bool", b)
			}
			return pb, nil
		default:
			f, err := toFloat(raw)
			if err != nil {
				return nil, fmt.Errorf("value %v is not a bool", raw)
			}
			return f != 0, nil
		}
	case TypeString:
		if s, ok := raw.(string); ok {
			return s, nil
		}
		return fmt.Sprintf("%v", raw), nil
	case TypeEnum:
		// Enum signals deliver an int64 index into the metadata strings; map it
		// to this parameter's enum value. A string already in range is kept.
		if s, ok := raw.(string); ok {
			if slices.Contains(p.EnumValues, s) {
				return s, nil
			}
			return nil, fmt.Errorf("value %q is not an allowed enum value", s)
		}
		f, err := toFloat(raw)
		if err != nil {
			return nil, fmt.Errorf("enum value %v is not an index", raw)
		}
		idx := int(math.Round(f))
		if idx < 0 || idx >= len(p.EnumValues) {
			return nil, fmt.Errorf("enum index %d out of range", idx)
		}
		return p.EnumValues[idx], nil
	case TypeFloatArray:
		return toFloatArray(raw)
	default:
		return raw, nil
	}
}
