"""
fw_doctor/checks.py — Individual diagnostic checks for the dusun framework.

Each check function returns a CheckResult with pass/fail, timing, and detail.
Checks are INDEPENDENT (KV₇) — no shared state between them.

Design:
  AX5  (Hayat):  This module IS the integration prerequisite check — it
                  verifies all other modules come alive as a coordinated whole.
  AX52 (Gate):   If ANY check returns zero health, the system gate collapses.
  AX57 (Transparency): Every check reports exactly what it tested.
  T17:  At most 6/7 lenses are fully reachable — we report the bound.
"""

from __future__ import annotations

import time
import traceback
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, List, Optional


class CheckStatus(Enum):
    """Outcome of a single diagnostic check."""
    PASS = "pass"
    WARN = "warn"
    FAIL = "fail"
    SKIP = "skip"


@dataclass
class CheckResult:
    """Result of one diagnostic check."""
    name: str
    status: CheckStatus
    message: str
    elapsed_ms: float = 0.0
    detail: Optional[Dict[str, Any]] = None


# ── Helpers ──────────────────────────────────────────────────────────

def _timed(func):
    """Decorator that measures wall-clock time and wraps exceptions."""
    def wrapper(*args, **kwargs) -> CheckResult:
        t0 = time.perf_counter()
        try:
            result = func(*args, **kwargs)
            result.elapsed_ms = (time.perf_counter() - t0) * 1000
            return result
        except Exception as exc:
            elapsed = (time.perf_counter() - t0) * 1000
            return CheckResult(
                name=func.__name__.replace("check_", ""),
                status=CheckStatus.FAIL,
                message=f"Exception: {exc}",
                elapsed_ms=elapsed,
                detail={"traceback": traceback.format_exc()},
            )
    wrapper.__name__ = func.__name__
    wrapper.__doc__ = func.__doc__
    return wrapper


# =====================================================================
# CHECK 1: Module imports
# =====================================================================

REQUIRED_PACKAGES = [
    ("fw_server", "MCP Server bridge"),
    ("fw_server.adapters", "Lens adapters"),
    ("fw_server.persistence", "State persistence"),
    ("fw_server.memory_bank", "Memory bank export"),
    ("bileshke", "Composite convergence engine"),
    ("kavram_sozlugu", "Ontology lens"),
    ("mereoloji", "Mereology lens"),
    ("fol_formalizasyon", "First-order logic lens"),
    ("bayes_analiz", "Bayesian analysis lens"),
    ("oyun_teorisi", "Game theory lens"),
    ("kategori_teorisi", "Category theory lens"),
    ("holografik", "Holographic/topology lens"),
    ("hcp", "Holographic Context Protocol"),
    ("kaskad", "Generative Cascade Engine"),
    ("fidelity_funnel", "Fidelity funnel"),
    ("emergence", "Emergence cascade module"),
]


@_timed
def check_imports() -> CheckResult:
    """Verify all required packages import without error."""
    import importlib

    failures: List[str] = []
    successes: List[str] = []

    for pkg, desc in REQUIRED_PACKAGES:
        try:
            importlib.import_module(pkg)
            successes.append(pkg)
        except Exception as exc:
            failures.append(f"{pkg}: {exc}")

    if failures:
        return CheckResult(
            name="imports",
            status=CheckStatus.FAIL,
            message=f"{len(failures)}/{len(REQUIRED_PACKAGES)} imports failed",
            detail={"failures": failures, "successes": successes},
        )
    return CheckResult(
        name="imports",
        status=CheckStatus.PASS,
        message=f"All {len(REQUIRED_PACKAGES)} packages import OK",
        detail={"count": len(successes)},
    )


# =====================================================================
# CHECK 2: Version consistency
# =====================================================================

@_timed
def check_version() -> CheckResult:
    """Verify dusun version is accessible and consistent."""
    try:
        from importlib.metadata import version as get_version
        ver = get_version("dusun")
    except Exception:
        return CheckResult(
            name="version",
            status=CheckStatus.WARN,
            message="Cannot read package version (editable install?)",
        )
    return CheckResult(
        name="version",
        status=CheckStatus.PASS,
        message=f"dusun v{ver}",
        detail={"version": ver},
    )


# =====================================================================
# CHECK 3: All 7 lenses execute
# =====================================================================

