#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
# See LICENSE file for details.
"""Anti-Pattern Gate: blocking PreToolUse hook for Claude Code and Gemini.

Runs 5 gates: AP FTS5, failure scan, prediction alerts, decision conflict, temporal prereq.
NEVER crash (fail-open), NEVER call LLMs, < 1.5s total.
- Must handle empty stdin (UserPromptSubmit, no content).
"""

import json
import os
import sqlite3
import sys
import time
from pathlib import Path

if os.name == "nt" and __name__ == "__main__":
    import io
    if hasattr(sys.stdout, 'reconfigure'):
        sys.stdout.reconfigure(encoding='utf-8', errors='replace')
    if hasattr(sys.stderr, 'reconfigure'):
        sys.stderr.reconfigure(encoding='utf-8', errors='replace')

_SCRIPT_DIR = Path(__file__).resolve().parent
_ROOT_DIR = _SCRIPT_DIR.parent.parent.parent
if str(_ROOT_DIR) not in sys.path:
    sys.path.insert(0, str(_ROOT_DIR))

# Timing budget: abort remaining gates after this many seconds elapsed
_BUDGET_SECONDS = 1.4

# Gate 3 threshold
_PREDICTION_CONFIDENCE_THRESHOLD = 0.9

# ── Import gates 1, 2, 4, 5 from gate_checks ──────────────────────────────────
from pulse.brain.integrity.gate_checks import (  # noqa: E402
    gate1_anti_pattern,
    gate2_failure_scan,
    gate4_decision_conflict,
    gate5_temporal_advisory,
    _keyword_overlap,
    _extract_keywords as _keywords_from_content,
    _query_anti_patterns,
    _resolve_db_path as _get_db_path,
    _format_ap_block,
)

# Underscore aliases expected by existing tests
_gate1_anti_pattern = gate1_anti_pattern
_gate2_failure_scan = gate2_failure_scan
_gate4_decision_conflict = gate4_decision_conflict
_gate5_temporal_advisory = gate5_temporal_advisory

# ── Input extraction helpers ───────────────────────────────────────────────────

def _extract_file_path(hook_input: dict) -> str:
    tool_input = hook_input.get("tool_input", {})
    for key in ("file_path", "path"):
        val = tool_input.get(key, "")
        if val:
            return str(val)
    for key in ("file_path", "path"):
        val = hook_input.get(key, "")
        if val:
            return str(val)
    return ""


def _extract_content(hook_input: dict) -> str:
    """Edit -> new_string, Write -> content."""
    tool_input = hook_input.get("tool_input", {})
    v = tool_input.get("new_string", "")
    if v:
        return str(v)
    v = tool_input.get("content", "")
    if v:
        return str(v)
    return ""


# ── Gate 3: Oracle prediction alerts (defined here so tests can patch _load) ───

def _load_prediction_alerts() -> list:
    """Load alerts from state/prediction_alerts.json or SQLite predictions table."""
    try:
        from pulse.core.paths import get_state_dir
        state_dir = get_state_dir()
    except Exception:
        state_dir = _ROOT_DIR / "state"
    alerts_file = state_dir / "prediction_alerts.json"
    if alerts_file.exists():
        try:
            data = json.loads(alerts_file.read_text(encoding="utf-8"))
            if isinstance(data, dict):
                return data.get("alerts", [])
            if isinstance(data, list):
                return data
        except Exception:
            pass
    # Fallback: SQLite predictions table
    try:
        db_path = _get_db_path()
        if db_path.exists():
            conn = sqlite3.connect(str(db_path), timeout=3)
            conn.row_factory = sqlite3.Row
            try:
                rows = conn.execute(
                    "SELECT content, confidence, domain, metadata FROM predictions "
                    "WHERE confidence >= ? LIMIT 50",
                    (_PREDICTION_CONFIDENCE_THRESHOLD,),
                ).fetchall()
                alerts = [
                    {
                        "prediction": r["content"] or "",
                        "confidence": r["confidence"] or 0.0,
                        "status": "active",
                        "risk_components": [],
                    }
                    for r in rows
                ]
                conn.close()
                return alerts
            except Exception:
                conn.close()
    except Exception:
        pass
    return []


def _gate3_prediction_alert(file_path: str, keywords: list) -> tuple:
    """Return (blocked, alert_prediction_text_or_None)."""
    try:
        alerts = _load_prediction_alerts()
        fp_lower = file_path.lower() if file_path else ""
        for alert in alerts:
            if alert.get("status") != "active":
                continue
            confidence = float(alert.get("confidence") or 0.0)
            if confidence < _PREDICTION_CONFIDENCE_THRESHOLD:
                continue
            prediction = alert.get("prediction", "")
            components = alert.get("risk_components", [])
            matched = False
            for comp in components:
                comp_stem = comp.replace(".py", "").replace(".js", "").lower()
                if comp_stem and comp_stem in fp_lower:
                    matched = True
                    break
            if not matched and keywords:
                matched = _keyword_overlap(prediction, keywords)
            if matched:
                return True, prediction
        return False, None
    except Exception:
        return False, None

