#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""JSONL storage engine: entry building, dedup cache, append/rewrite operations."""
import json, logging, os, re, uuid
from pathlib import Path
from pulse.core.brain_io import _acquire
from pulse.core.brain_utils import _make_fingerprint as make_fingerprint
_TRIGGER_RE = re.compile(
    r'(?:when|if|before|after|once|until)\s+(.{10,80}?)(?:\.|,|$)',
    re.IGNORECASE
)
# Item 62: Implementation intentions — WHEN/DO extraction
_WHEN_DO_RE = re.compile(
    r'when\s+(.{10,80}?),?\s*(?:then\s+|do\s+)?(.{10,80})',
    re.IGNORECASE
)

_CACHE_MAX = 15000
_JSONL_TABLE_MAP = {"auto_lessons.jsonl": "lessons", "auto_fails.jsonl": "failures",
    "auto_patterns.jsonl": "patterns", "private_lessons.jsonl": "private",
    "auto_decisions.jsonl": "decisions", "auto_intents.jsonl": "intents"}
def _jsonl_to_table(path: Path) -> str:
    """Map a JSONL file path to its SQLite table name. Returns '' if unmapped."""
    return _JSONL_TABLE_MAP.get(Path(path).name, "")

_TYPE_PREFIX = {"lesson": "L", "failure": "F", "pattern": "P", "private": "V"}
_MD_TO_TYPE = {"auto_lessons.md": "lesson", "auto_fails.md": "failure",
    "auto_patterns.md": "pattern", "private_lessons.md": "private",
    "auto_lessons.jsonl": "lesson", "auto_fails.jsonl": "failure",
    "auto_patterns.jsonl": "pattern", "private_lessons.jsonl": "private"}
_JSONL_DEDUP_CACHE: dict[str, dict[str, None]] = {}
_DEDUP_CACHE = _JSONL_DEDUP_CACHE
_LAST_SESSION_ITEM_ID = None
def _make_fingerprint(text: str) -> str:
    """Thin wrapper for backward compat."""
    return make_fingerprint(text)
def _make_entry_id(knowledge_type: str) -> str:
    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 JSONL knowledge entry with 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)
    # Stamp embedding at write time
    try:
        from pulse.brain.rule_embeddings import embed_texts, EMBED_INDEX_FILE
        if EMBED_INDEX_FILE.exists():  # Model was already built — safe to embed
            vectors = embed_texts([description[:500]])
            if vectors and len(vectors) > 0 and len(vectors[0]) > 0:
                entry["_embedding"] = vectors[0]
    except Exception:
        pass
    try:  # Stamp valence for emotion-gated decay
        from pulse.core.valence import compute_valence
        valence, arousal = compute_valence(description)
        if valence != 0.0 or arousal > 0.0:
            entry["_valence"] = valence; entry["_arousal"] = arousal
    except Exception: pass
    # Item 10: Flashbulb memory — high-emotion + high-salience = permanent validity
    try:
        ei = float(entry.get("emotional_intensity", 0) or entry.get("_arousal", 0))
        if ei >= 0.8 and float(entry.get("salience", 0)) >= 3.0:
            entry["validity"] = "permanent"
    except Exception:
        pass
    try:  # Prospective memory: extract trigger conditions
        if knowledge_type == "intent":
            _m = _TRIGGER_RE.search(description)
            if _m: entry["trigger_condition"] = _m.group(1).strip()
    except Exception: pass
    # Item 62: Implementation intentions — WHEN/DO structured extraction
    try:
        if knowledge_type == "intent":
            wd_match = _WHEN_DO_RE.search(description)
            if wd_match:
                entry["when_trigger"] = wd_match.group(1).strip(); entry["do_action"] = wd_match.group(2).strip()
    except Exception: pass
    try:  # Item 21: Generation effect
        _sl = str(source).lower(); _pa = (provenance or {}).get("saving_agent", "") or ""
        if ((_pa and source == _pa) or "self" in _sl or "generated" in _sl):
            entry["salience"] = entry.get("salience", 1.0) + 0.3; entry["_generation_effect"] = True
    except Exception: pass
    try:  # Item 25: Emotional salience boost
        _em = float((provenance or {}).get("emotional_intensity", 0) or entry.get("emotional_intensity", 0) or 0)
        if _em >= 0.6: entry["salience"] = entry.get("salience", 1.0) + (_em * 0.5); entry["_emotional_salience"] = True
    except Exception: pass
    try:  # Bayesian hyperpriors
        from pulse.brain.metamemory import get_metamemory
        _cp = get_metamemory().get(domain, {}).get("competence_level", "")
        if _cp == "expert": entry["confidence"] = max(entry.get("confidence", 0.5), 0.7)
        elif _cp == "novice": entry["confidence"] = min(entry.get("confidence", 0.5), 0.3)
    except Exception: pass
    try:  # Episode ID binding via EPISODE_OF edges
        global _LAST_SESSION_ITEM_ID
        _eid = entry.get("id")
        if _LAST_SESSION_ITEM_ID and _eid:
            from pulse.core.brain_db import get_conn as _ep_conn
            _c = _ep_conn()
            _c.execute("INSERT OR IGNORE INTO graph_edges (from_node, to_node, edge_type, confidence) VALUES (?, ?, 'EPISODE_OF', 0.7)",
                       (_LAST_SESSION_ITEM_ID, _eid))
            _c.commit()
        _LAST_SESSION_ITEM_ID = entry.get("id")
    except Exception: pass  # fail-open

    try:
        if knowledge_type == "failure" and float(entry.get("confidence", 0)) >= 0.8:
            entry["broadcast"] = True; entry["_social_broadcast"] = True
    except Exception: pass
    # Item 147: Domain emotional valence tracking
    try:
        from pulse.brain.metamemory_p34 import record_domain_valence
        record_domain_valence(entry.get("domain", "general"), entry.get("content", "") or str(float(entry.get("_arousal", 0) or 0)))
    except Exception: pass
    # Item 80: Emotional contagion
    try:
        from datetime import datetime, timezone, timedelta; from pulse.core.brain_db import get_conn
        _ei = float(entry.get("emotional_intensity", 0) or entry.get("_arousal", 0) or 0)
        if _ei >= 0.7:
            _ec = get_conn(); _r = (datetime.now(timezone.utc) - timedelta(minutes=5)).isoformat()
            for _tb in ("lessons","failures"): _ec.execute(f"UPDATE {_tb} SET salience=MIN(10.0,salience+0.2) WHERE timestamp>? AND domain=?", (_r, entry.get("domain","general")))
            _ec.commit()
    except Exception: pass

    return entry

