#!/usr/bin/env python3
"""Durable evidence-gated bake-in for locally imported CARBON intent tests."""

from __future__ import annotations

import argparse
import datetime as dt
import html
import json
import os
import re
import tempfile
from pathlib import Path
from typing import Any

from carbon_quality_confidence import assess_quality_confidence
from carbon_run import REPORT_FAVICON, REPORT_THEME_CSS, carbon_lockup, report_heading, report_icon
from carbon_schemas import INTENT_LIBRARY_SCHEMA, INTENT_TEST_SCHEMA, ensure_intent_library


SCHEMA = "carbon.import-bake/v1"
LIBRARY_SCHEMA = INTENT_LIBRARY_SCHEMA
TEST_SCHEMA = INTENT_TEST_SCHEMA
STATUSES = {"prepared", "running", "completed"}
ITEM_STATUSES = {"queued", "running", "needs-repair", "passed", "failed", "blocked", "skipped"}
RESULT_STATUSES = {"passed", "failed", "blocked", "skipped"}
CLASSIFICATIONS = {"none", "test-definition", "infrastructure", "product-defect", "unknown"}
REPAIR_FIELDS = {"intentPrompt", "scope", "setup", "inputs", "oracles", "evidenceRequirements", "adapter"}
SECRET_PATTERNS = [
    re.compile(r"\bsk-[A-Za-z0-9_-]{16,}\b"),
    re.compile(r"\bghp_[A-Za-z0-9]{20,}\b"),
    re.compile(r"\bAKIA[0-9A-Z]{16}\b"),
    re.compile(r"\bBearer\s+[A-Za-z0-9._~+/-]{16,}\b", re.I),
    re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"),
]


def now() -> str:
    return dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")


def atomic_json(path: Path, value: Any) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
        json.dump(value, handle, indent=2, ensure_ascii=False)
        handle.write("\n")
        temporary = Path(handle.name)
    os.replace(temporary, path)


def library_path(root: Path) -> Path:
    return root / ".carbon" / "tests" / "intent-tests.json"


def load_library(root: Path) -> dict[str, Any]:
    path = library_path(root)
    value = ensure_intent_library(path, root, create=False)
    if not isinstance(value, dict) or value.get("schema") != LIBRARY_SCHEMA or not isinstance(value.get("tests"), list):
        raise ValueError(f"Unsupported intent-test library: {path}")
    return value


def save_library(root: Path, library: dict[str, Any]) -> None:
    library["updatedAt"] = now()
    atomic_json(library_path(root), library)


def bake_dir(root: Path, run_id: str) -> Path:
    if not re.fullmatch(r"BAKE-[A-Z0-9-]+", run_id):
        raise ValueError("Invalid bake run ID.")
    return root / ".carbon" / "tests" / "bake-runs" / run_id


def bake_path(root: Path, run_id: str) -> Path:
    return bake_dir(root, run_id) / "bake.json"


def load_bake(root: Path, run_id: str) -> dict[str, Any]:
    path = bake_path(root, run_id)
    try:
        value = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as error:
        raise ValueError(f"Cannot read CARBON bake run {path}: {error}") from error
    if not isinstance(value, dict) or value.get("schema") != SCHEMA:
        raise ValueError(f"Unsupported bake run: {path}")
    return value


def find_test(library: dict[str, Any], test_id: str) -> tuple[int, dict[str, Any]]:
    for index, test in enumerate(library["tests"]):
        if test.get("id") == test_id:
            return index, test
    raise ValueError(f"Unknown intent test: {test_id}")


def find_item(run: dict[str, Any], test_id: str) -> dict[str, Any]:
    for item in run["tests"]:
        if item.get("testId") == test_id:
            return item
    raise ValueError(f"Test {test_id} is not part of bake run {run['id']}.")


def string_list(value: Any, limit: int = 2000) -> list[str]:
    if value is None:
        return []
    rows = value if isinstance(value, list) else [value]
    return [str(row).strip()[:limit] for row in rows if str(row).strip()]


def reject_secrets(value: Any) -> None:
    text = json.dumps(value, ensure_ascii=False)
    if any(pattern.search(text) for pattern in SECRET_PATTERNS):
        raise ValueError("Bake evidence or repair appears to contain a secret; store only a redacted handle or fixture name.")


