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

import argparse
import hashlib
import json
import os
import sqlite3
import sys
import tempfile
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"
STATE = ACADEMY / "runtime/practice_state.json"
AUTOPILOT = ACADEMY / "runtime/autopilot_state.json"
INDEX = ACADEMY / "runtime/academy_index.sqlite3"

from services.cadquery_engine_service import (
    cadquery_engine_service,
)
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 verify_hash(
    path: Path,
    expected: str,
    label: str,
) -> None:
    actual = sha256(path)

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


def validate_report(
    report: dict[str, Any],
    output_root: Path,
    skill_id: str,
) -> dict[str, Any]:
    required = {
        "ok": True,
        "passed": True,
        "engine": "CadQuery",
        "skill_id": skill_id,
        "primary_build_engine": True,
        "blender_used": False,
        "forge_ready_exported": False,
        "physical_validation_claimed": False,
        "professional_validation_claimed": False,
    }

    for key, expected in required.items():
        actual = report.get(key)

        if actual != expected:
            raise RuntimeError(
                f"Unsafe or invalid report field "
                f"{key}: {actual!r}"
            )

    cases = report.get("cases") or []

    if len(cases) != 3:
        raise RuntimeError(
            "CadQuery report must contain exactly three parameter cases."
        )

    expected_names = {
        "compact",
        "nominal",
        "expanded",
    }

    actual_names = {
        str(case.get("name") or "")
        for case in cases
    }

    if actual_names != expected_names:
        raise RuntimeError(
            f"Unexpected parameter cases: {sorted(actual_names)}"
        )

    for case in cases:
        if case.get("passed") is not True:
            raise RuntimeError(
                f"CadQuery case failed: {case.get('name')}"
            )

        mesh = case.get("mesh") or {}

        if mesh.get("watertight") is not True:
            raise RuntimeError(
                f"Non-watertight case: {case.get('name')}"
            )

        if int(mesh.get("connected_bodies") or 0) != 1:
            raise RuntimeError(
                f"Unexpected body count: {case.get('name')}"
            )

        if mesh.get("valid_volume") is not True:
            raise RuntimeError(
                f"Invalid mesh volume: {case.get('name')}"
            )

        if case.get("step_header_valid") is not True:
            raise RuntimeError(
                f"Invalid STEP output: {case.get('name')}"
            )

    mandatory = report.get("mandatory_checks") or {}

    if not mandatory:
        raise RuntimeError(
            "CadQuery report contains no mandatory checks."
        )

    failed_checks = [
        name
        for name, passed in mandatory.items()
        if passed is not True
    ]

    if failed_checks:
        raise RuntimeError(
            f"Mandatory CadQuery checks failed: {failed_checks}"
        )

    files = report.get("files") or {}

    if len(files) < 10:
        raise RuntimeError(
            "CadQuery evidence contains too few output files."
        )

    validated_files = []

    resolved_root = output_root.resolve()

    for relative, expected_hash in files.items():
        path = (output_root / relative).resolve()

        try:
            path.relative_to(resolved_root)
        except ValueError as exc:
            raise RuntimeError(
                f"Unsafe report file path: {relative}"
            ) from exc

        if not path.is_file():
            raise RuntimeError(
                f"Evidence output is missing: {path}"
            )

        actual_hash = sha256(path)

        if actual_hash != expected_hash:
            raise RuntimeError(
                f"Evidence checksum mismatch: {relative}"
            )

        validated_files.append(str(path))

    return {
        "case_count": len(cases),
        "mandatory_check_count": len(mandatory),
        "validated_file_count": len(validated_files),
        "validated_files": validated_files,
    }


