"""Closure Engine — Sprinkler Behavior implementation (purpose.md Section 4).

Implements the full closure loop:
  detect -> trace -> propose fix -> verify -> record trust case

This closes the gap between detection-only behavior and full automated
root-cause resolution described in purpose.md.
"""

from __future__ import annotations

import json
import time as _time
from pathlib import Path
from typing import Any, Dict, List, Optional

from src.autonomous_fix.fix_policies import (
    POLICY_VERSION,
    human_approval_required,
    should_mark_closure_achieved,
    should_mark_closure_likely,
)

from .core import (
    DEFAULT_INDEX_PATH,
    DEFAULT_DOCS_ROOT,
    DEFAULT_RUNTIME_TRUST_CASES_PATH,
    _normalize_discrepancy_event,
    build_index,
    ingest_discrepancy_event,
    load_index,
    search_index,
    tokenize,
)
from .trust_policy import POLICY_MODE_SHADOW, compute_policy_decision

DEFAULT_CLOSURE_METRICS_PATH = "data/codex_graphrag/closure_metrics.json"


def _load_closure_metrics(path: str = DEFAULT_CLOSURE_METRICS_PATH) -> List[Dict[str, Any]]:
    try:
        p = Path(path)
        if not p.exists():
            return []
        data = json.loads(p.read_text(encoding="utf-8"))
        if isinstance(data, list):
            return [x for x in data if isinstance(x, dict)]
        return []
    except Exception:
        return []


def _save_closure_metrics(records: List[Dict[str, Any]], path: str = DEFAULT_CLOSURE_METRICS_PATH) -> None:
    p = Path(path)
    p.parent.mkdir(parents=True, exist_ok=True)
    p.write_text(json.dumps(records, indent=2), encoding="utf-8")


def trace_discrepancy(
    event: Dict[str, Any],
    index_path: str = DEFAULT_INDEX_PATH,
) -> Dict[str, Any]:
    """Phase 1: Trace lineage for a discrepancy event.

    Given a discrepancy event (metric, source_system, symptom),
    search the GraphRAG index for related nodes, then use lineage
    metadata from trust cases + the index to build a root-cause trace.

    Returns: {lineage_path, suspect_nodes, confidence, trace_method}
    """
    normalized = _normalize_discrepancy_event(event)
    metric = normalized.get("metric", "")
    symptom = normalized.get("symptom", "")
    source_system = normalized.get("source_system", "")

    query = f"{metric} {symptom} {source_system} reconciliation lineage"

    p = Path(index_path)
    if not p.exists():
        return {
            "lineage_path": [],
            "suspect_nodes": [],
            "confidence": 0.0,
            "trace_method": "no_index",
        }

    index = load_index(p)

    # Search for related trust cases (evidence)
    trust_results = search_index(
        index_path=index_path,
        query=query,
        k=5,
        expand_depth=2,
        category="evidence",
    )

    # Search for related tools/workflows
    tool_results = search_index(
        index_path=index_path,
        query=query,
        k=5,
        expand_depth=1,
    )

    # Build lineage path from trust case matches
    lineage_path = list(normalized.get("lineage_path", []) or [])
    suspect_nodes = []
    confidence = 0.0

    # Check existing trust cases for matching metric
    for case in index.get("trust_cases", []) or []:
        case_metric = str(case.get("metric", "")).lower()
        if metric.lower() in case_metric or case_metric in metric.lower():
            case_lineage = case.get("lineage_path", []) or []
            if case_lineage and not lineage_path:
                lineage_path = list(case_lineage)
            root_cause = case.get("root_cause", "")
            if root_cause and root_cause != "unknown":
                suspect_nodes.append({
                    "source": "trust_case",
                    "case_id": case.get("case_id", ""),
                    "root_cause": root_cause,
                })
                confidence = max(confidence, case.get("confidence", 0.5))

    # Add tool/workflow nodes from search
    for r in tool_results.get("results", [])[:3]:
        suspect_nodes.append({
            "source": "graph_search",
            "node_id": r.get("id", ""),
            "name": r.get("name", ""),
            "category": r.get("category", ""),
            "score": r.get("score", 0),
        })

    if suspect_nodes and confidence == 0.0:
        confidence = 0.3

    trace_method = "trust_case_match" if any(
        s.get("source") == "trust_case" for s in suspect_nodes
    ) else "graph_search"

    return {
        "lineage_path": lineage_path,
        "suspect_nodes": suspect_nodes,
        "confidence": round(confidence, 4),
        "trace_method": trace_method,
    }