# Public alias for callers that use the unprefixed name
gate3_prediction_alert = _gate3_prediction_alert

# ── Orchestrator ───────────────────────────────────────────────────────────────

def _check_gate(hook_input: dict) -> tuple:
    """Run all gates. Returns (blocked, message_or_None, advisories_list)."""
    content = _extract_content(hook_input)
    if not content:
        return False, None, []
    keywords = _keywords_from_content(content)
    if not keywords:
        return False, None, []

    file_path = _extract_file_path(hook_input)

    # Skip all gates for non-code files (docs, reports, data).
    # The gate guards against bad code patterns, not documentation content.
    _NON_CODE_EXT = (
        ".md", ".txt", ".rst", ".yaml", ".yml", ".toml", ".cfg", ".ini",
        ".tsx", ".jsx", ".ts", ".js", ".css", ".html", ".svg", ".graphql",
    )
    if file_path and any(file_path.lower().endswith(ext) for ext in _NON_CODE_EXT):
        return False, None, []

    advisories: list = []
    t0 = time.monotonic()

    try:
        db_path = _get_db_path()
    except Exception:
        return False, None, []

    # Gate 1: Anti-pattern FTS5
    try:
        blocked, msg = _gate1_anti_pattern(keywords, content, db_path, file_path)
        if blocked and msg:
            if file_path and file_path not in msg:
                msg = msg.replace(
                    "BRAIN BLOCK: Anti-pattern detected",
                    f"BRAIN BLOCK: Anti-pattern detected in {file_path}",
                    1,
                )
            return True, msg, []
    except Exception:
        pass
    if time.monotonic() - t0 > _BUDGET_SECONDS:
        return False, None, advisories

    # Gate 2: High-confidence failure scan
    try:
        blocked, failure_text = _gate2_failure_scan(keywords, db_path, content=content, file_path=file_path)
        if blocked:
            msg = (
                f"BRAIN BLOCK: Known high-confidence failure matches this code\n"
                f"Failure: {(failure_text or '')[:200]}\n"
                f"Action: Review known failures before proceeding."
            )
            return True, msg, []
    except Exception:
        pass
    if time.monotonic() - t0 > _BUDGET_SECONDS:
        return False, None, advisories

    # Gate 3: Oracle prediction alerts
    try:
        blocked, prediction_text = _gate3_prediction_alert(file_path, keywords)
        if blocked:
            msg = (
                f"BRAIN BLOCK: Oracle predicts failure in this domain\n"
                f"Prediction: {(prediction_text or '')[:200]}\n"
                f"Action: Address predicted failure before writing this code."
            )
            return True, msg, []
    except Exception:
        pass
    if time.monotonic() - t0 > _BUDGET_SECONDS:
        return False, None, advisories

    # Gate 4: Decision conflict
    try:
        blocked, decision_text = _gate4_decision_conflict(keywords, db_path)
        if blocked:
            msg = (
                f"BRAIN BLOCK: This conflicts with architectural decision\n"
                f"Decision: {(decision_text or '')[:200]}\n"
                f"Action: Review contested decision log before proceeding."
            )
            return True, msg, []
    except Exception:
        pass
    if time.monotonic() - t0 > _BUDGET_SECONDS:
        return False, None, advisories

    # Gate 5: Temporal advisory (never blocks)
    try:
        advisories = _gate5_temporal_advisory(keywords, db_path)
    except Exception:
        advisories = []

    return False, None, advisories


def main() -> None:
    try:
        raw = sys.stdin.read()
    except Exception:
        sys.exit(0)
    if not raw or not raw.strip():
        sys.exit(0)
    try:
        hook_input = json.loads(raw)
    except (json.JSONDecodeError, ValueError):
        sys.exit(0)
    try:
        blocked, message, advisories = _check_gate(hook_input)
    except Exception:
        sys.exit(0)
    if blocked and message:
        sys.stderr.write(message + "\n")
        sys.stderr.flush()
        sys.exit(2)
    if advisories:
        advice_lines = "\n".join(f"  - {a}" for a in advisories)
        sys.stderr.write(
            f"BRAIN ADVISORY: Temporal prerequisites detected:\n{advice_lines}\n"
        )
        sys.stderr.flush()
    sys.exit(0)


if __name__ == "__main__":
    main()
