#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
Precognitive Constraint Engine — Brain V8 Phase 4 (D4)

Reads long-term WANTS + current open tasks, uses LLM to simulate
conflicts, generates synthetic DON'T_WANTS as architectural guardrails.
Injected into every agent's boot briefing to prevent today's code
from creating tomorrow's debt.

Scheduled by orchestrator every 6h. Also runnable: --once --verbose
"""
from __future__ import annotations

import hashlib
import json
import logging
import os
import sys
import time
from datetime import datetime, timezone, timedelta
from pathlib import Path

if os.name == "nt" and __name__ == "__main__":
    sys.stdout.reconfigure(encoding="utf-8")
    sys.stderr.reconfigure(encoding="utf-8")

ROOT_DIR = Path(__file__).resolve().parents[3]
if str(ROOT_DIR) not in sys.path:
    sys.path.insert(0, str(ROOT_DIR))

from pulse.core.brain_utils import (
    load_json, load_json_dict, save_json,
    EVIDENCE_DIR, STATE_DIR, TASK_BOARD_FILE,
    now_iso as _now_iso,
)
from pulse.core.brain_io import _acquire

log = logging.getLogger("pulse.constraint_engine")

CONSTRAINTS_FILE = STATE_DIR / "synthetic_constraints.json"
WANTS_FILE = EVIDENCE_DIR / "Intents" / "wants.json"
POLL_INTERVAL = 6 * 3600  # 6 hours
DECAY_DAYS = 7
DECAY_CONFIDENCE_FLOOR = 0.4
MAX_ACTIVE = 20
MIN_LONG_TERM_WANTS = 3
CONFIDENCE_BUMP = 0.05
CONFIDENCE_CAP = 0.95

# --- LLM Prompt ---

CONSTRAINT_PROMPT = """You are the PULSE Precognitive Constraint Engine — a forward-looking technical debt prevention system.