def propose_fix(
    trace_result: Dict[str, Any],
    index_path: str = DEFAULT_INDEX_PATH,
    samples: Optional[Dict] = None,
    column_metadata: Optional[Dict] = None,
    cortex_client: Optional[Any] = None,
    erp_type: str = "UNKNOWN",
    event: Optional[Dict] = None,
    allow_cortex: bool = False,
) -> Dict[str, Any]:
    """Phase 2: Propose a fix using three-tier FixGenerator.

    Tier 1 (trust_replay): Replay proven fix from trust cases.
    Tier 2 (relationship_rag): Infer relationships + generate schema patch.
    Tier 3 (cortex_ai): Cortex AI fallback for non-schema issues.

    Returns: {fix_type, fix_detail, similar_cases, confidence, risk_tier, patch, validation_suite}
    """
    from src.autonomous_fix.fix_generator import generate_fix

    result = generate_fix(
        event=event or {},
        trace_result=trace_result,
        index_path=index_path,
        samples=samples,
        column_metadata=column_metadata,
        cortex_client=cortex_client,
        erp_type=erp_type,
        allow_cortex=allow_cortex,
    )
    return {
        "fix_type": result["fix_type"],
        "fix_detail": result["fix_detail"],
        "similar_cases": result["similar_cases"],
        "confidence": result["confidence"],
        "risk_tier": result.get("risk_tier", "medium"),
        "patch": result.get("patch"),
        "validation_suite": result.get("validation_suite"),
        "tier_path": result.get("tier_path", []),
        "decision_reasons": result.get("decision_reasons", []),
        "requires_human_approval": bool(result.get("requires_human_approval", False)),
    }