@_timed
def check_lenses() -> CheckResult:
    """Run each of the 7 lenses with default params and verify they return scores."""
    from fw_server.adapters import ADAPTER_MAP, run_lens

    results: Dict[str, Any] = {}
    failures: List[str] = []

    for lens_id in ADAPTER_MAP:
        try:
            lr = run_lens(lens_id, {})
            results[lens_id] = {
                "score": lr.score,
                "grade": lr.grade.value if hasattr(lr.grade, "value") else str(lr.grade),
                "checks": f"{lr.checks_passed}/{lr.checks_total}",
            }
            # Verify structural bound: score < 1.0 (T6/KV₄)
            if lr.score >= 1.0:
                failures.append(f"{lens_id}: score={lr.score} violates T6 (must be <1.0)")
        except Exception as exc:
            failures.append(f"{lens_id}: {exc}")
            results[lens_id] = {"error": str(exc)}

    if failures:
        return CheckResult(
            name="lenses",
            status=CheckStatus.FAIL,
            message=f"{len(failures)} lens(es) failed: {', '.join(f.split(':')[0] for f in failures)}",
            detail={"results": results, "failures": failures},
        )
    return CheckResult(
        name="lenses",
        status=CheckStatus.PASS,
        message=f"All 7 lenses execute OK",
        detail={"results": results},
    )


# =====================================================================
# CHECK 4: Bileshke composite pipeline
# =====================================================================

@_timed
def check_bileshke() -> CheckResult:
    """Run all 7 lenses then bileshke composite and verify structural constraints."""
    from fw_server.adapters import ADAPTER_MAP, run_lens
    from bileshke.engine import run_bileshke

    # Run all 7 lenses to get LensResult list (required by run_bileshke)
    lens_results = []
    for lens_id in ADAPTER_MAP:
        lr = run_lens(lens_id, {})
        lens_results.append(lr)

    report = run_bileshke(lens_results)

    issues: List[str] = []

    # KV₄: composite must be in (0, 1)
    if report.composite_score >= 1.0:
        issues.append(f"Composite score {report.composite_score} >= 1.0 (KV₄ violation)")

    # AX52: check multiplicative gate (practical_gate = latife_gate AND ortam_gate)
    if not report.practical_gate:
        issues.append("Coverage gate FAIL (AX52: zero in a coverage dimension)")

    # T17: completeness bound
    completeness = report.completeness
    if completeness > 6 / 7 + 0.01:
        issues.append(f"Completeness {completeness:.4f} exceeds T17 bound (6/7)")

    status = CheckStatus.PASS if not issues else CheckStatus.WARN
    return CheckResult(
        name="bileshke",
        status=status,
        message=f"Composite={report.composite_score:.4f}, completeness={completeness:.2f}",
        detail={
            "composite_score": report.composite_score,
            "practical_gate": report.practical_gate,
            "completeness": completeness,
            "degrees": report.degree_distribution,
            "issues": issues,
        },
    )


# =====================================================================
# CHECK 5: Kavaid constraints
# =====================================================================

@_timed
def check_kavaid() -> CheckResult:
    """Evaluate the 8 kavaid constraints with default state."""
    from fw_server.adapters import ADAPTER_MAP, run_lens
    from bileshke.engine import run_bileshke

    # Run all 7 lenses then bileshke to get a QualityReport
    lens_results = []
    for lens_id in ADAPTER_MAP:
        lr = run_lens(lens_id, {})
        lens_results.append(lr)

    report = run_bileshke(lens_results)
    kv_results = report.kavaid_checks  # Dict[str, bool]

    failed = [k for k, v in kv_results.items() if not v]
    passed = [k for k, v in kv_results.items() if v]

    if failed:
        return CheckResult(
            name="kavaid",
            status=CheckStatus.WARN,
            message=f"{len(passed)}/{len(kv_results)} kavaid pass ({', '.join(failed)} failed)",
            detail={"results": kv_results},
        )
    return CheckResult(
        name="kavaid",
        status=CheckStatus.PASS,
        message=f"All {len(kv_results)} kavaid constraints pass",
        detail={"results": kv_results},
    )


# =====================================================================
# CHECK 6: Kaskad graph integrity
# =====================================================================

