#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
# See LICENSE file for details.
"""
Decision Parser: Extract structured decisions from conversation text.

Captures: "let's go with X", "decided to X", "chose X over Y because Z"
These are commitment-language choices that intents (wants) don't cover.
Intents = aspirational ("I want X"). Decisions = committed ("We chose X").

Dependencies: Python stdlib only (Law 4)
"""

import hashlib
import re

from pulse.brain.extraction.sentence_boundary import truncate_at_sentence as _truncate_sentence
from datetime import datetime, timedelta, timezone

from pulse.brain.extraction.wants_classify import _classify_domain

# === DECISION PATTERNS ===
# Each: (compiled_regex, named groups expected)
# Must capture at minimum: choice. Optionally: alternative, reasoning.

_COMMITMENT_PATTERNS = [
    # "let's go with X" / "going with X" / "let's use X"
    re.compile(
        r"(?:let'?s\s+(?:go\s+with|use|do|try|ship|deploy|pick|stick\s+with))\s+"
        r"(?P<choice>.{3,80}?)(?:\s+(?:instead\s+of|over|rather\s+than)\s+(?P<alt>.{3,60}?))?"
        r"(?:\s+because\s+(?P<reason>.{5,120}))?"
        r"(?:\.|,|!|$)", re.IGNORECASE),

    # "decided to X" / "we decided X" / "I decided X"
    re.compile(
        r"(?:(?:we|i)\s+)?decided\s+(?:to\s+)?(?:go\s+with\s+|use\s+|pick\s+)?"
        r"(?P<choice>.{3,80}?)(?:\s+(?:instead\s+of|over)\s+(?P<alt>.{3,60}))?"
        r"(?:\s+because\s+(?P<reason>.{5,120}))?"
        r"(?:\.|,|!|$)", re.IGNORECASE),

    # "chose X over Y" / "choosing X over Y"
    re.compile(
        r"(?:chose|choosing|picked)\s+(?P<choice>.{3,60}?)\s+"
        r"(?:over|instead\s+of)\s+(?P<alt>.{3,60}?)"
        r"(?:\s+because\s+(?P<reason>.{5,120}))?"
        r"(?:\.|,|!|$)", re.IGNORECASE),

    # "going with X for Y" / "we're going with X"
    re.compile(
        r"(?:we'?re\s+)?going\s+(?:to\s+go\s+)?with\s+(?P<choice>.{3,80}?)"
        r"(?:\s+for\s+(?P<reason>.{5,80}))?"
        r"(?:\.|,|!|$)", re.IGNORECASE),

    # "our choice is X" / "final answer is X" / "final decision: X"
    re.compile(
        r"(?:our\s+choice\s+is|final\s+(?:answer|decision|call)(?:\s+is|:))\s+"
        r"(?P<choice>.{3,80}?)"
        r"(?:\.|,|!|$)", re.IGNORECASE),

    # "switch to X from Y" / "switching to X" / "switched to X"
    re.compile(
        r"switch(?:ing|ed)?\s+to\s+(?P<choice>.{3,80}?)"
        r"(?:\s+from\s+(?P<alt>.{3,60}?))?"
        r"(?:\s+because\s+(?P<reason>.{5,120}))?"
        r"(?:\.|,|!|$)", re.IGNORECASE),

    # "X over Y because Z" (standalone comparative — no IGNORECASE, requires caps)
    re.compile(
        r"(?P<choice>[A-Z][a-zA-Z0-9_\s]{2,40}?)\s+over\s+(?P<alt>[A-Z][a-zA-Z0-9_\s]{2,40}?)"
        r"\s+because\s+(?P<reason>.{5,120})"
        r"(?:\.|,|!|$)"),

    # "settled on X" / "landed on X"
    re.compile(
        r"(?:settled|landed)\s+on\s+(?P<choice>.{3,80}?)"
        r"(?:\s+because\s+(?P<reason>.{5,120}))?"
        r"(?:\.|,|!|$)", re.IGNORECASE),

    # "confirmed: X"
    re.compile(
        r"confirmed:\s*(?P<choice>.{3,80}?)"
        r"(?:\.|,|!|$)", re.IGNORECASE),

    # "X it is"
    re.compile(
        r"(?P<choice>[A-Z][a-zA-Z0-9_\s]{2,40}?)\s+it\s+is"
        r"(?:\.|,|!|$)", re.IGNORECASE),
]

