#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
Item 111: Spaced retrieval scheduling (Memory) — SM-2 per-item.

Per-item scheduling persisted to SQLite (spaced_schedule table).
Uses SM-2 algorithm: new_interval = old_interval * EF
where EF (easiness factor) starts at 2.5 and adjusts per retrieval outcome.

Old domain-level schedule retained as legacy shim for backward compat.
"""
from __future__ import annotations

import logging
import time
from datetime import datetime, timezone, timedelta
from typing import Optional

log = logging.getLogger("pulse.brain.spaced_retrieval")

# ---------------------------------------------------------------------------
# Legacy domain-level schedule (retained for backward compat)
# ---------------------------------------------------------------------------
_DOMAIN_QUERY_SCHEDULE: dict = {}
_SPACING_INTERVALS_HOURS = [1, 6, 24, 72]


def _schedule_next_retrieval(domain: str) -> dict:
    """Legacy: compute next retrieval time for a domain (fixed intervals)."""
    try:
        now = datetime.now(timezone.utc)
        entry = _DOMAIN_QUERY_SCHEDULE.get(domain, {})
        prev_interval = entry.get("interval_hours", 0)
        next_interval = _SPACING_INTERVALS_HOURS[0]
        for iv in _SPACING_INTERVALS_HOURS:
            if iv > prev_interval:
                next_interval = iv
                break
        else:
            next_interval = _SPACING_INTERVALS_HOURS[-1]
        next_dt = now + timedelta(hours=next_interval)
        record = {
            "domain": domain,
            "last_queried": now.isoformat(),
            "interval_hours": next_interval,
            "next_retrieval": next_dt.isoformat(),
        }
        _DOMAIN_QUERY_SCHEDULE[domain] = record
        return record
    except Exception:
        return {"domain": domain, "interval_hours": 1, "next_retrieval": "", "last_queried": ""}


def get_due_domains() -> list:
    """Return list of domains whose next_retrieval time has passed (legacy)."""
    due = []
    try:
        now = datetime.now(timezone.utc)
        for domain, entry in _DOMAIN_QUERY_SCHEDULE.items():
            nxt = entry.get("next_retrieval", "")
            if not nxt:
                continue
            try:
                nxt_dt = datetime.fromisoformat(nxt.replace("Z", "+00:00"))
                if nxt_dt.tzinfo is None:
                    nxt_dt = nxt_dt.replace(tzinfo=timezone.utc)
                if now >= nxt_dt:
                    due.append(domain)
            except (ValueError, TypeError):
                pass
    except Exception:
        pass
    return due


# ---------------------------------------------------------------------------
# SM-2 per-item scheduling — SQLite-backed
# ---------------------------------------------------------------------------
_SM2_INITIAL_INTERVAL_HOURS = 1.0   # first review after 1 hour
_SM2_INITIAL_EF = 2.5               # SM-2 easiness factor start
_SM2_MIN_EF = 1.3                   # SM-2 EF lower bound
_SM2_FAIL_RESET_HOURS = 1.0         # reset interval on failure


def _get_db():
    """Return the shared pulse brain_db SQLite connection."""
    try:
        from pulse.core.brain_db import get_conn
        return get_conn()
    except Exception:
        return None


def _ensure_table(conn) -> None:
    """Create spaced_schedule table if it doesn't exist."""
    conn.execute("""
        CREATE TABLE IF NOT EXISTS spaced_schedule (
            item_id      TEXT PRIMARY KEY,
            item_type    TEXT NOT NULL DEFAULT 'lesson',
            interval_hours REAL NOT NULL DEFAULT 1.0,
            ef           REAL NOT NULL DEFAULT 2.5,
            next_review_ts REAL NOT NULL,
            last_reviewed_ts REAL,
            review_count INTEGER NOT NULL DEFAULT 0,
            created_ts   REAL NOT NULL
        )
    """)
    conn.commit()


def schedule_item(item_id: str, item_type: str = "lesson") -> dict:
    """Register a new item for spaced retrieval.

    Sets initial interval to 1h and EF to 2.5.
    Returns the schedule record dict.
    """
    conn = _get_db()
    if conn is None:
        return _schedule_item_in_memory(item_id, item_type)
    try:
        _ensure_table(conn)
        now_ts = time.time()
        next_review_ts = now_ts + _SM2_INITIAL_INTERVAL_HOURS * 3600
        conn.execute("""
            INSERT OR IGNORE INTO spaced_schedule
                (item_id, item_type, interval_hours, ef, next_review_ts, last_reviewed_ts,
                 review_count, created_ts)
            VALUES (?, ?, ?, ?, ?, NULL, 0, ?)
        """, (item_id, item_type, _SM2_INITIAL_INTERVAL_HOURS, _SM2_INITIAL_EF,
              next_review_ts, now_ts))
        conn.commit()
        return _load_item_record(conn, item_id)
    except Exception as exc:
        log.debug("schedule_item SQLite failed: %s", exc)
        return _schedule_item_in_memory(item_id, item_type)


