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

import (
	"errors"
	"fmt"
	"strings"
	"sync"
)

// parseCache memoizes Parse results. Keyed on the raw format string.
// Safe under concurrent use (sync.Map). PRD §5.0 / §5.4 parse-once.
var parseCache sync.Map // map[string]parseCacheEntry

type parseCacheEntry struct {
	spec FormatSpec
	refs []CrossRecordRef
	err  error
}

// Parser validation limits — PRD §6.2 rules 1..10.
const (
	// maxFormatLen guards rule 5 lower bound; rule 5's upper bound
	// (rendered-length vs column MaxLength) is enforced at seed time
	// against metadata, not at Parse time.
	maxFormatLen = 255
	// minCounterWidth — PRD §6.2 rule 2: width >= 1.
	minCounterWidth = 1
	// maxCounterWidth — PRD §6.2 rule 2: width <= 18 (BIGINT overflow safety).
	maxCounterWidth = 18
)

// ErrParse is the sentinel wrapping all parser validation errors.
// Call-sites classify via errors.Is(err, ErrParse).
var ErrParse = errors.New("autonumber: parse error")

// parseError constructs a wrapped parse error with structured context.
// Offset is 0-based index into the format string where the problem was
// detected; -1 indicates a whole-format violation (e.g. "no counter
// token").
func parseError(reason, format string, offset int) error {
	if offset < 0 {
		return fmt.Errorf("%w: %s: format=%q", ErrParse, reason, format)
	}
	return fmt.Errorf("%w: %s at offset %d: format=%q", ErrParse, reason, offset, format)
}

// Parse converts an `auto_number_format` string into a validated
// FormatSpec and the list of CrossRecordRef descriptors that will be
// stored in `_System_Field.auto_number_token_refs` (PRD §4.1, §5.4).
//
// Validation follows PRD §6.2 rules 1..10. On any rule violation Parse
// returns a structured error (wrapping ErrParse) naming the offending
// input.
//
// Results are cached in parseCache; calling Parse multiple times on the
// same format string returns the memoized result without re-scanning.
func Parse(format string) (FormatSpec, []CrossRecordRef, error) {
	if cached, ok := parseCache.Load(format); ok {
		e := cached.(parseCacheEntry)
		return e.spec, e.refs, e.err
	}
	spec, refs, err := parseUncached(format)
	parseCache.Store(format, parseCacheEntry{spec: spec, refs: refs, err: err})
	return spec, refs, err
}

// parseUncached is the underlying scanner. Exported Parse() wraps it
// with memoization.
func parseUncached(format string) (FormatSpec, []CrossRecordRef, error) {
	if format == "" {
		return FormatSpec{}, nil, parseError("empty format", format, -1)
	}
	if len(format) > maxFormatLen {
		return FormatSpec{}, nil, parseError(
			fmt.Sprintf("format length %d exceeds max %d", len(format), maxFormatLen),
			format, -1,
		)
	}

	var (
		tokens       []Token
		refs         []CrossRecordRef
		counterSeen  bool
		counterWidth int
	)

	// Literal accumulator flushed whenever a `{` opens.
	var lit strings.Builder
	flushLiteral := func() {
		if lit.Len() == 0 {
			return
		}
		tokens = append(tokens, Token{Kind: TokenKindLiteral, Lit: lit.String()})
		lit.Reset()
	}

	// Linear scan. i tracks absolute index in `format`.
	i := 0
	for i < len(format) {
		ch := format[i]
		if ch == '}' {
			return FormatSpec{}, nil, parseError("stray '}'", format, i)
		}
		if ch != '{' {
			// Literal character.
			lit.WriteByte(ch)
			i++
			continue
		}

		// Start of a token — find matching '}'.
		start := i
		end := strings.IndexByte(format[i+1:], '}')
		if end < 0 {
			return FormatSpec{}, nil, parseError("unclosed '{'", format, start)
		}
		end += i + 1 // absolute
		inner := format[i+1 : end]

		// Guard: no nested '{' inside token body.
		if strings.IndexByte(inner, '{') >= 0 {
			return FormatSpec{}, nil, parseError("unexpected '{' inside token", format, start)
		}

		// Reject whitespace anywhere inside the token body.
		if hasSpace(inner) {
			return FormatSpec{}, nil, parseError("whitespace inside token", format, start)
		}
		if inner == "" {
			return FormatSpec{}, nil, parseError("empty token '{}'", format, start)
		}

		// Classify token.
		flushLiteral()
		tok, ref, kind, err := classifyToken(inner, format, start)
		if err != nil {
			return FormatSpec{}, nil, err
		}
		if kind == TokenKindCounter {
			if counterSeen {
				return FormatSpec{}, nil, parseError("more than one counter token", format, start)
			}
			counterSeen = true
			counterWidth = tok.Width
		}
		tokens = append(tokens, tok)
		if ref != nil {
			refs = append(refs, *ref)
		}
		i = end + 1
	}
	flushLiteral()

	// PRD §6.2 rule 1: exactly one counter token.
	if !counterSeen {
		return FormatSpec{}, nil, parseError("no counter token", format, -1)
	}

	spec := FormatSpec{
		Raw:          format,
		Tokens:       tokens,
		CounterWidth: counterWidth,
	}

	// PRD §6.2 rule 9: per_day scope requires a date token — but scope
	// is metadata-side. Parse itself does not know the scope; the
	// seed-time validator ties scope + spec.HasDateToken() together.

	return spec, refs, nil
}

