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

import (
	"fmt"
	"math"
	"strings"
	"time"
)

// Render produces the final assigned string for a record given a
// parsed FormatSpec, the reservation-txn-sampled `assignedAt` time,
// the reserved counter value, and the resolved cross-record token
// values keyed by `Token.RefKey` (PRD §5.5).
//
// Render MUST NOT perform I/O; callers pre-resolve all cross-record
// values. Missing cross-record values render as ErrTokenResolve
// (fail-closed per PRD §3.4 / user-decision Q1).
//
// Date tokens are always rendered in UTC (PRD §12.2 Q6 — UTC-only).
// `assignedAt` is sampled from `UTC_TIMESTAMP(6)` inside the
// reservation txn so `time.Location` != UTC indicates a bug upstream;
// Render normalizes defensively via `assignedAt.UTC()`.
//
// ErrCounterOverflow is returned if counter >= 10^CounterWidth —
// rendering would either truncate or produce a value wider than
// declared, both forbidden (PRD §5.1 step 5 / §5.5).
func Render(spec FormatSpec, assignedAt time.Time, counter int64, crossValues map[string]any) (string, error) {
	// Defensive: normalize to UTC even if caller passed non-UTC.
	at := assignedAt.UTC()

	// Counter overflow check — PRD §5.1 step 5.
	if spec.CounterWidth <= 0 {
		return "", fmt.Errorf("autonumber: render: invalid counter width %d", spec.CounterWidth)
	}
	if counter < 0 {
		return "", fmt.Errorf("autonumber: render: negative counter %d", counter)
	}
	maxCounter := counterMaxValue(spec.CounterWidth)
	if maxCounter > 0 && counter > maxCounter {
		return "", fmt.Errorf("%w: field counter %d exceeds width=%d max=%d",
			ErrCounterOverflow, counter, spec.CounterWidth, maxCounter)
	}

	var b strings.Builder
	b.Grow(len(spec.Raw) + 16)
	for _, tok := range spec.Tokens {
		switch tok.Kind {
		case TokenKindLiteral:
			b.WriteString(tok.Lit)
		case TokenKindDate:
			if err := writeDate(&b, tok.DateName, at); err != nil {
				return "", err
			}
		case TokenKindCounter:
			fmt.Fprintf(&b, "%0*d", tok.Width, counter)
		case TokenKindField, TokenKindRecordType:
			// TokenKindField and TokenKindRecordType share the same
			// resolution path: the engine feeds both through crossValues
			// keyed on RefKey, and the renderer treats them uniformly.
			// Parser distinguishes the two kinds at validation time
			// (§6.2 rule 7 taxonomy) and seed derive-regex treats them
			// with different regex shapes; runtime rendering is uniform.
			raw, ok := crossValues[tok.RefKey]
			if !ok {
				return "", fmt.Errorf("%w: missing cross-record value for token %s",
					ErrTokenResolve, tok.RefKey)
			}
			str, err := stringifyCrossValue(raw)
			if err != nil {
				return "", fmt.Errorf("%w: token %s: %v", ErrTokenResolve, tok.RefKey, err)
			}
			if str == "" {
				return "", fmt.Errorf("%w: empty cross-record value for token %s",
					ErrTokenResolve, tok.RefKey)
			}
			b.WriteString(str)
		default:
			return "", fmt.Errorf("autonumber: render: unknown token kind %d", tok.Kind)
		}
	}
	return b.String(), nil
}

// RenderScopePrefix produces the portion of a format string that
// precedes the counter token, with all non-counter tokens resolved.
// It is used as the `scope_key` value for ScopePerPrefix fields — each
// distinct rendered prefix owns its own counter sequence (e.g.
// "JP-QT-260418-" and "US-QT-260418-" each have their own 01, 02, ...).
//
// Fail-closed: a missing cross-record value returns ErrTokenResolve
// just like Render, so per_prefix scope semantics match Render's
// fail-closed contract (PRD §3.4 / §5.5).
//
// No counter value is produced by this function; the counter token is
// simply skipped. This means a degenerate counter-only format (e.g.
// "{00000000}") yields an empty scope key — equivalent to global
// scope, which is the sensible fallback for formats with no
// partitioning dimension.
func RenderScopePrefix(spec FormatSpec, assignedAt time.Time, crossValues map[string]any) (string, error) {
	at := assignedAt.UTC()

	var b strings.Builder
	b.Grow(len(spec.Raw))
	for _, tok := range spec.Tokens {
		switch tok.Kind {
		case TokenKindLiteral:
			b.WriteString(tok.Lit)
		case TokenKindDate:
			if err := writeDate(&b, tok.DateName, at); err != nil {
				return "", err
			}
		case TokenKindCounter:
			// Skip counter in prefix — that's the partition we are
			// keying on, not part of the scope key.
			continue
		case TokenKindField, TokenKindRecordType:
			raw, ok := crossValues[tok.RefKey]
			if !ok {
				return "", fmt.Errorf("%w: missing cross-record value for token %s",
					ErrTokenResolve, tok.RefKey)
			}
			str, err := stringifyCrossValue(raw)
			if err != nil {
				return "", fmt.Errorf("%w: token %s: %v", ErrTokenResolve, tok.RefKey, err)
			}
			if str == "" {
				return "", fmt.Errorf("%w: empty cross-record value for token %s",
					ErrTokenResolve, tok.RefKey)
			}
			b.WriteString(str)
		default:
			return "", fmt.Errorf("autonumber: RenderScopePrefix: unknown token kind %d", tok.Kind)
		}
	}
	return b.String(), nil
}