@_timed
def check_kaskad() -> CheckResult:
    """Verify the kaskad DAG is structurally sound."""
    from kaskad import build_framework_graph, ChainId
    from kaskad.verification import verify_all

    dag = build_framework_graph()
    results = verify_all(dag)

    failures = [r for r in results if not r.passed]
    passed = [r for r in results if r.passed]

    detail = {
        "nodes": dag.node_count,
        "edges": dag.edge_count,
        "chains": len(ChainId),
        "checks_passed": len(passed),
        "checks_total": len(results),
    }

    if failures:
        detail["failures"] = [
            {"check": r.check_name, "message": r.message} for r in failures
        ]
        return CheckResult(
            name="kaskad",
            status=CheckStatus.FAIL,
            message=f"{len(failures)} kaskad check(s) failed",
            detail=detail,
        )
    return CheckResult(
        name="kaskad",
        status=CheckStatus.PASS,
        message=f"DAG OK: {detail['nodes']} nodes, {detail['edges']} edges, {detail['chains']} chains",
        detail=detail,
    )


# =====================================================================
# CHECK 7: HCP protocol (ingest → query round-trip)
# =====================================================================

@_timed
def check_hcp() -> CheckResult:
    """Ingest sample text into HCP and verify query retrieval works."""
    from hcp.protocol import HCPProtocol
    from hcp.types import SourceMetadata

    proto = HCPProtocol()

    # Ingest a known text
    sample = (
        "The integration prerequisite (AX5/Hayat) states that independent "
        "capabilities require an integrating principle to become functional. "
        "Without Hayat, all other attributes remain inert."
    )
    meta = SourceMetadata(tool_name="fw_doctor", tags={"source": "diagnostic"})
    proto.ingest(sample, source_metadata=meta)

    if not proto.is_ready:
        return CheckResult(
            name="hcp",
            status=CheckStatus.FAIL,
            message="HCP not ready after ingest",
        )

    # Query back
    results = proto.query("integration prerequisite Hayat", top_k=3)

    if not results:
        return CheckResult(
            name="hcp",
            status=CheckStatus.FAIL,
            message="HCP query returned 0 results after ingest",
        )

    # results is List[Tuple[SeededChunk, float]] — access score as item[1]
    top_score = results[0][1]

    return CheckResult(
        name="hcp",
        status=CheckStatus.PASS,
        message=f"Ingest + query OK ({len(results)} result(s), top score={top_score:.4f})",
        detail={
            "n_chunks": len(proto.seeded_chunks),
            "n_results": len(results),
            "top_score": top_score,
        },
    )


# =====================================================================
# CHECK 8: Module framework_summary() functions
# =====================================================================

MODULE_SUMMARIES = [
    ("kavram_sozlugu.constraints", "Ontoloji"),
    ("mereoloji.constraints", "Mereoloji"),
    ("fol_formalizasyon.inference", "FOL"),
    ("bayes_analiz.verification", "Bayes"),
    ("oyun_teorisi.verification", "OyunTeorisi"),
    ("kategori_teorisi.verification", "KategoriTeorisi"),
    ("holografik.verification", "Holografik"),
    ("bileshke.quality", "Bileshke"),
    ("kaskad.verification", "Kaskad"),
    ("emergence.emergence", "Emergence"),
]


@_timed
def check_framework_summaries() -> CheckResult:
    """Verify each module exposes a working framework_summary()."""
    import importlib

    healthy: List[str] = []
    broken: List[str] = []

    for mod_path, label in MODULE_SUMMARIES:
        try:
            mod = importlib.import_module(mod_path)
            fn = getattr(mod, "framework_summary")
            summary = fn()
            if not isinstance(summary, dict):
                broken.append(f"{label}: returned {type(summary).__name__}, expected dict")
            elif len(summary) == 0:
                broken.append(f"{label}: returned empty dict")
            else:
                healthy.append(label)
        except Exception as exc:
            broken.append(f"{label}: {exc}")

    if broken:
        return CheckResult(
            name="summaries",
            status=CheckStatus.FAIL,
            message=f"{len(broken)} module(s) have broken framework_summary()",
            detail={"healthy": healthy, "broken": broken},
        )
    return CheckResult(
        name="summaries",
        status=CheckStatus.PASS,
        message=f"All {len(healthy)} modules report healthy",
        detail={"count": len(healthy)},
    )


# =====================================================================
# CHECK 9: Persistence infrastructure
# =====================================================================

