#!/usr/bin/env python3
from __future__ import annotations

import argparse
import hashlib
import importlib.util
import json
import os
import shutil
import sqlite3
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

REPO = Path("/home/shire3d/ARMOR")

if str(REPO) not in sys.path:
    sys.path.insert(0, str(REPO))

ACADEMY = REPO / "data/academy/design_mastery"
PRACTICE = ACADEMY / "runtime/practice_state.json"
AUTOPILOT = ACADEMY / "runtime/autopilot_state.json"
SPRINT = ACADEMY / "runtime/commercial_design_sprint.json"

GENERIC_QUALIFIER = (
    REPO / "tools/design_academy_generic_qualify.py"
)

from ai.brain_server import (
    canonicalise_academy_answer,
)
from services.design_academy_practice_service import (
    _atomic_json_write,
    _slug_path,
    _utc_now,
    practice_service,
)


def load(path: Path) -> dict[str, Any]:
    return json.loads(path.read_text(encoding="utf-8"))


def atomic(path: Path, payload: Any) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)

    temporary = path.with_suffix(
        path.suffix + ".tmp"
    )

    temporary.write_text(
        json.dumps(
            payload,
            indent=2,
            sort_keys=True,
            ensure_ascii=False,
        ) + "\n",
        encoding="utf-8",
    )

    os.replace(temporary, path)


