# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
# See LICENSE file for details.
"""
D23: Cognitive Contention Cortex — Lock Contention Self-Repair.

Reads file-lock contention data (brain_io.py records waits >500ms),
identifies hotspot brain files, and triggers targeted dedup on those
regions. The more agents fight over a file, the faster it consolidates.

Source: DMN session_20260311T162910 | Usage: pulse contention
"""

import json
import logging
import time
from datetime import datetime, timezone
from pathlib import Path

_log = logging.getLogger("pulse.d23_contention")

from pulse.core.brain_utils import (
    STATE_DIR, LESSONS_JSONL, FAILS_JSONL, PATTERNS_JSONL,
)

CONTENTION_FILE = STATE_DIR / "health" / "contention_tracker.json"
METRICS_FILE = STATE_DIR / "health" / "contention_cortex.json"

_HOTSPOT_MIN_WAITS = 5       # need N waits before flagging
_HOTSPOT_MIN_AVG_WAIT = 1.0  # avg wait seconds to qualify
_MAX_CONSOLIDATE = 3         # max files to consolidate per cycle

# Map filenames to their JSONL brain paths
_BRAIN_FILE_MAP = {}


def _build_file_map() -> dict[str, Path]:
    """Map contention filenames to actual JSONL paths."""
    if _BRAIN_FILE_MAP:
        return _BRAIN_FILE_MAP
    for path in (LESSONS_JSONL, FAILS_JSONL, PATTERNS_JSONL):
        _BRAIN_FILE_MAP[path.name] = path
    return _BRAIN_FILE_MAP


def _load_contention() -> dict:
    """Read lock contention tracker data."""
    if not CONTENTION_FILE.exists():
        return {}
    try:
        return json.loads(CONTENTION_FILE.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError):
        return {}


def _identify_hotspots(contention: dict) -> list[dict]:
    """Rank files by contention severity. Returns sorted hotspot list."""
    hotspots = []
    for fname, data in contention.items():
        waits = data.get("total_waits", 0)
        total_s = data.get("total_seconds", 0.0)
        if waits < _HOTSPOT_MIN_WAITS:
            continue
        avg = total_s / waits if waits > 0 else 0
        if avg < _HOTSPOT_MIN_AVG_WAIT:
            continue
        hotspots.append({
            "file": fname,
            "waits": waits,
            "total_seconds": round(total_s, 2),
            "avg_wait": round(avg, 3),
            "max_wait": data.get("max_wait", 0),
            "severity": round(waits * avg, 2),
        })
    hotspots.sort(key=lambda h: h["severity"], reverse=True)
    return hotspots


def _targeted_dedup(jsonl_path: Path, verbose: bool = False) -> int:
    """Dedup a single JSONL file by fingerprint. Returns entries removed."""
    if not jsonl_path.exists():
        return 0
    try:
        entries = []
        seen_fps = set()
        removed = 0
        with open(jsonl_path, "r", encoding="utf-8") as f:
            for line in f:
                line = line.strip()
                if not line:
                    continue
                try:
                    item = json.loads(line)
                except json.JSONDecodeError:
                    continue
                fp = item.get("fingerprint", "")
                if fp and fp in seen_fps:
                    removed += 1
                    continue
                if fp:
                    seen_fps.add(fp)
                entries.append(item)
        if removed > 0:
            # SQLite-first: delete duplicate fingerprints keeping earliest row
            try:
                from pulse.brain.save_writers.jsonl_store import _jsonl_to_table
                from pulse.core.brain_db import get_conn
                table = _jsonl_to_table(jsonl_path)
                if table:
                    conn = get_conn()
                    with conn:
                        conn.execute(
                            f"DELETE FROM {table} WHERE rowid NOT IN "
                            f"(SELECT MIN(rowid) FROM {table} GROUP BY fingerprint)"
                        )
            except Exception:
                pass  # SQLite unavailable — JSONL rewrite below is the fallback
            from pulse.brain.save_writers import rewrite_jsonl
            rewrite_jsonl(jsonl_path, entries)
            if verbose:
                _log.info("  Deduped %s: removed %d duplicates", jsonl_path.name, removed)
        return removed
    except Exception as e:
        _log.warning("Targeted dedup failed for %s: %s", jsonl_path.name, e)
        return 0


