#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
Save Writers: Brain file writers + session logging for pulse_save.

Extracted from pulse_save.py per Law 7 (files <= 300 lines).
DRY'd three identical write functions into _write_entries_to_md().

Functions:
  - write_learnings / write_failures / write_private_learnings
  - log_session (agent_sessions.json)
  - count_brain_queries, record_skill_usage, record_procedure
"""
import hashlib
import json
import logging
import re
import sys
from datetime import datetime, timezone, timedelta
from pathlib import Path

from pulse.core.brain_io import _acquire  # Cross-process file locking (filelock)
from pulse.core.pii_filter import redact_pii

SCRIPT_DIR = Path(__file__).resolve().parent
ROOT_DIR = SCRIPT_DIR.parent.parent

from pulse.core.brain_utils import (
    atomic_update_json, HIPPOCAMPUS_DIR,
    LESSONS_FILE as _LESSONS_FILE, FAILS_FILE as _FAILS_FILE,
    DECISIONS_FILE as _DECISIONS_FILE,
    now_iso as _now_iso,
)

# Quality gate: reject fragments without actionable content
_QUALITY_SIGNALS = re.compile(
    r"(?:use|replace|fix|add|remove|change|switch|avoid|always|never|"
    r"install|update|check|run|set|create|delete|move|rename|enable|disable|"
    r"should|must|need|works|fails|breaks|causes|prevents|requires|reduces|improves|"
    r"discovered|learned|realized|confirmed|verified|tested|proved|migrated|refactored|"
    r"error|exception|crash|timeout|race condition|"
    r"broke|lost|missing|corrupt|regress|overflow|deadlock|leak)",
    re.IGNORECASE
)

# Brain output files (use canonical paths from brain_utils)
LESSONS_FILE = _LESSONS_FILE
FAILS_FILE = _FAILS_FILE
SESSIONS_FILE = HIPPOCAMPUS_DIR / "Evidence" / "Sessions" / "agent_sessions.json"
PRIVATE_LESSONS_FILE = HIPPOCAMPUS_DIR / "Private" / "private_lessons.md"


def _passes_quality_gate(text: str, salience: float = 0.0, confidence: float = 0.0) -> bool:
    """Check if text passes all quality gates.

    Density Bypass: concise but high-value items (salience >= 2.0 or
    confidence >= 0.9) pass with lower thresholds (25 chars / 5 words).
    Per-item salience scale: 1.0=base, 2.0=has signal words, 3.0+=critical.
    """
    words = text.split()
    if salience >= 2.0 or confidence >= 0.9:
        # Diamond signals — concise but high-value (must have signal words)
        if len(text) < 15 or len(words) < 3:
            return False
    else:
        if len(text) < 40 or len(words) < 7:
            return False
    if not _QUALITY_SIGNALS.search(text):
        return False
    if text.rstrip().endswith("..") or text.rstrip().endswith("\u2014"):
        return False
    return True


from pulse.core.brain_analysis import text_similarity

def _write_entries_to_md(filepath: Path, header_title: str, header_desc: str,
                         entries: list, agent_name: str) -> int:
    """Write entries to a markdown brain file with quality gates.

    Shared by write_learnings, write_failures, and write_private_learnings.
    Returns count of entries written.
    """
    if not entries:
        return 0

    filepath.parent.mkdir(parents=True, exist_ok=True)

    if not filepath.exists():
        header = "---\ntype: auto-extracted\n---\n\n"
        header += f"# {header_title}\n\n"
        header += f"> {header_desc}\n\n---\n"
        filepath.write_text(header, encoding="utf-8")

    count = 0
    with _acquire(filepath):  # Cross-process lock — safe for 8+ concurrent agents
        existing_text = filepath.read_text(encoding="utf-8") if filepath.exists() else ""
        existing_chunks = [c.strip() for c in existing_text.split("## ") if c.strip()]

        with open(filepath, "a", encoding="utf-8") as f:
            for item in entries:
                text = (item or "").strip()
                if not _passes_quality_gate(text):
                    continue

                # PII redaction — quality gate ran on raw text, now redact for storage
                text = redact_pii(text)

                is_duplicate = False
                for chunk in existing_chunks:
                    if text_similarity(text, chunk) > 0.65:
                        print(f"\n[BRAIN REJECTED] This lesson already exists in {filepath.name}.")
                        print(f"[RECONSOLIDATION REQUIRED] You MUST use `--refine \"{text[:20]}...\" --updated-text \"...\"` to update the existing knowledge instead of appending a duplicate.\n")
                        is_duplicate = True
                        break

                if is_duplicate:
                    continue

                entry = f"\n\n## {text[:80]}\n\n"
                entry += f"{text}\n\n"
                entry += f"**Source:** {agent_name} | **Date:** {_now_iso()[:10]}\n\n---\n"
                f.write(entry)
                count += 1
                existing_chunks.append(text)
    return count


def write_learnings(learnings: list, agent_name: str) -> int:
    """Write learnings to the lessons file. Returns count written."""
    return _write_entries_to_md(
        LESSONS_FILE, "Auto-Extracted Lessons",
        "Automatically saved by agents via pulse_save.py.",
        learnings, agent_name,
    )


def write_failures(failures: list, agent_name: str) -> int:
    """Write failures to the fails file. Returns count written."""
    return _write_entries_to_md(
        FAILS_FILE, "Auto-Extracted Failures",
        "Automatically saved by agents via pulse_save.py.",
        failures, agent_name,
    )


def write_private_learnings(learnings: list, agent_name: str) -> int:
    """Write learnings to private brain. Returns count written."""
    return _write_entries_to_md(
        PRIVATE_LESSONS_FILE, "Private Lessons",
        "Private learnings \u2014 not shared publicly.",
        learnings, agent_name,
    )


def log_session(agent_name: str, task_id: str, learnings: list,
                failures: list, files_touched: list) -> None:
    """Log session to agent_sessions.json for historical tracking."""
    SESSIONS_FILE.parent.mkdir(parents=True, exist_ok=True)

    session = {
        "agent": agent_name,
        "task_id": task_id,
        "learnings": learnings,
        "failures": failures,
        "files_touched": files_touched,
        "timestamp": _now_iso(),
    }
    from pulse.core.pulse_crypto import sign_item
    session = sign_item(session, agent_name)

    def _updater(data):
        if not isinstance(data, list):
            data = []
        data.append(session)
        return data

    atomic_update_json(SESSIONS_FILE, _updater, default=[])


def count_brain_queries(agent_name: str = "") -> int:
    """Count brain queries from this session (last 1 hour)."""
    log_file = ROOT_DIR / "state" / "brain_query_log.json"
    if not log_file.exists():
        return 0
    try:
        entries = json.loads(log_file.read_text(encoding="utf-8"))
        if not isinstance(entries, list):
            return 0
        cutoff = datetime.now(timezone.utc) - timedelta(hours=1)
        count = 0
        for e in entries:
            ts = e.get("timestamp", "")
            if ts:
                try:
                    entry_time = datetime.fromisoformat(ts.replace("Z", "+00:00"))
                    if entry_time > cutoff:
                        count += 1
                except (ValueError, TypeError):
                    logging.debug("Suppressed exception in save_writers", exc_info=True)
        return count
    except Exception:
        return 0


def record_skill_usage(skills_used: list, had_failures: bool) -> int:
    """Record skill usage for effectiveness tracking."""
    if not skills_used:
        return 0
    try:
        from pulse.admin.skills.skill_effectiveness import record_usage
        count = 0
        for skill in skills_used:
            skill = skill.strip()
            if skill:
                record_usage(skill, success=not had_failures)
                count += 1
        return count
    except ImportError:
        return 0


def record_procedure(procedure_id: str, had_failures: bool) -> dict:
    """Record a procedure execution via procedural_tracker."""
    if not procedure_id:
        return {}
    try:
        from pulse.admin.procedural_tracker import record_execution
        return record_execution(procedure_id, success=not had_failures)
    except ImportError:
        return {}


# === LRU DEDUP CACHE (insertion-ordered dict for O(1) checks) ===

_DEDUP_CACHE: dict[str, dict[str, None]] = {}  # filepath -> OrderedDict-style dict
_CACHE_MAX = 500  # max fingerprints per file


def _entry_fingerprint(text: str) -> str:
    """Hash first 80 chars lowercase for fast dedup."""
    return hashlib.md5(text[:80].lower().strip().encode()).hexdigest()


def _load_dedup_cache(filepath: Path) -> dict[str, None]:
    """Lazy-load cache from file on first access only (one disk read)."""
    key = str(filepath)
    if key not in _DEDUP_CACHE:
        ordered: dict[str, None] = {}
        if filepath.exists():
            content = filepath.read_text(encoding="utf-8")
            for chunk in content.split("\n## ")[1:]:
                ordered[_entry_fingerprint(chunk)] = None
        _DEDUP_CACHE[key] = ordered
    return _DEDUP_CACHE[key]


# === CANONICAL WRITE FUNCTIONS (shared by bridge_routing + retroactive_routing) ===


def append_knowledge_md(filepath, description, source, timestamp, domain,
                        confidence, salience=1.0, dry_run=False):
    """Canonical writer: append a knowledge entry to a markdown brain file.

    Used by bridge_routing.py and retroactive_routing.py — single write path
    for all Hippocampus markdown targets (lessons, patterns, failures).
    Includes LRU dedup cache (O(1) per-append, loaded from disk once per file).
    """
    if dry_run:
        return
    filepath = Path(filepath)
    filepath.parent.mkdir(parents=True, exist_ok=True)

    # Quality gate — reject fragments without actionable content
    if not _passes_quality_gate(description, salience=salience, confidence=confidence):
        return

    # Defense-in-depth: reject SCRIBE_PROMPT example echoes at write layer
    try:
        from pulse.admin.retroactive.retroactive_llm_extractor import _is_prompt_echo
        if _is_prompt_echo(description):
            return
    except ImportError:
        pass  # Extractor not available — skip check

    # PII redaction — quality gate ran on raw text, now redact for storage
    description = redact_pii(description)

    # LRU dedup check — O(1) via in-memory cache (pre-lock, optimistic)
    cache = _load_dedup_cache(filepath)
    fp = _entry_fingerprint(description)
    if fp in cache:
        return  # duplicate — skip

    with _acquire(filepath):  # Cross-process lock — safe for 8+ concurrent agents
        # Re-check dedup inside lock (another worker may have written)
        cache = _load_dedup_cache(filepath)
        if fp in cache:
            return  # duplicate written by another worker

        # Insert with LRU eviction (dict preserves insertion order in Python 3.7+)
        cache[fp] = None
        if len(cache) > _CACHE_MAX:
            oldest = next(iter(cache))
            del cache[oldest]

        if not filepath.exists():
            header = f"---\ntype: auto-extracted\nlast_updated: {timestamp[:10]}\n---\n\n"
            header += f"# {filepath.stem.replace('auto_', 'Auto-Extracted ').replace('_', ' ').title()}\n\n"
            header += "> Automatically extracted from agent conversations.\n\n---\n"
            filepath.write_text(header, encoding="utf-8")

        entry = f"\n\n## {description[:80]}\n\n"
        entry += f"{description}\n\n"
        entry += f"**Source:** {source} | **Domain:** {domain} | **Confidence:** {confidence} | **Date:** {timestamp[:10]}\n"
        entry += f"**Salience:** {salience} | **Consolidation Count:** 1\n\n---\n"

        with open(filepath, "a", encoding="utf-8") as f:
            f.write(entry)


def append_decision(item, dry_run=False):
    """Canonical writer: append a decision to the decisions JSON file.

    Used by bridge_routing.py — single write path for all decision entries.
    atomic_update_json already uses filelock internally via brain_io._acquire().
    """
    if dry_run:
        return
    _DECISIONS_FILE.parent.mkdir(parents=True, exist_ok=True)

    def _updater(data):
        if not isinstance(data, dict):
            data = {"decisions": []}
        if "decisions" not in data:
            data["decisions"] = []
        from pulse.core.pulse_crypto import sign_item
        decision_entry = {
            "description": item.get("description", ""),
            "domain": item.get("domain", "general"),
            "confidence": item.get("confidence", 0.5),
            "timestamp": item.get("timestamp", ""),
            "source_agent": item.get("source_agent", "unknown"),
        }
        decision_entry = sign_item(decision_entry, item.get("source_agent", "unknown"))
        data["decisions"].append(decision_entry)
        return data

    atomic_update_json(_DECISIONS_FILE, _updater, default={})


def append_to_json_list(filepath: Path, item: dict, dedup_key: str = None):
    """Thread-safe append to a JSON list file (wants.json, dont_wants.json, etc).

    Uses filelock for cross-process safety with concurrent swarm workers.
    Optional dedup_key skips items where that key already exists in the list.
    """
    filepath = Path(filepath)
    filepath.parent.mkdir(parents=True, exist_ok=True)

    with _acquire(filepath):
        data = json.loads(filepath.read_text(encoding="utf-8")) if filepath.exists() else []
        if not isinstance(data, list):
            data = []
        if dedup_key and any(e.get(dedup_key) == item.get(dedup_key) for e in data):
            return False
        data.append(item)
        filepath.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
        return True