def _load_jsonl_dedup_cache(filepath: Path) -> dict[str, None]:
    """Lazy-load fingerprint cache. Cross-session dedup, O(1)."""
    key = str(filepath)
    if key not in _JSONL_DEDUP_CACHE:
        ordered: dict[str, None] = {}
        if filepath.exists():
            try:
                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 not fp:
                                content = obj.get("content", "")
                                if content:
                                    fp = _make_fingerprint(content)
                            if fp:
                                ordered[fp] = None
                        except json.JSONDecodeError:
                            continue
            except OSError:
                pass
        _JSONL_DEDUP_CACHE[key] = ordered
    return _JSONL_DEDUP_CACHE[key]

def rewrite_jsonl(jsonl_path: Path, entries: list[dict], dry_run: bool = False) -> int:
    """Atomic full rewrite (tmp+replace). Returns count of entries written."""
    if dry_run:
        return len(entries)
    jsonl_path = Path(jsonl_path)
    tmp = jsonl_path.with_suffix(".jsonl.tmp")
    with _acquire(jsonl_path):
        with open(tmp, "w", encoding="utf-8") as f:
            for entry in entries:
                f.write(json.dumps(entry, ensure_ascii=False, separators=(",", ":")) + "\n")
        os.replace(str(tmp), str(jsonl_path))
        key = str(jsonl_path)
        if key in _JSONL_DEDUP_CACHE:
            del _JSONL_DEDUP_CACHE[key]
    return len(entries)

def _micro_consolidation_hook(entry: dict):
    """Post-write micro consolidation hook."""
    try:
        from pulse.core.brain_db import get_conn
        conn = get_conn()
        domain = entry.get("domain", "")
        if domain:
            with conn:
                conn.execute("""
                    UPDATE graph_edges
                    SET confidence = MIN(confidence + 0.05, 1.0)
                    WHERE edge_type = 'RELATED_TO' AND (from_node = ? OR to_node = ?)
                """, (domain, domain))
    except Exception:
        pass

    salience = float(entry.get("salience", 1.0))
    if salience >= 3.0:
        try:
            from pulse.core.paths import get_state_dir
            hot_cache_file = get_state_dir() / "memory" / "hot_cache.json"
            hot_cache_file.parent.mkdir(parents=True, exist_ok=True)
            cache_data = []
            if hot_cache_file.exists():
                try:
                    with open(hot_cache_file, "r", encoding="utf-8") as f:
                        cache_data = json.load(f)
                except Exception:
                    pass
            cache_data.append(entry)
            if len(cache_data) > 50:
                cache_data.pop(0)
            with open(hot_cache_file, "w", encoding="utf-8") as f:
                json.dump(cache_data, f)
        except Exception:
            pass

def append_knowledge_jsonl(jsonl_path, entry: dict, dry_run: bool = False) -> bool:
    """Append single JSONL entry with dedup + filelock."""
    if dry_run:
        return False
    jsonl_path = Path(jsonl_path)
    fp = entry.get("fingerprint", "")
    if not fp:
        content = entry.get("content", "")
        if not content:
            return False
        fp = _make_fingerprint(content)
        entry["fingerprint"] = fp

    cache = _load_jsonl_dedup_cache(jsonl_path)
    if fp in cache:
        return False

    with _acquire(jsonl_path):
        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]

        sqlite_ok = False
        try:
            from pulse.core.brain_db import insert_knowledge
            _table = _jsonl_to_table(jsonl_path)
            if _table:
                insert_knowledge(_table, entry)
                sqlite_ok = True
        except Exception:
            pass  # SQLite failure → fall through to JSONL

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

        try:
            _micro_consolidation_hook(entry)
        except Exception:
            pass
        try:
            from pulse.brain.resurrection import check_resurrection
            check_resurrection(entry.get("content", ""))
        except Exception:
            pass
        try:  # Item 137+135: Temporal bridge + consolidation debt (P33/P32)
            from pulse.brain.save_writers.temporal_bridge import maybe_create_temporal_bridge; maybe_create_temporal_bridge(entry)
            from pulse.brain.metamemory_tracking import increment_consolidation_debt; increment_consolidation_debt(1)
        except Exception: pass

    return True

def append_to_json_list(filepath: Path, item: dict, dedup_key: str = None):
    """Thread-safe append to a JSON list file. filelock + optional dedup."""
    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