def _load_state() -> dict:
    if not METRICS_FILE.exists():
        return {"runs": 0, "consolidations": 0, "hotspots_detected": 0}
    try:
        return json.loads(METRICS_FILE.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError):
        return {"runs": 0, "consolidations": 0, "hotspots_detected": 0}


def _save_state(state: dict):
    import os
    METRICS_FILE.parent.mkdir(parents=True, exist_ok=True)
    tmp = METRICS_FILE.with_suffix(".tmp")
    try:
        with open(tmp, "w", encoding="utf-8") as f:
            json.dump(state, f, indent=2, ensure_ascii=False)
        os.replace(str(tmp), str(METRICS_FILE))
    except OSError:
        if tmp.exists():
            tmp.unlink()


def run_once(verbose: bool = False) -> dict:
    """Run one contention analysis cycle: scan → identify → consolidate."""
    contention = _load_contention()
    if not contention:
        if verbose:
            _log.info("No contention data available")
        return {"hotspots": 0, "consolidated": 0, "status": "no_data"}

    hotspots = _identify_hotspots(contention)
    if not hotspots:
        if verbose:
            _log.info("No hotspots detected (below thresholds)")
        state = _load_state()
        state["runs"] = state.get("runs", 0) + 1
        state["last_run"] = datetime.now(timezone.utc).isoformat()
        _save_state(state)
        return {"hotspots": 0, "consolidated": 0, "status": "below_threshold"}

    if verbose:
        _log.info("Detected %d hotspot(s)", len(hotspots))

    # Consolidate brain JSONL hotspots
    file_map = _build_file_map()
    consolidated = 0
    total_removed = 0
    for hs in hotspots[:_MAX_CONSOLIDATE]:
        fname = hs["file"]
        if fname not in file_map:
            if verbose:
                _log.info("  [SKIP] %s — not a brain JSONL file", fname)
            continue
        if verbose:
            _log.info("  [DEDUP] %s (severity=%.1f, waits=%d, avg=%.2fs)",
                      fname, hs["severity"], hs["waits"], hs["avg_wait"])
        removed = _targeted_dedup(file_map[fname], verbose)
        if removed > 0:
            consolidated += 1
            total_removed += removed

    # Update state
    state = _load_state()
    state["runs"] = state.get("runs", 0) + 1
    state["consolidations"] = state.get("consolidations", 0) + consolidated
    state["hotspots_detected"] = state.get("hotspots_detected", 0) + len(hotspots)
    state["last_run"] = datetime.now(timezone.utc).isoformat()
    state["last_hotspots"] = hotspots[:10]
    state["total_entries_removed"] = state.get("total_entries_removed", 0) + total_removed
    _save_state(state)

    return {"hotspots": len(hotspots), "consolidated": consolidated,
            "entries_removed": total_removed, "status": "ok"}


def cmd_contention(args: list):
    """CLI entry point for `pulse contention`."""
    logging.basicConfig(level=logging.INFO, format="%(message)s")
    if args and args[0] == "status":
        s = _load_state()
        print(f"[D23] Contention Cortex — Runs: {s.get('runs', 0)}, "
              f"Consolidations: {s.get('consolidations', 0)}, "
              f"Entries removed: {s.get('total_entries_removed', 0)}")
        hs = s.get("last_hotspots", [])
        if hs:
            print(f"  Top hotspots:")
            for h in hs[:5]:
                print(f"    {h['file']}: {h['waits']} waits, "
                      f"avg {h['avg_wait']}s, severity {h['severity']}")
        print(f"  Last run: {s.get('last_run', 'never')}")
        return
    result = run_once(verbose=True)
    print(json.dumps(result, indent=2))


if __name__ == "__main__":
    import sys
    cmd_contention(sys.argv[1:])
