# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
# See LICENSE file for details.
"""
D25: Intent Topology Daemon — Goal Dependency DAG.

Builds a directed acyclic graph of user intents using causal edge
extraction, then topological-sorts for dependency-optimal execution.
Marks superseded and contradicted intents. Tasks execute in causal order.

Source: DMN session_20260311T163020 | Usage: pulse topology
"""

import json
import logging
from collections import deque
from datetime import datetime, timezone
from pathlib import Path

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

try:
    from pulse.core.brain_utils import STATE_DIR, INTENTS_DIR
except Exception:
    STATE_DIR = Path("state")
    INTENTS_DIR = Path("Hippocampus/Intents")

METRICS_FILE = STATE_DIR / "health" / "intent_topology.json"
_MIN_PRIORITY = 9          # only process wants with priority >= this
_MIN_TEXT_LEN = 15         # skip tiny intents
_MAX_WANTS = 200           # cap wants to prevent O(n²) explosion
_OVERLAP_SUPERSEDE = 0.6   # word overlap threshold for supersession


def _load_intents() -> list[dict]:
    """Load high-priority wants + strategic intents (scoped to avoid 20M pairs)."""
    intents = []
    # Strategic intents (compact, ~270)
    si_path = INTENTS_DIR / "strategic_intents.json"
    if si_path.exists():
        try:
            data = json.loads(si_path.read_text(encoding="utf-8"))
            for item in data.get("intents", []):
                title = item.get("title", "")
                if len(title) >= _MIN_TEXT_LEN:
                    intents.append({
                        "id": f"SI-{len(intents):04d}",
                        "text": title,
                        "type": "strategic",
                        "priority": 8,
                        "domain": item.get("domain", "general"),
                    })
        except (json.JSONDecodeError, OSError):
            pass
    # High-priority wants from SQLite (JSON Kill List migration)
    try:
        from pulse.core.brain_db import get_wants
        items = get_wants()
        items.sort(key=lambda x: x.get("priority", 1), reverse=True)
        for item in items[:_MAX_WANTS * 5]:
            if len(intents) >= _MAX_WANTS + 300:
                break
            pri = item.get("priority", 1)
            text = item.get("want", "")
            if pri >= _MIN_PRIORITY and len(text) >= _MIN_TEXT_LEN:
                intents.append({
                    "id": f"W-{len(intents):04d}",
                    "text": text[:200],
                    "type": "want",
                    "priority": pri,
                    "domain": item.get("domain", "general"),
                })
    except Exception:
        pass
    return intents


def _build_dag(intents: list[dict]) -> tuple[dict, list[dict]]:
    """Extract causal edges between intents. Returns (adjacency, edges)."""
    try:
        from pulse.graph.graph_parser import extract_causal_edges
    except ImportError:
        return {}, []

    adj: dict[str, list[str]] = {i["id"]: [] for i in intents}
    edges = []
    intent_map = {i["id"]: i for i in intents}

    # Only compare intents in the same or related domains
    by_domain: dict[str, list[dict]] = {}
    for i in intents:
        by_domain.setdefault(i["domain"], []).append(i)
        by_domain.setdefault("general", []).append(i)

    seen_pairs = set()
    for domain_intents in by_domain.values():
        for i, a in enumerate(domain_intents):
            for b in domain_intents[i + 1:]:
                pair = (a["id"], b["id"]) if a["id"] < b["id"] else (b["id"], a["id"])
                if pair in seen_pairs:
                    continue
                seen_pairs.add(pair)
                combined = a["text"] + ". " + b["text"]
                causal = extract_causal_edges(combined)
                for edge in causal:
                    etype = edge.get("type", "")
                    if etype in ("REQUIRES", "ENABLES", "LEADS_TO"):
                        adj.setdefault(b["id"], []).append(a["id"])
                        edges.append({"from": a["id"], "to": b["id"], "type": etype})
                    elif etype == "PREVENTS":
                        edges.append({"from": a["id"], "to": b["id"], "type": "CONTRADICTS"})
    return adj, edges


def _topological_sort(adj: dict) -> tuple[list[str], int]:
    """Kahn's algorithm. Returns (sorted_ids, cycles_broken)."""
    in_degree = {n: 0 for n in adj}
    for deps in adj.values():
        for d in deps:
            in_degree[d] = in_degree.get(d, 0) + 1

    queue = deque(n for n, deg in in_degree.items() if deg == 0)
    order = []
    cycles = 0

    while queue:
        node = queue.popleft()
        order.append(node)
        for dep in adj.get(node, []):
            in_degree[dep] -= 1
            if in_degree[dep] == 0:
                queue.append(dep)

    # Handle cycles: remaining nodes have unresolved deps
    remaining = set(adj.keys()) - set(order)
    if remaining:
        cycles = len(remaining)
        order.extend(sorted(remaining))
    return order, cycles


def _detect_superseded(intents: list[dict]) -> list[str]:
    """Find intents superseded by newer same-domain intents."""
    superseded = []
    # Check dont_wants against wants
    dont_texts = set()
    try:
        from pulse.core.brain_db import get_dont_wants
        for item in get_dont_wants():
            dont_texts.add(item.get("dont_want", "").lower()[:100])
    except Exception:
        pass
    for intent in intents:
        words_i = set(intent["text"].lower().split())
        if len(words_i) < 3:
            continue
        for dw in dont_texts:
            words_d = set(dw.split())
            if not words_d:
                continue
            overlap = len(words_i & words_d) / min(len(words_i), len(words_d))
            if overlap >= _OVERLAP_SUPERSEDE:
                superseded.append(intent["id"])
                break
    return superseded


def _load_state() -> dict:
    if not METRICS_FILE.exists():
        return {"runs": 0, "total_intents": 0, "total_edges": 0}
    try:
        return json.loads(METRICS_FILE.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError):
        return {"runs": 0, "total_intents": 0, "total_edges": 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 topology cycle: load → DAG → sort → supersede."""
    intents = _load_intents()
    if not intents:
        if verbose:
            _log.info("No intents to process")
        return {"intents": 0, "status": "no_data"}

    if verbose:
        _log.info("Processing %d intents (strategic + priority>=%d wants)",
                  len(intents), _MIN_PRIORITY)

    adj, edges = _build_dag(intents)
    order, cycles = _topological_sort(adj)
    superseded = _detect_superseded(intents)

    if verbose:
        _log.info("DAG: %d edges, %d cycles broken, %d superseded",
                  len(edges), cycles, len(superseded))

    state = _load_state()
    state["runs"] = state.get("runs", 0) + 1
    state["last_run"] = datetime.now(timezone.utc).isoformat()
    state["total_intents"] = len(intents)
    state["total_edges"] = len(edges)
    state["cycles_broken"] = cycles
    state["superseded"] = superseded[:50]
    state["execution_order"] = order[:100]
    state["edges"] = edges[:100]
    _save_state(state)

    return {"intents": len(intents), "edges": len(edges),
            "cycles": cycles, "superseded": len(superseded), "status": "ok"}


def cmd_topology(args: list):
    """CLI entry point for `pulse topology`."""
    logging.basicConfig(level=logging.INFO, format="%(message)s")
    if args and args[0] == "status":
        s = _load_state()
        print(f"[D25] Intent Topology — Runs: {s.get('runs', 0)}, "
              f"Intents: {s.get('total_intents', 0)}, "
              f"Edges: {s.get('total_edges', 0)}")
        print(f"  Cycles broken: {s.get('cycles_broken', 0)}, "
              f"Superseded: {len(s.get('superseded', []))}")
        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_topology(sys.argv[1:])
