// Package validator is the Go port of Rust's `validator` crate
// (`/home/profit/lakehouse/crates/validator/`). Production safety
// nets for staffing-domain LLM outputs:
//
//   - FillValidator: catches phantom IDs / wrong-status workers /
//     blacklist violations / geo-or-role mismatches in fill proposals
//   - EmailValidator: catches SSN-shape sequences / salary
//     disclosure / wrong-target name in email/SMS drafts
//
// Per `reports/cutover/architecture_comparison.md`'s "Go missing"
// section: these were Rust-only until this port. Closes one of the
// two named gaps for Go-primary operation (the other being the
// materializer port).
//
// Architectural choice: we mirror the Rust shape exactly so the
// Validator + Artifact + Finding interfaces are call-compatible
// across runtimes. A future "validator service" daemon could expose
// either runtime's implementation behind a uniform HTTP contract.

package validator

import (
	"encoding/json"
	"fmt"
	"time"
)

// Artifact is the discriminated union of input shapes a Validator
// can receive. Mirrors Rust's `enum Artifact`. The first non-zero
// field selects the kind.
type Artifact struct {
	// FillProposal: {fills: [{candidate_id, name}], _context: {...}}
	FillProposal map[string]any
	// EmailDraft: {to, body, subject?, kind?, _context?: {candidate_id?}}
	EmailDraft map[string]any
	// Playbook: {operation, endorsed_names, target_count?, fingerprint}
	Playbook map[string]any
}

// Kind returns a short string for error messages — mirrors the
// Rust Debug shape used in "expected FillProposal, got X".
func (a Artifact) Kind() string {
	switch {
	case a.FillProposal != nil:
		return "FillProposal"
	case a.EmailDraft != nil:
		return "EmailDraft"
	case a.Playbook != nil:
		return "Playbook"
	default:
		return "Unknown"
	}
}

// Severity matches Rust's enum {Error, Warning, Info}.
type Severity string

const (
	SeverityError   Severity = "error"
	SeverityWarning Severity = "warning"
	SeverityInfo    Severity = "info"
)

// Finding is one warning-or-info note attached to a successful
// validation. Errors abort validation; findings come back alongside
// a passing report. Mirrors Rust's Finding shape exactly so JSON
// round-trips between runtimes.
type Finding struct {
	Field    string   `json:"field"`
	Severity Severity `json:"severity"`
	Message  string   `json:"message"`
}

// Report is the success-path return value: zero or more findings
// + per-validator wall-clock cost.
type Report struct {
	Findings  []Finding `json:"findings"`
	ElapsedMs int64     `json:"elapsed_ms"`
}

// ValidationErrorKind discriminates the failure modes. Mirrors
// Rust's ValidationError variants:
//   - Schema: input shape doesn't match contract
//   - Completeness: structural counts wrong (e.g. 3 fills, target_count=5)
//   - Consistency: cross-source disagreement (phantom worker, wrong city)
//   - Policy: org-level rule violation (blacklist, PII leak)
type ValidationErrorKind string

const (
	ErrSchema       ValidationErrorKind = "schema"
	ErrCompleteness ValidationErrorKind = "completeness"
	ErrConsistency  ValidationErrorKind = "consistency"
	ErrPolicy       ValidationErrorKind = "policy"
)

// ValidationError is the Go equivalent of Rust's enum + variant
// fields. Field is set for Schema errors (the failing field name);
// Reason carries the human-readable message for all variants.
type ValidationError struct {
	Kind   ValidationErrorKind
	Field  string
	Reason string
}

// Error makes ValidationError a Go error value. Format mirrors the
// Rust Debug print so log scraping behaves the same.
func (e *ValidationError) Error() string {
	if e.Field != "" {
		return string(e.Kind) + " (" + e.Field + "): " + e.Reason
	}
	return string(e.Kind) + ": " + e.Reason
}