def verify_closure(
    event: Dict[str, Any],
    fix_result: Dict[str, Any],
    policy_mode: str = POLICY_MODE_SHADOW,
) -> Dict[str, Any]:
    """Phase 3: Verify the fix achieved closure.

    Checks if the discrepancy event has been resolved by comparing
    the original symptom against current state. Records the
    verification result.

    Returns: {verified, reconciliation_status, residual_risk}
    """
    normalized = _normalize_discrepancy_event(event)
    fix_type = fix_result.get("fix_type", "")
    fix_confidence = fix_result.get("confidence", 0.0)
    similar_cases = fix_result.get("similar_cases", [])

    approval_required = bool(
        fix_result.get("requires_human_approval", human_approval_required(fix_result.get("risk_tier", "high"), fix_type=fix_type))
    )
    policy = compute_policy_decision(
        trust_score=float(fix_confidence),
        risk_tier=str(fix_result.get("risk_tier", "unknown")),
        fix_type=str(fix_type),
        policy_mode=policy_mode,
    )
    if policy_mode == "enforce" and policy.get("policy_decision") == "manual_review_required":
        return {
            "verified": False,
            "reconciliation_status": "needs_manual_review",
            "residual_risk": "high",
            "closure_decision_reason": "policy_enforced_manual_review",
            "closure_policy_version": POLICY_VERSION,
            "approval_gate": "required",
            "policy_decision": policy.get("policy_decision"),
            "policy_reason_codes": policy.get("policy_reason_codes", []),
            "policy_mode": policy.get("policy_mode", policy_mode),
        }
    approval_gate = "required" if approval_required else "not_required"
    if should_mark_closure_achieved(fix_type, fix_confidence, len(similar_cases)):
        return {
            "verified": True,
            "reconciliation_status": "closure_achieved",
            "residual_risk": "low",
            "closure_decision_reason": "trusted_playbook_replay_with_high_confidence",
            "closure_policy_version": POLICY_VERSION,
            "approval_gate": approval_gate,
            "policy_decision": policy.get("policy_decision"),
            "policy_reason_codes": policy.get("policy_reason_codes", []),
            "policy_mode": policy.get("policy_mode", policy_mode),
        }

    if should_mark_closure_likely(fix_type, fix_confidence, len(similar_cases)):
        reason = "inferred_relationship_patch_requires_followup_validation"
        if fix_type in {"trust_replay", "replay_proven"}:
            reason = "trusted_playbook_replay_with_partial_confidence"
        return {
            "verified": True,
            "reconciliation_status": "closure_likely",
            "residual_risk": "medium",
            "closure_decision_reason": reason,
            "closure_policy_version": POLICY_VERSION,
            "approval_gate": approval_gate,
            "policy_decision": policy.get("policy_decision"),
            "policy_reason_codes": policy.get("policy_reason_codes", []),
            "policy_mode": policy.get("policy_mode", policy_mode),
        }

    # Tier 3: cortex_ai -> always needs manual review
    # no_fix -> always needs manual review
    return {
        "verified": False,
        "reconciliation_status": "needs_manual_review",
        "residual_risk": "high",
        "closure_decision_reason": "insufficient_automated_evidence_for_safe_auto_closure",
        "closure_policy_version": POLICY_VERSION,
        "approval_gate": approval_gate,
        "policy_decision": policy.get("policy_decision"),
        "policy_reason_codes": policy.get("policy_reason_codes", []),
        "policy_mode": policy.get("policy_mode", policy_mode),
    }


def run_closure_loop(
    event: Dict[str, Any],
    index_path: str = DEFAULT_INDEX_PATH,
    docs_root: str = DEFAULT_DOCS_ROOT,
    runtime_events_path: str = DEFAULT_RUNTIME_TRUST_CASES_PATH,
    max_retries: int = 1,
    auto_ingest: bool = True,
    metrics_path: str = DEFAULT_CLOSURE_METRICS_PATH,
    allow_cortex: bool = False,
    tenant_id: str = "default",
    ingest_mode: str = "dual",
    dedupe_window_seconds: int = 900,
    policy_mode: str = POLICY_MODE_SHADOW,
) -> Dict[str, Any]:
    """Full Sprinkler cycle: detect -> trace -> propose -> verify.

    Orchestrates the 3 phases. On success, ingests a completed
    trust case into the GraphRAG memory. On failure, ingests a
    partial trust case with status="needs_manual_review".

    Returns: {status, phases, trust_case_id}
    """
    if not isinstance(event, dict):
        return {"error": "event must be a JSON object"}

    start_time = _time.time()

    # Ensure index exists
    p = Path(index_path)
    if not p.exists():
        build_index(docs_root=docs_root, index_path=index_path)

    # Phase 1: Trace
    trace = trace_discrepancy(event, index_path=index_path)

    # Phase 2: Propose
    fix = propose_fix(trace, index_path=index_path, event=event, allow_cortex=allow_cortex)

    # Phase 3: Verify
    verification = verify_closure(event, fix, policy_mode=policy_mode)

    elapsed = round(_time.time() - start_time, 3)

    # Determine overall status
    status = str(verification.get("reconciliation_status", "needs_manual_review"))

    # Build enriched event for trust case ingestion
    normalized = _normalize_discrepancy_event(event)
    trust_case_id = None

    if auto_ingest:
        enriched_event = dict(event)
        enriched_event["root_cause"] = fix.get("fix_detail", "pending")
        enriched_event["fix"] = fix.get("fix_type", "pending")
        enriched_event["verification"] = verification.get("reconciliation_status", "pending")
        enriched_event["time_to_closure"] = f"{elapsed}s"
        enriched_event["confidence"] = fix.get("confidence", 0.0)
        tags = list(event.get("tags", []) or [])
        if "closure-loop" not in tags:
            tags.append("closure-loop")
        if status == "closure_achieved" and "auto-closed" not in tags:
            tags.append("auto-closed")
        enriched_event["tags"] = tags

        ingest_result = ingest_discrepancy_event(
            event=enriched_event,
            index_path=index_path,
            docs_root=docs_root,
            runtime_events_path=runtime_events_path,
            auto_build=False,
            tenant_id=tenant_id,
            ingest_mode=ingest_mode,
            dedupe_window_seconds=dedupe_window_seconds,
            policy_mode=policy_mode,
        )
        trust_case_id = ingest_result.get("case_id")

    # Record closure metrics
    _record_closure_metric(
        event=normalized,
        trace=trace,
        fix=fix,
        verification=verification,
        elapsed=elapsed,
        status=status,
        metrics_path=metrics_path,
        event_key=str(normalized.get("event_key", "")),
        tenant_id=str(normalized.get("tenant_id", tenant_id or "default")),
    )

    return {
        "status": status,
        "phases": {
            "trace": trace,
            "propose": fix,
            "verify": verification,
        },
        "approval_gate": verification.get("approval_gate", "required"),
        "closure_policy_version": verification.get("closure_policy_version", POLICY_VERSION),
        "trust_case_id": trust_case_id,
        "elapsed_seconds": elapsed,
    }