LONG-TERM USER GOALS (the user's future aspirations):
{wants}

CURRENT OPEN TASKS (what agents are working on RIGHT NOW):
{tasks}

Analyze: Which current tasks could create technical debt that CONFLICTS with the long-term goals?

Think deeply about architectural implications. For each genuine conflict:
- What constraint agents should follow (phrased as "Don't..." or "Avoid...")
- Which long-term goal it conflicts with
- Confidence (0.3-0.9) — higher = more certain conflict

OUTPUT strict JSON only (no markdown fences):
{{"constraints": [{{"constraint": "Don't add tight coupling between X and Y", "reason": "Conflicts with goal: migrate to microservices", "source_goal": "the long-term goal text", "conflicting_task": "the task title", "confidence": 0.7}}]}}
If no genuine conflicts exist: {{"constraints": []}}"""


def _constraint_id(source_goal: str, constraint: str) -> str:
    """Stable ID from source_goal + constraint prefix."""
    raw = f"{source_goal.strip().lower()}|{constraint[:60].strip().lower()}"
    return f"d4_{hashlib.md5(raw.encode()).hexdigest()[:12]}"


def _get_long_term_wants() -> list[dict]:
    """Load wants with temporal_scope == 'long_term'."""
    wants = load_json(WANTS_FILE)
    if not isinstance(wants, list):
        return []
    return [w for w in wants if w.get("temporal_scope") == "long_term"]


def _get_open_tasks() -> list[dict]:
    """Get open/active tasks from the task board."""
    board = load_json_dict(TASK_BOARD_FILE)
    tasks = []
    for d in board.get("dispatches", []):
        for t in d.get("tasks", []):
            if t.get("status") in ("open", "active", "escalated"):
                tasks.append({
                    "title": t.get("title", "")[:100],
                    "domain": t.get("domain", "general"),
                    "complexity": t.get("complexity", "low"),
                })
    return tasks


def _format_wants(wants: list[dict]) -> str:
    """Format wants for LLM prompt."""
    lines = []
    for w in wants[:15]:
        text = w.get("want", w.get("description", str(w)))
        domain = w.get("domain", "general")
        lines.append(f"- [{domain}] {text}")
    return "\n".join(lines) if lines else "No long-term goals recorded."


def _format_tasks(tasks: list[dict]) -> str:
    """Format tasks for LLM prompt."""
    lines = []
    for t in tasks[:15]:
        lines.append(f"- [{t['domain']}] {t['title']} (complexity: {t['complexity']})")
    return "\n".join(lines) if lines else "No open tasks."


def _merge_constraints(existing: list[dict], new: list[dict]) -> list[dict]:
    """Merge new constraints with existing. Dedup by ID, bump confidence on repeat."""
    by_id = {c["id"]: c for c in existing if c.get("status") == "active"}
    for nc in new:
        cid = nc["id"]
        if cid in by_id:
            old = by_id[cid]
            old["confidence"] = min(old["confidence"] + CONFIDENCE_BUMP, CONFIDENCE_CAP)
            old["timestamp"] = _now_iso()
        else:
            by_id[cid] = nc
    merged = sorted(by_id.values(), key=lambda c: -c.get("confidence", 0))
    return merged[:MAX_ACTIVE]


def _decay_constraints(constraints: list[dict]) -> list[dict]:
    """Expire constraints older than DECAY_DAYS with low confidence."""
    cutoff = datetime.now(timezone.utc) - timedelta(days=DECAY_DAYS)
    result = []
    for c in constraints:
        try:
            ts = datetime.fromisoformat(c.get("timestamp", ""))
            if ts.tzinfo is None:
                ts = ts.replace(tzinfo=timezone.utc)
            if ts < cutoff and c.get("confidence", 0) < DECAY_CONFIDENCE_FLOOR:
                c["status"] = "expired"
        except (ValueError, TypeError):
            pass
        result.append(c)
    return result


def generate_constraints(verbose=False) -> list[dict]:
    """Generate synthetic constraints by simulating conflicts via premium LLM."""
    long_term = _get_long_term_wants()
    if len(long_term) < MIN_LONG_TERM_WANTS:
        if verbose:
            log.info(f"Only {len(long_term)} long-term wants (need {MIN_LONG_TERM_WANTS}). Skipping.")
        return []

    tasks = _get_open_tasks()
    if not tasks:
        if verbose:
            log.info("No open tasks. Nothing to check for conflicts.")
        return []

    prompt = CONSTRAINT_PROMPT.format(
        wants=_format_wants(long_term),
        tasks=_format_tasks(tasks),
    )

    try:
        from pulse.core.llm_router import dispatch_json
        result = dispatch_json(prompt, task_type="premium")
    except Exception as e:
        if verbose:
            log.error(f"LLM dispatch failed: {e}")
        return []

    raw_constraints = result.get("constraints", [])
    if not raw_constraints:
        if verbose:
            log.info("No conflicts detected — long-term goals are safe.")
        return []

    new_constraints = []
    for rc in raw_constraints:
        if not isinstance(rc, dict) or not rc.get("constraint"):
            continue
        conf = rc.get("confidence", 0.5)
        if not isinstance(conf, (int, float)):
            conf = 0.5
        conf = max(0.3, min(0.9, conf))

        source_goal = rc.get("source_goal", "")
        cid = _constraint_id(source_goal, rc["constraint"])
        new_constraints.append({
            "id": cid,
            "constraint": rc["constraint"][:300],
            "reason": rc.get("reason", "")[:200],
            "source_goal": source_goal[:200],
            "conflicting_task": rc.get("conflicting_task", "")[:100],
            "confidence": round(conf, 2),
            "timestamp": _now_iso(),
            "status": "active",
            "source": "constraint_engine_d4",
        })

    # Merge with existing (filelock for concurrent boot reads)
    with _acquire(CONSTRAINTS_FILE):
        existing_data = load_json_dict(CONSTRAINTS_FILE)
        existing = existing_data.get("constraints", [])
        merged = _merge_constraints(existing, new_constraints)
        merged = _decay_constraints(merged)

        save_json(CONSTRAINTS_FILE, {
            "constraints": merged,
            "last_updated": _now_iso(),
            "total_generated": existing_data.get("total_generated", 0) + len(new_constraints),
        })

    if verbose:
        for nc in new_constraints:
            log.info(f"  [D4] {nc['constraint'][:80]} (conf={nc['confidence']})")

    return new_constraints


def get_active_constraints() -> list[dict]:
    """Return active synthetic constraints, sorted by confidence desc."""
    data = load_json_dict(CONSTRAINTS_FILE)
    active = [c for c in data.get("constraints", []) if c.get("status") == "active"]
    active.sort(key=lambda c: -c.get("confidence", 0))
    return active


def run_daemon(verbose=False):
    """Run constraint engine as a polling loop."""
    print("[D4] Precognitive Constraint Engine starting...")
    while True:
        try:
            new = generate_constraints(verbose=verbose)
            if new and verbose:
                print(f"[D4] Generated {len(new)} new constraint(s)")
        except Exception as e:
            if verbose:
                print(f"[D4] Error: {e}")
        time.sleep(POLL_INTERVAL)


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO, format="%(asctime)s [D4] %(message)s", datefmt="%H:%M:%S")
    if "--once" in sys.argv:
        verbose = "--verbose" in sys.argv or "-v" in sys.argv
        constraints = generate_constraints(verbose=True)
        print(f"\n[D4] Generated {len(constraints)} constraint(s)")
        if constraints:
            print(json.dumps(constraints, indent=2))
    elif "--status" in sys.argv:
        active = get_active_constraints()
        print(f"Active constraints: {len(active)}")
        for c in active:
            print(f"  [{c['id']}] (conf={c['confidence']}) {c['constraint'][:80]}")
    else:
        run_daemon(verbose="--verbose" in sys.argv or "-v" in sys.argv)