// MarshalJSON emits the Rust-compatible serde-tagged-enum shape:
//
//	{"Schema":       {"field": "...", "reason": "..."}}
//	{"Completeness": {"reason": "..."}}
//	{"Consistency":  {"reason": "..."}}
//	{"Policy":       {"reason": "..."}}
//
// Aligns with `crates/validator/src/lib.rs::ValidationError` so callers
// that parse the error envelope work across runtimes. Surfaced by the
// 2026-05-02 cross-runtime parity probe (validator_parity.sh) which
// found 5/6 body shapes diverging on identical input.
func (e ValidationError) MarshalJSON() ([]byte, error) {
	variant := variantName(e.Kind)
	inner := map[string]string{}
	if e.Kind == ErrSchema {
		inner["field"] = e.Field
	}
	inner["reason"] = e.Reason
	return json.Marshal(map[string]any{variant: inner})
}

// UnmarshalJSON accepts BOTH the Rust serde-tagged shape AND the
// legacy flat-struct shape Go used to emit, so existing on-disk JSONL
// rows still parse. New writes always use the Rust shape (per
// MarshalJSON above).
func (e *ValidationError) UnmarshalJSON(data []byte) error {
	// Try the Rust serde shape first: {"<Variant>":{...}}
	var tagged map[string]map[string]string
	if err := json.Unmarshal(data, &tagged); err == nil && len(tagged) == 1 {
		for variant, inner := range tagged {
			kind, ok := kindFromVariant(variant)
			if !ok {
				return fmt.Errorf("unknown ValidationError variant %q", variant)
			}
			e.Kind = kind
			e.Field = inner["field"]
			e.Reason = inner["reason"]
			return nil
		}
	}
	// Fall back to the legacy flat shape: {"Kind":"...","Field":"...","Reason":"..."}
	var flat struct {
		Kind   ValidationErrorKind `json:"Kind"`
		Field  string              `json:"Field"`
		Reason string              `json:"Reason"`
	}
	if err := json.Unmarshal(data, &flat); err != nil {
		return fmt.Errorf("ValidationError: neither serde nor flat shape: %w", err)
	}
	if flat.Kind == "" {
		return fmt.Errorf("ValidationError: missing kind")
	}
	e.Kind = flat.Kind
	e.Field = flat.Field
	e.Reason = flat.Reason
	return nil
}

// variantName maps the lowercase Go Kind to the CamelCase Rust serde
// variant name. Kept in sync with `crates/validator/src/lib.rs::ValidationError`.
func variantName(k ValidationErrorKind) string {
	switch k {
	case ErrSchema:
		return "Schema"
	case ErrCompleteness:
		return "Completeness"
	case ErrConsistency:
		return "Consistency"
	case ErrPolicy:
		return "Policy"
	}
	return string(k) // fallback — preserves Kind for debugging
}

func kindFromVariant(v string) (ValidationErrorKind, bool) {
	switch v {
	case "Schema":
		return ErrSchema, true
	case "Completeness":
		return ErrCompleteness, true
	case "Consistency":
		return ErrConsistency, true
	case "Policy":
		return ErrPolicy, true
	}
	return "", false
}

// Validator is the interface every validator implements.
// Stateless — construction takes any deps (e.g. WorkerLookup)
// upfront, validate() is pure on its inputs.
type Validator interface {
	Name() string
	Validate(artifact Artifact) (Report, error)
}

// WorkerRecord is the lookup-side worker shape. Pointer fields
// for City/State/Role mirror Rust's Option<String> — None means
// "we don't know," which is operationally distinct from empty
// string (we know it's empty). Validators only enforce
// city/state/role matches when both expected (from contract)
// and actual (from lookup) are non-nil.
type WorkerRecord struct {
	CandidateID         string
	Name                string
	Status              string // "active" / "inactive" / etc.
	City                *string
	State               *string
	Role                *string
	BlacklistedClients  []string
}

// WorkerLookup is the gate validators go through to ask "does
// this candidate_id exist + what's their record?" Implementations
// can be in-memory (test fixture), DuckDB-backed (production
// queryd), or HTTP-backed (cross-daemon). FillValidator + EmailValidator
// take Arc<dyn WorkerLookup> on Rust side; in Go, an interface value.
type WorkerLookup interface {
	Find(candidateID string) (*WorkerRecord, bool)
}

// elapsed converts a start time into the milliseconds-elapsed
// shape matched in Report.ElapsedMs (mirrors Rust's
// .elapsed().as_millis() as u64).
func elapsed(start time.Time) int64 {
	return time.Since(start).Milliseconds()
}
