#!/usr/bin/env python3
"""Deterministic eval harness for Jeannie and homelab RAG behavior."""

from __future__ import annotations

import argparse
import csv
import json
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path


REPO_ROOT = Path(__file__).resolve().parents[1]
CASES_FILE = REPO_ROOT / "infra" / "ai-evals" / "cases.tsv"
FIXTURES_DIR = REPO_ROOT / "infra" / "ai-evals" / "fixtures"


@dataclass
class Case:
    case_id: str
    category: str
    mode: str
    fixture: str
    required_terms: list[str]
    forbidden_terms: list[str]
    notes: str


def split_terms(value: str) -> list[str]:
    return [item.strip() for item in value.split(";") if item.strip()]


def load_cases() -> list[Case]:
    with CASES_FILE.open(encoding="utf-8", newline="") as handle:
        rows = csv.DictReader(handle, delimiter="\t")
        return [
            Case(
                case_id=row["id"],
                category=row["category"],
                mode=row["mode"],
                fixture=row["fixture"],
                required_terms=split_terms(row["required_terms"]),
                forbidden_terms=split_terms(row["forbidden_terms"]),
                notes=row["notes"],
            )
            for row in rows
        ]


def fixture_text(case: Case) -> str:
    path = FIXTURES_DIR / case.fixture
    if not path.is_file():
        raise SystemExit(f"Missing fixture for {case.case_id}: {path}")
    return path.read_text(encoding="utf-8")


def run_explain_output(case: Case) -> str:
    process = subprocess.run(
        [str(REPO_ROOT / "scripts" / "explain"), "output"],
        input=fixture_text(case),
        text=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        cwd=REPO_ROOT,
        check=False,
    )
    return process.stdout


def run_repo_scan(case: Case) -> str:
    haystack_parts: list[str] = [fixture_text(case), "\nRETRIEVED CONTEXT\n"]
    for path in [
        REPO_ROOT / "README.md",
        REPO_ROOT / "docs" / "jeannie.1.md",
        REPO_ROOT / "docs" / "runbooks" / "cluster-stop-start-failures.md",
        REPO_ROOT / "docs" / "runbooks" / "edge-failures.md",
        REPO_ROOT / "docs" / "runbooks" / "gitea-failures.md",
    ]:
        if path.is_file():
            rel = path.relative_to(REPO_ROOT)
            lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
            matches: list[str] = []
            for index, line in enumerate(lines):
                lowered = line.lower()
                if any(term.lower() in lowered for term in case.required_terms):
                    start = max(index - 1, 0)
                    end = min(index + 2, len(lines))
                    matches.extend(lines[start:end])
            if matches:
                haystack_parts.append(f"\nSOURCE {rel}\n")
                haystack_parts.extend(matches[:24])
    return "\n".join(haystack_parts)


def run_case(case: Case) -> dict[str, object]:
    if case.mode == "explain-output":
        output = run_explain_output(case)
    elif case.mode == "repo-scan":
        output = run_repo_scan(case)
    else:
        return {
            "id": case.case_id,
            "status": "fail",
            "reason": f"unknown mode {case.mode}",
            "missing": [],
            "forbidden_hits": [],
            "output": "",
        }

    lowered = output.lower()
    missing = [term for term in case.required_terms if term.lower() not in lowered]
    forbidden_hits = [term for term in case.forbidden_terms if term.lower() in lowered]
    status = "pass" if not missing and not forbidden_hits else "fail"
    reason = "ok" if status == "pass" else "term checks failed"
    return {
        "id": case.case_id,
        "category": case.category,
        "status": status,
        "reason": reason,
        "missing": missing,
        "forbidden_hits": forbidden_hits,
        "notes": case.notes,
        "output": output,
    }


def print_list(cases: list[Case]) -> int:
    for case in cases:
        print(f"{case.case_id}\t{case.category}\t{case.mode}\t{case.notes}")
    return 0


def print_show(cases: list[Case], case_id: str) -> int:
    for case in cases:
        if case.case_id == case_id:
            print(f"id: {case.case_id}")
            print(f"category: {case.category}")
            print(f"mode: {case.mode}")
            print(f"fixture: {case.fixture}")
            print(f"required: {', '.join(case.required_terms)}")
            print(f"forbidden: {', '.join(case.forbidden_terms)}")
            print(f"notes: {case.notes}")
            return 0
    print(f"Unknown eval case: {case_id}", file=sys.stderr)
    return 2


def print_run(cases: list[Case], case_id: str | None, as_json: bool, details: bool) -> int:
    selected = [case for case in cases if case_id in (None, case.case_id)]
    if not selected:
        print(f"Unknown eval case: {case_id}", file=sys.stderr)
        return 2

    results = [run_case(case) for case in selected]
    if as_json:
        print(json.dumps(results, indent=2, sort_keys=True))
    else:
        failures = sum(1 for result in results if result["status"] != "pass")
        print("Homelab AI Evals")
        print("================")
        print(f"cases={len(results)} failures={failures}")
        print()
        for result in results:
            print(f"{result['status']:5} {result['id']} - {result['reason']}")
            if result["missing"]:
                print(f"      missing: {', '.join(result['missing'])}")
            if result["forbidden_hits"]:
                print(f"      forbidden: {', '.join(result['forbidden_hits'])}")
            if details:
                print("      output:")
                for line in str(result["output"]).splitlines()[:40]:
                    print(f"        {line}")
    return 1 if any(result["status"] != "pass" for result in results) else 0


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    subparsers = parser.add_subparsers(dest="command", required=True)
    subparsers.add_parser("list")
    show_parser = subparsers.add_parser("show")
    show_parser.add_argument("case_id")
    run_parser = subparsers.add_parser("run")
    run_parser.add_argument("--case", dest="case_id")
    run_parser.add_argument("--json", action="store_true")
    run_parser.add_argument("--details", action="store_true")
    args = parser.parse_args()

    cases = load_cases()
    if args.command == "list":
        return print_list(cases)
    if args.command == "show":
        return print_show(cases, args.case_id)
    if args.command == "run":
        return print_run(cases, args.case_id, args.json, args.details)
    return 2


if __name__ == "__main__":
    raise SystemExit(main())
