# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
# See LICENSE file for details.
"""D19: EchoRefine — Prompt Echo Filter + Golden Path Cross-Ref."""

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

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

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

METRICS_FILE = STATE_DIR / "health" / "echo_refine_metrics.json"
_CONFLICT_PENALTY = 0.15   # confidence reduction for constraint-violating items
_GOLDEN_BOOST = 0.10       # confidence boost for golden-path-aligned items
_STALE_HOURS = 6           # scan entries from last 6h


_JSONL_TO_TABLE = {
    str(LESSONS_JSONL): "lessons",
    str(FAILS_JSONL): "failures",
    str(PATTERNS_JSONL): "patterns",
}


def _load_recent(jsonl_path: Path, since: str) -> list[dict]:
    """Load entries newer than `since` ISO timestamp — SQLite-first, JSONL fallback."""
    table = _JSONL_TO_TABLE.get(str(jsonl_path))
    if table:
        try:
            from pulse.core.brain_db import get_conn
            conn = get_conn()
            rows = conn.execute(
                f"SELECT * FROM {table} WHERE timestamp >= ? ORDER BY timestamp",
                (since,),
            ).fetchall()
            if rows:
                entries = []
                for r in rows:
                    item = dict(r)
                    item["_src"] = str(jsonl_path)
                    entries.append(item)
                return entries
        except Exception:
            pass
    # Fallback: JSONL
    if not jsonl_path.exists():
        return []
    entries = []
    try:
        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)
                    if item.get("timestamp", "") >= since:
                        item["_src"] = str(jsonl_path)
                        entries.append(item)
                except json.JSONDecodeError:
                    continue
    except OSError:
        pass
    return entries


def _text_overlap(text_words: set, phrase_words: set, threshold: float = 0.6) -> bool:
    """Check if word overlap exceeds threshold."""
    return bool(phrase_words) and len(phrase_words & text_words) / len(phrase_words) >= threshold


def _cross_ref_golden(entries: list[dict], golden: list[dict]) -> tuple[list[dict], int]:
    """Boost entries aligned with golden path steps. Returns (entries, aligned_count)."""
    if not golden:
        return entries, 0
    steps = set()
    for gp in golden:
        for s in gp.get("steps", []):
            words = s.lower().split()
            if len(words) >= 3:
                steps.add(" ".join(words[:5]))
    aligned = 0
    for e in entries:
        content_words = set((e.get("content") or e.get("title") or "").lower().split())
        if not content_words:
            continue
        for sp in steps:
            if _text_overlap(content_words, set(sp.split())):
                e["confidence"] = min(1.0, e.get("confidence", 0.5) + _GOLDEN_BOOST)
                e["_modified"] = True
                aligned += 1
                break
    return entries, aligned


def _cross_ref_constraints(entries: list[dict], constraints: list[dict]) -> tuple[list[dict], int]:
    """Penalize entries that conflict with active constraints. Returns (entries, conflict_count)."""
    if not constraints:
        return entries, 0
    phrases = [set(c.get("constraint", "").lower().split())
               for c in constraints if len(c.get("constraint", "")) >= 10]
    conflicts = 0
    for e in entries:
        content_words = set((e.get("content") or e.get("title") or "").lower().split())
        if not content_words:
            continue
        for phr in phrases:
            if _text_overlap(content_words, phr, 0.5):
                e["confidence"] = max(0.1, e.get("confidence", 0.5) - _CONFLICT_PENALTY)
                e["_modified"] = True
                conflicts += 1
                break
    return entries, conflicts


def _calibrate(metrics: dict) -> dict:
    """Adjust filter sensitivity from task outcomes."""
    tb = STATE_DIR / "task_board.json"
    if not tb.exists():
        return metrics
    try:
        data = json.loads(tb.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError):
        return metrics
    stats: dict[str, dict] = {}
    for d in data.get("dispatches", []):
        for t in d.get("tasks", []):
            if t.get("status") != "done":
                continue
            dom = t.get("domain", "general")
            if dom not in stats:
                stats[dom] = {"ok": 0, "fail": 0}
            if "POISON_PILL" in (t.get("outcome") or ""):
                stats[dom]["fail"] += 1
            else:
                stats[dom]["ok"] += 1
    sens = metrics.get("domain_sensitivity", {})
    for dom, s in stats.items():
        total = s["ok"] + s["fail"]
        if total < 3:
            continue
        rate = s["ok"] / total
        sens[dom] = "tight" if rate < 0.5 else ("relaxed" if rate > 0.8 else "normal")
    metrics["domain_sensitivity"] = sens
    return metrics