def mark_recalled(item_id: str, success: bool) -> dict:
    """Update schedule after retrieval attempt.

    success=True  → interval multiplies by EF (SM-2 style)
    success=False → interval resets to _SM2_FAIL_RESET_HOURS

    EF adjusts: +0.1 on success, -0.2 on failure (clamped to _SM2_MIN_EF).
    Returns updated record dict.
    """
    conn = _get_db()
    if conn is None:
        return _mark_recalled_in_memory(item_id, success)
    try:
        _ensure_table(conn)
        row = conn.execute(
            "SELECT interval_hours, ef, review_count FROM spaced_schedule WHERE item_id=?",
            (item_id,)
        ).fetchone()
        if row is None:
            # Auto-register then update
            schedule_item(item_id)
            row = conn.execute(
                "SELECT interval_hours, ef, review_count FROM spaced_schedule WHERE item_id=?",
                (item_id,)
            ).fetchone()
        interval, ef, count = float(row[0]), float(row[1]), int(row[2])
        now_ts = time.time()
        if success:
            new_interval = interval * ef
            new_ef = min(ef + 0.1, 10.0)   # cap EF growth
        else:
            new_interval = _SM2_FAIL_RESET_HOURS
            new_ef = max(ef - 0.2, _SM2_MIN_EF)
        next_review_ts = now_ts + new_interval * 3600
        conn.execute("""
            UPDATE spaced_schedule
            SET interval_hours=?, ef=?, next_review_ts=?, last_reviewed_ts=?, review_count=?
            WHERE item_id=?
        """, (new_interval, new_ef, next_review_ts, now_ts, count + 1, item_id))
        conn.commit()
        return _load_item_record(conn, item_id)
    except Exception as exc:
        log.debug("mark_recalled SQLite failed: %s", exc)
        return _mark_recalled_in_memory(item_id, success)


def get_due_items(item_type: Optional[str] = None) -> list:
    """Return item_ids due for review (next_review_ts <= now)."""
    conn = _get_db()
    if conn is None:
        return []
    try:
        _ensure_table(conn)
        now_ts = time.time()
        if item_type:
            rows = conn.execute(
                "SELECT item_id FROM spaced_schedule WHERE next_review_ts <= ? AND item_type=?",
                (now_ts, item_type)
            ).fetchall()
        else:
            rows = conn.execute(
                "SELECT item_id FROM spaced_schedule WHERE next_review_ts <= ?",
                (now_ts,)
            ).fetchall()
        return [r[0] for r in rows]
    except Exception as exc:
        log.debug("get_due_items failed: %s", exc)
        return []


def _load_item_record(conn, item_id: str) -> dict:
    row = conn.execute(
        "SELECT item_id, item_type, interval_hours, ef, next_review_ts, "
        "last_reviewed_ts, review_count FROM spaced_schedule WHERE item_id=?",
        (item_id,)
    ).fetchone()
    if not row:
        return {}
    return {
        "item_id": row[0], "item_type": row[1],
        "interval_hours": row[2], "ef": row[3],
        "next_review_ts": row[4], "last_reviewed_ts": row[5],
        "review_count": row[6],
    }


# ---------------------------------------------------------------------------
# In-memory fallback (no DB available)
# ---------------------------------------------------------------------------
_MEM_SCHEDULE: dict = {}


def _schedule_item_in_memory(item_id: str, item_type: str) -> dict:
    now_ts = time.time()
    record = {
        "item_id": item_id, "item_type": item_type,
        "interval_hours": _SM2_INITIAL_INTERVAL_HOURS, "ef": _SM2_INITIAL_EF,
        "next_review_ts": now_ts + _SM2_INITIAL_INTERVAL_HOURS * 3600,
        "last_reviewed_ts": None, "review_count": 0,
    }
    _MEM_SCHEDULE[item_id] = record
    return record


def _mark_recalled_in_memory(item_id: str, success: bool) -> dict:
    record = _MEM_SCHEDULE.get(item_id) or _schedule_item_in_memory(item_id, "lesson")
    now_ts = time.time()
    interval = record["interval_hours"]
    ef = record["ef"]
    if success:
        new_interval = interval * ef
        new_ef = min(ef + 0.1, 10.0)
    else:
        new_interval = _SM2_FAIL_RESET_HOURS
        new_ef = max(ef - 0.2, _SM2_MIN_EF)
    record.update({
        "interval_hours": new_interval, "ef": new_ef,
        "next_review_ts": now_ts + new_interval * 3600,
        "last_reviewed_ts": now_ts,
        "review_count": record["review_count"] + 1,
    })
    _MEM_SCHEDULE[item_id] = record
    return record