// renderScopePrefixParts is the split-aware sibling of
// RenderScopePrefix. Returns the rendered text BEFORE the counter
// token and AFTER the counter token as two separate strings. Their
// concatenation equals RenderScopePrefix's output.
//
// Used by the lazy-init live-scan (Option B, Codex round-6 HIGH):
// queryLiveMaxForPrefix needs to match live values by TWO ends
// (prefix + suffix) so formats with a counter-middle position — e.g.
// `{00}-END` or `{Country}-{00}-X` — return the correct live_max.
// Using only the concatenated prefix as a LIKE start anchor would
// miss those values entirely.
//
// Fail-closed on missing cross-record values, same as
// RenderScopePrefix.
func renderScopePrefixParts(spec FormatSpec, assignedAt time.Time, crossValues map[string]any) (before, after string, err error) {
	at := assignedAt.UTC()
	var beforeBuf, afterBuf strings.Builder
	target := &beforeBuf
	for _, tok := range spec.Tokens {
		switch tok.Kind {
		case TokenKindLiteral:
			target.WriteString(tok.Lit)
		case TokenKindDate:
			if err := writeDate(target, tok.DateName, at); err != nil {
				return "", "", err
			}
		case TokenKindCounter:
			// Switch output buffer from before → after.
			target = &afterBuf
		case TokenKindField, TokenKindRecordType:
			raw, ok := crossValues[tok.RefKey]
			if !ok {
				return "", "", fmt.Errorf("%w: missing cross-record value for token %s",
					ErrTokenResolve, tok.RefKey)
			}
			str, err := stringifyCrossValue(raw)
			if err != nil {
				return "", "", fmt.Errorf("%w: token %s: %v", ErrTokenResolve, tok.RefKey, err)
			}
			if str == "" {
				return "", "", fmt.Errorf("%w: empty cross-record value for token %s",
					ErrTokenResolve, tok.RefKey)
			}
			target.WriteString(str)
		default:
			return "", "", fmt.Errorf("autonumber: renderScopePrefixParts: unknown token kind %d", tok.Kind)
		}
	}
	return beforeBuf.String(), afterBuf.String(), nil
}

// writeDate appends the UTC-rendered date segment for the named token.
func writeDate(b *strings.Builder, name string, at time.Time) error {
	switch name {
	case "YYYY":
		fmt.Fprintf(b, "%04d", at.Year())
	case "YY":
		fmt.Fprintf(b, "%02d", at.Year()%100)
	case "MM":
		fmt.Fprintf(b, "%02d", int(at.Month()))
	case "DD":
		fmt.Fprintf(b, "%02d", at.Day())
	default:
		return fmt.Errorf("autonumber: render: unknown date token %q", name)
	}
	return nil
}

// counterMaxValue returns `10^width - 1` for width in [1, 18], or 0 for
// invalid widths (caller must treat that as skip-check; Render also
// rejects width <= 0 earlier). Kept as int64; BIGINT fits width=19
// (9.2e18) but parser enforces <= 18.
func counterMaxValue(width int) int64 {
	if width < 1 || width > maxCounterWidth {
		return 0
	}
	// 10^width. For width<=18, 10^18 = 1e18 fits int64 (max 9.22e18).
	return int64(math.Pow10(width)) - 1
}

// stringifyCrossValue converts a resolved cross-record value into its
// string rendering. Supports the scalar types that TiDB + Go driver
// commonly hand back (string, []byte, intXY, float).
func stringifyCrossValue(raw any) (string, error) {
	switch v := raw.(type) {
	case nil:
		return "", nil
	case string:
		return v, nil
	case []byte:
		return string(v), nil
	case int:
		return fmt.Sprintf("%d", v), nil
	case int32:
		return fmt.Sprintf("%d", v), nil
	case int64:
		return fmt.Sprintf("%d", v), nil
	case float32:
		// Cross-record values should be string columns in practice;
		// render numeric via %g to avoid gratuitous zeros.
		return fmt.Sprintf("%g", v), nil
	case float64:
		return fmt.Sprintf("%g", v), nil
	case bool:
		if v {
			return "true", nil
		}
		return "false", nil
	default:
		return "", fmt.Errorf("unsupported cross-record value type %T", raw)
	}
}
