"""
cortex/commitments/store.py — CRUD operations for commitments and open loops.

Uses the StorageAdapter pattern: functions accept a storage adapter instance
and execute parameterised SQL queries via its internal connection.

All write operations are serialised through the adapter's _write_lock to
prevent SQLite SQLITE_BUSY errors under concurrent access.

Column mapping (v8/v9 schema → dataclass):
  commitment.title          → Commitment.content
  commitment.completed_at   → Commitment.closed_at
  commitment.source_turn_id → Commitment.source_turn_id  (v9)
  open_loop.title           → OpenLoop.content
  open_loop.description     → OpenLoop.context
  open_loop.carry_count     → OpenLoop.carry_count       (v9)
"""

from __future__ import annotations

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

from cortex.commitments.models import Commitment, OpenLoop


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _now() -> str:
    """Return current UTC time as ISO 8601 string (millisecond precision)."""
    return datetime.now(timezone.utc).isoformat(timespec="milliseconds")


def _row_to_commitment(row: Any) -> Commitment:
    """Convert a DB row (aiosqlite.Row or dict) to a Commitment dataclass."""
    d = dict(row)
    return Commitment(
        id=d["id"],
        owner_id=d["owner_id"],
        content=d["title"],            # title stores the commitment content
        due_at=d.get("due_at"),
        status=d["status"],
        source_turn_id=d.get("source_turn_id"),
        created_at=d["created_at"],
        closed_at=d.get("completed_at"),  # completed_at stores closed_at
    )


def _row_to_open_loop(row: Any) -> OpenLoop:
    """Convert a DB row (aiosqlite.Row or dict) to an OpenLoop dataclass."""
    d = dict(row)
    return OpenLoop(
        id=d["id"],
        owner_id=d["owner_id"],
        content=d["title"],            # title stores the loop content
        context=d.get("description"),  # description stores the context
        status=d["status"],
        carry_count=d.get("carry_count", 0) or 0,
        created_at=d["created_at"],
        resolved_at=d.get("resolved_at"),
    )


# ---------------------------------------------------------------------------
# Commitment CRUD
# ---------------------------------------------------------------------------

async def create_commitment(
    storage: Any,
    owner_id: str,
    content: str,
    due_at: Optional[str] = None,
    status: str = Commitment.OPEN,
    source_turn_id: Optional[str] = None,
) -> Commitment:
    """Insert a new commitment and return the created Commitment.

    Args:
        storage: SQLiteAdapter instance.
        owner_id: Multi-tenancy scope key.
        content: What was committed to.
        due_at: Optional ISO 8601 deadline.
        status: Initial status (default "open").
        source_turn_id: Optional reference to source conversation turn.

    Returns:
        The newly created Commitment.
    """
    if status not in Commitment.VALID_STATUSES:
        raise ValueError(f"Invalid status {status!r}. Must be one of {Commitment.VALID_STATUSES}")

    async with storage._write_lock:
        conn = await storage._get_conn()
        now = _now()
        async with conn.execute(
            """
            INSERT INTO commitment
                (owner_id, title, status, due_at, source_turn_id, created_at, updated_at)
            VALUES (?, ?, ?, ?, ?, ?, ?)
            RETURNING *
            """,
            (owner_id, content, status, due_at, source_turn_id, now, now),
        ) as cur:
            row = await cur.fetchone()
        await conn.commit()
    return _row_to_commitment(row)


async def list_commitments(
    storage: Any,
    owner_id: str = "default",
    status: Optional[str] = None,
    due_before: Optional[str] = None,
    limit: int = 100,
    offset: int = 0,
) -> List[Commitment]:
    """List commitments with optional filters.

    Args:
        storage: SQLiteAdapter instance.
        owner_id: Multi-tenancy scope key.
        status: Filter by status ("open" | "completed" | "cancelled" | None for all).
        due_before: ISO 8601 timestamp — return only commitments due before this time.
        limit: Maximum rows to return.
        offset: Row offset for pagination.

    Returns:
        List of matching Commitment objects.
    """
    conn = await storage._get_conn()
    conditions = ["owner_id = ?"]
    params: list = [owner_id]

    if status is not None:
        conditions.append("status = ?")
        params.append(status)

    if due_before is not None:
        conditions.append("due_at IS NOT NULL AND due_at <= ?")
        params.append(due_before)

    where = " AND ".join(conditions)
    params.extend([limit, offset])

    async with conn.execute(
        f"""
        SELECT * FROM commitment
        WHERE {where}
        ORDER BY created_at DESC
        LIMIT ? OFFSET ?
        """,
        params,
    ) as cur:
        rows = await cur.fetchall()

    return [_row_to_commitment(r) for r in rows]


