package ucca

import (
	"bufio"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"
)

// EvidenceRequirement is the last edge of the compliance graph: it says WHAT concrete
// evidence proves a framework control is met, and how fresh that evidence must be. This is
// what lets the Advisor eventually state "the CRA requirement is fulfilled" — not because a
// document exists, but because the required, current evidence is present. Authored/curated,
// not retriever-generated.
type EvidenceRequirement struct {
	Framework            string `json:"framework"`             // e.g. "OWASP ASVS"
	Control              string `json:"control"`               // e.g. "V6.3.1"
	EvidenceType         string `json:"evidence_type"`         // sbom|test_report|config_export|repo_scan|policy|ticket|audit_log|pentest
	EvidenceSource       string `json:"evidence_source"`       // github|ci|scanner|manual_upload
	FreshnessRequirement string `json:"freshness_requirement"` // per_release|quarterly|annually|continuous
	Required             bool   `json:"required"`
	Rationale            string `json:"rationale"`
	Version              string `json:"version"`
}

// Allowed enum values — the rule layer that keeps the evidence catalog clean.
var (
	evidenceTypeValues   = map[string]bool{"sbom": true, "test_report": true, "config_export": true, "repo_scan": true, "policy": true, "ticket": true, "audit_log": true, "pentest": true}
	evidenceSourceValues = map[string]bool{"github": true, "ci": true, "scanner": true, "manual_upload": true}
	freshnessValues      = map[string]bool{"per_release": true, "quarterly": true, "annually": true, "continuous": true}
)

// Validate checks required fields + enum membership. Fail-closed at load.
func (e EvidenceRequirement) Validate() error {
	switch {
	case e.Framework == "":
		return fmt.Errorf("evidence requirement: framework required")
	case e.Control == "":
		return fmt.Errorf("evidence requirement: control required")
	case !evidenceTypeValues[e.EvidenceType]:
		return fmt.Errorf("evidence requirement: invalid evidence_type %q", e.EvidenceType)
	case !evidenceSourceValues[e.EvidenceSource]:
		return fmt.Errorf("evidence requirement: invalid evidence_source %q", e.EvidenceSource)
	case !freshnessValues[e.FreshnessRequirement]:
		return fmt.Errorf("evidence requirement: invalid freshness_requirement %q", e.FreshnessRequirement)
	}
	return nil
}

// EvidenceRequirementSet is the loaded, indexed evidence catalog.
type EvidenceRequirementSet struct {
	All       []EvidenceRequirement
	byControl map[string][]EvidenceRequirement
}

// For returns all evidence requirements declared for a framework control.
func (s *EvidenceRequirementSet) For(framework, control string) []EvidenceRequirement {
	return s.byControl[controlKey(framework, control)]
}

// RequiredFor returns only the required evidence for a control — the minimum that must be
// present before the control may be treated as met.
func (s *EvidenceRequirementSet) RequiredFor(framework, control string) []EvidenceRequirement {
	out := make([]EvidenceRequirement, 0)
	for _, e := range s.byControl[controlKey(framework, control)] {
		if e.Required {
			out = append(out, e)
		}
	}
	return out
}

// LoadEvidenceRequirements reads every *.jsonl file under dir (one requirement per line;
// blank and //-prefixed lines ignored), validates each, and builds the per-control index.
// An invalid row aborts the load — fail-closed.
func LoadEvidenceRequirements(dir string) (*EvidenceRequirementSet, error) {
	files, err := filepath.Glob(filepath.Join(dir, "*.jsonl"))
	if err != nil {
		return nil, err
	}
	set := &EvidenceRequirementSet{byControl: map[string][]EvidenceRequirement{}}
	for _, f := range files {
		fh, err := os.Open(f)
		if err != nil {
			return nil, err
		}
		sc := bufio.NewScanner(fh)
		sc.Buffer(make([]byte, 0, 64*1024), 1024*1024)
		line := 0
		for sc.Scan() {
			line++
			raw := strings.TrimSpace(sc.Text())
			if raw == "" || strings.HasPrefix(raw, "//") {
				continue
			}
			var e EvidenceRequirement
			if err := json.Unmarshal([]byte(raw), &e); err != nil {
				fh.Close()
				return nil, fmt.Errorf("%s:%d: %w", f, line, err)
			}
			if err := e.Validate(); err != nil {
				fh.Close()
				return nil, fmt.Errorf("%s:%d: %w", f, line, err)
			}
			set.All = append(set.All, e)
			k := controlKey(e.Framework, e.Control)
			set.byControl[k] = append(set.byControl[k], e)
		}
		fh.Close()
		if err := sc.Err(); err != nil {
			return nil, err
		}
	}
	return set, nil
}
