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

import (
	"fmt"
	"sync"
)

// QuarantineRegistry is the in-memory set of fieldIDs whose rule has
// been flagged quarantined by engine init or metadata-cache reload
// re-parse/re-validate (PRD §5.6 admin-bypass closure; fixpack F9).
//
// Design properties:
//   - In-memory ONLY. Recomputed from DB state at every engine init.
//     Persisting would create a staleness window between data-spec
//     repair and process restart.
//   - Concurrent-safe. All methods are goroutine-safe via sync.RWMutex.
//   - Non-crash. Setting a rule quarantined during init must NOT panic
//     or abort; the engine continues serving other fields (F9).
//
// Use via NewQuarantineRegistry; do not construct the zero value.
type QuarantineRegistry struct {
	mu      sync.RWMutex
	reasons map[string]string // fieldID -> reason
}

// NewQuarantineRegistry returns a ready-to-use registry with no
// quarantined fields.
func NewQuarantineRegistry() *QuarantineRegistry {
	return &QuarantineRegistry{reasons: make(map[string]string)}
}

// Quarantine marks the given fieldID as quarantined with a
// human-readable reason. Safe to call multiple times; the last-written
// reason wins. Does NOT wrap in any panic-recovery or logging —
// callers are expected to log at the boundary (engine init logs each
// parse-fail).
func (q *QuarantineRegistry) Quarantine(fieldID, reason string) {
	if q == nil || fieldID == "" {
		return
	}
	q.mu.Lock()
	defer q.mu.Unlock()
	q.reasons[fieldID] = reason
}

// IsQuarantined returns (true, reason) if fieldID was previously
// Quarantined; (false, "") otherwise.
func (q *QuarantineRegistry) IsQuarantined(fieldID string) (bool, string) {
	if q == nil {
		return false, ""
	}
	q.mu.RLock()
	defer q.mu.RUnlock()
	reason, ok := q.reasons[fieldID]
	return ok, reason
}

// Clear removes any quarantine record for fieldID. Used by the
// metadata-cache reload when a previously quarantined field now parses
// cleanly.
func (q *QuarantineRegistry) Clear(fieldID string) {
	if q == nil {
		return
	}
	q.mu.Lock()
	defer q.mu.Unlock()
	delete(q.reasons, fieldID)
}

// Len returns the current count of quarantined fields. Exposed
// primarily for metrics emission and tests.
func (q *QuarantineRegistry) Len() int {
	if q == nil {
		return 0
	}
	q.mu.RLock()
	defer q.mu.RUnlock()
	return len(q.reasons)
}

// List returns a snapshot of all quarantined fields. The returned map
// is a copy; callers may retain it safely.
func (q *QuarantineRegistry) List() map[string]string {
	if q == nil {
		return nil
	}
	q.mu.RLock()
	defer q.mu.RUnlock()
	out := make(map[string]string, len(q.reasons))
	for k, v := range q.reasons {
		out[k] = v
	}
	return out
}

// buildQuarantineErrorFromRegistry constructs an ErrRuleQuarantined if
// the given fieldID is quarantined; returns (nil, false) otherwise.
// Helper used by AssignOne / AssignBatch to avoid duplicated string
// formatting at call sites.
func buildQuarantineErrorFromRegistry(q *QuarantineRegistry, fieldID string) (error, bool) {
	if q == nil {
		return nil, false
	}
	ok, reason := q.IsQuarantined(fieldID)
	if !ok {
		return nil, false
	}
	return NewQuarantinedRuleError(fieldID, reason), true
}

// QuarantineReasonFormatFunc formats a human-readable quarantine
// reason from an underlying error. Exposed so call sites produce a
// uniform string.
func QuarantineReasonFormatFunc(stage string, err error) string {
	return fmt.Sprintf("%s failed: %v", stage, err)
}