def _record_closure_metric(
    event: Dict[str, Any],
    trace: Dict[str, Any],
    fix: Dict[str, Any],
    verification: Dict[str, Any],
    elapsed: float,
    status: str,
    metrics_path: str = DEFAULT_CLOSURE_METRICS_PATH,
    event_key: str = "",
    tenant_id: str = "default",
) -> None:
    """Record a closure metric entry after each closure loop run."""
    records = _load_closure_metrics(metrics_path)
    records.append({
        "timestamp": int(_time.time()),
        "metric": event.get("metric", ""),
        "source_system": event.get("source_system", ""),
        "tenant_id": tenant_id or event.get("tenant_id", "default"),
        "event_key": event_key or event.get("event_key", ""),
        "status": status,
        "time_to_closure": elapsed,
        "fix_type": fix.get("fix_type", ""),
        "confidence": fix.get("confidence", 0.0),
        "verified": verification.get("verified", False),
        "trace_method": trace.get("trace_method", ""),
        "risk_tier": fix.get("risk_tier", "medium"),
        "policy_decision": verification.get("policy_decision", "unknown"),
        "policy_mode": verification.get("policy_mode", "shadow"),
    })
    _save_closure_metrics(records, metrics_path)


def get_closure_metrics(metrics_path: str = DEFAULT_CLOSURE_METRICS_PATH) -> Dict[str, Any]:
    """Aggregate closure metrics for dashboard display."""
    records = _load_closure_metrics(metrics_path)
    if not records:
        return {
            "total_runs": 0,
            "auto_closed_count": 0,
            "manual_review_count": 0,
            "reconciliation_pass_rate": 0.0,
            "avg_time_to_closure": 0.0,
            "playbook_reuse_rate": 0.0,
            "records": [],
        }

    total = len(records)
    auto_closed = sum(1 for r in records if r.get("verified") is True)
    manual_review = total - auto_closed
    avg_time = sum(r.get("time_to_closure", 0) for r in records) / max(total, 1)
    replay_proven = sum(1 for r in records if r.get("fix_type") in ("replay_proven", "trust_replay"))
    reuse_rate = (replay_proven / max(total, 1)) * 100.0

    return {
        "total_runs": total,
        "auto_closed_count": auto_closed,
        "manual_review_count": manual_review,
        "reconciliation_pass_rate": round((auto_closed / max(total, 1)) * 100.0, 2),
        "avg_time_to_closure": round(avg_time, 3),
        "playbook_reuse_rate": round(reuse_rate, 2),
        "records": records,
    }