def readiness(test: dict[str, Any]) -> dict[str, Any]:
    missing = []
    if not test.get("scope"): missing.append("target scope or start URL")
    if not test.get("setup"): missing.append("starting state, role, or fixture")
    if not test.get("oracles"): missing.append("objective pass/fail oracle")
    if not test.get("evidenceRequirements"): missing.append("evidence requirement")
    score = max(0, 100 - 18 * len(missing))
    return {"score": score, "band": "high" if score >= 82 else ("medium" if score >= 55 else "low"), "missing": missing}


def summary(run: dict[str, Any]) -> dict[str, int]:
    counts = {status: 0 for status in ITEM_STATUSES}
    for item in run["tests"]:
        counts[item["status"]] += 1
    counts["total"] = len(run["tests"])
    counts["repairs"] = sum(len(item.get("repairs", [])) for item in run["tests"])
    counts["attempts"] = sum(len(item.get("attempts", [])) for item in run["tests"])
    return counts


def render_report(root: Path, run: dict[str, Any]) -> Path:
    counts = summary(run)
    quality = assess_quality_confidence(run)
    confidence = quality["confidenceInQuality"]
    remaining = quality["remainingRisk"]
    coverage = quality["coverage"]

    def report_list(values: list[str]) -> str:
        return "<ul>" + "".join(f"<li>{html.escape(value)}</li>" for value in values) + "</ul>"

    cards = "".join(
        f'<div class="metric"><div class="metric-top"><span>{html.escape(label)}</span>{report_icon({"total":"import","passed":"confidence","failed":"findings","blocked":"risk","repairs":"change"}.get(key, "tests"))}</div><strong>{counts.get(key, 0)}</strong></div>'
        for key, label in (("total", "Imported"), ("passed", "Baked in"), ("failed", "Product/test failures"), ("blocked", "Blocked"), ("repairs", "Repairs"))
    )
    rows = []
    for item in run["tests"]:
        latest = (item.get("attempts") or [{}])[-1]
        evidence = " · ".join(html.escape(value) for value in latest.get("evidence", [])[:5]) or "No execution evidence recorded"
        rows.append(
            f'<article class="test {html.escape(item["status"])}"><div><p class="section-kicker">{report_icon("tests")}{html.escape(item["testId"])}</p>'
            f'<h2>{html.escape(item["title"])}</h2><p>{html.escape(latest.get("actual") or item.get("blockedReason") or "Awaiting execution")}</p>'
            f'<p class="evidence">{evidence}</p></div><div class="result"><strong>{html.escape(item["status"].upper())}</strong>'
            f'<span>{len(item.get("attempts", []))} attempts · {len(item.get("repairs", []))} repairs</span></div></article>'
        )
    document = f"""<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><link rel="icon" href="{REPORT_FAVICON}"><title>CARBON import bake · {html.escape(run['id'])}</title><style>
    {REPORT_THEME_CSS}
    main{{max-width:1080px}}.bake-hero{{margin-bottom:22px}}.bake-hero h1{{max-width:840px}}.lede{{max-width:760px;color:var(--muted);font-size:18px}}.confidence{{margin:0 0 30px}}.bake-decision{{display:grid;grid-template-columns:minmax(0,1fr) minmax(260px,.55fr);gap:12px;margin-top:18px}}.confidence-main{{padding:22px;border:1px solid var(--accent-line);background:linear-gradient(145deg,var(--accent-soft),transparent),var(--panel-soft)}}.risk-panel{{padding:22px;border:1px solid var(--risk-line);background:linear-gradient(145deg,var(--risk-soft),transparent),var(--panel-soft)}}.confidence-grid{{display:grid;grid-template-columns:repeat(3,1fr);border-top:1px solid var(--line);border-bottom:1px solid var(--line);margin-top:12px}}.confidence-card{{padding:18px;border-right:1px solid var(--line)}}.confidence-card:last-child{{border-right:0}}.confidence-card h3{{margin:0 0 8px;font-size:12px;color:var(--text);letter-spacing:.04em}}.confidence-card strong{{font-size:26px;text-transform:capitalize}}.confidence-card ul{{margin:10px 0 0;padding-left:18px}}.confidence-card li{{margin:.35em 0}}.test{{display:grid;grid-template-columns:1fr auto;gap:24px;align-items:center}}.test h2{{margin:.15em 0;font-size:20px}}.test p{{margin:.45em 0}}.evidence,.result span,.note{{color:var(--muted)}}.result{{text-align:right}}.result strong,.result span{{display:block}}.passed{{border-color:#235f56}}.passed .result strong{{color:var(--good)}}.failed{{border-color:#6e2e3a}}.failed .result strong{{color:var(--bad)}}.blocked,.needs-repair{{border-color:var(--risk-line)}}.blocked .result strong,.needs-repair .result strong{{color:var(--risk)}}@media(max-width:820px){{.bake-decision,.confidence-grid{{grid-template-columns:1fr}}.confidence-card{{border-right:0;border-bottom:1px solid var(--line)}}.confidence-card:last-child{{border-bottom:0}}}}@media(max-width:640px){{.test{{grid-template-columns:1fr}}.result{{text-align:left}}}}
    </style></head><body><main><header class="masthead">{carbon_lockup("Import bake")}<div class="masthead-meta"><span class="status-pill status-{html.escape(run['status'])}">{html.escape(run['status'])}</span></div></header><section class="hero bake-hero"><p class="eyebrow">Local import bake · {html.escape(run['id'])}</p><h1>{html.escape(run['status'].title())}: legacy tests converted and exercised.</h1><p class="lede">Passing tests have observed evidence and may join active intent execution. Failed and blocked cases remain drafts. Original imports were never rewritten.</p></section><section class="metrics">{cards}</section><section class="panel confidence"><p class="eyebrow">Decision evidence</p>{report_heading("Confidence in quality", "confidence")}<div class="bake-decision"><div class="confidence-main"><p class="section-kicker">{report_icon("confidence")} Decision signal</p><strong style="font-size:42px;text-transform:capitalize">{html.escape(confidence['band'])}</strong><p>{html.escape(confidence['basis'])}</p></div><div class="risk-panel"><p class="section-kicker risk-text">{report_icon("risk")} Remaining risk</p><strong class="risk-text" style="font-size:42px;text-transform:capitalize">{html.escape(remaining['level'])}</strong>{report_list(remaining['reasons'])}</div></div><div class="confidence-grid"><div class="confidence-card">{report_heading("Coverage achieved", "coverage", level=3)}<strong>{coverage['conclusivePercent']}%</strong><p>{coverage['conclusive']} of {coverage['total']} tests conclusive · {coverage['evidenceBacked']} evidence-backed</p></div><div class="confidence-card">{report_heading("Coverage missing", "unknown", level=3)}{report_list(quality['coverageMissing']) if quality['coverageMissing'] else '<p>None recorded within this scope.</p>'}</div><div class="confidence-card">{report_heading("What changes confidence", "change", level=3)}{report_list(quality['whatWouldChangeConfidence'])}</div></div><p class="note">{html.escape(quality['interpretation'])}</p></section>{''.join(rows)}<footer class="footer"><span>CARBON is a <a href="https://testers.ai">testers.ai</a> product.</span><span>Imported tests remain evidence-gated.</span></footer></main></body></html>"""
    output = bake_dir(root, run["id"]) / "report.html"
    output.write_text(document, encoding="utf-8")
    atomic_json(bake_dir(root, run["id"]) / "summary.json", {"schema": SCHEMA, "id": run["id"], "status": run["status"], "summary": counts, "qualityConfidence": quality, "report": str(output)})
    return output


