#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
Model-Free Procedural Cache (Phase 4, Architecture 4).

Maps task signatures to successful tool-call sequences. When a familiar
task type arrives, the system can suggest a proven procedure instead of
reasoning from scratch every time — like basal ganglia habit formation.

Cache is advisory: the LLM can follow or deviate from cached procedures.

Workflow:
1. After task success: extract tool-call sequence → store in cache keyed
   by task signature (domain + key terms hash)
2. Before task execution: lookup signature → if found with N >= 3 hits,
   return cached procedure as a suggested plan
3. After task failure with cached procedure: decrement success count,
   remove if below threshold
"""
import json
import hashlib
import logging
import re
from datetime import datetime, timezone

from pulse.core.paths import get_state_dir

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

CACHE_FILE = get_state_dir() / "procedural_cache.json"
MIN_SUCCESSES = 3       # Need this many successes before suggesting
MAX_CACHE_ENTRIES = 200  # Cap total procedures
SIGNATURE_TERMS = 8     # Key terms in signature

# Stop words for signature extraction
_STOP = frozenset({
    "the", "a", "an", "is", "are", "was", "were", "be", "been", "have",
    "has", "had", "do", "does", "did", "will", "would", "could", "should",
    "may", "might", "can", "shall", "to", "of", "in", "for", "on", "with",
    "at", "by", "from", "as", "into", "that", "this", "it", "its", "and",
    "or", "but", "not", "no", "if", "when", "then", "than", "so", "up",
    "out", "about", "all", "each", "every", "both", "few", "more", "most",
})


def compute_signature(domain: str, title: str, description: str = "") -> str:
    """Compute a stable hash signature for a task type.

    Uses domain + top N significant terms from title/description.
    Generalizes across similar tasks with different specific details.
    """
    text = f"{title} {description}".lower()
    words = re.findall(r'\b[a-z_]{3,}\b', text)
    significant = sorted(set(w for w in words if w not in _STOP))[:SIGNATURE_TERMS]
    key = f"{domain}:{'|'.join(significant)}"
    return hashlib.sha256(key.encode()).hexdigest()[:16]


def record_procedure(domain: str, title: str, description: str,
                     tool_sequence: list[dict]) -> bool:
    """Record a successful tool-call sequence for a task type.

    Args:
        domain: Task domain
        title: Task title
        description: Task description
        tool_sequence: List of {"tool": name, "args_pattern": generalized_args}

    Returns True if cache was updated.
    """
    if not tool_sequence or len(tool_sequence) < 2:
        return False  # Too short to be a useful procedure

    sig = compute_signature(domain, title, description)
    cache = _load_cache()

    if sig in cache:
        entry = cache[sig]
        entry["success_count"] += 1
        entry["last_success"] = datetime.now(timezone.utc).isoformat()
        # Update sequence if this one is more recent
        if len(tool_sequence) <= len(entry["tool_sequence"]) + 2:
            entry["tool_sequence"] = tool_sequence
    else:
        cache[sig] = {
            "domain": domain,
            "title_sample": title[:100],
            "tool_sequence": tool_sequence,
            "success_count": 1,
            "failure_count": 0,
            "last_success": datetime.now(timezone.utc).isoformat(),
            "created": datetime.now(timezone.utc).isoformat(),
        }

    # Prune if over limit — remove lowest success_count entries
    if len(cache) > MAX_CACHE_ENTRIES:
        sorted_keys = sorted(cache.keys(),
                             key=lambda k: cache[k]["success_count"])
        for k in sorted_keys[:len(cache) - MAX_CACHE_ENTRIES]:
            del cache[k]

    _save_cache(cache)
    log.info("Procedural cache: recorded %s (%d tools, sig=%s)",
             title[:50], len(tool_sequence), sig)
    return True


def lookup_procedure(domain: str, title: str,
                     description: str = "") -> list[dict] | None:
    """Look up a cached procedure for a task type.

    Returns the tool sequence if found with sufficient successes,
    or None if no cached procedure exists.
    """
    sig = compute_signature(domain, title, description)
    cache = _load_cache()

    entry = cache.get(sig)
    if not entry:
        return None
    if entry["success_count"] < MIN_SUCCESSES:
        return None
    # Don't suggest if failure rate is too high
    total = entry["success_count"] + entry["failure_count"]
    if total > 0 and entry["failure_count"] / total > 0.3:
        return None

    log.info("Procedural cache HIT: %s (sig=%s, successes=%d)",
             entry.get("title_sample", "?"), sig, entry["success_count"])
    return entry["tool_sequence"]


def record_failure(domain: str, title: str, description: str = ""):
    """Record that a cached procedure led to task failure."""
    sig = compute_signature(domain, title, description)
    cache = _load_cache()

    if sig in cache:
        cache[sig]["failure_count"] += 1
        # Remove if too many failures
        total = cache[sig]["success_count"] + cache[sig]["failure_count"]
        if total >= 5 and cache[sig]["failure_count"] / total > 0.5:
            del cache[sig]
            log.info("Procedural cache: evicted %s (too many failures)", sig)
        _save_cache(cache)


def extract_tool_sequence(events: list[dict]) -> list[dict]:
    """Extract a generalized tool-call sequence from worker session events.

    Takes raw session events and extracts tool names + generalized arg patterns.
    Specific file paths are replaced with {file} placeholders.
    """
    sequence = []
    for event in events:
        if event.get("type") != "tool_call":
            continue
        tool = event.get("tool", "")
        if not tool:
            continue
        # Generalize arguments — accept both "arguments" (spec) and "args" (worker trace key)
        args = event.get("arguments") or event.get("args", {})
        pattern = _generalize_args(tool, args)
        sequence.append({"tool": tool, "args_pattern": pattern})
    return sequence


def _get_cache_stats() -> dict:
    """Return cache statistics for monitoring."""
    cache = _load_cache()
    if not cache:
        return {"entries": 0, "total_successes": 0, "domains": []}
    return {
        "entries": len(cache),
        "total_successes": sum(e["success_count"] for e in cache.values()),
        "total_failures": sum(e["failure_count"] for e in cache.values()),
        "domains": list(set(e["domain"] for e in cache.values())),
        "top_procedures": sorted(
            [{"sig": k, "title": v["title_sample"], "successes": v["success_count"]}
             for k, v in cache.items()],
            key=lambda x: x["successes"], reverse=True
        )[:5],
    }


def _generalize_args(tool: str, args: dict) -> dict:
    """Replace specific values with type placeholders."""
    pattern = {}
    for k, v in args.items():
        if isinstance(v, str):
            # Replace file paths with placeholder
            if "/" in v or "\\" in v or v.endswith((".py", ".json", ".md")):
                pattern[k] = "{file_path}"
            elif len(v) > 100:
                pattern[k] = "{long_text}"
            else:
                pattern[k] = v  # Keep short strings (likely tool names, flags)
        elif isinstance(v, bool):
            pattern[k] = v
        elif isinstance(v, (int, float)):
            pattern[k] = "{number}"
        else:
            pattern[k] = "{data}"
    return pattern


def _load_cache() -> dict:
    if not CACHE_FILE.exists():
        return {}
    try:
        return json.loads(CACHE_FILE.read_text(encoding="utf-8"))
    except Exception:
        return {}


def _save_cache(cache: dict):
    CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
    try:
        CACHE_FILE.write_text(
            json.dumps(cache, indent=2, ensure_ascii=False), encoding="utf-8")
    except Exception as e:
        log.debug("Cache save failed: %s", e)