def _apply_modifications(entries: list[dict]):
    """Rewrite modified entries back to their source JSONL files."""
    from pulse.core.brain_io import _acquire
    by_file: dict[str, dict[str, dict]] = {}
    for e in entries:
        if not e.get("_modified"):
            continue
        src = e.pop("_src", "")
        e.pop("_modified", None)
        eid = e.get("id", "")
        if src and eid:
            by_file.setdefault(src, {})[eid] = e

    for fpath_str, mod_map in by_file.items():
        fpath = Path(fpath_str)
        if not fpath.exists():
            continue
        with _acquire(fpath):
            lines = []
            try:
                with open(fpath, "r", encoding="utf-8") as f:
                    for raw in f:
                        raw = raw.strip()
                        if not raw:
                            continue
                        try:
                            item = json.loads(raw)
                            eid = item.get("id", "")
                            if eid in mod_map:
                                item["confidence"] = mod_map[eid].get("confidence", item.get("confidence", 0.5))
                            lines.append(json.dumps(item, ensure_ascii=False))
                        except json.JSONDecodeError:
                            lines.append(raw)
                with open(fpath, "w", encoding="utf-8") as f:
                    f.write("\n".join(lines) + "\n" if lines else "")
            except OSError:
                continue


def _load_metrics() -> dict:
    if not METRICS_FILE.exists():
        return {"last_run": "", "runs": 0, "entries_scanned": 0,
                "golden_aligned": 0, "constraint_conflicts": 0, "domain_sensitivity": {}}
    try:
        return json.loads(METRICS_FILE.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError):
        return {"last_run": "", "runs": 0, "entries_scanned": 0,
                "golden_aligned": 0, "constraint_conflicts": 0, "domain_sensitivity": {}}


def _save_metrics(metrics: 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(metrics, 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 EchoRefine synthesis cycle."""
    metrics = _load_metrics()
    now = datetime.now(timezone.utc)
    since = metrics.get("last_run") or (now - timedelta(hours=_STALE_HOURS)).isoformat()

    # Load recent entries from all brain files
    entries = []
    for p in [LESSONS_JSONL, FAILS_JSONL, PATTERNS_JSONL]:
        entries.extend(_load_recent(p, since))
    if not entries:
        if verbose:
            _log.info("No new brain entries since last run")
        metrics["last_run"] = now.isoformat()
        _save_metrics(metrics)
        return {"entries": 0, "status": "no_data"}

    if verbose:
        _log.info("Scanning %d recent brain entries", len(entries))

    # Load golden paths + constraints
    try:
        from pulse.daemons.synthesis.pathweaver.pathweaver import get_golden_paths
        golden = get_golden_paths(max_paths=10)
    except Exception:
        golden = []
    try:
        from pulse.daemons.synthesis.planning.constraint_engine import get_active_constraints
        constraints = get_active_constraints()
    except Exception:
        constraints = []

    # Cross-reference
    entries, aligned = _cross_ref_golden(entries, golden)
    entries, conflicts = _cross_ref_constraints(entries, constraints)
    if verbose:
        _log.info("Golden-aligned: %d, Constraint-conflicts: %d", aligned, conflicts)

    # Self-calibrate + apply
    metrics = _calibrate(metrics)
    _apply_modifications(entries)

    # Update metrics
    metrics["last_run"] = now.isoformat()
    metrics["runs"] = metrics.get("runs", 0) + 1
    metrics["entries_scanned"] = metrics.get("entries_scanned", 0) + len(entries)
    metrics["golden_aligned"] = metrics.get("golden_aligned", 0) + aligned
    metrics["constraint_conflicts"] = metrics.get("constraint_conflicts", 0) + conflicts
    _save_metrics(metrics)

    return {"entries": len(entries), "golden_aligned": aligned,
            "constraint_conflicts": conflicts, "golden_paths": len(golden),
            "constraints": len(constraints), "status": "ok"}


def cmd_echorefine(args: list):
    """CLI entry point for `pulse echorefine`."""
    logging.basicConfig(level=logging.INFO, format="%(message)s")
    if args and args[0] == "status":
        m = _load_metrics()
        print(f"[D19] EchoRefine — Runs: {m.get('runs', 0)}, "
              f"Scanned: {m.get('entries_scanned', 0)}, "
              f"Aligned: {m.get('golden_aligned', 0)}, "
              f"Conflicts: {m.get('constraint_conflicts', 0)}")
        sens = m.get("domain_sensitivity", {})
        if sens:
            print(f"  Sensitivity: {json.dumps(sens)}")
        return
    verbose = "--verbose" in args or "-v" in args or "--dry-run" in args
    result = run_once(verbose=True)
    print(json.dumps(result, indent=2))


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