#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
STDP Hebbian Update Engine — temporal edge upsert, triplet shortcuts,
monopoly penalty, session retrieval recording.

Split from stdp.py for Law 7 compliance.
Contains: record_retrieval, _upsert_temporal_edge, _check_monopoly,
          and the triplet STDP motif logic called from apply_stdp.

BCM threshold, burst detection, reward modulation, and apply_stdp
remain in stdp.py which re-exports this module's public surface.

Phase 3 AGI refinement — Studies #69-71 (Dan & Poo, Markram, Song).
"""
import json
import logging
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"

_SILENT_SYNAPSE_THRESHOLD = 3  # F10: new edges need 3+ co-activations before becoming active

# Item 24: Eligibility traces — reward-modulated STDP
_ELIGIBILITY_TRACE: list = []  # [(edge_id, delta, timestamp)]
_MAX_TRACE = 20


def record_retrieval(session_id: str, item_id: str):
    """Record that an item was retrieved in this session (called from brain_search)."""
    STDP_DIR.mkdir(parents=True, exist_ok=True)
    now = datetime.now(timezone.utc).isoformat()
    try:
        sessions = {}
        if SESSIONS_FILE.exists():
            sessions = json.loads(SESSIONS_FILE.read_text(encoding="utf-8"))
        seq = sessions.setdefault(session_id, [])
        # Avoid duplicate consecutive entries
        if seq and seq[-1].get("item_id") == item_id:
            return
        seq.append({"item_id": item_id, "ts": now})
        # Cap session length
        if len(seq) > 50:
            sessions[session_id] = seq[-50:]
        # Cap total sessions (keep last 24)
        if len(sessions) > 24:
            sorted_keys = sorted(sessions.keys())
            for k in sorted_keys[:-24]:
                del sessions[k]
        SESSIONS_FILE.write_text(json.dumps(sessions), encoding="utf-8")
    except Exception as e:
        log.debug("STDP record failed: %s", e)


def _check_monopoly(node_id: str, conn) -> bool:
    """Item 27: Return True if node has >30% of all graph edges (monopoly hub)."""
    try:
        total = conn.execute("SELECT COUNT(*) FROM graph_edges").fetchone()[0]
        node_edges = conn.execute(
            "SELECT COUNT(*) FROM graph_edges WHERE to_node = ?", (node_id,)
        ).fetchone()[0]
        return total > 0 and (node_edges / total) > 0.3
    except Exception:
        return False


def _upsert_temporal_edge(conn, from_id: str, to_id: str, delta: float) -> int:
    """Create or update a TEMPORAL edge. F10: new edges start silent (conf=0)."""
    existing = conn.execute(
        "SELECT id, confidence, metadata FROM graph_edges "
        "WHERE from_node=? AND to_node=? AND edge_type='TEMPORAL'",
        (from_id, to_id)
    ).fetchone()
    if existing:
        try:
            meta = json.loads(existing["metadata"] or "{}")
        except Exception:
            meta = {}
        coact = meta.get("coactivation_count", 1) + 1
        meta["coactivation_count"] = coact
        # Promote from silent only after threshold reached
        current_conf = existing["confidence"]
        if coact >= _SILENT_SYNAPSE_THRESHOLD:
            if delta < 0:
                # Item 45: Depotentiation vs LTD distinction
                try:
                    import time as _t45
                    last_pot = meta.get("last_potentiated", 0)
                    if _t45.time() - last_pot < 300:
                        # Depotentiation: revert recently-potentiated edge to zero
                        new_conf = 0.0
                    else:
                        # LTD: gradual long-term depression
                        new_conf = max(0.0, current_conf * 0.7)
                except Exception:
                    new_conf = max(0.0, current_conf * 0.7)
            else:
                new_conf = max(0.0, min(1.0, max(current_conf, 0.01) + delta))
                # Track potentiation timestamp for depotentiation logic
                try:
                    import time as _t45p
                    meta["last_potentiated"] = _t45p.time()
                except Exception:
                    pass
        else:
            new_conf = current_conf  # stays silent
        if new_conf < 0.01 and coact >= _SILENT_SYNAPSE_THRESHOLD:
            conn.execute("DELETE FROM graph_edges WHERE id=?", (existing["id"],))
        else:
            conn.execute(
                "UPDATE graph_edges SET confidence=?, metadata=? WHERE id=?",
                (round(new_conf, 4), json.dumps(meta), existing["id"])
            )
        # Item 24: Record to eligibility trace
        try:
            import time as _t
            _ELIGIBILITY_TRACE.append((existing["id"], delta, _t.time()))
            if len(_ELIGIBILITY_TRACE) > _MAX_TRACE:
                _ELIGIBILITY_TRACE.pop(0)
        except Exception:
            pass
    else:
        if delta > 0:
            now = datetime.now(timezone.utc).isoformat()
            # F10: new edges start silent — confidence=0.0 until 3+ co-activations
            conn.execute(
                "INSERT INTO graph_edges (from_node, to_node, edge_type, confidence, metadata) "
                "VALUES (?, ?, 'TEMPORAL', ?, ?)",
                (from_id, to_id, 0.0,
                 json.dumps({"source": "stdp", "created": now, "coactivation_count": 1,
                             "natural_confidence": round(max(0.1, delta), 4)}))
            )
        # Item 24: Record new edge to eligibility trace
        try:
            import time as _tnew
            lastrowid = conn.execute(
                "SELECT id FROM graph_edges WHERE from_node=? AND to_node=? AND edge_type='TEMPORAL'",
                (from_id, to_id)
            ).fetchone()
            if lastrowid:
                _ELIGIBILITY_TRACE.append((lastrowid[0], delta, _tnew.time()))
                if len(_ELIGIBILITY_TRACE) > _MAX_TRACE:
                    _ELIGIBILITY_TRACE.pop(0)
        except Exception:
            pass
    return 1


def apply_triplet_shortcuts(conn, seq: list):
    """Item 41: Triplet STDP — A->C shortcuts for A->B->C motifs.

    Called from apply_stdp after pair processing. Fail-open.
    """
    try:
        _seen = set()
        _pairs = [
            (seq[i]["item_id"], seq[j]["item_id"])
            for i in range(len(seq))
            for j in range(i + 1, len(seq))
            if seq[i]["item_id"] != seq[j]["item_id"]
        ]
        for _pair_pre, _pq in _pairs:
            if (_pair_pre, _pq) in _seen:
                continue
            _seen.add((_pair_pre, _pq))
            for _row in conn.execute(
                "SELECT to_node FROM graph_edges "
                "WHERE from_node=? AND edge_type='CAUSAL' AND confidence>0.3 LIMIT 3",
                (_pq,)
            ).fetchall():
                if _row[0] != _pair_pre:
                    conn.execute(
                        "INSERT OR IGNORE INTO graph_edges "
                        "(from_node,to_node,edge_type,confidence) VALUES (?,?,'CAUSAL', 0.3)",
                        (_pair_pre, _row[0])
                    )
    except Exception:
        pass  # fail-open
