#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""JSONL storage engine: dedup cache, append/rewrite operations."""
import json
import logging
import os
import threading
import uuid
from pathlib import Path
from pulse.core.brain_io import _acquire
from pulse.core.brain_utils import _make_fingerprint as make_fingerprint
from pulse.brain.save_writers.jsonl_entry_builder import (
    build_jsonl_entry, _TYPE_PREFIX, _MD_TO_TYPE, _make_fingerprint,
    _make_entry_id, _SESSION_ITEM_LOCAL,
)

_CACHE_MAX = 15000
# Sentinel returned by insert_knowledge when BCM load-shedding gate drops an item.
# Caller must treat this as a write miss and fall through to JSONL fallback.
_BCM_SHED_SENTINEL = "bcm_shed"
_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
_SESSION_ITEM_LOCAL = threading.local()  # Thread-local storage for _LAST_SESSION_ITEM_ID

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, items: list[dict], dry_run: bool = False) -> int:
    """Sync consolidated items to SQLite. JSONL write only as fallback.

    Consolidation stages call this after processing (dedup, prune, quality gate).
    Primary path: bulk upsert to SQLite, skip flat file.
    Fallback: atomic JSONL rewrite if SQLite unavailable.
    """
    if dry_run:
        return len(items)
    jsonl_path = Path(jsonl_path)
    table = _jsonl_to_table(jsonl_path)

    # Primary: sync to SQLite — no JSONL write needed
    if table:
        try:
            from pulse.core.brain_db import bulk_upsert
            ok = bulk_upsert(table, items)
            if ok > 0:
                logging.getLogger(__name__).debug(
                    "rewrite_jsonl: %d/%d items synced to SQLite[%s], skipping JSONL",
                    ok, len(items), table)
                key = str(jsonl_path)
                if key in _JSONL_DEDUP_CACHE:
                    del _JSONL_DEDUP_CACHE[key]
                return ok
        except Exception:
            pass  # SQLite unavailable — fall through to JSONL

    # Fallback: atomic JSONL rewrite
    tmp = jsonl_path.with_suffix(".jsonl.tmp")
    with _acquire(jsonl_path):
        with open(tmp, "w", encoding="utf-8") as f:
            for item in items:
                f.write(json.dumps(item, 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(items)

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
            from pulse.core.brain_io import _acquire as _hc_acquire
            hot_cache_file = get_state_dir() / "memory" / "hot_cache.json"
            hot_cache_file.parent.mkdir(parents=True, exist_ok=True)
            with _hc_acquire(hot_cache_file):
                # Read INSIDE the lock to prevent TOCTOU race
                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 knowledge item directly to SQLite. JSONL is deprecated."""
    if dry_run:
        return False
    
    try:
        from pulse.core.brain_db import insert_knowledge
        table_name = _jsonl_to_table(jsonl_path)
        if not table_name:
            return False

        if 'type' not in entry:
            entry['type'] = table_name.rstrip('s')

        # The core write operation
        write_status = insert_knowledge(table_name, entry)

        if write_status and write_status not in ("bcm_shed", "quality_rejected"):
            # Post-write hooks only on successful, non-rejected writes
            try:
                _micro_consolidation_hook(entry)
                from pulse.brain.plasticity.resurrection import check_resurrection
                check_resurrection(entry.get("content", ""))
                from pulse.brain.save_writers.temporal_bridge import maybe_create_temporal_bridge
                maybe_create_temporal_bridge(entry)
                from pulse.brain.metacognition.metamemory_tracking import increment_consolidation_debt
                increment_consolidation_debt(1)
                from pulse.brain.plasticity.spaced_retrieval import schedule_item
                item_id = entry.get("id") or entry.get("fingerprint", "")
                if item_id:
                    schedule_item(item_id, item_type=table_name.rstrip("s"))
            except Exception:
                pass # Post-write hooks should not block the main path
            return True
        return False
    except Exception:
        return False

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
