#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
STDP — BCM sliding threshold, burst detection, reward modulation, apply_stdp.

Split from the original combined file for Law 7 compliance.
Hebbian edge upsert (_upsert_temporal_edge), monopoly check (_check_monopoly),
triplet shortcuts (apply_triplet_shortcuts), and session recording
(record_retrieval) live in stdp_hebbian.py and are re-exported here so all
existing callers keep working with a single import path.

Temporal window: items retrieved within 120s of each other form pairs.
Asymmetry: A->B (causal) gets stronger boost than B->A (anti-causal).

Phase 3 AGI refinement — Studies #69-71 (Dan & Poo, Markram, Song).
"""
import json
import logging
import math
from datetime import datetime, timezone
from pulse.core.paths import get_state_dir

log = logging.getLogger("pulse.stdp")

STDP_DIR = get_state_dir() / "stdp"
SESSIONS_FILE = STDP_DIR / "session_retrievals.json"
TEMPORAL_WINDOW_S = 120  # Items within 120s form pairs
_HEBB_WINDOW = 300       # Item 58: 5-minute biological co-occurrence window
LTP_DELTA = 0.08         # Causal strengthening (A before B, task success)
LTD_DELTA = -0.04        # Anti-causal / failure weakening
ANTI_CAUSAL_RATIO = 0.3  # B->A gets 30% of A->B signal

# P10-33: Burst detection — 6+ co-activations in 5s = burst LTP (double delta)
_BURST_TIMESTAMPS: list = []
_BURST_THRESHOLD = 6
_BURST_WINDOW = 5.0  # seconds

_BCM_MAX_EXPECTED = 20  # Expected max events per minute for BCM normalisation



def _get_adaptive_temporal_window(domain: str) -> float:
    """
    Phase C Item 18: Adaptive STDP Temporal Windows.
    Replaces the global 120s STDP window with a learned, per-domain window 
    that narrows as the domain matures.
    """
    try:
        from pulse.core.brain_utils import STATE_DIR
        import json
        
        # We will infer domain maturity from the metamemory confidence map
        map_file = STATE_DIR / "metamemory_confidence.json"
        if not map_file.exists():
            return 120.0
            
        data = json.loads(map_file.read_text(encoding="utf-8"))
        domains = data.get("domains", {})
        
        domain_data = domains.get(domain, {})
        # Confidence acts as a proxy for maturity (0.0 to 1.0)
        maturity = domain_data.get("avg_confidence", 0.5)
        
        # Max window 180s for raw/new domains, narrowing down to 30s for highly mature domains
        window = 180.0 - (maturity * 150.0)
        return max(30.0, min(180.0, window))
    except Exception as e:
        return 120.0


def _get_bcm_theta() -> float:
    """BCM sliding modification threshold theta_M (Bienenstock, Cooper & Munro 1982).

    theta_M = (recent_count / MAX_EXPECTED)^2
    High activity -> high theta -> LTP harder to induce (homeostatic).
    Low  activity -> low  theta -> LTP easier (lower bar).
    """
    import time
    now = time.time()
    try:
        recent_count = len([t for t in _BURST_TIMESTAMPS if now - t < 60])
        theta = (recent_count / _BCM_MAX_EXPECTED) ** 2
        return max(0.01, min(1.0, theta))  # Clamp to (0.01, 1.0]
    except Exception:
        return 0.25  # Neutral fallback


def _check_burst() -> bool:
    """Return True when 6+ STDP events occurred within the last 5 seconds."""
    import time
    now = time.time()
    _BURST_TIMESTAMPS.append(now)
    while _BURST_TIMESTAMPS and _BURST_TIMESTAMPS[0] < now - _BURST_WINDOW:
        _BURST_TIMESTAMPS.pop(0)
    recent = [t for t in _BURST_TIMESTAMPS if now - t < _BURST_WINDOW]
    return len(recent) >= _BURST_THRESHOLD


def _apply_reward_modulation(reward: float):
    """Retroactively strengthen recent STDP updates by reward signal."""
    import time
    from .stdp_hebbian import _ELIGIBILITY_TRACE
    now = time.time()
    _TRACE_WINDOW = 60  # seconds
    try:
        from pulse.core.brain_db import get_conn
        conn = get_conn()
        for edge_id, delta, ts in list(_ELIGIBILITY_TRACE):
            if now - ts < _TRACE_WINDOW and reward > 0:
                try:
                    conn.execute(
                        "UPDATE graph_edges SET confidence = MIN(1.0, confidence + ?) WHERE id = ?",
                        (round(delta * reward * 0.5, 4), edge_id)
                    )
                except Exception:
                    pass
        conn.commit()
    except Exception:
        pass  # fail-open
    _ELIGIBILITY_TRACE.clear()


def apply_stdp(session_id: str, success: bool) -> int:
    """Apply STDP learning on task completion. Returns count of edges updated."""
    try:
        from pulse.core.brain_db import get_conn
    except ImportError:
        return 0
    if not SESSIONS_FILE.exists():
        return 0
    try:
        sessions = json.loads(SESSIONS_FILE.read_text(encoding="utf-8"))
    except Exception:
        return 0
    seq = sessions.get(session_id, [])
    if len(seq) < 2:
        return 0

    from .stdp_hebbian import (
        _upsert_temporal_edge, _check_monopoly, apply_triplet_shortcuts
    )

    conn = get_conn()
    updated = 0
    _burst_active = False
    try:
        _burst_active = _check_burst()
    except Exception:
        pass

    for i in range(len(seq)):
        # Phase C Item 18: Adaptive Temporal Windows
        domain = seq[i].get("domain", "general")
        adaptive_window = _get_adaptive_temporal_window(domain)
        
        for j in range(i + 1, len(seq)):
            try:
                ts_i = datetime.fromisoformat(seq[i]["ts"].replace("Z", "+00:00"))
                ts_j = datetime.fromisoformat(seq[j]["ts"].replace("Z", "+00:00"))
                delta_s = abs((ts_j - ts_i).total_seconds())
            except (ValueError, TypeError):
                continue
            if delta_s > adaptive_window:
                break  # Beyond adaptive window
            pre_id = seq[i]["item_id"]
            post_id = seq[j]["item_id"]
            if pre_id == post_id:
                continue
            # P1-4 FIX: Exponential temporal decay — closer retrievals get stronger signal.
            # Item 139: Error correction -- failure LTD proportional to surprise (P33)
            if success:
                raw_delta = LTP_DELTA
            else:
                try:
                    from .stdp_annealing import compute_failure_ltd as _cfl139
                    raw_delta = _cfl139(LTD_DELTA)
                except Exception:
                    raw_delta = LTD_DELTA
            causal_delta = raw_delta * math.exp(-delta_s / 30.0)
            try:
                if _burst_active:
                    causal_delta *= 2.0  # P10-33: burst LTP
            except Exception:
                pass
            # N49: BCM sliding threshold — single gate replaces redundant metaplasticity+frequency multipliers
            try:
                theta_M = _get_bcm_theta()
                if raw_delta > 0:
                    if causal_delta > theta_M * LTP_DELTA:
                        pass   # LTP — above threshold, keep delta as-is
                    else:
                        causal_delta = -abs(causal_delta) * 0.5  # Below threshold -> LTD
            except Exception:
                pass
            # Item 27: Monopoly penalty — if post_id dominates graph, halve confidence delta
            if _check_monopoly(post_id, conn):
                causal_delta *= 0.5
            updated += _upsert_temporal_edge(conn, pre_id, post_id, causal_delta)
            # Anti-causal: post -> pre — real STDP (Bi & Poo 1998): anti-causal → LTD on success
            if causal_delta > 0:
                anti_delta = -abs(causal_delta) * ANTI_CAUSAL_RATIO
            else:
                anti_delta = abs(causal_delta) * ANTI_CAUSAL_RATIO * 0.5
            updated += _upsert_temporal_edge(conn, post_id, pre_id, anti_delta)

    # Item 41: Triplet STDP shortcuts
    apply_triplet_shortcuts(conn, seq)

    conn.commit()
    # Clean up session after processing
    if session_id in sessions:
        del sessions[session_id]
        try:
            SESSIONS_FILE.write_text(json.dumps(sessions), encoding="utf-8")
        except Exception:
            pass
    log.info("STDP: %d edges updated for session %s (success=%s)", updated, session_id, success)
    return updated


# ---------------------------------------------------------------------------
# Re-export Hebbian surface so all existing callers keep working with
# `from pulse.brain.stdp import record_retrieval, apply_stdp, ...`
# ---------------------------------------------------------------------------
from .stdp_hebbian import (  # noqa: E402, F401
    record_retrieval,
    _check_monopoly,
    _upsert_temporal_edge,
    apply_triplet_shortcuts,
    SESSIONS_FILE as _SESSIONS_FILE_HEBB,
)