def save_bake(root: Path, run: dict[str, Any]) -> dict[str, Any]:
    run["updatedAt"] = now()
    run["summary"] = summary(run)
    run["qualityConfidence"] = assess_quality_confidence(run)
    atomic_json(bake_path(root, run["id"]), run)
    run["report"] = str(render_report(root, run))
    return run


def prepare_bake_run(root: Path, test_ids: list[str], max_repairs: int = 2, target: str = "") -> dict[str, Any]:
    library = load_library(root)
    selected = []
    for test_id in dict.fromkeys(test_ids):
        _, test = find_test(library, test_id)
        safety = test.get("safety") or {}
        gated = [key for key in ("destructive", "paid", "public", "highVolume") if safety.get(key)]
        selected.append({
            "testId": test["id"], "title": test["title"], "status": "blocked" if gated else "queued",
            "source": test.get("source"), "readiness": test.get("readiness"), "attempts": [], "repairs": [],
            **({"blockedReason": f"Explicit approval required for: {', '.join(gated)}", "requiresApproval": gated} if gated else {}),
        })
    token = dt.datetime.now(dt.timezone.utc).strftime("%Y%m%d-%H%M%S-%f")
    run = {
        "schema": SCHEMA, "id": f"BAKE-{token}", "projectRoot": str(root), "status": "prepared",
        "target": target.strip()[:2000], "maxRepairs": max(0, min(5, int(max_repairs))),
        "tests": selected, "createdAt": now(), "updatedAt": now(),
        "authority": "CARBON may repair only imported intent-test definitions during this bake. Product code, external systems, paid/public/destructive actions, and trusted-regression promotion remain approval-gated.",
    }
    return save_bake(root, run)


