// Package autonumber implements metadata-driven identifier assignment for NexusCRM.
package autonumber

import (
	"bytes"
	"encoding/json"
	"fmt"
	"math"
	"sort"
	"strconv"
)

// CanonicalizeTokenRefs produces the canonical JSON byte representation
// of an `auto_number_token_refs` value (PRD §4.1, P2-R2-A +
// P2-R3-canonicalize-owner).
//
// The canonical form
//   - sorts map keys lexicographically at every nesting level,
//   - preserves array order as declared in the source data-spec,
//   - uses no extraneous whitespace,
//   - is deterministic: CanonicalizeTokenRefs(x) == CanonicalizeTokenRefs(x)
//     across any number of invocations, regardless of Go map iteration
//     randomization (F12 fixpack).
//
// All three callers (seed generator, runtime reader round-trip check,
// test harness J1) MUST route through this function; ad-hoc
// `json.Marshal` is forbidden. Drift between callers is detected by
// `cmd/setup verify` V2.
//
// The input can be:
//   - a `map[string]any` or `[]any` (as produced by json.Unmarshal into
//     an interface{} target);
//   - a typed struct (e.g. `[]CrossRecordRef`) — it will be round-tripped
//     through json.Marshal/Unmarshal first to normalize to the generic
//     shape.
//
// Unsupported leaf types (func, chan, complex, …) return an error
// wrapping json.Marshaler semantics.
func CanonicalizeTokenRefs(v any) ([]byte, error) {
	// Normalize typed values to generic JSON shape. We do not call
	// json.Marshal on the input directly because Go map iteration is
	// randomized — the stringified bytes would not be deterministic.
	normalized, err := normalizeForCanonical(v)
	if err != nil {
		return nil, fmt.Errorf("autonumber canonicalize: %w", err)
	}
	var buf bytes.Buffer
	if err := writeCanonical(&buf, normalized); err != nil {
		return nil, fmt.Errorf("autonumber canonicalize: %w", err)
	}
	return buf.Bytes(), nil
}

// normalizeForCanonical converts a typed Go value into the generic
// JSON value shape (`map[string]any`, `[]any`, scalar). Fast-path:
// values that are already in the generic shape pass through directly.
// Typed structs are round-tripped through encoding/json.
func normalizeForCanonical(v any) (any, error) {
	switch t := v.(type) {
	case nil, bool, string, float64, json.Number:
		return t, nil
	case int:
		return float64(t), nil
	case int32:
		return float64(t), nil
	case int64:
		// int64 > 2^53 loses precision as float64. Cross-record seed
		// data is strings + small ints; keep as json.Number to
		// preserve exactness.
		return json.Number(strconv.FormatInt(t, 10)), nil
	case uint:
		return float64(t), nil
	case uint32:
		return float64(t), nil
	case uint64:
		return json.Number(strconv.FormatUint(t, 10)), nil
	case []byte:
		// JSON encodes []byte as base64; tokens_refs does not use
		// binary fields in practice, but keep the symmetry.
		return string(t), nil
	case map[string]any:
		out := make(map[string]any, len(t))
		for k, val := range t {
			nv, err := normalizeForCanonical(val)
			if err != nil {
				return nil, err
			}
			out[k] = nv
		}
		return out, nil
	case []any:
		out := make([]any, len(t))
		for i, val := range t {
			nv, err := normalizeForCanonical(val)
			if err != nil {
				return nil, err
			}
			out[i] = nv
		}
		return out, nil
	}
	// Fallback for typed structs / slices of structs / pointers —
	// round-trip through encoding/json with UseNumber so integer
	// precision is preserved.
	raw, err := json.Marshal(v)
	if err != nil {
		return nil, fmt.Errorf("marshal %T: %w", v, err)
	}
	dec := json.NewDecoder(bytes.NewReader(raw))
	dec.UseNumber()
	var generic any
	if err := dec.Decode(&generic); err != nil {
		return nil, fmt.Errorf("decode %T: %w", v, err)
	}
	// Recurse to convert any nested typed entries.
	return normalizeForCanonical(generic)
}

// writeCanonical serializes normalized generic JSON into w using:
//   - sorted map keys (sort.Strings)
//   - compact form (no whitespace)
//   - standard JSON escaping via json.Marshal for scalars
//
// The serialization is entirely byte-deterministic — no dependence on
// Go map iteration order.
func writeCanonical(buf *bytes.Buffer, v any) error {
	switch t := v.(type) {
	case nil:
		buf.WriteString("null")
	case bool:
		if t {
			buf.WriteString("true")
		} else {
			buf.WriteString("false")
		}
	case string:
		return writeJSONString(buf, t)
	case float64:
		return writeCanonicalNumber(buf, t)
	case json.Number:
		// Already a canonical numeric literal as decoded from input.
		buf.WriteString(string(t))
	case []any:
		buf.WriteByte('[')
		for i, el := range t {
			if i > 0 {
				buf.WriteByte(',')
			}
			if err := writeCanonical(buf, el); err != nil {
				return err
			}
		}
		buf.WriteByte(']')
	case map[string]any:
		keys := make([]string, 0, len(t))
		for k := range t {
			keys = append(keys, k)
		}
		sort.Strings(keys)
		buf.WriteByte('{')
		for i, k := range keys {
			if i > 0 {
				buf.WriteByte(',')
			}
			if err := writeJSONString(buf, k); err != nil {
				return err
			}
			buf.WriteByte(':')
			if err := writeCanonical(buf, t[k]); err != nil {
				return err
			}
		}
		buf.WriteByte('}')
	default:
		return fmt.Errorf("canonicalize: unsupported type %T", v)
	}
	return nil
}

// writeJSONString encodes s as a JSON string literal using
// encoding/json's rules (quoting + escaping) so the output round-trips
// via json.Unmarshal.
func writeJSONString(buf *bytes.Buffer, s string) error {
	raw, err := json.Marshal(s)
	if err != nil {
		return err
	}
	buf.Write(raw)
	return nil
}

// writeCanonicalNumber emits a float64 in a deterministic compact
// form. For integer values, emits without trailing ".0"; for
// non-integers, uses strconv.FormatFloat with 'g' verb.
func writeCanonicalNumber(buf *bytes.Buffer, f float64) error {
	if math.IsNaN(f) || math.IsInf(f, 0) {
		return fmt.Errorf("canonicalize: non-finite number %v", f)
	}
	// Integer fast-path.
	if f == math.Trunc(f) && math.Abs(f) < 1e15 {
		buf.WriteString(strconv.FormatInt(int64(f), 10))
		return nil
	}
	// General path — 'g' verb with -1 precision matches json.Marshal
	// output closely enough that downstream decode is stable.
	buf.WriteString(strconv.FormatFloat(f, 'g', -1, 64))
	return nil
}
