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

import (
	"errors"
	"fmt"
)

// Sentinel errors exposed by the engine (PRD §3.4 "Error surface").
// All runtime errors produced by the engine wrap one of these sentinels
// with `fmt.Errorf("%w: ...", ErrXxx)` so call-sites can classify via
// `errors.Is`.
var (
	// ErrCounterMissing — a counter row with `scope=global` was not
	// found. For `global` fields this is a CORRUPTION signal because
	// Approach X pre-seeds all global rows at cutover (§5.2 step 6.d
	// policy gate; §3.4).
	ErrCounterMissing = errors.New("autonumber: counter row missing")

	// ErrCounterOverflow — rendering the reserved counter at the
	// format's declared width would exceed `10^width - 1`
	// (PRD §3.4; §5.1 step 5; §5.6 rule-change semantics).
	ErrCounterOverflow = errors.New("autonumber: counter overflow")

	// ErrTokenResolve — a cross-record token failed to resolve due to a
	// USER-DATA condition (FK null / target row missing / target field
	// null). Fail-closed per PRD §3.4. The downstream classifier can
	// surface these as 400 FIELD_INTEGRITY_EXCEPTION because the user
	// can fix the data and retry.
	//
	// Use NewTokenResolveError to construct a structured variant that
	// carries the token literal so callers can build actionable messages
	// without string-parsing the error chain.
	//
	// Infrastructure failures (DB query error, resolver wiring error)
	// MUST wrap ErrChainResolverFailure instead so multi-chain inference
	// in resolveSingleRef can distinguish them from user-data conditions.
	ErrTokenResolve = errors.New("autonumber: token resolve failed")

	// ErrChainResolverFailure — the FK chain resolver hit an infrastructure
	// condition the user cannot fix (DB query failure, lookup wiring error).
	// Distinct sentinel from ErrTokenResolve so resolveSingleRef can keep
	// the two error classes apart when one chain in a multi-chain token
	// fails on infra and another loads a parent with a null target field.
	// The classifier downstream maps this to 500 InternalError.
	ErrChainResolverFailure = errors.New("autonumber: chain resolver failure")

	// ErrRuleQuarantined — the rule for this field has been marked
	// quarantined by the metadata-cache reload re-parse/re-validate
	// defense (PRD §5.6 P1-R3-admin-bypass closure). Subsequent
	// AssignOne calls fail-closed until a proper data-spec repair
	// runs.
	ErrRuleQuarantined = errors.New("autonumber: rule quarantined")
)

// quarantinedRuleError carries structured detail about a quarantined
// rule: which field, and the reason recorded when the metadata cache
// last reloaded (PRD §5.6). It wraps ErrRuleQuarantined so `errors.Is`
// works against the sentinel.
type quarantinedRuleError struct {
	FieldID string
	Reason  string
}

// Error renders the full diagnostic string.
func (e *quarantinedRuleError) Error() string {
	return fmt.Sprintf("autonumber: rule quarantined: fieldID=%s reason=%s", e.FieldID, e.Reason)
}

// Unwrap exposes the sentinel for errors.Is.
func (e *quarantinedRuleError) Unwrap() error { return ErrRuleQuarantined }

// tokenResolveError carries the token literal that failed to resolve
// (e.g. "{Account.BillingCountryCode}"). It wraps ErrTokenResolve so
// errors.Is and errors.As work against the sentinel.
//
// Construct via NewTokenResolveError; inspect via TokenResolveDetail.
type tokenResolveError struct {
	// TokenLiteral is the raw format token, e.g. "{Account.BillingCountryCode}".
	TokenLiteral string
}

func (e *tokenResolveError) Error() string {
	return fmt.Sprintf("autonumber: token resolve failed: token %s could not be resolved (parent field is null or unset)", e.TokenLiteral)
}

func (e *tokenResolveError) Unwrap() error { return ErrTokenResolve }

// NewTokenResolveError constructs a structured ErrTokenResolve error carrying
// the token literal so callers can build user-facing messages without
// parsing the error string.
func NewTokenResolveError(tokenLiteral string) error {
	return &tokenResolveError{TokenLiteral: tokenLiteral}
}

// TokenResolveDetail extracts the token literal from an error produced by
// NewTokenResolveError. Returns ("", false) for any other error type.
func TokenResolveDetail(err error) (tokenLiteral string, ok bool) {
	var te *tokenResolveError
	if errors.As(err, &te) && te != nil {
		return te.TokenLiteral, true
	}
	return "", false
}

// NewQuarantinedRuleError constructs a structured ErrRuleQuarantined
// with the field id and reason populated. Exposed so that the
// metadata-cache defense hook can construct it without stringly-typed
// formatting at call sites.
func NewQuarantinedRuleError(fieldID, reason string) error {
	return &quarantinedRuleError{FieldID: fieldID, Reason: reason}
}

// QuarantineDetails extracts FieldID and Reason from an error if it was
// produced by NewQuarantinedRuleError; returns empty strings + false
// otherwise.
func QuarantineDetails(err error) (fieldID, reason string, ok bool) {
	var qe *quarantinedRuleError
	if errors.As(err, &qe) && qe != nil {
		return qe.FieldID, qe.Reason, true
	}
	return "", "", false
}