def self_test(skill_id: str) -> dict[str, Any]:
    status = cadquery_engine_service.status(force=True)

    if status.get("available") is not True:
        raise RuntimeError(
            status.get("error")
            or "CadQuery engine is unavailable."
        )

    if not cadquery_engine_service.supports(skill_id):
        raise RuntimeError(
            f"CadQuery adapter does not support {skill_id}"
        )

    with tempfile.TemporaryDirectory(
        prefix="shire-safe-cadquery-selftest-"
    ) as temporary:
        root = Path(temporary)

        files: dict[str, str] = {}

        for case_name in (
            "compact",
            "nominal",
            "expanded",
        ):
            case_dir = root / case_name
            case_dir.mkdir()

            for filename in (
                f"{case_name}.stl",
                f"{case_name}.step",
                "parameters.json",
            ):
                path = case_dir / filename
                path.write_text(
                    f"self-test:{case_name}:{filename}\n",
                    encoding="utf-8",
                )
                files[str(path.relative_to(root))] = sha256(path)

        model_path = root / "model.py"
        model_path.write_text(
            "# safe CadQuery self-test fixture\n",
            encoding="utf-8",
        )
        files["model.py"] = sha256(model_path)

        cases = []

        for case_name in (
            "compact",
            "nominal",
            "expanded",
        ):
            cases.append({
                "name": case_name,
                "passed": True,
                "mesh": {
                    "watertight": True,
                    "connected_bodies": 1,
                    "valid_volume": True,
                },
                "step_header_valid": True,
            })

        fixture = {
            "ok": True,
            "passed": True,
            "engine": "CadQuery",
            "skill_id": skill_id,
            "primary_build_engine": True,
            "blender_used": False,
            "forge_ready_exported": False,
            "physical_validation_claimed": False,
            "professional_validation_claimed": False,
            "mandatory_checks": {
                "supported_skill": True,
                "cadquery_version_present": True,
                "three_valid_parameter_cases": True,
                "parameter_variation_changes_geometry": True,
                "invalid_parameters_blocked": True,
                "editable_source_present": True,
                "blender_not_loaded": True,
                "physical_claims_absent": True,
                "professional_claims_absent": True,
            },
            "cases": cases,
            "files": files,
        }

        validation = validate_report(
            fixture,
            root,
            skill_id,
        )

    return {
        "ok": True,
        "classification":
            "SAFE_CADQUERY_GATE_SELF_TEST_PASS",
        "skill_id": skill_id,
        "engine": status.get("engine"),
        "engine_version": status.get("engine_version"),
        "bridge_version": status.get("bridge_version"),
        "forge_allowed": False,
        **validation,
    }


