#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
Spaced Retrieval Scheduler: Testing Effect + Spacing Effect (Phase 3).

Highest-utility learning techniques per Dunlosky 2013 meta-analysis.
During each consolidation cycle:
1. Select items whose review interval has elapsed
2. Generate synthetic query from item content
3. Run brain_search — if item found: extend interval, if not: shorten + boost

Leitner box intervals: 1d, 3d, 7d, 14d, 30d, 60d
"""
import json
import logging
from datetime import datetime, timezone, timedelta

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

INTERVALS_DAYS = [1, 3, 7, 14, 30, 60]
MAX_REVIEWS_PER_CYCLE = 20
MAX_BOX = len(INTERVALS_DAYS) - 1


def schedule_reviews(verbose: bool = False) -> dict:
    """Main entry: called from consolidation daemon. Returns review stats."""
    try:
        from pulse.core.brain_db import get_conn
    except ImportError:
        return {"status": "skipped", "reason": "no SQLite"}
    conn = get_conn()
    now = datetime.now(timezone.utc)
    now_str = now.isoformat()

    newly = _initialize_unscheduled(conn, now)
    due = _get_due_items(conn, now_str, MAX_REVIEWS_PER_CYCLE)
    if not due:
        if verbose:
            print(f"  [SPACED] No items due. {newly} newly scheduled.")
        return {"reviewed": 0, "passed": 0, "failed": 0, "newly_scheduled": newly}

    passed = failed = 0
    for item in due:
        if _test_retrieval(item):
            _advance_box(conn, item, now)
            passed += 1
        else:
            _regress_box(conn, item, now)
            failed += 1
    conn.commit()
    if verbose:
        print(f"  [SPACED] Reviewed {len(due)}: {passed} passed, {failed} failed, {newly} new")
    return {"reviewed": len(due), "passed": passed, "failed": failed,
            "newly_scheduled": newly}


def _initialize_unscheduled(conn, now: datetime) -> int:
    """Add review metadata to items that don't have it yet."""
    count = 0
    first_review = (now + timedelta(days=INTERVALS_DAYS[0])).isoformat()
    for table in ["lessons", "failures", "patterns"]:
        try:
            rows = conn.execute(f"""
                SELECT id, metadata FROM {table}
                WHERE validity = 'valid'
                  AND confidence >= 0.4
                  AND (metadata IS NULL OR metadata NOT LIKE '%spaced_box%')
                LIMIT 50
            """).fetchall()
            for row in rows:
                existing = {}
                if row["metadata"]:
                    try:
                        existing = json.loads(row["metadata"])
                    except (json.JSONDecodeError, TypeError):
                        existing = {}
                existing.update({
                    "spaced_box": 0,
                    "next_review_at": first_review,
                    "review_count": 0,
                    "last_reviewed": None,
                })
                conn.execute(f"""
                    UPDATE {table} SET metadata = ? WHERE id = ?
                """, (json.dumps(existing), row["id"]))
                count += 1
        except Exception as e:
            log.debug("Init scheduling for %s failed: %s", table, e)
    if count:
        conn.commit()
    return count


def _get_due_items(conn, now_str: str, limit: int) -> list[dict]:
    """Find items whose next_review_at has passed."""
    due = []
    for table in ["lessons", "failures", "patterns"]:
        try:
            rows = conn.execute(f"""
                SELECT id, title, content, confidence, salience, metadata, '{table}' as tbl
                FROM {table}
                WHERE validity = 'valid'
                  AND metadata LIKE '%next_review_at%'
                  AND json_extract(metadata, '$.next_review_at') <= ?
                ORDER BY json_extract(metadata, '$.next_review_at') ASC
                LIMIT ?
            """, (now_str, limit - len(due))).fetchall()
            due.extend([dict(r) for r in rows])
        except Exception as e:
            log.debug("Get due items from %s failed: %s", table, e)
        if len(due) >= limit:
            break
    return due[:limit]


def _test_retrieval(item: dict) -> bool:
    """Test if the brain can retrieve this item via synthetic query."""
    text = (item.get("title") or item.get("content") or "")[:100]
    if len(text) < 15:
        return True  # Too short to test meaningfully — pass
    # Generate synthetic query: use first 5 significant words
    words = [w for w in text.split() if len(w) > 3][:5]
    query = " ".join(words)
    if not query:
        return True
    try:
        from pulse.brain.brain_search import brain_search
        result = brain_search(query, max_per_source=3)
        total = result.get("total_results", 0)
        if total == 0:
            return False
        # Check if original item appears in results
        item_text = text.lower()[:60]
        for source_hits in result.get("results", {}).values():
            if not isinstance(source_hits, list):
                continue
            for hit in source_hits:
                hit_text = (hit.get("text") or hit.get("title") or "").lower()[:60]
                if item_text[:30] in hit_text or hit_text[:30] in item_text:
                    return True
        return False
    except Exception:
        return True  # Don't penalize on search errors


def _advance_box(conn, item: dict, now: datetime):
    """Success: advance to next box, extend interval."""
    meta = json.loads(item.get("metadata") or "{}")
    box = min(MAX_BOX, meta.get("spaced_box", 0) + 1)
    interval = INTERVALS_DAYS[box]
    meta["spaced_box"] = box
    meta["next_review_at"] = (now + timedelta(days=interval)).isoformat()
    meta["review_count"] = meta.get("review_count", 0) + 1
    meta["last_reviewed"] = now.isoformat()
    meta["last_result"] = "pass"
    table = item["tbl"]
    conn.execute(f"UPDATE {table} SET metadata = ? WHERE id = ?",
                 (json.dumps(meta), item["id"]))


def _regress_box(conn, item: dict, now: datetime):
    """Failure: regress box, shorten interval, boost salience."""
    meta = json.loads(item.get("metadata") or "{}")
    box = max(0, meta.get("spaced_box", 0) - 1)
    interval = INTERVALS_DAYS[box]
    meta["spaced_box"] = box
    meta["next_review_at"] = (now + timedelta(days=interval)).isoformat()
    meta["review_count"] = meta.get("review_count", 0) + 1
    meta["last_reviewed"] = now.isoformat()
    meta["last_result"] = "fail"
    table = item["tbl"]
    conn.execute(f"""
        UPDATE {table}
        SET metadata = ?,
            salience = MIN(salience + 0.5, 7.0)
        WHERE id = ?
    """, (json.dumps(meta), item["id"]))