async def close_commitment(
    storage: Any,
    commitment_id: str,
    status: str,
    owner_id: Optional[str] = None,
) -> Optional[Commitment]:
    """Close (complete or cancel) a commitment.

    Args:
        storage: SQLiteAdapter instance.
        commitment_id: UUID of the commitment to close.
        status: New status — "completed" | "cancelled".
        owner_id: If provided, scopes the update to this owner.

    Returns:
        Updated Commitment, or None if not found.

    Raises:
        ValueError: If status is not a valid closing status.
    """
    if status not in (Commitment.COMPLETED, Commitment.CANCELLED):
        raise ValueError(
            f"close_commitment requires status 'completed' or 'cancelled', got {status!r}"
        )

    async with storage._write_lock:
        conn = await storage._get_conn()
        now = _now()

        if owner_id is not None:
            await conn.execute(
                """
                UPDATE commitment
                SET status = ?, completed_at = ?, updated_at = ?
                WHERE id = ? AND owner_id = ?
                """,
                (status, now, now, commitment_id, owner_id),
            )
            async with conn.execute(
                "SELECT * FROM commitment WHERE id = ? AND owner_id = ?",
                (commitment_id, owner_id),
            ) as cur:
                row = await cur.fetchone()
        else:
            await conn.execute(
                """
                UPDATE commitment
                SET status = ?, completed_at = ?, updated_at = ?
                WHERE id = ?
                """,
                (status, now, now, commitment_id),
            )
            async with conn.execute(
                "SELECT * FROM commitment WHERE id = ?",
                (commitment_id,),
            ) as cur:
                row = await cur.fetchone()

        await conn.commit()

    return _row_to_commitment(row) if row else None


async def get_due_commitments(
    storage: Any,
    owner_id: str = "default",
    as_of: Optional[str] = None,
) -> List[Commitment]:
    """Return open commitments that are due on or before *as_of*.

    Args:
        storage: SQLiteAdapter instance.
        owner_id: Multi-tenancy scope key.
        as_of: ISO 8601 timestamp (defaults to current time).

    Returns:
        List of overdue or due-now open Commitment objects.
    """
    cutoff = as_of or _now()
    return await list_commitments(
        storage,
        owner_id=owner_id,
        status=Commitment.OPEN,
        due_before=cutoff,
    )


# ---------------------------------------------------------------------------
# OpenLoop CRUD
# ---------------------------------------------------------------------------

async def create_open_loop(
    storage: Any,
    owner_id: str,
    content: str,
    context: Optional[str] = None,
    status: str = OpenLoop.OPEN,
) -> OpenLoop:
    """Insert a new open loop and return the created OpenLoop.

    Args:
        storage: SQLiteAdapter instance.
        owner_id: Multi-tenancy scope key.
        content: What the loop is about.
        context: Optional description of what triggered the loop.
        status: Initial status (default "open").

    Returns:
        The newly created OpenLoop.
    """
    if status not in OpenLoop.VALID_STATUSES:
        raise ValueError(f"Invalid status {status!r}. Must be one of {OpenLoop.VALID_STATUSES}")

    async with storage._write_lock:
        conn = await storage._get_conn()
        now = _now()
        async with conn.execute(
            """
            INSERT INTO open_loop
                (owner_id, title, description, status, carry_count, created_at, updated_at)
            VALUES (?, ?, ?, ?, 0, ?, ?)
            RETURNING *
            """,
            (owner_id, content, context, status, now, now),
        ) as cur:
            row = await cur.fetchone()
        await conn.commit()

    return _row_to_open_loop(row)


async def list_open_loops(
    storage: Any,
    owner_id: str = "default",
    status: Optional[str] = None,
    limit: int = 100,
    offset: int = 0,
) -> List[OpenLoop]:
    """List open loops with optional status filter.

    Args:
        storage: SQLiteAdapter instance.
        owner_id: Multi-tenancy scope key.
        status: Filter by status ("open" | "resolved" | None for all).
        limit: Maximum rows to return.
        offset: Row offset for pagination.

    Returns:
        List of matching OpenLoop objects.
    """
    conn = await storage._get_conn()
    conditions = ["owner_id = ?"]
    params: list = [owner_id]

    if status is not None:
        conditions.append("status = ?")
        params.append(status)

    where = " AND ".join(conditions)
    params.extend([limit, offset])

    async with conn.execute(
        f"""
        SELECT * FROM open_loop
        WHERE {where}
        ORDER BY created_at DESC
        LIMIT ? OFFSET ?
        """,
        params,
    ) as cur:
        rows = await cur.fetchall()

    return [_row_to_open_loop(r) for r in rows]


async def resolve_open_loop(
    storage: Any,
    loop_id: str,
    owner_id: Optional[str] = None,
) -> Optional[OpenLoop]:
    """Mark an open loop as resolved.

    Args:
        storage: SQLiteAdapter instance.
        loop_id: UUID of the loop to resolve.
        owner_id: If provided, scopes the update to this owner.

    Returns:
        Updated OpenLoop, or None if not found.
    """
    async with storage._write_lock:
        conn = await storage._get_conn()
        now = _now()

        if owner_id is not None:
            await conn.execute(
                """
                UPDATE open_loop
                SET status = 'resolved', resolved_at = ?, updated_at = ?
                WHERE id = ? AND owner_id = ?
                """,
                (now, now, loop_id, owner_id),
            )
            async with conn.execute(
                "SELECT * FROM open_loop WHERE id = ? AND owner_id = ?",
                (loop_id, owner_id),
            ) as cur:
                row = await cur.fetchone()
        else:
            await conn.execute(
                """
                UPDATE open_loop
                SET status = 'resolved', resolved_at = ?, updated_at = ?
                WHERE id = ?
                """,
                (now, now, loop_id),
            )
            async with conn.execute(
                "SELECT * FROM open_loop WHERE id = ?",
                (loop_id,),
            ) as cur:
                row = await cur.fetchone()

        await conn.commit()

    return _row_to_open_loop(row) if row else None