// classifyToken inspects a token body (the contents between `{` and
// `}`) and returns the resulting Token, optional CrossRecordRef, and
// the inferred kind.
//
// start is the absolute offset of the opening '{' so wrapped errors
// can point at the user's input accurately.
func classifyToken(inner, format string, start int) (Token, *CrossRecordRef, TokenKind, error) {
	// Counter — PRD §6.1 counter_token. All-zero body.
	if isAllZeros(inner) {
		width := len(inner)
		if width < minCounterWidth || width > maxCounterWidth {
			return Token{}, nil, 0, parseError(
				fmt.Sprintf("counter width %d outside [%d,%d]", width, minCounterWidth, maxCounterWidth),
				format, start,
			)
		}
		return Token{Kind: TokenKindCounter, Width: width}, nil, TokenKindCounter, nil
	}

	// Date — PRD §6.1 date_token.
	if isDateName(inner) {
		return Token{Kind: TokenKindDate, DateName: inner}, nil, TokenKindDate, nil
	}

	// Cross-record or taxonomy — `<object>.<field>`.
	dotCount := strings.Count(inner, ".")
	if dotCount == 0 {
		// Unknown bare-word token.
		return Token{}, nil, 0, parseError(
			fmt.Sprintf("unknown token %q", inner), format, start,
		)
	}
	if dotCount > 1 {
		// PRD §6.2 rule 8 + §1.2 non-goal: depth > 1 via dotted
		// syntax is not supported. Depth-2 fallback chains must be
		// expressed via auto_number_token_refs.fkChains.
		return Token{}, nil, 0, parseError(
			fmt.Sprintf("cross-record token with depth > 1: %q", inner),
			format, start,
		)
	}
	// Exactly one dot → split and validate identifiers.
	dot := strings.IndexByte(inner, '.')
	obj := inner[:dot]
	fld := inner[dot+1:]
	if obj == "" || !isIdent(obj) {
		return Token{}, nil, 0, parseError(
			fmt.Sprintf("invalid object name %q in token %q", obj, inner),
			format, start,
		)
	}
	if fld == "" || !isIdent(fld) {
		return Token{}, nil, 0, parseError(
			fmt.Sprintf("invalid field name %q in token %q", fld, inner),
			format, start,
		)
	}

	literal := "{" + inner + "}"
	kind := TokenKindField
	if obj == "RecordType" {
		kind = TokenKindRecordType
	}
	tok := Token{
		Kind:   kind,
		Object: obj,
		Field:  fld,
		RefKey: literal,
	}
	ref := &CrossRecordRef{
		TokenLiteral: literal,
		TargetObject: obj,
		TargetField:  fld,
		ReadStrategy: "caller_txn",
		FkChains: [][]CrossRecordStep{
			{{TargetObject: obj, TargetField: fld}},
		},
	}
	return tok, ref, kind, nil
}

// --- small predicates ---

// hasSpace reports whether s contains any ASCII whitespace byte. The
// parser rejects whitespace inside tokens — see PRD §6.1 (no whitespace
// in the grammar).
func hasSpace(s string) bool {
	for i := 0; i < len(s); i++ {
		switch s[i] {
		case ' ', '\t', '\n', '\r', '\v', '\f':
			return true
		}
	}
	return false
}

// isAllZeros reports whether every byte in s is '0' and s is non-empty.
// Used to recognize counter tokens `{0+}`.
func isAllZeros(s string) bool {
	if s == "" {
		return false
	}
	for i := 0; i < len(s); i++ {
		if s[i] != '0' {
			return false
		}
	}
	return true
}

// isDateName reports whether s is one of the recognized date-token names
// (PRD §6.1 date_name).
func isDateName(s string) bool {
	switch s {
	case "YYYY", "YY", "MM", "DD":
		return true
	}
	return false
}

// isIdent reports whether s matches the SF API-name grammar
// `[A-Za-z][A-Za-z0-9_]*` (PRD §6.1).
func isIdent(s string) bool {
	if s == "" {
		return false
	}
	c := s[0]
	if !isAlpha(c) {
		return false
	}
	for i := 1; i < len(s); i++ {
		c := s[i]
		if !isAlpha(c) && !isDigit(c) && c != '_' {
			return false
		}
	}
	return true
}

func isAlpha(c byte) bool { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') }
func isDigit(c byte) bool { return c >= '0' && c <= '9' }

// HasDateToken reports whether spec contains at least one date token.
// Exposed for seed-time validation of PRD §6.2 rule 9 (per_day scope
// requires at least one date token).
func (s FormatSpec) HasDateToken() bool {
	for _, t := range s.Tokens {
		if t.Kind == TokenKindDate {
			return true
		}
	}
	return false
}

// RequiresRecordType reports whether spec contains a RecordType
// taxonomy token. Used by PRD §6.2 rule 7 + seed-time verify.
func (s FormatSpec) RequiresRecordType() bool {
	for _, t := range s.Tokens {
		if t.Kind == TokenKindRecordType {
			return true
		}
	}
	return false
}
