#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
Item 121: Learning rate annealing per domain (Neuroplasticity — P29).

Mature domains (many items, high avg confidence) get lower STDP learning rates.
New/sparse domains get higher rates. Modeled after neuroplasticity research
showing critical periods of high plasticity in novel domains.

Extracted from stdp.py to keep that file under 300 lines.
"""
import logging
import threading
import math
from datetime import datetime, timezone, timedelta

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

_anneal_lock = threading.Lock()


# --- Phase C Item 19: Unified MaturityState Coordinator ---
def get_maturity_state(domain: str) -> float:
    """Return a combined maturity score (0.0-1.0) for a domain.
    Combines knowledge depth (item count, average confidence) with domain age.
    """
    if not domain:
        return 0.5  # Neutral maturity for unknown domain

    try:
        from pulse.core.brain_db import get_conn as _gc_maturity
        conn = _gc_maturity()

        # 1. Knowledge depth and confidence
        row = conn.execute(
            "SELECT COUNT(*) as cnt, AVG(confidence) as avg_conf, MIN(timestamp) as first_seen FROM lessons "
            "WHERE domain = ? AND validity = 'valid'",
            (domain,)
        ).fetchone()

        if not row or not row["cnt"]:
            return 0.1  # Very low maturity for new/empty domains

        cnt = int(row["cnt"])
        avg_conf = float(row["avg_conf"] or 0.5)
        first_seen_str = row["first_seen"]

        # Max 1.0 if 50 items and 1.0 confidence. This represents "knowledge depth maturity"
        maturity_from_knowledge = min(1.0, (cnt / 50.0) * avg_conf)
        
        # 2. Age maturity (sigmoid-like scaling for time)
        age_maturity = 0.0
        if first_seen_str:
            first_seen_dt = datetime.fromisoformat(first_seen_str.replace("Z", "+00:00"))
            now = datetime.now(timezone.utc)
            age_days = (now - first_seen_dt).days
            
            # Sigmoid-like function: quickly matures for first ~10 days/weeks, then slows
            # Centered around 10 days for 0.5 maturity, maxing out around 60-90 days
            age_maturity = 1.0 / (1.0 + math.exp(-0.2 * (age_days - 10))) 

        # 3. Combine factors: Blend knowledge-based maturity (70%) with age-based maturity (30%)
        maturity_score = (0.7 * maturity_from_knowledge) + (0.3 * age_maturity)
        
        return round(max(0.0, min(1.0, maturity_score)), 4)

    except Exception as e:
        log.warning(f"Error computing maturity state for domain {domain}, falling back to neutral. Error: {e}")
        return 0.5  # fail-open, neutral maturity




def apply_stdp_with_annealing(session_id: str, success: bool, domain: str = "") -> int:
    """Apply STDP with per-domain learning rate annealing (Item 121).

    Wraps apply_stdp() and scales the global LTP/LTD deltas by domain maturity
    before calling the core algorithm. Restores original deltas afterward.
    """
    try:
        import pulse.brain.plasticity.stdp as _stdp
        
        # Use the unified maturity state
        maturity_score = get_maturity_state(domain)
        
        # Factor: 1.5 (new domain/low maturity) -> 0.5 (mature domain/high maturity)
        # This means low maturity (e.g. 0.1) gets factor ~1.4, high maturity (e.g. 0.9) gets factor ~0.6
        factor = 1.5 - maturity_score
        factor = max(0.5, min(1.5, factor)) # Clamp factor
        
        with _anneal_lock:
            old_ltp = _stdp.LTP_DELTA
            old_ltd = _stdp.LTD_DELTA
            try:
                _stdp.LTP_DELTA = round(old_ltp * factor, 5)
                _stdp.LTD_DELTA = round(old_ltd * factor, 5)
                return _stdp.apply_stdp(session_id, success)
            except Exception as e:
                log.debug("apply_stdp_with_annealing inner failed: %s", e)
                return 0
            finally:
                _stdp.LTP_DELTA = old_ltp
                _stdp.LTD_DELTA = old_ltd
    except Exception as e:
        log.debug("apply_stdp_with_annealing outer failed: %s", e)
        return 0  # fail-open


# ---------------------------------------------------------------------------
# Item 139: Error correction signal strength (P33)
# ---------------------------------------------------------------------------
def compute_failure_ltd(base_delta: float) -> float:
    """Make LTD proportional to how surprising the failure was.
    Expected failures (high failure rate domain) get mild weakening.
    Unexpected failures get strong weakening.
    Returns a negative delta.
    """
    try:
        from pulse.core.brain_db import get_conn
        conn = get_conn()
        row = conn.execute(
            "SELECT COUNT(*) as total, "
            "SUM(CASE WHEN type='failure' THEN 1 ELSE 0 END) as fails "
            "FROM lessons WHERE timestamp >= datetime('now','-7 days')"
        ).fetchone()
        if row and row[0] and int(row[0]) > 0:
            failure_rate = float(row[1] or 0) / float(row[0])
        else:
            failure_rate = 0.3
        surprise = max(0.1, 1.0 - failure_rate)
        return round(base_delta * surprise, 4)  # base_delta negative (LTD)
    except Exception:
        return base_delta  # fail-open