@_timed
def check_persistence() -> CheckResult:
    """Verify persistence save/restore round-trip works (HCP + session)."""
    import tempfile
    from pathlib import Path

    from hcp.protocol import HCPProtocol
    from hcp.types import SourceMetadata
    from fw_server.persistence import (
        save_hcp_state,
        restore_hcp_state,
        get_persistence_info,
        save_session_state,
        restore_session_state,
        get_session_persistence_info,
    )
    from fw_server.session import SessionLedger

    proto = HCPProtocol()
    proto.ingest(
        "Diagnostic persistence test — AX27 transparent vessel.",
        source_metadata=SourceMetadata(tool_name="fw_doctor", tags={"source": "test"}),
    )

    with tempfile.TemporaryDirectory(prefix="fw_doctor_") as tmpdir:
        state_dir = Path(tmpdir)

        # ── HCP round-trip ──
        saved = save_hcp_state(state_dir, proto)
        if saved is None:
            return CheckResult(
                name="persistence",
                status=CheckStatus.FAIL,
                message="save_hcp_state returned None",
            )

        proto2 = HCPProtocol()
        restored = restore_hcp_state(state_dir, proto2)
        if not restored:
            return CheckResult(
                name="persistence",
                status=CheckStatus.FAIL,
                message="restore_hcp_state returned False",
            )

        info = get_persistence_info(state_dir)
        if not info.get("checkpoint_exists"):
            return CheckResult(
                name="persistence",
                status=CheckStatus.FAIL,
                message="HCP checkpoint not found after save",
                detail=info,
            )

        # ── Session round-trip ──
        ledger = SessionLedger()
        ledger.record_call("fw_doctor_test", grade="Tasdik", composite_score=0.72)

        session_saved = save_session_state(state_dir, ledger)
        if session_saved is None:
            return CheckResult(
                name="persistence",
                status=CheckStatus.FAIL,
                message="save_session_state returned None",
            )

        ledger2 = SessionLedger()
        session_restored = restore_session_state(state_dir, ledger2)
        if not session_restored:
            return CheckResult(
                name="persistence",
                status=CheckStatus.FAIL,
                message="restore_session_state returned False",
            )

        if ledger2.call_count != 1:
            return CheckResult(
                name="persistence",
                status=CheckStatus.FAIL,
                message=f"Session round-trip count mismatch: expected 1, got {ledger2.call_count}",
            )

        session_info = get_session_persistence_info(state_dir)
        if not session_info.get("session_checkpoint_exists"):
            return CheckResult(
                name="persistence",
                status=CheckStatus.FAIL,
                message="Session checkpoint not found after save",
                detail=session_info,
            )

    return CheckResult(
        name="persistence",
        status=CheckStatus.PASS,
        message="HCP + session save → restore round-trip OK",
    )


# =====================================================================
# CHECK 10: Memory bank export
# =====================================================================

@_timed
def check_memory_bank() -> CheckResult:
    """Verify memory bank export produces expected markdown files."""
    import tempfile
    from pathlib import Path

    from hcp.protocol import HCPProtocol
    from hcp.types import SourceMetadata
    from fw_server.memory_bank import sync_memory_bank

    proto = HCPProtocol()
    proto.ingest(
        "Memory bank diagnostic test content.",
        source_metadata=SourceMetadata(tool_name="fw_doctor", tags={"source": "test"}),
    )

    with tempfile.TemporaryDirectory(prefix="fw_doctor_mb_") as tmpdir:
        # sync_memory_bank creates memory-bank/ as sibling of state_dir,
        # so nest state_dir one level deep so parent stays within tmpdir
        state_dir = Path(tmpdir) / ".hcp_state"
        state_dir.mkdir()
        sync_memory_bank(state_dir, proto, session_note="fw_doctor diagnostic")

        mb_dir = Path(tmpdir) / "memory-bank"
        expected_files = [
            "activeContext.md",
            "progress.md",
            "sessionLog.md",
            "techContext.md",
        ]

        missing = [f for f in expected_files if not (mb_dir / f).exists()]

    if missing:
        return CheckResult(
            name="memory_bank",
            status=CheckStatus.FAIL,
            message=f"Missing files: {', '.join(missing)}",
            detail={"missing": missing},
        )
    return CheckResult(
        name="memory_bank",
        status=CheckStatus.PASS,
        message=f"All {len(expected_files)} memory bank files generated",
    )


# =====================================================================
# Master check list
# =====================================================================

ALL_CHECKS = [
    check_imports,
    check_version,
    check_lenses,
    check_bileshke,
    check_kavaid,
    check_kaskad,
    check_hcp,
    check_framework_summaries,
    check_persistence,
    check_memory_bank,
]
