package validator

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

// PlaybookValidator is the Go port of Rust's
// `crates/validator/src/staffing/playbook.rs`. Sealed playbook
// validation per Phase 25:
//
//   - Operation must be a non-empty string starting with `fill:`
//   - endorsed_names must be a non-empty array, ≤ target_count × 2
//   - fingerprint must be non-empty (validity-window requirement)
//
// PlaybookValidator is stateless — no WorkerLookup dependency, unlike
// FillValidator and EmailValidator. The whole validation runs on the
// artifact body alone.
type PlaybookValidator struct{}

// NewPlaybookValidator returns a zero-deps validator. Constructor for
// symmetry with the other two; not strictly required.
func NewPlaybookValidator() *PlaybookValidator { return &PlaybookValidator{} }

// Name satisfies Validator. Matches Rust's "staffing.playbook" so
// audit-log scrapes work across runtimes.
func (PlaybookValidator) Name() string { return "staffing.playbook" }

// Validate runs the four PRD checks. Errors abort the run; warnings
// (none today) would attach to a passing Report.
func (v PlaybookValidator) Validate(a Artifact) (Report, error) {
	started := time.Now()
	if a.Playbook == nil {
		return Report{}, &ValidationError{
			Kind:   ErrSchema,
			Field:  "artifact",
			Reason: fmt.Sprintf("PlaybookValidator expects Playbook, got %s", a.Kind()),
		}
	}
	body := a.Playbook

	op, ok := stringField(body, "operation")
	if !ok {
		return Report{}, &ValidationError{
			Kind:   ErrSchema,
			Field:  "operation",
			Reason: "missing or not a string",
		}
	}
	if !strings.HasPrefix(op, "fill:") {
		return Report{}, &ValidationError{
			Kind:   ErrSchema,
			Field:  "operation",
			Reason: fmt.Sprintf("expected `fill: ...` prefix, got %q", op),
		}
	}

	endorsed, ok := body["endorsed_names"].([]any)
	if !ok {
		return Report{}, &ValidationError{
			Kind:   ErrSchema,
			Field:  "endorsed_names",
			Reason: "missing or not an array",
		}
	}
	if len(endorsed) == 0 {
		return Report{}, &ValidationError{
			Kind:   ErrCompleteness,
			Reason: "endorsed_names must be non-empty",
		}
	}

	if target, ok := uintField(body, "target_count"); ok {
		max := target * 2
		if uint64(len(endorsed)) > max {
			return Report{}, &ValidationError{
				Kind:   ErrCompleteness,
				Reason: fmt.Sprintf("endorsed_names (%d) exceeds target_count × 2 (%d)", len(endorsed), max),
			}
		}
	}

	if fp, _ := stringField(body, "fingerprint"); fp == "" {
		return Report{}, &ValidationError{
			Kind:   ErrSchema,
			Field:  "fingerprint",
			Reason: "missing — required for Phase 25 validity window",
		}
	}

	return Report{Findings: []Finding{}, ElapsedMs: elapsed(started)}, nil
}

// stringField returns (val, true) if body[key] is a string, else
// ("", false). Matches Rust's serde_json::Value::as_str() shape.
func stringField(body map[string]any, key string) (string, bool) {
	v, ok := body[key]
	if !ok {
		return "", false
	}
	s, ok := v.(string)
	return s, ok
}

// uintField returns (val, true) if body[key] is a non-negative whole
// number; matches Rust as_u64. JSON numbers come in as float64, which
// is why we do the conversion explicitly.
func uintField(body map[string]any, key string) (uint64, bool) {
	v, ok := body[key]
	if !ok || v == nil {
		return 0, false
	}
	switch t := v.(type) {
	case float64:
		if t < 0 {
			return 0, false
		}
		return uint64(t), true
	case int:
		if t < 0 {
			return 0, false
		}
		return uint64(t), true
	case int64:
		if t < 0 {
			return 0, false
		}
		return uint64(t), true
	}
	return 0, false
}
