"""Ebbinghaus Decay Engine — SQLite-backed.

Adapted from PostgreSQL + Memgraph to use SQLite via soagila.db.sqlite_init.
"""

import math
import json
import logging
from datetime import datetime, timezone
from pydantic import BaseModel

from soagila.db.sqlite_init import get_db
from soagila.db.event_bus import publish_event
from soagila.utils.time_utils import get_current_time

logger = logging.getLogger("soagila.tcmg.decay")

CHANNEL = "soagila:metrics"

# Ebbinghaus decay constants
DEFAULT_DECAY_RATE = 0.1       # lambda -- higher = faster decay
DEFAULT_STABILITY = 1.0        # S -- initial stability
REINFORCEMENT_FACTOR = 0.2     # R -- reinforcement weight per access
DECAY_THRESHOLD = 0.15         # Below this -> marked as decayed
ARCHIVE_THRESHOLD = 0.05       # Below this -> archive candidate


class DecayCycleResult(BaseModel):
    memories_evaluated: int = 0
    memories_decayed: int = 0
    memories_archived: int = 0
    memories_reinforced: int = 0
    avg_relevance: float = 0.0


def calculate_relevance(
    created_at: datetime,
    stability: float,
    decay_rate: float,
    access_count: int,
    last_accessed: datetime | None,
    now: datetime | None = None,
) -> float:
    """
    Ebbinghaus decay formula:
    RS(t) = S * exp(-lambda * t) + R * access_reinforcement

    Where:
    - S = stability (starts 1.0, grows with access)
    - lambda = decay_rate
    - t = hours since creation
    - R = reinforcement factor
    - access_reinforcement = recency-weighted access bonus
    """
    if now is None:
        now = get_current_time()

    # Time since creation in hours
    t = max((now - created_at).total_seconds() / 3600, 0.001)

    # Base decay
    base_decay = stability * math.exp(-decay_rate * t)

    # Access reinforcement: each access adds a bonus, weighted by recency
    reinforcement = 0.0
    if access_count > 0 and last_accessed:
        hours_since_access = max((now - last_accessed).total_seconds() / 3600, 0.001)
        # More recent access -> stronger reinforcement
        recency_weight = math.exp(-0.05 * hours_since_access)
        reinforcement = REINFORCEMENT_FACTOR * access_count * recency_weight

    relevance = base_decay + reinforcement

    # Clamp to [0, 1]
    return max(0.0, min(1.0, relevance))


async def run_decay_cycle(user_id: str | None = None) -> DecayCycleResult:
    """
    Run decay calculation on all non-decayed episodic memories.
    If user_id provided, only process that user's memories.
    """
    now = get_current_time()
    result = DecayCycleResult()

    db = await get_db()

    # Fetch non-decayed memories
    if user_id:
        cursor = await db.execute(
            """
            SELECT id, created_at, stability, decay_rate, access_count,
                   last_accessed, relevance_score
            FROM episodic_memories
            WHERE user_id = ? AND is_decayed = 0
            """,
            (user_id,),
        )
    else:
        cursor = await db.execute(
            """
            SELECT id, created_at, stability, decay_rate, access_count,
                   last_accessed, relevance_score
            FROM episodic_memories
            WHERE is_decayed = 0
            """
        )

    rows = await cursor.fetchall()

    result.memories_evaluated = len(rows)

    total_relevance = 0.0
    updates = []

    for row in rows:
        mem_id = str(row[0])
        created_at = row[1]
        # Parse created_at if it's a string, ensure timezone-aware
        if isinstance(created_at, str):
            created_at = datetime.fromisoformat(created_at)
        if created_at.tzinfo is None:
            created_at = created_at.replace(tzinfo=timezone.utc)
        stability = row[2] or DEFAULT_STABILITY
        decay_rate_val = row[3] or DEFAULT_DECAY_RATE
        access_count = row[4] or 0
        last_accessed = row[5]
        if isinstance(last_accessed, str):
            last_accessed = datetime.fromisoformat(last_accessed)
        if last_accessed and last_accessed.tzinfo is None:
            last_accessed = last_accessed.replace(tzinfo=timezone.utc)
        old_score = row[6] or 1.0

        new_score = calculate_relevance(
            created_at=created_at,
            stability=stability,
            decay_rate=decay_rate_val,
            access_count=access_count,
            last_accessed=last_accessed,
            now=now,
        )

        total_relevance += new_score

        is_decayed = new_score < DECAY_THRESHOLD
        is_archived = new_score < ARCHIVE_THRESHOLD

        if is_decayed:
            result.memories_decayed += 1
        if is_archived:
            result.memories_archived += 1

        updates.append({
            "id": mem_id,
            "relevance_score": new_score,
            "is_decayed": is_decayed,
            "old_score": old_score,
        })

    # Batch update SQLite
    now_iso = now.isoformat()
    for upd in updates:
        await db.execute(
            """
            UPDATE episodic_memories
            SET relevance_score = ?,
                is_decayed = ?,
                updated_at = ?
            WHERE id = ?
            """,
            (
                upd["relevance_score"],
                int(upd["is_decayed"]),
                now_iso,
                upd["id"],
            ),
        )
    await db.commit()

    # Calculate average
    if result.memories_evaluated > 0:
        result.avg_relevance = round(total_relevance / result.memories_evaluated, 4)

    # Publish decay events for significant changes
    for upd in updates:
        if upd["is_decayed"] or abs(upd["relevance_score"] - upd["old_score"]) > 0.05:
            event = json.dumps({
                "type": "decay_event",
                "memory_id": upd["id"],
                "old_score": round(upd["old_score"], 4),
                "new_score": round(upd["relevance_score"], 4),
                "decayed": upd["is_decayed"],
                "timestamp": now_iso,
            })
            await publish_event(CHANNEL, event)

    logger.info(
        f"Decay cycle: evaluated={result.memories_evaluated}, "
        f"decayed={result.memories_decayed}, archived={result.memories_archived}, "
        f"avg_relevance={result.avg_relevance}"
    )

    return result


async def reinforce(memory_id: str):
    """Reinforce a memory when accessed -- boosts stability and relevance."""
    now = get_current_time()
    now_iso = now.isoformat()

    db = await get_db()
    await db.execute(
        """
        UPDATE episodic_memories
        SET access_count = access_count + 1,
            last_accessed = ?,
            stability = MIN(stability + 0.15, 3.0),
            updated_at = ?
        WHERE id = ?
        """,
        (now_iso, now_iso, memory_id),
    )
    await db.commit()

    logger.debug(f"Reinforced memory {memory_id}")
