#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
Reconsolidation CRUD — labile window tracking, reinforce, contradict, tombstone.

Manages the per-item lability state file and performs DB mutations when
memory items are opened for reconsolidation (mark_labile) and then
either reinforced or contradicted.

Split from reconsolidation.py for Law 7 compliance.
Public API: mark_labile, reinforce, contradict, tombstone_item, _sweep_expired,
            _resolve_superseded_chain.
"""
import json
import threading
from datetime import datetime, timezone, timedelta

from pulse.core.paths import get_state_dir
from pulse.core import event_bus
from pulse.core.brain_db import get_conn

MEMORY_DIR = get_state_dir() / "memory"
LABILE_ITEMS_FILE = MEMORY_DIR / "labile_items.json"
TTL_SECONDS = 300  # default fallback

# F45 Per-table reconsolidation lability window (seconds).
# Patterns need longer to stabilize; facts are quickly verified.
_TABLE_TTL = {"lessons": 300, "patterns": 600, "facts": 120}

# Whitelist of valid DB tables — prevents SQL injection via f-string table names
_VALID_TABLES = frozenset({"lessons", "failures", "patterns", "facts",
                           "schemas", "procedures", "decisions", "episodic"})

_lock = threading.RLock()
_labile_state = {}


def _get_ttl(table_name: str) -> int:
    """Return lability TTL for a given table. Patterns longest, facts shortest."""
    return _TABLE_TTL.get(table_name, TTL_SECONDS)


def _load_labile():
    global _labile_state
    with _lock:
        if not LABILE_ITEMS_FILE.exists():
            _labile_state = {}
            return
        try:
            with open(LABILE_ITEMS_FILE, "r", encoding="utf-8") as f:
                _labile_state = json.load(f)
        except Exception:
            _labile_state = {}


def _save_labile():
    with _lock:
        MEMORY_DIR.mkdir(parents=True, exist_ok=True)
        with open(LABILE_ITEMS_FILE, "w", encoding="utf-8") as f:
            json.dump(_labile_state, f, indent=2)


def mark_labile(item_id: str, table: str, prediction_error: float = 0.0):
    """Marks an item as labile (open to modification) with a TTL.

    N45: Per Sevenster et al. 2014, reconsolidation requires prediction error.
    Only opens the lability window if prediction_error > 0.05.
    Default prediction_error=0.0 preserves backward compatibility — callers
    that don't pass PE will NOT open a lability window (no silent regressions).
    Callers that pass PE=0.1 (retrieval.py, session_replay.py) trigger lability.
    """
    # N45: Prediction error gate — no PE, no reconsolidation window
    if prediction_error <= 0.05:
        return
    _sweep_expired()  # Clean up stale entries first
    if table not in _VALID_TABLES:
        return  # Reject unknown table names (SQL injection prevention)
    with _lock:
        if not _labile_state:
            _load_labile()

        _labile_state[item_id] = {
            "table": table,
            "expires_at": (datetime.now(timezone.utc) + timedelta(seconds=_get_ttl(table))).isoformat(),
            "prediction_error": round(float(prediction_error), 4),
        }
        _save_labile()
    # Item 164: Track reconsolidation rate for stability monitoring
    try:
        from pulse.brain.monitoring.reconsolidation_stability import record_reconsolidation_event
        record_reconsolidation_event()
    except Exception:
        pass


def reinforce(item_id: str):
    """Reinforce a labile item (salience += 0.3, reset decay). Loss aversion: reinforce < contradict."""
    with _lock:
        if not _labile_state:
            _load_labile()

        if item_id not in _labile_state:
            return False  # Not labile

        entry = _labile_state[item_id]
        if datetime.fromisoformat(entry["expires_at"]) < datetime.now(timezone.utc):
            del _labile_state[item_id]
            _save_labile()
            return False

        # Perform DB update (validate table against whitelist)
        conn = get_conn()
        table = entry["table"]
        if table not in _VALID_TABLES:
            del _labile_state[item_id]
            _save_labile()
            return False
        now_iso = datetime.now(timezone.utc).isoformat()
        # BCM Metaplasticity (Phase 3): diminishing returns for strong items.
        # Formula: delta = 0.3 / (1 + access_count/10) — ranges from 0.3 (new) to ~0.03 (100+ accesses)
        row = conn.execute(f"SELECT access_count, salience, domain FROM {table} WHERE id = ?", (item_id,)).fetchone()
        ac = row["access_count"] if row else 0
        bcm_delta = round(0.3 / (1 + ac / 10), 3)
        # Critical Periods (Phase 4): mature domains resist change
        from .reconsolidation import get_domain_plasticity
        plasticity = get_domain_plasticity(row["domain"] if row else None)
        bcm_delta = round(bcm_delta * plasticity, 3)
        with conn:
            conn.execute(f"""
                UPDATE {table}
                SET salience = MIN(salience + ?, 7.0),
                    last_accessed = ?,
                    last_reinforced = ?,
                    access_count = access_count + 1
                WHERE id = ?
            """, (bcm_delta, now_iso, now_iso, item_id))

        event_bus.publish("memory.reconsolidation.reinforce", "reconsolidation", {"item_id": item_id})
        del _labile_state[item_id]
        _save_labile()
        return True


def contradict(item_id: str):
    """Contradict a labile item (confidence -= 0.2, flag). Loss aversion: contradict > reinforce."""
    with _lock:
        if not _labile_state:
            _load_labile()

        if item_id not in _labile_state:
            return False

        entry = _labile_state[item_id]
        if datetime.fromisoformat(entry["expires_at"]) < datetime.now(timezone.utc):
            del _labile_state[item_id]
            _save_labile()
            return False

        conn = get_conn()
        table = entry["table"]
        if table not in _VALID_TABLES:
            del _labile_state[item_id]
            _save_labile()
            return False
        # Critical Periods (Phase 4): mature domains resist contradiction
        from .reconsolidation import get_domain_plasticity
        row = conn.execute(f"SELECT domain FROM {table} WHERE id = ?", (item_id,)).fetchone()
        plasticity = get_domain_plasticity(row["domain"] if row else None)
        contradict_delta = round(0.2 * plasticity, 3)
        with conn:
            conn.execute(f"""
                UPDATE {table}
                SET confidence = MAX(confidence - ?, 0.0),
                    validity = CASE WHEN confidence - ? <= 0.0 THEN 'contested' ELSE validity END
                WHERE id = ?
            """, (contradict_delta, contradict_delta, item_id))
        # Item 50 (P13): Tombstone if confidence hit zero (superseded, not deleted)
        try:
            _row50 = conn.execute(f"SELECT confidence FROM {table} WHERE id = ?", (item_id,)).fetchone()
            if _row50 and float(_row50["confidence"]) <= 0.0:
                tombstone_item(item_id, table, new_id="")
        except Exception:
            pass

        event_bus.publish("memory.reconsolidation.contradict", "reconsolidation",
                         {"item_id": item_id, "plasticity": plasticity})
        del _labile_state[item_id]
        _save_labile()
        return True


def _sweep_expired():
    """Clean up items past the lability window."""
    with _lock:
        if not _labile_state:
            _load_labile()
        now = datetime.now(timezone.utc)
        expired = [k for k, v in _labile_state.items() if datetime.fromisoformat(v["expires_at"]) < now]
        if expired:
            for k in expired:
                del _labile_state[k]
            _save_labile()


def _resolve_superseded_chain(item_id: str, table: str, max_depth: int = 5) -> str:
    """Follow superseded_by chain to find final active version (P18-74).

    If A→B→C, resolves A directly to C. Prevents stale indirect references
    and models synaptic rewiring in neuroplasticity (Bhatt & Bhatt 2009).
    """
    if table not in _VALID_TABLES:
        return item_id
    try:
        import json as _json
        conn = get_conn()
        current = item_id
        for _ in range(max_depth):
            row = conn.execute(
                f"SELECT metadata FROM {table} WHERE id = ? AND validity = 'superseded'",
                (current,)
            ).fetchone()
            if not row:
                return current
            meta = _json.loads(row["metadata"] or "{}")
            next_id = meta.get("superseded_by")
            if not next_id or next_id == current:
                return current
            current = next_id
        return current
    except Exception:
        return item_id


def tombstone_item(old_id: str, table: str, new_id: str = "") -> bool:
    """Item 50 (P13): Tombstone instead of destructive rewrite.

    Marks old_id as superseded with a pointer to new_id, preserving history
    and enabling rollback. Tombstoned items remain queryable with status filter.

    Returns True on success, False on any failure (fail-open).
    """
    if table not in _VALID_TABLES:
        return False
    try:
        conn = get_conn()
        conn.execute(
            f"UPDATE {table} SET validity = 'superseded', "
            f"metadata = json_set(COALESCE(metadata, '{{}}'), '$.superseded_by', ?) "
            f"WHERE id = ? AND validity != 'historical'",
            (new_id, old_id)
        )
        conn.commit()
        return conn.execute("SELECT changes()").fetchone()[0] > 0
    except Exception:
        return False