def run_gate(
    args: argparse.Namespace,
) -> tuple[dict[str, Any], int]:
    skill_id = args.skill_id
    receipt_path = Path(args.receipt)

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

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

    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",
    )

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

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

    state = load(STATE)
    autopilot = load(AUTOPILOT)

    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 record.get("lane") != "tool_evidence":
        raise RuntimeError(
            "Skill is not in the tool-evidence lane."
        )

    if record.get("knowledge_certified") is not True:
        raise RuntimeError(
            "Knowledge certification is incomplete."
        )

    if record.get("cadquery_supported") is not True:
        raise RuntimeError(
            "CadQuery support is not enabled for this skill."
        )

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

    if autopilot.get("forge_unlock_allowed") is not False:
        raise RuntimeError(
            "Autopilot Forge gate is already open."
        )

    attempts = int(
        record.get("tool_evidence_attempts") or 0
    )

    if attempts >= 3:
        raise RuntimeError(
            "CadQuery evidence attempts are exhausted."
        )

    latest = Path(
        str(record.get("latest_run") or "")
    )

    if not latest.is_dir():
        raise RuntimeError(
            "Latest counted knowledge evidence is missing."
        )

    response_path = latest / "practice_response.json"
    snapshot_path = latest / "lesson_snapshot.json"

    if not response_path.is_file():
        raise RuntimeError(
            "Latest practice response is missing."
        )

    if not snapshot_path.is_file():
        raise RuntimeError(
            "Latest lesson snapshot is missing."
        )

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

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

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

    output_root = (
        run_dir / "cadquery-tool-evidence"
    )

    try:
        report = cadquery_engine_service.run_evidence(
            skill_id=skill_id,
            output_directory=output_root,
            practice_round=3,
            practice_response_path=response_path,
            lesson_snapshot_path=snapshot_path,
        )

        validation = validate_report(
            report,
            output_root,
            skill_id,
        )

        _atomic_json_write(
            run_dir / "cadquery_bridge_result.json",
            report,
        )

    except Exception as exc:
        report_path = (
            output_root
            / "cadquery_evidence_report.json"
        )

        semantic_failure = False
        failed_report: dict[str, Any] | None = None

        if report_path.is_file():
            try:
                failed_report = load(report_path)

                semantic_failure = (
                    failed_report.get("passed") is False
                    or not all(
                        (
                            failed_report.get(
                                "mandatory_checks"
                            )
                            or {}
                        ).values()
                    )
                )
            except Exception:
                semantic_failure = False

        error_text = str(exc)

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

        if semantic_failure:
            with practice_service._lock() as lock_handle:
                del lock_handle

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

                record["tool_evidence_attempts"] = (
                    int(
                        record.get(
                            "tool_evidence_attempts"
                        )
                        or 0
                    )
                    + 1
                )

                record["status"] = "tool_failed"
                record["forge_allowed"] = False
                record["cadquery_evidence_validated"] = False
                record["latest_tool_run"] = str(run_dir)
                record["last_error"] = error_text
                record["updated_at"] = _utc_now()

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

                _atomic_json_write(
                    STATE,
                    state,
                )

            result = {
                "ok": False,
                "classification":
                    "SAFE_CADQUERY_EVIDENCE_FAIL",
                "skill_id": skill_id,
                "tool_attempt_consumed": True,
                "tool_evidence_attempts":
                    record["tool_evidence_attempts"],
                "knowledge_certified": True,
                "forge_allowed": False,
                "evidence": str(run_dir),
                "error": error_text,
            }

            returncode = 2

        else:
            result = {
                "ok": False,
                "classification":
                    "SAFE_CADQUERY_INFRASTRUCTURE_RETRY",
                "skill_id": skill_id,
                "tool_attempt_consumed": False,
                "tool_evidence_attempts": attempts,
                "knowledge_certified": True,
                "forge_allowed": False,
                "evidence": str(run_dir),
                "error": error_text,
            }

            returncode = 3

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

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

        return result, returncode

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

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

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

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

    verify_hash(
        paths["tests"],
        args.expected_tests_sha,
        "Tests before commit",
    )

    with practice_service._lock() as lock_handle:
        del lock_handle

        network = practice_service._verify_evidence_root()

        state = load(STATE)
        autopilot = load(AUTOPILOT)

        record = state["skills"][skill_id]
        qualification = autopilot["skills"][skill_id]

        if int(
            record.get("tool_evidence_attempts") or 0
        ) != attempts:
            raise RuntimeError(
                "Tool-evidence attempt count changed before commit."
            )

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

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

        certification = load(
            paths["certification"]
        )

        manifest = load(
            paths["manifest"]
        )

        evidence_paths = list(
            certification.get("evidence_paths") or []
        )

        if str(run_dir) not in evidence_paths:
            evidence_paths.append(str(run_dir))

        tool_attempts = attempts + 1
        now = _utc_now()

        record.update({
            "status":
                "cadquery_validated_waiting_forge_review",
            "tool_evidence_attempts": tool_attempts,
            "cadquery_evidence_validated": True,
            "tool_evidence_complete": True,
            "forge_review_pending": True,
            "forge_allowed": False,
            "latest_tool_run": str(run_dir),
            "last_error": None,
            "updated_at": now,
        })

        certification.update({
            "status": "validation",
            "knowledge_certified": True,
            "tool_evidence_validated": True,
            "tool_evidence_complete": True,
            "forge_review_pending": True,
            "forge_allowed": False,
            "tool_engine": "CadQuery",
            "tool_engine_version":
                report.get("engine_version"),
            "tool_bridge_version":
                report.get("bridge_version"),
            "tool_evidence_attempts": tool_attempts,
            "tool_evidence_report":
                report.get("report_path"),
            "tool_evidence_validated_at": now,
            "tool_evidence_validated_by":
                "SHiRE Safe CadQuery Evidence Gate 0001",
            "evidence_paths": evidence_paths,
            "remaining_requirements": [
                "Explicit separate Forge permission review and approval."
            ],
            "limitations": [
                "Validation covers digital CadQuery modelling and file integrity only.",
                "Printer, material, physical, medical and professional performance are not implied.",
                "Forge permission remains separately locked.",
            ],
        })

        manifest.update({
            "status": "validation",
            "tool_evidence_validated": True,
            "forge_review_pending": True,
            "forge_allowed": False,
        })

        qualification.update({
            "status":
                "cadquery_validated_waiting_forge_review",
            "knowledge_certified": True,
            "counted_knowledge_complete": True,
            "tool_evidence_validated": True,
            "tool_evidence_complete": True,
            "tool_evidence_certified": False,
            "forge_review_pending": True,
            "forge_allowed": False,
            "latest_tool_evidence": str(run_dir),
            "latest_tool_evidence_report":
                report.get("report_path"),
            "updated_at": now,
        })

        plan = autopilot.get("current_plan")

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

        plan.update({
            "skill_id": skill_id,
            "next_action":
                "install_safe_autonomous_curriculum_scheduler",
            "status":
                "cadquery_validated_waiting_forge_review",
            "knowledge_certified": True,
            "tool_evidence_validated": True,
            "tool_evidence_certified": False,
            "forge_review_pending": True,
            "forge_allowed": False,
        })

        autopilot["current_plan"] = plan
        autopilot["enabled"] = False
        autopilot["paused"] = True
        autopilot["counted_execution_enabled"] = False
        autopilot["forge_unlock_allowed"] = False
        autopilot["updated_at"] = now

        state["skills"][skill_id] = record
        state["current_skill"] = None
        state["network"] = network
        state["updated_at"] = now

        _atomic_json_write(
            paths["certification"],
            certification,
        )

        _atomic_json_write(
            paths["manifest"],
            manifest,
        )

        _atomic_json_write(
            STATE,
            state,
        )

        _atomic_json_write(
            AUTOPILOT,
            autopilot,
        )

        practice_service._update_index(
            skill_id,
            "validation",
            False,
        )

    result = {
        "ok": True,
        "classification":
            "SAFE_CADQUERY_EVIDENCE_PASS",
        "skill_id": skill_id,
        "tool_attempt_consumed": True,
        "tool_evidence_attempts": tool_attempts,
        "knowledge_certified": True,
        "tool_evidence_validated": True,
        "tool_evidence_certified": False,
        "forge_review_pending": True,
        "forge_allowed": False,
        "status":
            "cadquery_validated_waiting_forge_review",
        "engine": report.get("engine"),
        "engine_version": report.get("engine_version"),
        "bridge_version": report.get("bridge_version"),
        "evidence": str(run_dir),
        "report": report.get("report_path"),
        **validation,
    }

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

    receipt = {
        **result,
        "recorded_at": _utc_now(),
        "practice_state_sha256": sha256(STATE),
        "autopilot_state_sha256": sha256(AUTOPILOT),
        "certification_sha256":
            sha256(paths["certification"]),
        "manifest_sha256":
            sha256(paths["manifest"]),
        "tests_sha256":
            sha256(paths["tests"]),
    }

    atomic(
        receipt_path,
        receipt,
    )

    return receipt, 0


def main() -> int:
    parser = argparse.ArgumentParser(
        description=(
            "Generate and validate CadQuery Academy evidence "
            "without granting Forge permission."
        )
    )

    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("--receipt", required=True)
    run.add_argument("--expected-state-sha", required=True)
    run.add_argument("--expected-auto-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":
        payload = self_test(args.skill_id)

        print(json.dumps(
            payload,
            indent=2,
            sort_keys=True,
        ))

        return 0

    payload, returncode = run_gate(args)

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

    return returncode


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