def sha256(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def text_sha256(value: str) -> str:
    return hashlib.sha256(
        value.encode("utf-8")
    ).hexdigest()


def resolved(value: object) -> Path | None:
    if not isinstance(value, str) or not value:
        return None

    try:
        return Path(value).resolve()
    except Exception:
        return None


def load_repair_module():
    specification = importlib.util.spec_from_file_location(
        "shire_generic_qualifier_repair",
        GENERIC_QUALIFIER,
    )

    if (
        specification is None
        or specification.loader is None
    ):
        raise RuntimeError(
            "Unable to load the generic qualification repair module."
        )

    module = importlib.util.module_from_spec(
        specification
    )

    specification.loader.exec_module(module)

    repair = getattr(
        module,
        "repair_round3_response",
        None,
    )

    if not callable(repair):
        raise RuntimeError(
            "Generic Round 3 repair function is unavailable."
        )

    return module, repair


def verify_hash(path: Path, expected: str, label: str) -> None:
    actual = sha256(path)

    if actual != expected:
        raise RuntimeError(
            f"{label} changed before recovery: "
            f"{actual} != {expected}"
        )


def self_test(skill_id: str) -> dict[str, Any]:
    module, repair = load_repair_module()

    repair_source = GENERIC_QUALIFIER.read_text(
        encoding="utf-8"
    )

    for marker in (
        "repair_round3_response",
        "transfer_project",
        "boundary_cases",
        "provenance_review",
        "safety_gate",
        "forge_execution_allowed false",
        "ACADEMY_ROUND_JSON_SCHEMAS[3]",
    ):
        if marker not in repair_source:
            raise RuntimeError(
                f"Repair contract marker is missing: {marker}"
            )

    bundle = practice_service._lesson_bundle(
        skill_id
    )

    fixture = {
        "round": 3,
        "transfer_project": {
            "title":
                "Bounding-envelope storage layout",
            "goal":
                "Transfer bounding-envelope reasoning to a "
                "harmless household storage layout with measurable checks.",
            "inputs": [
                "Cabinet width, depth and height in millimetres."
            ],
            "steps": [
                "Define the available cabinet envelope using explicit units.",
                "Place item envelopes inside the available volume.",
                "Check containment, clearance and invalid input rejection.",
            ],
            "outputs": [
                "A knowledge-only storage-envelope validation plan."
            ],
            "acceptance_criteria": [
                "Every item envelope remains inside the cabinet envelope.",
                "Invalid dimensions are rejected before any modelling step.",
            ],
        },
        "boundary_cases": {
            "minimum":
                "Minimum valid positive dimensions remain non-zero.",
            "nominal":
                "Nominal dimensions represent the intended household case.",
            "maximum":
                "Maximum dimensions remain within the cabinet envelope.",
            "invalid":
                "Negative or zero dimensions are rejected.",
        },
        "provenance_review": {
            "licence_status": "unknown",
            "decision": "quarantine",
            "commercial_use": "blocked",
            "reason":
                "Unknown source permission blocks reuse and "
                "commercial release until provenance is verified.",
        },
        "safety_gate": {
            "risk_class": "low",
            "action": "continue",
            "forge_execution_allowed": False,
            "reason":
                "Continue knowledge-only assessment while withholding "
                "tool execution and Forge release authority.",
        },
        "limitations": [
            "No physical cabinet or stored object has been measured.",
            "No manufacturing or commercial release is authorised.",
        ],
    }

    normalised = canonicalise_academy_answer(
        json.dumps(fixture),
        3,
    )

    canonical = json.loads(
        normalised["canonical"]
    )

    if canonical != fixture:
        raise RuntimeError(
            "Strict Round 3 canonicalisation changed a valid fixture."
        )

    assessment = practice_service.evaluate_response(
        skill_id,
        bundle,
        canonical,
        3,
    )

    if assessment.get("passed") is not True:
        raise RuntimeError(
            "Official evaluator rejected the strict recovery fixture: "
            + json.dumps(assessment, ensure_ascii=False)
        )

    return {
        "ok": True,
        "classification":
            "RECOVERY_RUNNER_SELF_TEST_PASS",
        "skill_id": skill_id,
        "score_percent":
            assessment.get("score_percent"),
        "forge_allowed": False,
        "repair_function":
            repair.__name__,
        "repair_module":
            str(GENERIC_QUALIFIER),
    }


def run_recovery(args: argparse.Namespace) -> tuple[dict[str, Any], int]:
    skill_id = args.skill_id
    source = Path(
        args.source_evidence
    ).resolve()

    receipt_path = Path(args.receipt)

    verify_hash(
        PRACTICE,
        args.expected_state_sha,
        "Practice state",
    )

    verify_hash(
        AUTOPILOT,
        args.expected_auto_sha,
        "Autopilot state",
    )

    verify_hash(
        SPRINT,
        args.expected_sprint_sha,
        "Commercial sprint",
    )

    domain, name = _slug_path(skill_id)

    paths = {
        "certification":
            ACADEMY
            / "skills"
            / domain
            / name
            / "certification.json",
        "manifest":
            ACADEMY
            / "skills"
            / domain
            / name
            / "manifest.json",
        "tests":
            ACADEMY
            / "skills"
            / domain
            / name
            / "tests.json",
    }

    verify_hash(
        paths["certification"],
        args.expected_cert_sha,
        "Certification record",
    )

    verify_hash(
        paths["manifest"],
        args.expected_manifest_sha,
        "Manifest",
    )

    verify_hash(
        paths["tests"],
        args.expected_tests_sha,
        "Test contract",
    )

    approved_source_root = Path(
        "/SHiREVault/SHiREAcademy/Practice"
    ).resolve()

    try:
        source.relative_to(
            approved_source_root
        )
    except ValueError as exc:
        raise RuntimeError(
            "Source evidence is outside the approved Practice root."
        ) from exc

    required_source_files = (
        "prompt.txt",
        "raw_model_answer.txt",
        "brain_api_response.json",
        "result.json",
        "lesson_snapshot.json",
    )

    for filename in required_source_files:
        path = source / filename

        if not path.is_file():
            raise RuntimeError(
                f"Preserved source file is missing: {path}"
            )

    brain = load(
        source / "brain_api_response.json"
    )

    source_result = load(
        source / "result.json"
    )

    rejected_raw = (
        source / "raw_model_answer.txt"
    ).read_text(
        encoding="utf-8",
        errors="strict",
    ).strip()

    if (
        rejected_raw
        != str(brain.get("raw_answer") or "").strip()
    ):
        raise RuntimeError(
            "Preserved raw answer differs from the Brain evidence."
        )

    if (
        text_sha256(rejected_raw)
        != brain.get("raw_answer_sha256")
    ):
        raise RuntimeError(
            "Preserved raw answer checksum is invalid."
        )

    if (
        source_result.get("infrastructure_failure")
        is not True
        or source_result.get("retryable")
        is not True
    ):
        raise RuntimeError(
            "Source evidence is not an eligible retryable "
            "infrastructure failure."
        )

    state = load(PRACTICE)
    autopilot = load(AUTOPILOT)
    sprint = load(SPRINT)

    record = (
        state.get("skills") or {}
    ).get(skill_id)

    qualification = (
        autopilot.get("skills") or {}
    ).get(skill_id)

    if not isinstance(record, dict):
        raise RuntimeError(
            "Candidate practice record is missing."
        )

    if not isinstance(qualification, dict):
        raise RuntimeError(
            "Candidate Autopilot record is missing."
        )

    if autopilot.get("current_skill") != skill_id:
        raise RuntimeError(
            "Autopilot is pointing at a different skill."
        )

    if sprint.get("current_skill") != skill_id:
        raise RuntimeError(
            "Commercial sprint is pointing at a different skill."
        )

    if int(record.get("attempts") or 0) != 2:
        raise RuntimeError(
            "Recovery requires exactly two counted attempts."
        )

    if int(record.get("successful_attempts") or 0) != 2:
        raise RuntimeError(
            "Recovery requires exactly two successful rounds."
        )

    if int(record.get("infrastructure_failures") or 0) != 1:
        raise RuntimeError(
            "Expected one recorded infrastructure failure."
        )

    if record.get("knowledge_certified") is not False:
        raise RuntimeError(
            "Skill is already knowledge-certified."
        )

    if record.get("forge_allowed") is not False:
        raise RuntimeError(
            "Practice Forge gate is open."
        )

    run_id = (
        datetime.now(timezone.utc)
        .strftime("%Y%m%dT%H%M%S%fZ")
        + "-COUNTED-R3-RECOVERY"
    )

    run_dir = (
        practice_service.evidence_root
        / domain
        / name
        / run_id
    )

    run_dir.mkdir(
        parents=True,
        exist_ok=False,
    )

    source_hashes = {
        filename: sha256(source / filename)
        for filename in required_source_files
    }

    _atomic_json_write(
        run_dir / "recovery_source.json",
        {
            "source":
                "preserved_round3_infrastructure_failure",
            "source_evidence": str(source),
            "source_file_sha256": source_hashes,
            "source_raw_answer_sha256":
                text_sha256(rejected_raw),
            "counted_attempt_previously_consumed":
                False,
            "cherry_picking_allowed":
                False,
            "replacement_generation_allowed":
                False,
            "repair_required":
                True,
            "recorded_at": _utc_now(),
        },
    )

    for filename in required_source_files:
        shutil.copy2(
            source / filename,
            run_dir / ("source-" + filename),
        )

    prompt = (
        source / "prompt.txt"
    ).read_text(
        encoding="utf-8",
        errors="strict",
    )

    module, repair_round3_response = (
        load_repair_module()
    )

    try:
        repaired_payload = repair_round3_response(
            prompt,
            rejected_raw,
            run_dir,
        )

        response = repaired_payload.get("answer")

        if not isinstance(response, dict):
            raise RuntimeError(
                "Repair did not return a structured answer."
            )

        strict = canonicalise_academy_answer(
            json.dumps(response),
            3,
        )

        canonical = json.loads(
            strict["canonical"]
        )

        if canonical != response:
            raise RuntimeError(
                "Repaired response changed during strict canonicalisation."
            )

        bundle = practice_service._lesson_bundle(
            skill_id
        )

        guard_error = (
            practice_service
            ._response_contract_guard_error(
                bundle,
                response,
                3,
            )
        )

        if guard_error:
            raise RuntimeError(guard_error)

        assessment = practice_service.evaluate_response(
            skill_id,
            bundle,
            response,
            3,
        )

        _atomic_json_write(
            run_dir / "practice_response.json",
            response,
        )

        _atomic_json_write(
            run_dir / "assessment.json",
            assessment,
        )

    except Exception as exc:
        failure = {
            "ok": False,
            "classification":
                "COUNTED_ROUND3_RECOVERY_NOT_CONSUMED",
            "skill_id": skill_id,
            "retryable": True,
            "infrastructure_failure": True,
            "counted_attempt_consumed": False,
            "attempts": 2,
            "successful_attempts": 2,
            "knowledge_certified": False,
            "forge_allowed": False,
            "source_evidence": str(source),
            "recovery_evidence": str(run_dir),
            "error": str(exc),
        }

        (
            run_dir / "ERROR.txt"
        ).write_text(
            str(exc) + "\n",
            encoding="utf-8",
        )

        _atomic_json_write(
            run_dir / "result.json",
            failure,
        )

        atomic(
            receipt_path,
            {
                **failure,
                "recorded_at": _utc_now(),
            },
        )

        return failure, 3

    verify_hash(
        PRACTICE,
        args.expected_state_sha,
        "Practice state before counted commit",
    )

    verify_hash(
        AUTOPILOT,
        args.expected_auto_sha,
        "Autopilot before counted commit",
    )

    verify_hash(
        SPRINT,
        args.expected_sprint_sha,
        "Commercial sprint before counted commit",
    )

    with practice_service._lock() as lock_handle:
        del lock_handle

        verify_hash(
            PRACTICE,
            args.expected_state_sha,
            "Locked practice state",
        )

        network = (
            practice_service
            ._verify_evidence_root()
        )

        state = load(PRACTICE)
        record = state["skills"][skill_id]

        if int(record.get("attempts") or 0) != 2:
            raise RuntimeError(
                "Counted attempt state changed before commit."
            )

        if int(record.get("successful_attempts") or 0) != 2:
            raise RuntimeError(
                "Successful-round state changed before commit."
            )

        if record.get("knowledge_certified") is not False:
            raise RuntimeError(
                "Knowledge certification changed before commit."
            )

        if record.get("forge_allowed") is not False:
            raise RuntimeError(
                "Forge gate changed before commit."
            )

        state["current_skill"] = skill_id
        state["network"] = network

        record["status"] = "practising"
        record["attempts"] = (
            int(record.get("attempts") or 0) + 1
        )
        record["updated_at"] = _utc_now()
        state["updated_at"] = _utc_now()

        _atomic_json_write(
            PRACTICE,
            state,
        )

        record["infrastructure_failures"] = 0

        record = practice_service._record_outcome(
            skill_id,
            record,
            bundle,
            assessment,
            run_dir,
            response,
        )

        state["skills"][skill_id] = record
        state["current_skill"] = None
        state["updated_at"] = _utc_now()

        result = {
            "ok": bool(assessment["passed"]),
            "classification": (
                "COUNTED_ROUND3_RECOVERY_PASS"
                if assessment["passed"]
                else "COUNTED_ROUND3_RECOVERY_FAIL"
            ),
            "skill_id": skill_id,
            "counted_round": 3,
            "counted_attempt_consumed": True,
            "attempts": record["attempts"],
            "successful_attempts":
                record["successful_attempts"],
            "score_percent":
                assessment["score_percent"],
            "status": record["status"],
            "knowledge_certified":
                record["knowledge_certified"],
            "forge_allowed":
                record["forge_allowed"],
            "tool_evidence_attempts":
                int(
                    record.get("tool_evidence_attempts")
                    or 0
                ),
            "source_evidence": str(source),
            "recovery_evidence": str(run_dir),
            "repair_used": True,
            "replacement_generation_used": False,
            "cherry_picking_allowed": False,
        }

        _atomic_json_write(
            PRACTICE,
            state,
        )

        _atomic_json_write(
            run_dir / "result.json",
            result,
        )

    autopilot = load(AUTOPILOT)
    sprint = load(SPRINT)

    qualification = autopilot["skills"][skill_id]
    runs = qualification.setdefault(
        "counted_practice_runs",
        [],
    )

    run_evidence = str(run_dir.resolve())

    existing = [
        item
        for item in runs
        if (
            isinstance(item, dict)
            and resolved(item.get("evidence"))
            == run_dir.resolve()
        )
    ]

    if len(existing) > 1:
        raise RuntimeError(
            "Duplicate recovery records already exist."
        )

    already_recorded = len(existing) == 1

    if not already_recorded:
        runs.append({
            "recorded_at": _utc_now(),
            "round": 3,
            "outcome": result["classification"],
            "ok": result["ok"],
            "score_percent":
                result["score_percent"],
            "counted_attempt_consumed": True,
            "knowledge_certified":
                result["knowledge_certified"],
            "forge_allowed": False,
            "evidence": run_evidence,
            "recovery_source_evidence":
                str(source),
            "repair_used": True,
            "replacement_generation_used": False,
            "source":
                "official_evaluator_preserved_answer_recovery",
        })

    if result["ok"]:
        status = (
            "knowledge_certified_waiting_safe_tool_evidence"
        )

        next_action = (
            "build_safe_non_unlocking_cadquery_evidence_gate"
        )
    else:
        status = "counted_practice_retry_required"

        next_action = (
            "study_and_requalify_before_counted_practice_retry"
        )

    qualification["counted_attempts"] = (
        result["attempts"]
    )

    qualification["successful_counted_rounds"] = (
        result["successful_attempts"]
    )

    qualification["knowledge_certified"] = (
        result["knowledge_certified"]
    )

    qualification["counted_knowledge_complete"] = (
        result["knowledge_certified"] is True
        and int(result["successful_attempts"]) >= 3
    )

    qualification[
        "ready_for_counted_certification"
    ] = False

    qualification["forge_allowed"] = False
    qualification["tool_evidence_certified"] = False
    qualification["status"] = status
    qualification["latest_counted_evidence"] = (
        run_evidence
    )
    qualification[
        "latest_counted_score_percent"
    ] = result["score_percent"]
    qualification["updated_at"] = _utc_now()

    autopilot["current_skill"] = skill_id
    autopilot["enabled"] = False
    autopilot["paused"] = True
    autopilot["counted_execution_enabled"] = False
    autopilot["forge_unlock_allowed"] = False
    autopilot["practice_state_sha256_now"] = (
        sha256(PRACTICE)
    )
    autopilot["last_counted_practice"] = result
    autopilot["updated_at"] = _utc_now()

    plan = autopilot.get("current_plan")

    if not isinstance(plan, dict):
        plan = {}

    plan.update({
        "skill_id": skill_id,
        "next_action": next_action,
        "status": status,
        "counted_attempts":
            result["attempts"],
        "successful_rounds":
            result["successful_attempts"],
        "required_successful_rounds": 3,
        "knowledge_certified":
            result["knowledge_certified"],
        "tool_evidence_certified": False,
        "forge_allowed": False,
    })

    autopilot["current_plan"] = plan

    sprint["current_skill"] = skill_id
    sprint["updated_at"] = _utc_now()

    atomic(AUTOPILOT, autopilot)
    atomic(SPRINT, sprint)

    receipt = {
        **result,
        "recorded_at": _utc_now(),
        "already_recorded": already_recorded,
        "next_action": next_action,
        "autopilot_status": status,
        "practice_state_sha256":
            sha256(PRACTICE),
        "autopilot_state_sha256":
            sha256(AUTOPILOT),
        "commercial_sprint_sha256":
            sha256(SPRINT),
        "certification_sha256":
            sha256(paths["certification"]),
        "manifest_sha256":
            sha256(paths["manifest"]),
        "tests_sha256":
            sha256(paths["tests"]),
    }

    atomic(
        receipt_path,
        receipt,
    )

    return receipt, 0 if result["ok"] else 2


def main() -> int:
    parser = argparse.ArgumentParser(
        description=(
            "Safely recover one preserved SHiRE Academy "
            "counted Round 3 infrastructure failure."
        )
    )

    sub = parser.add_subparsers(
        dest="command",
        required=True,
    )

    test = sub.add_parser("self-test")
    test.add_argument("--skill-id", required=True)

    run = sub.add_parser("run")
    run.add_argument("--skill-id", required=True)
    run.add_argument(
        "--source-evidence",
        required=True,
    )
    run.add_argument(
        "--receipt",
        required=True,
    )
    run.add_argument(
        "--expected-state-sha",
        required=True,
    )
    run.add_argument(
        "--expected-auto-sha",
        required=True,
    )
    run.add_argument(
        "--expected-sprint-sha",
        required=True,
    )
    run.add_argument(
        "--expected-cert-sha",
        required=True,
    )
    run.add_argument(
        "--expected-manifest-sha",
        required=True,
    )
    run.add_argument(
        "--expected-tests-sha",
        required=True,
    )

    args = parser.parse_args()

    if args.command == "self-test":
        result = self_test(args.skill_id)
        print(
            json.dumps(
                result,
                indent=2,
                sort_keys=True,
            )
        )
        return 0

    result, returncode = run_recovery(args)

    print(
        json.dumps(
            result,
            indent=2,
            sort_keys=True,
            ensure_ascii=False,
        )
    )

    return returncode


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