#!/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
import uuid
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 >= 4.0 or
    confidence >= 0.85) pass with lower thresholds (25 chars / 5 words).
    Per-item salience scale: 1.0=ambient, 2.0=standard, 4.0=actionable, 7.0=critical.
    """
    words = text.split()
    if salience >= 4.0 or confidence >= 0.85:
        # 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 JSONL brain via append_knowledge_md.

    Legacy name kept for backward compat — no longer writes MD.
    Shared by write_learnings, write_failures, and write_private_learnings.
    """
    if not entries:
        return 0
    from pulse.core.brain_analysis import compute_item_salience, compute_item_confidence
    count = 0
    for item in entries:
        text = (item or "").strip()
        if not text or len(text) < 25:
            continue
        sal = compute_item_salience(text, priority=1.0)
        conf = compute_item_confidence(text, source_type="scribe")
        append_knowledge_md(
            filepath, text, agent_name, _now_iso(), "general",
            confidence=conf, salience=sal,
        )
        count += 1
    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 {}


# === JSONL KNOWLEDGE STORAGE ===

_CACHE_MAX = 500  # max fingerprints per file

# Type prefix map for generating IDs
_TYPE_PREFIX = {"lesson": "L", "failure": "F", "pattern": "P", "private": "V"}

# Map MD filenames to knowledge types
_MD_TO_TYPE = {
    "auto_lessons.md": "lesson",
    "auto_fails.md": "failure",
    "auto_patterns.md": "pattern",
    "private_lessons.md": "private",
}


def _make_fingerprint(text: str) -> str:
    """Content fingerprint for O(1) dedup — MD5 of normalized first 120 chars."""
    return hashlib.md5(text[:120].lower().strip().encode()).hexdigest()[:16]


def _make_entry_id(knowledge_type: str) -> str:
    """Generate short unique ID like L-a1b2c3d4."""
    prefix = _TYPE_PREFIX.get(knowledge_type, "K")
    return f"{prefix}-{uuid.uuid4().hex[:8]}"


def build_jsonl_entry(description: str, source: str, timestamp: str, domain: str,
                      confidence: float, salience: float, knowledge_type: str = "lesson",
                      provenance: dict = None) -> dict:
    """Build a structured JSONL knowledge entry with D+E temporal/uncertainty fields."""
    from pulse.core.temporal import stamp_temporal_fields
    from pulse.core.uncertainty import stamp_uncertainty_fields
    entry = {
        "id": _make_entry_id(knowledge_type),
        "fingerprint": _make_fingerprint(description),
        "type": knowledge_type,
        "timestamp": timestamp,
        "agent": source,
        "content": description,
        "title": description[:80],
        "domain": domain,
        "salience": salience,
        "confidence": confidence,
        "consolidation_count": 1,
        "evidence_count": 1,
        "tags": [],
        "cross_refs": [],
        "_provenance": provenance or {},
    }
    entry = stamp_temporal_fields(entry, knowledge_type)
    entry = stamp_uncertainty_fields(entry)
    return entry


def append_knowledge_jsonl(jsonl_path, entry: dict, dry_run: bool = False) -> bool:
    """Append a single JSONL entry with fingerprint-based O(1) dedup.

    Uses an in-memory LRU fingerprint cache (loaded once from file) + filelock.
    Returns True if written, False if duplicate or skipped.
    """
    if dry_run:
        return False
    jsonl_path = Path(jsonl_path)
    jsonl_path.parent.mkdir(parents=True, exist_ok=True)

    fp = entry.get("fingerprint", "")
    if not fp:
        return False

    # LRU dedup check (pre-lock, optimistic)
    cache = _load_jsonl_dedup_cache(jsonl_path)
    if fp in cache:
        return False

    with _acquire(jsonl_path):
        # Re-check inside lock
        cache = _load_jsonl_dedup_cache(jsonl_path)
        if fp in cache:
            return False

        cache[fp] = None
        if len(cache) > _CACHE_MAX:
            oldest = next(iter(cache))
            del cache[oldest]

        line = json.dumps(entry, ensure_ascii=False, separators=(",", ":"))
        with open(jsonl_path, "a", encoding="utf-8") as f:
            f.write(line + "\n")
    return True


# === JSONL DEDUP CACHE ===

_JSONL_DEDUP_CACHE: dict[str, dict[str, None]] = {}


def _load_jsonl_dedup_cache(filepath: Path) -> dict[str, None]:
    """Lazy-load fingerprint cache from JSONL file (one disk read per file)."""
    key = str(filepath)
    if key not in _JSONL_DEDUP_CACHE:
        ordered: dict[str, None] = {}
        if filepath.exists():
            with open(filepath, "r", encoding="utf-8") as f:
                for line in f:
                    line = line.strip()
                    if not line:
                        continue
                    try:
                        obj = json.loads(line)
                        fp = obj.get("fingerprint", "")
                        if fp:
                            ordered[fp] = None
                    except json.JSONDecodeError:
                        continue
        _JSONL_DEDUP_CACHE[key] = ordered
    return _JSONL_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 the JSONL brain.

    Despite the legacy name (kept for backward compat), this now writes
    JSONL-only. MD writes were killed in Phase 4 — all readers are JSONL-first.
    The filepath param is used to determine knowledge_type via _MD_TO_TYPE.
    """
    if dry_run:
        return
    filepath = Path(filepath)

    # 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

    # PII redaction
    description = redact_pii(description)

    # Write to JSONL (sole storage — Phase 4+)
    from pulse.core.brain_utils import BRAIN_JSONL_FILES
    knowledge_type = _MD_TO_TYPE.get(filepath.name, "lesson")
    jsonl_path = BRAIN_JSONL_FILES.get(knowledge_type)
    if jsonl_path:
        jsonl_entry = build_jsonl_entry(
            description=description, source=source, timestamp=timestamp,
            domain=domain, confidence=confidence, salience=salience,
            knowledge_type=knowledge_type,
            provenance={"source_agent": source, "domain": domain},
        )
        append_knowledge_jsonl(jsonl_path, jsonl_entry, dry_run=dry_run)


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