def command_prepare(args: argparse.Namespace) -> dict[str, Any]:
    root = Path(args.root).expanduser().resolve()
    return prepare_bake_run(root, args.test_id, args.max_repairs, args.target)


def command_next(args: argparse.Namespace) -> dict[str, Any]:
    root = Path(args.root).expanduser().resolve()
    run = load_bake(root, args.run_id)
    if run["status"] == "completed": return {"done": True, **save_bake(root, run)}
    item = next((row for row in run["tests"] if row["status"] == "running"), None)
    resumed = item is not None
    if item is None: item = next((row for row in run["tests"] if row["status"] == "queued"), None)
    if not item:
        waiting = [row["testId"] for row in run["tests"] if row["status"] in {"running", "needs-repair"}]
        return {"done": not waiting, "waiting": waiting, **save_bake(root, run)}
    library = load_library(root)
    _, test = find_test(library, item["testId"])
    item["status"] = "running"
    run["status"] = "running"
    save_bake(root, run)
    return {
        "done": False, "runId": run["id"], "test": test, "attempt": len(item["attempts"]) + 1, "resumed": resumed,
        "maxRepairs": run["maxRepairs"], "target": run.get("target") or test.get("scope"),
        "requiredEvidence": test.get("evidenceRequirements") or ["actual result", "environment/build", "reproducible evidence handle"],
        "instruction": "Execute through the safest matching local adapter. Record observed results only; do not infer a pass from the imported steps.",
    }


def command_result(args: argparse.Namespace) -> dict[str, Any]:
    root = Path(args.root).expanduser().resolve()
    run = load_bake(root, args.run_id)
    item = find_item(run, args.test_id)
    if item["status"] != "running": raise ValueError(f"Test {args.test_id} is not currently running.")
    if args.status not in RESULT_STATUSES: raise ValueError(f"status must be one of {sorted(RESULT_STATUSES)}")
    classification = args.classification or ("none" if args.status == "passed" else "unknown")
    if classification not in CLASSIFICATIONS: raise ValueError(f"classification must be one of {sorted(CLASSIFICATIONS)}")
    if args.status == "passed" and classification != "none": raise ValueError("A passed result must use classification none.")
    if args.status in {"failed", "blocked"} and classification == "none": raise ValueError("A non-pass requires test-definition, infrastructure, product-defect, or unknown classification.")
    evidence = string_list(args.evidence)
    actual = str(args.actual or "").strip()[:8000]
    reject_secrets({"actual": actual, "evidence": evidence, "failure": args.failure})
    if args.status == "passed" and (not actual or not evidence):
        raise ValueError("A passed bake result requires an observed actual result and at least one evidence handle.")
    attempt = {
        "number": len(item["attempts"]) + 1, "status": args.status, "classification": classification,
        "actual": actual, "failure": str(args.failure or "").strip()[:8000], "evidence": evidence, "observedAt": now(),
    }
    item["attempts"].append(attempt)
    library = load_library(root)
    index, test = find_test(library, args.test_id)
    if args.status == "passed":
        item["status"] = "passed"
        test["status"] = "active"
        test["lifecycle"] = "observed"
        test["revision"] = int(test.get("revision", 1)) + 1
        test["updatedAt"] = now()
        test["bakeIn"] = {"runId": run["id"], "status": "passed", "attempts": len(item["attempts"]), "evidence": evidence, "observedAt": now()}
        library["tests"][index] = test
        save_library(root, library)
    elif args.status == "failed" and classification == "test-definition" and len(item["repairs"]) < run["maxRepairs"]:
        item["status"] = "needs-repair"
    elif args.status == "blocked" or classification == "infrastructure":
        item["status"] = "blocked"
    elif args.status == "skipped":
        item["status"] = "skipped"
    else:
        item["status"] = "failed"
    saved = save_bake(root, run)
    return {"runId": run["id"], "testId": item["testId"], "status": item["status"], "repairEligible": item["status"] == "needs-repair", "summary": saved["summary"], "report": saved["report"]}