# Negative patterns — skip questions, hypotheticals, past references
_NEGATIVE_RE = re.compile(
    r"(?:should\s+we|could\s+we|would\s+you|what\s+if|maybe\s+we|"
    r"thinking\s+about|considering|might\s+want\s+to)\s",
    re.IGNORECASE)

# Questions end with ? — hard negative
_QUESTION_RE = re.compile(r"\?\s*$")

# Negation in the choice — "NOT going with X" is not an affirmative decision
_NEGATION_RE = re.compile(r"\bnot\b|\bdon.?t\b|\bnever\b|\bno\b", re.IGNORECASE)


def _default_ttl(scope: str) -> int:
    """Return TTL in days based on decision scope. 0 = no expiry."""
    return {
        "tooling": 90, "infrastructure": 90, "process": 120,
        "architecture": 180, "pricing": 180, "design": 60,
    }.get(scope, 0)


def parse_decisions(user_text: str, agent_text: str = "") -> list:
    """Extract structured decisions from conversation text.

    Returns list of decision dicts matching the N0 schema.
    Only captures commitment language, not questions or hypotheticals.
    """
    combined = f"{user_text}\n{agent_text}" if agent_text else user_text
    if not combined or len(combined) < 20:
        return []

    decisions = []
    seen_fps = set()

    # Split on newlines and sentence boundaries, preserving delimiters for ? check
    for sentence in re.split(r"(?<=[.!?\n])\s+", combined):
        sentence = sentence.strip()
        if len(sentence) < 10 or len(sentence) > 1000:
            continue

        # Skip questions and hypotheticals
        if _QUESTION_RE.search(sentence) or _NEGATIVE_RE.search(sentence):
            continue

        for pattern in _COMMITMENT_PATTERNS:
            m = pattern.search(sentence)
            if not m:
                continue

            groups = m.groupdict()
            choice = (groups.get("choice") or "").strip().rstrip(".,!;:")
            if not choice or len(choice) < 3:
                continue

            # Skip negated decisions ("NOT going with X") — check 15 chars before match
            pre_start = max(0, m.start() - 15)
            context_window = sentence[pre_start:m.end()][:60]
            if _NEGATION_RE.search(context_window):
                continue

            # Fingerprint dedup within this conversation
            fp = hashlib.md5(choice[:80].lower().strip().encode("utf-8")).hexdigest()[:12]
            if fp in seen_fps:
                continue
            seen_fps.add(fp)

            alt_raw = (groups.get("alt") or "").strip().rstrip(".,!;:")
            alternatives = [alt_raw] if alt_raw and len(alt_raw) >= 2 else []
            reasoning = (groups.get("reason") or "").strip().rstrip(".,!;:")

            scope = _classify_scope(sentence, choice)
            ttl_days = _default_ttl(scope)
            decision = {
                "type": "decision",
                "timestamp": datetime.now(timezone.utc).isoformat(),
                "choice": choice[:120],
                "alternatives_rejected": alternatives,
                "reasoning": _truncate_sentence(reasoning, 500) if reasoning else "",
                "context": _truncate_sentence(sentence, 500),
                "confidence": 0.75,
                "scope": scope,
                "ttl_days": ttl_days,
                "domain": _extract_domain(sentence),
                "reversibility": "moderate",
                "extraction_method": "regex",
                "status": "active",
                "superseded_by": None,
                "tags": [],
            }
            if ttl_days > 0:
                expires = datetime.now(timezone.utc) + timedelta(days=ttl_days)
                decision["expires_at"] = expires.isoformat()
            decisions.append(decision)
            break  # One match per sentence

    return decisions


def _classify_scope(text: str, choice: str) -> str:
    """Classify decision scope from context."""
    combined = f"{text} {choice}".lower()
    scope_keywords = {
        "architecture": ["architecture", "design", "structure", "system", "module", "refactor"],
        "pricing": ["price", "pricing", "cost", "$", "tier", "subscription", "payment", "stripe"],
        "tooling": ["tool", "library", "package", "framework", "sdk", "api", "plugin"],
        "infrastructure": ["deploy", "host", "server", "docker", "cloud", "ci/cd", "database"],
        "process": ["workflow", "process", "protocol", "procedure", "convention", "rule"],
        "design": ["ui", "ux", "frontend", "css", "layout", "color", "theme", "brand"],
    }
    for scope, keywords in scope_keywords.items():
        if any(kw in combined for kw in keywords):
            return scope
    return "general"


def _extract_domain(text: str) -> str:
    """Classify domain using shared classifier."""
    try:
        return _classify_domain(text) or "general"
    except Exception:
        return "general"
