"""
cortex/commitments/carry_forward.py — Sleep/wake carry-forward logic for open loops.

On each sleep/wake cycle, open loops have their carry_count incremented.
Loops that survive more than STALE_THRESHOLD cycles are auto-resolved as stale
to prevent indefinite accumulation.

Boot injection uses get_boot_items() to surface due commitments and open loops
to the agent at startup, ensuring continuity across sessions.
"""

from __future__ import annotations

from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Tuple

from cortex.commitments.models import Commitment, OpenLoop
from cortex.commitments.store import (
    _now,
    _row_to_commitment,
    _row_to_open_loop,
    get_due_commitments,
    list_open_loops,
)


# ---------------------------------------------------------------------------
# Sleep/wake carry
# ---------------------------------------------------------------------------

async def carry_forward_loops(
    storage: Any,
    session_id: str,
    owner_id: str = "default",
    stale_threshold: int | None = None,
) -> Dict[str, int]:
    """Increment carry_count on all open loops for *owner_id*.

    Loops that reach carry_count > *stale_threshold* are automatically
    resolved with status "resolved" (marked stale) to prevent indefinite
    accumulation.

    This should be called once per sleep/wake cycle (e.g. during the
    sleep phase of the CircadianEngine).

    Args:
        storage: SQLiteAdapter instance.
        session_id: The session ID of the cycle being processed (for logging).
        owner_id: Multi-tenancy scope key.
        stale_threshold: Number of cycles before auto-resolve. Defaults to
            OpenLoop.STALE_THRESHOLD (5) when None.

    Returns:
        Dict with keys:
            - "incremented": number of loops whose carry_count was bumped
            - "auto_resolved": number of loops auto-resolved as stale
    """
    if stale_threshold is None:
        stale_threshold = OpenLoop.STALE_THRESHOLD
    async with storage._write_lock:
        conn = await storage._get_conn()
        now = _now()

        # Increment carry_count for all open loops
        await conn.execute(
            """
            UPDATE open_loop
            SET carry_count = carry_count + 1,
                updated_at  = ?
            WHERE owner_id = ? AND status = 'open'
            """,
            (now, owner_id),
        )

        # Count how many were incremented
        async with conn.execute(
            "SELECT COUNT(*) FROM open_loop WHERE owner_id = ? AND status = 'open'",
            (owner_id,),
        ) as cur:
            row = await cur.fetchone()
        incremented = row[0] if row else 0

        # Auto-resolve stale loops (carry_count > STALE_THRESHOLD)
        await conn.execute(
            """
            UPDATE open_loop
            SET status      = 'resolved',
                resolved_at = ?,
                updated_at  = ?
            WHERE owner_id = ?
              AND status    = 'open'
              AND carry_count > ?
            """,
            (now, now, owner_id, stale_threshold),
        )

        # Count auto-resolved
        async with conn.execute(
            """
            SELECT COUNT(*) FROM open_loop
            WHERE owner_id = ?
              AND status    = 'resolved'
              AND resolved_at = ?
            """,
            (owner_id, now),
        ) as cur:
            row = await cur.fetchone()
        auto_resolved = row[0] if row else 0

        await conn.commit()

    return {
        "incremented": incremented,
        "auto_resolved": auto_resolved,
    }


# ---------------------------------------------------------------------------
# Boot injection
# ---------------------------------------------------------------------------

async def get_boot_items(
    storage: Any,
    owner_id: str = "default",
    as_of: Optional[str] = None,
) -> Dict[str, List]:
    """Return due commitments and open loops for boot-time context injection.

    Called during the agent's wake path to surface outstanding obligations
    and unresolved threads. The returned data is intended to be injected
    into the agent's context window alongside cornerstones.

    Args:
        storage: SQLiteAdapter instance.
        owner_id: Multi-tenancy scope key.
        as_of: ISO 8601 timestamp for "due" cutoff (defaults to now).

    Returns:
        Dict with keys:
            - "due_commitments": List[Commitment] — open commitments due on/before as_of
            - "open_loops": List[OpenLoop] — all open loops (not yet resolved)
    """
    due_commitments = await get_due_commitments(storage, owner_id=owner_id, as_of=as_of)
    open_loops = await list_open_loops(storage, owner_id=owner_id, status=OpenLoop.OPEN)

    return {
        "due_commitments": due_commitments,
        "open_loops": open_loops,
    }