def command_repair(args: argparse.Namespace) -> dict[str, Any]:
    root = Path(args.root).expanduser().resolve()
    run = load_bake(root, args.run_id)
    item = find_item(run, args.test_id)
    if item["status"] != "needs-repair": raise ValueError("Only a test-definition failure marked needs-repair can be repaired automatically.")
    if len(item["repairs"]) >= run["maxRepairs"]: raise ValueError("The bounded repair limit has been reached.")
    patch = json.loads(args.patch_json)
    if not isinstance(patch, dict) or not patch: raise ValueError("repair patch must be a non-empty JSON object")
    rejected = sorted(set(patch) - REPAIR_FIELDS)
    if rejected: raise ValueError(f"Bake repair may change only {sorted(REPAIR_FIELDS)}; rejected: {rejected}")
    reject_secrets(patch)
    library = load_library(root)
    index, test = find_test(library, args.test_id)
    before = {key: test.get(key) for key in patch}
    for key, value in patch.items():
        if key in {"setup", "inputs", "oracles", "evidenceRequirements"}: test[key] = string_list(value)
        else: test[key] = str(value).strip()[:12000 if key == "intentPrompt" else 2000]
    test["readiness"] = readiness(test)
    test["revision"] = int(test.get("revision", 1)) + 1
    test["updatedAt"] = now()
    repair = {"number": len(item["repairs"]) + 1, "reason": str(args.reason or "test-definition failure").strip()[:2000], "before": before, "patch": patch, "repairedAt": now()}
    item["repairs"].append(repair)
    item["status"] = "queued"
    test.setdefault("bakeIn", {})["repairs"] = item["repairs"]
    library["tests"][index] = test
    save_library(root, library)
    saved = save_bake(root, run)
    return {"runId": run["id"], "test": test, "repair": repair, "next": "Rerun this repaired intent through the same local adapter and record fresh evidence.", "report": saved["report"]}


def command_complete(args: argparse.Namespace) -> dict[str, Any]:
    root = Path(args.root).expanduser().resolve()
    run = load_bake(root, args.run_id)
    unfinished = [row["testId"] for row in run["tests"] if row["status"] in {"queued", "running", "needs-repair"}]
    if unfinished: raise ValueError(f"Bake run still has unfinished tests: {unfinished[:20]}")
    run["status"] = "completed"
    run["completedAt"] = now()
    saved = save_bake(root, run)
    return {"runId": run["id"], "status": run["status"], "summary": saved["summary"], "qualityConfidence": saved["qualityConfidence"], "report": saved["report"], "library": str(library_path(root)), "next": "Passing tests are active with observed lifecycle; failed and blocked tests remain drafts for review."}


def command_status(args: argparse.Namespace) -> dict[str, Any]:
    root = Path(args.root).expanduser().resolve()
    run = load_bake(root, args.run_id)
    return save_bake(root, run)


def parser() -> argparse.ArgumentParser:
    root = argparse.ArgumentParser(description=__doc__)
    sub = root.add_subparsers(dest="command", required=True)
    prepare = sub.add_parser("prepare"); prepare.add_argument("--root", required=True); prepare.add_argument("--test-id", action="append", required=True); prepare.add_argument("--target", default=""); prepare.add_argument("--max-repairs", type=int, default=2); prepare.set_defaults(func=command_prepare)
    next_test = sub.add_parser("next"); next_test.add_argument("--root", required=True); next_test.add_argument("--run-id", required=True); next_test.set_defaults(func=command_next)
    result = sub.add_parser("result"); result.add_argument("--root", required=True); result.add_argument("--run-id", required=True); result.add_argument("--test-id", required=True); result.add_argument("--status", choices=sorted(RESULT_STATUSES), required=True); result.add_argument("--classification", choices=sorted(CLASSIFICATIONS)); result.add_argument("--actual"); result.add_argument("--failure"); result.add_argument("--evidence", action="append", default=[]); result.set_defaults(func=command_result)
    repair = sub.add_parser("repair"); repair.add_argument("--root", required=True); repair.add_argument("--run-id", required=True); repair.add_argument("--test-id", required=True); repair.add_argument("--patch-json", required=True); repair.add_argument("--reason"); repair.set_defaults(func=command_repair)
    complete = sub.add_parser("complete"); complete.add_argument("--root", required=True); complete.add_argument("--run-id", required=True); complete.set_defaults(func=command_complete)
    status = sub.add_parser("status"); status.add_argument("--root", required=True); status.add_argument("--run-id", required=True); status.set_defaults(func=command_status)
    return root


def main() -> int:
    try:
        args = parser().parse_args()
        print(json.dumps(args.func(args), indent=2, ensure_ascii=False))
        return 0
    except (OSError, ValueError, json.JSONDecodeError) as error:
        print(f"carbon_bake: {error}", file=os.sys.stderr)
        return 2


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