"""Thread management using LangGraph's built-in checkpoint persistence."""

import os
import secrets
import uuid
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from datetime import UTC, datetime
from pathlib import Path

import aiosqlite
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
from langgraph.store.sqlite.aio import AsyncSqliteStore
from rich.table import Table

from ieum.config import COLORS, console

# Patch aiosqlite.Connection to add is_alive() method required by langgraph-checkpoint>=2.1.0
# See: https://github.com/langchain-ai/langgraph/issues/6583
if not hasattr(aiosqlite.Connection, "is_alive"):

    def _is_alive(self: aiosqlite.Connection) -> bool:
        """Check if the connection is still alive."""
        return self._connection is not None

    aiosqlite.Connection.is_alive = _is_alive


def _format_timestamp(iso_timestamp: str | None) -> str:
    """Format ISO timestamp for display (e.g., 'Dec 30, 6:10pm')."""
    if not iso_timestamp:
        return ""
    try:
        dt = datetime.fromisoformat(iso_timestamp).astimezone()
        # Use %I and strip leading zero manually for cross-platform compatibility
        hour = dt.strftime("%I").lstrip("0") or "12"
        return f"{dt.strftime('%b %d')}, {hour}:{dt.strftime('%M%p').lower()}"
    except (ValueError, TypeError):
        return ""


def get_db_path() -> Path:
    """Get path to checkpoints database."""
    db_dir = Path.home() / ".ieum"
    db_dir.mkdir(parents=True, exist_ok=True)
    return db_dir / "sessions.db"


def get_store_db_path() -> Path:
    """Get path to memories store database."""
    db_dir = Path.home() / ".ieum"
    db_dir.mkdir(parents=True, exist_ok=True)
    return db_dir / "memories.db"


def get_web_sessions_db_path() -> Path:
    """Get path to web sessions database (separate from TUI)."""
    db_dir = Path.home() / ".ieum"
    db_dir.mkdir(parents=True, exist_ok=True)
    return db_dir / "web_sessions.db"


async def init_conversation_messages_db() -> None:
    """Initialize conversation messages table in the sessions database."""
    db_path = str(get_db_path())
    async with aiosqlite.connect(db_path, timeout=30.0) as conn:
        await conn.execute("""
            CREATE TABLE IF NOT EXISTS conversation_messages (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                thread_id TEXT NOT NULL,
                role TEXT NOT NULL,
                content TEXT NOT NULL,
                tokens INTEGER DEFAULT 0,
                created_at TEXT NOT NULL
            )
        """)
        await conn.execute("""
            CREATE INDEX IF NOT EXISTS idx_conv_msg_thread
            ON conversation_messages(thread_id, created_at)
        """)
        await conn.commit()


async def save_conversation_message(
    thread_id: str, role: str, content: str, tokens: int = 0,
) -> None:
    """Save a conversation message to the database.

    Args:
        thread_id: The thread/session identifier.
        role: Message role ('user' or 'assistant').
        content: The message text.
        tokens: Token count (optional).
    """
    db_path = str(get_db_path())
    async with aiosqlite.connect(db_path, timeout=30.0) as conn:
        if not await _table_exists(conn, "conversation_messages"):
            await init_conversation_messages_db()
        await conn.execute(
            """
            INSERT INTO conversation_messages (thread_id, role, content, tokens, created_at)
            VALUES (?, ?, ?, ?, ?)
            """,
            (thread_id, role, content, tokens, datetime.now(UTC).isoformat()),
        )
        await conn.commit()


async def get_conversation_messages(
    thread_id: str, limit: int = 50,
) -> list[dict]:
    """Retrieve conversation messages for a thread.

    Args:
        thread_id: The thread identifier.
        limit: Maximum messages to return.

    Returns:
        List of message dicts with role, content, tokens, created_at.
    """
    db_path = str(get_db_path())
    async with aiosqlite.connect(db_path, timeout=30.0) as conn:
        if not await _table_exists(conn, "conversation_messages"):
            return []
        query = """
            SELECT role, content, tokens, created_at
            FROM conversation_messages
            WHERE thread_id = ?
            ORDER BY created_at ASC
            LIMIT ?
        """
        async with conn.execute(query, (thread_id, limit)) as cursor:
            rows = await cursor.fetchall()
            return [
                {"role": r[0], "content": r[1], "tokens": r[2], "created_at": r[3]}
                for r in rows
            ]


async def get_conversation_preview(thread_id: str) -> str | None:
    """Get the first user message as a preview for a thread.

    Args:
        thread_id: The thread identifier.

    Returns:
        First user message content (truncated) or None.
    """
    db_path = str(get_db_path())
    async with aiosqlite.connect(db_path, timeout=30.0) as conn:
        if not await _table_exists(conn, "conversation_messages"):
            return None
        query = """
            SELECT content FROM conversation_messages
            WHERE thread_id = ? AND role = 'user'
            ORDER BY created_at ASC
            LIMIT 1
        """
        async with conn.execute(query, (thread_id,)) as cursor:
            row = await cursor.fetchone()
            return row[0] if row else None


async def get_thread_message_count(thread_id: str) -> int:
    """Count messages in a thread.

    Args:
        thread_id: The thread identifier.

    Returns:
        Number of messages.
    """
    db_path = str(get_db_path())
    async with aiosqlite.connect(db_path, timeout=30.0) as conn:
        if not await _table_exists(conn, "conversation_messages"):
            return 0
        query = "SELECT COUNT(*) FROM conversation_messages WHERE thread_id = ?"
        async with conn.execute(query, (thread_id,)) as cursor:
            row = await cursor.fetchone()
            return row[0] if row else 0


async def init_web_sessions_db() -> None:
    """Initialize web sessions database table."""
    db_path = str(get_web_sessions_db_path())
    async with aiosqlite.connect(db_path, timeout=30.0) as conn:
        await conn.execute("""
            CREATE TABLE IF NOT EXISTS web_sessions (
                id TEXT PRIMARY KEY,
                thread_id TEXT NOT NULL,
                agent_id TEXT NOT NULL,
                working_directory TEXT NOT NULL,
                model_name TEXT,
                auto_approve INTEGER DEFAULT 0,
                approval_policy TEXT NOT NULL DEFAULT 'manual',
                timeout_minutes INTEGER,
                max_concurrent_sessions INTEGER,
                rate_limit_max_requests INTEGER,
                rate_limit_window_seconds INTEGER,
                created_at TEXT NOT NULL,
                last_activity TEXT NOT NULL,
                status TEXT DEFAULT 'active',
                user_id TEXT NOT NULL DEFAULT 'anonymous',
                workspace_id TEXT NOT NULL DEFAULT 'default'
            )
        """)
        await conn.execute("""
            CREATE TABLE IF NOT EXISTS web_audit_log (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                session_id TEXT NOT NULL,
                user_id TEXT NOT NULL,
                workspace_id TEXT NOT NULL,
                event_type TEXT NOT NULL,
                event_ts TEXT NOT NULL,
                payload TEXT NOT NULL
            )
        """)
        await _ensure_web_sessions_columns(conn)
        await conn.commit()


async def save_web_session(
    session_id: str,
    thread_id: str,
    agent_id: str,
    working_directory: str,
    model_name: str | None,
    auto_approve: bool,
    approval_policy: str,
    timeout_minutes: int | None,
    max_concurrent_sessions: int | None,
    rate_limit_max_requests: int | None,
    rate_limit_window_seconds: int | None,
    created_at: str,
    last_activity: str,
    status: str = "active",
    user_id: str = "anonymous",
    workspace_id: str = "default",
) -> None:
    """Save or update a web session to SQLite."""
    db_path = str(get_web_sessions_db_path())
    async with aiosqlite.connect(db_path, timeout=30.0) as conn:
        await _ensure_web_sessions_columns(conn)
        await conn.execute("""
            INSERT OR REPLACE INTO web_sessions
            (
                id, thread_id, agent_id, working_directory, model_name, auto_approve,
                approval_policy, timeout_minutes, max_concurrent_sessions,
                rate_limit_max_requests, rate_limit_window_seconds,
                created_at, last_activity, status, user_id, workspace_id
            )
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        """, (
            session_id,
            thread_id,
            agent_id,
            working_directory,
            model_name,
            int(auto_approve),
            approval_policy,
            timeout_minutes,
            max_concurrent_sessions,
            rate_limit_max_requests,
            rate_limit_window_seconds,
            created_at,
            last_activity,
            status,
            user_id,
            workspace_id,
        ))
        await conn.commit()


async def update_web_session_activity(session_id: str) -> None:
    """Update last_activity timestamp for a web session."""
    db_path = str(get_web_sessions_db_path())
    async with aiosqlite.connect(db_path, timeout=30.0) as conn:
        await conn.execute("""
            UPDATE web_sessions SET last_activity = ? WHERE id = ?
        """, (datetime.now(UTC).isoformat(), session_id))
        await conn.commit()


async def load_web_sessions() -> list[dict]:
    """Load all active web sessions from SQLite."""
    db_path = str(get_web_sessions_db_path())
    async with aiosqlite.connect(db_path, timeout=30.0) as conn:
        if not await _table_exists(conn, "web_sessions"):
            return []

        await _ensure_web_sessions_columns(conn)

        query = """
            SELECT id, thread_id, agent_id, working_directory, model_name,
                   auto_approve, approval_policy, timeout_minutes, max_concurrent_sessions,
                   rate_limit_max_requests, rate_limit_window_seconds,
                   created_at, last_activity, status, user_id, workspace_id
            FROM web_sessions
            WHERE status = 'active'
            ORDER BY last_activity DESC
        """
        async with conn.execute(query) as cursor:
            rows = await cursor.fetchall()
            return [
                {
                    "id": r[0],
                    "thread_id": r[1],
                    "agent_id": r[2],
                    "working_directory": r[3],
                    "model_name": r[4],
                    "auto_approve": bool(r[5]),
                    "approval_policy": r[6] or "manual",
                    "timeout_minutes": r[7],
                    "max_concurrent_sessions": r[8],
                    "rate_limit_max_requests": r[9],
                    "rate_limit_window_seconds": r[10],
                    "created_at": r[11],
                    "last_activity": r[12],
                    "status": r[13],
                    "user_id": r[14],
                    "workspace_id": r[15],
                }
                for r in rows
            ]


async def delete_web_session(session_id: str) -> bool:
    """Delete a web session from SQLite."""
    db_path = str(get_web_sessions_db_path())
    async with aiosqlite.connect(db_path, timeout=30.0) as conn:
        if not await _table_exists(conn, "web_sessions"):
            return False

        cursor = await conn.execute("DELETE FROM web_sessions WHERE id = ?", (session_id,))
        deleted = cursor.rowcount > 0
        await conn.commit()
        return deleted


async def append_web_audit_event(
    *,
    session_id: str,
    user_id: str,
    workspace_id: str,
    event_type: str,
    payload: dict,
) -> None:
    """Append one audit event for web operations."""
    import json

    db_path = str(get_web_sessions_db_path())
    async with aiosqlite.connect(db_path, timeout=30.0) as conn:
        await conn.execute("""
            INSERT INTO web_audit_log
            (session_id, user_id, workspace_id, event_type, event_ts, payload)
            VALUES (?, ?, ?, ?, ?, ?)
        """, (
            session_id,
            user_id,
            workspace_id,
            event_type,
            datetime.now(UTC).isoformat(),
            json.dumps(payload, ensure_ascii=False),
        ))
        await conn.commit()


async def list_web_audit_events(
    *,
    session_id: str,
    user_id: str,
    limit: int = 200,
    since_id: int | None = None,
) -> list[dict]:
    """List audit events for one user-owned session."""
    import json

    if limit < 1:
        limit = 1
    if limit > 1000:
        limit = 1000

    db_path = str(get_web_sessions_db_path())
    async with aiosqlite.connect(db_path, timeout=30.0) as conn:
        if since_id is None:
            query = """
                SELECT id, session_id, user_id, workspace_id, event_type, event_ts, payload
                FROM web_audit_log
                WHERE session_id = ? AND user_id = ?
                ORDER BY id DESC
                LIMIT ?
            """
            args = (session_id, user_id, limit)
        else:
            query = """
                SELECT id, session_id, user_id, workspace_id, event_type, event_ts, payload
                FROM web_audit_log
                WHERE session_id = ? AND user_id = ? AND id > ?
                ORDER BY id ASC
                LIMIT ?
            """
            args = (session_id, user_id, since_id, limit)

        async with conn.execute(query, args) as cursor:
            rows = await cursor.fetchall()

    events = []
    for row in rows:
        payload_text = row[6]
        try:
            payload = json.loads(payload_text)
        except Exception:
            payload = {"raw": payload_text}
        events.append(
            {
                "id": row[0],
                "session_id": row[1],
                "user_id": row[2],
                "workspace_id": row[3],
                "event_type": row[4],
                "event_ts": row[5],
                "payload": payload,
            }
        )

    # Preserve chronological order for default query too.
    if since_id is None:
        events.reverse()
    return events


async def init_web_settings_db() -> None:
    """Initialize web settings database table."""
    db_path = str(get_web_sessions_db_path())
    async with aiosqlite.connect(db_path, timeout=30.0) as conn:
        await conn.execute("""
            CREATE TABLE IF NOT EXISTS web_settings (
                key TEXT PRIMARY KEY,
                value TEXT NOT NULL,
                updated_at TEXT NOT NULL
            )
        """)
        await conn.commit()


async def save_web_settings(settings: dict) -> None:
    """Save web settings to SQLite."""
    import json
    db_path = str(get_web_sessions_db_path())
    await init_web_settings_db()
    async with aiosqlite.connect(db_path, timeout=30.0) as conn:
        now = datetime.now().isoformat()
        await conn.execute("""
            INSERT OR REPLACE INTO web_settings (key, value, updated_at)
            VALUES (?, ?, ?)
        """, ("model_settings", json.dumps(settings), now))
        await conn.commit()


def save_env_file(settings: dict, target: str = "global") -> str:
    """Save settings to .env file.

    Args:
        settings: Dict with provider, base_url, api_key, model keys.
        target: "global" for ~/.ieum/.env, "local" for cwd/.env

    Returns:
        Path to the saved .env file.
    """
    import os

    if target == "global":
        env_dir = Path.home() / ".ieum"
        env_dir.mkdir(parents=True, exist_ok=True)
        env_path = env_dir / ".env"
    else:
        env_path = Path(os.getcwd()) / ".env"

    # Read existing .env content to preserve other variables
    existing_lines = []
    managed_keys = {
        "IEUM_PROVIDER", "IEUM_BASE_URL", "IEUM_API_KEY", "IEUM_MODEL",
        "IEUM_LANG", "GITHUB_PAT", "GITHUB_HOST"
    }

    if env_path.exists():
        with open(env_path, "r", encoding="utf-8") as f:
            for line in f:
                line = line.strip()
                if line and not line.startswith("#"):
                    key = line.split("=")[0] if "=" in line else ""
                    # Keep lines that are not managed by us
                    if key not in managed_keys:
                        existing_lines.append(line)
                elif line.startswith("#"):
                    existing_lines.append(line)

    # Build new .env content
    lines = existing_lines.copy()

    # Add separator comment if we have existing content
    if lines and not lines[-1].startswith("# IEUM"):
        lines.append("")
        lines.append("# IEUM Settings")
    elif not lines:
        lines.append("# IEUM Settings")

    # Add IEUM settings
    if settings.get("provider"):
        lines.append(f"IEUM_PROVIDER={settings['provider']}")
    if settings.get("base_url"):
        lines.append(f"IEUM_BASE_URL={settings['base_url']}")
    if settings.get("api_key"):
        lines.append(f"IEUM_API_KEY={settings['api_key']}")
    if settings.get("model"):
        lines.append(f"IEUM_MODEL={settings['model']}")

    # Add language setting
    if settings.get("lang"):
        lines.append(f"IEUM_LANG={settings['lang']}")

    # Add GitHub settings
    if settings.get("github_pat"):
        lines.append(f"GITHUB_PAT={settings['github_pat']}")
    if settings.get("github_host"):
        lines.append(f"GITHUB_HOST={settings['github_host']}")

    # Write .env file
    with open(env_path, "w", encoding="utf-8") as f:
        f.write("\n".join(lines) + "\n")

    # Restrict file permissions: .env files contain API keys and secrets
    try:
        os.chmod(env_path, 0o600)
    except OSError:
        pass  # Windows may not support chmod fully

    return str(env_path)


def load_env_file(target: str = "global") -> dict | None:
    """Load settings from .env file.

    Args:
        target: "global" for ~/.ieum/.env, "local" for cwd/.env

    Returns:
        Dict with settings or None if file doesn't exist.
    """
    import os

    if target == "global":
        env_path = Path.home() / ".ieum" / ".env"
    else:
        env_path = Path(os.getcwd()) / ".env"

    if not env_path.exists():
        return None

    settings = {}
    with open(env_path, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if line and not line.startswith("#") and "=" in line:
                key, value = line.split("=", 1)
                if key == "IEUM_PROVIDER":
                    settings["provider"] = value
                elif key == "IEUM_BASE_URL":
                    settings["base_url"] = value
                elif key == "IEUM_API_KEY":
                    settings["api_key"] = value
                elif key == "IEUM_MODEL":
                    settings["model"] = value
                elif key == "IEUM_LANG":
                    settings["lang"] = value
                elif key == "GITHUB_PAT":
                    settings["github_pat"] = value
                elif key == "GITHUB_HOST":
                    settings["github_host"] = value

    return settings if settings else None


async def load_web_settings() -> dict | None:
    """Load web settings from SQLite."""
    import json
    db_path = str(get_web_sessions_db_path())
    async with aiosqlite.connect(db_path, timeout=30.0) as conn:
        if not await _table_exists(conn, "web_settings"):
            return None

        async with conn.execute(
            "SELECT value FROM web_settings WHERE key = ?",
            ("model_settings",)
        ) as cursor:
            row = await cursor.fetchone()
            if row:
                return json.loads(row[0])
            return None


@asynccontextmanager
async def get_web_checkpointer() -> AsyncIterator[AsyncSqliteSaver]:
    """Get AsyncSqliteSaver for web sessions (separate from TUI)."""
    db_dir = Path.home() / ".ieum"
    db_dir.mkdir(parents=True, exist_ok=True)
    db_path = db_dir / "web_checkpoints.db"
    async with AsyncSqliteSaver.from_conn_string(str(db_path)) as checkpointer:
        yield checkpointer


@asynccontextmanager
async def get_web_store() -> AsyncIterator[AsyncSqliteStore]:
    """Get AsyncSqliteStore for web sessions (separate from TUI)."""
    db_dir = Path.home() / ".ieum"
    db_dir.mkdir(parents=True, exist_ok=True)
    db_path = db_dir / "web_memories.db"
    async with AsyncSqliteStore.from_conn_string(str(db_path)) as store:
        yield store


def generate_thread_id() -> str:
    """Generate a new 8-char hex thread ID for LangGraph checkpoints."""
    return uuid.uuid4().hex[:8]


def generate_session_id() -> str:
    """Generate a cryptographically secure session identifier.

    Uses :mod:`secrets` to produce a URL-safe token with 256 bits of entropy,
    suitable for web session identifiers where predictability would allow
    session hijacking.

    Returns:
        A 43-character URL-safe base64 string.
    """
    return secrets.token_urlsafe(32)


async def _table_exists(conn: aiosqlite.Connection, table: str) -> bool:
    """Check if a table exists in the database."""
    query = "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?"
    async with conn.execute(query, (table,)) as cursor:
        return await cursor.fetchone() is not None


async def _table_columns(conn: aiosqlite.Connection, table: str) -> set[str]:
    """Return table column names."""
    async with conn.execute(f"PRAGMA table_info({table})") as cursor:
        rows = await cursor.fetchall()
        return {row[1] for row in rows}


async def _ensure_web_sessions_columns(conn: aiosqlite.Connection) -> None:
    """Apply lightweight schema migrations for web_sessions."""
    if not await _table_exists(conn, "web_sessions"):
        return

    columns = await _table_columns(conn, "web_sessions")
    if "user_id" not in columns:
        await conn.execute(
            "ALTER TABLE web_sessions ADD COLUMN user_id TEXT NOT NULL DEFAULT 'anonymous'"
        )
    if "workspace_id" not in columns:
        await conn.execute(
            "ALTER TABLE web_sessions ADD COLUMN workspace_id TEXT NOT NULL DEFAULT 'default'"
        )
    if "approval_policy" not in columns:
        await conn.execute(
            "ALTER TABLE web_sessions ADD COLUMN approval_policy TEXT NOT NULL DEFAULT 'manual'"
        )
    if "timeout_minutes" not in columns:
        await conn.execute(
            "ALTER TABLE web_sessions ADD COLUMN timeout_minutes INTEGER"
        )
    if "max_concurrent_sessions" not in columns:
        await conn.execute(
            "ALTER TABLE web_sessions ADD COLUMN max_concurrent_sessions INTEGER"
        )
    if "rate_limit_max_requests" not in columns:
        await conn.execute(
            "ALTER TABLE web_sessions ADD COLUMN rate_limit_max_requests INTEGER"
        )
    if "rate_limit_window_seconds" not in columns:
        await conn.execute(
            "ALTER TABLE web_sessions ADD COLUMN rate_limit_window_seconds INTEGER"
        )


async def _init_checkpoint_labels(conn: aiosqlite.Connection) -> None:
    """Create checkpoint_labels table if it doesn't exist."""
    await conn.execute("""
        CREATE TABLE IF NOT EXISTS checkpoint_labels (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            thread_id TEXT NOT NULL,
            label TEXT NOT NULL,
            checkpoint_id TEXT NOT NULL,
            description TEXT DEFAULT '',
            created_at TEXT NOT NULL,
            UNIQUE(thread_id, label)
        )
    """)
    await conn.execute("""
        CREATE INDEX IF NOT EXISTS idx_cp_labels_thread
        ON checkpoint_labels(thread_id, created_at)
    """)


async def save_checkpoint_label(
    thread_id: str,
    label: str,
    description: str = "",
) -> str | None:
    """Save a user label for the current LangGraph checkpoint.

    Reads the latest checkpoint_id from the checkpoints table and
    associates it with a human-readable label.

    Args:
        thread_id: The thread/session identifier.
        label: User-supplied label for this checkpoint.
        description: Optional description.

    Returns:
        The checkpoint_id that was labeled, or None if no checkpoint found.
    """
    db_path = str(get_db_path())
    async with aiosqlite.connect(db_path, timeout=30.0) as conn:
        await _init_checkpoint_labels(conn)

        # Find the latest checkpoint for this thread
        query = """
            SELECT checkpoint_id FROM checkpoints
            WHERE thread_id = ? AND checkpoint_ns = ''
            ORDER BY checkpoint_id DESC
            LIMIT 1
        """
        async with conn.execute(query, (thread_id,)) as cursor:
            row = await cursor.fetchone()
            if not row:
                return None
            cp_id = row[0]

        await conn.execute(
            """
            INSERT OR REPLACE INTO checkpoint_labels
            (thread_id, label, checkpoint_id, description, created_at)
            VALUES (?, ?, ?, ?, ?)
            """,
            (thread_id, label, cp_id, description, datetime.now(UTC).isoformat()),
        )
        await conn.commit()
        return cp_id


async def list_checkpoint_labels(thread_id: str) -> list[dict]:
    """List labeled checkpoints for a thread.

    Args:
        thread_id: The thread identifier.

    Returns:
        List of dicts with label, checkpoint_id, description, created_at.
    """
    db_path = str(get_db_path())
    async with aiosqlite.connect(db_path, timeout=30.0) as conn:
        if not await _table_exists(conn, "checkpoint_labels"):
            return []

        query = """
            SELECT label, checkpoint_id, description, created_at
            FROM checkpoint_labels
            WHERE thread_id = ?
            ORDER BY created_at DESC
        """
        async with conn.execute(query, (thread_id,)) as cursor:
            rows = await cursor.fetchall()
            return [
                {
                    "label": r[0],
                    "checkpoint_id": r[1],
                    "description": r[2],
                    "created_at": r[3],
                }
                for r in rows
            ]


async def get_checkpoint_by_label(thread_id: str, label: str) -> str | None:
    """Get the checkpoint_id associated with a label.

    Args:
        thread_id: The thread identifier.
        label: The checkpoint label.

    Returns:
        The checkpoint_id or None if not found.
    """
    db_path = str(get_db_path())
    async with aiosqlite.connect(db_path, timeout=30.0) as conn:
        if not await _table_exists(conn, "checkpoint_labels"):
            return None

        query = """
            SELECT checkpoint_id FROM checkpoint_labels
            WHERE thread_id = ? AND label = ?
        """
        async with conn.execute(query, (thread_id, label)) as cursor:
            row = await cursor.fetchone()
            return row[0] if row else None


async def delete_checkpoint_label(thread_id: str, label: str) -> bool:
    """Delete a checkpoint label.

    Args:
        thread_id: The thread identifier.
        label: The checkpoint label to delete.

    Returns:
        True if deleted, False if not found.
    """
    db_path = str(get_db_path())
    async with aiosqlite.connect(db_path, timeout=30.0) as conn:
        if not await _table_exists(conn, "checkpoint_labels"):
            return False

        cursor = await conn.execute(
            "DELETE FROM checkpoint_labels WHERE thread_id = ? AND label = ?",
            (thread_id, label),
        )
        deleted = cursor.rowcount > 0
        await conn.commit()
        return deleted


async def list_threads(
    agent_name: str | None = None,
    limit: int = 20,
) -> list[dict]:
    """List threads from checkpoints table."""
    db_path = str(get_db_path())
    async with aiosqlite.connect(db_path, timeout=30.0) as conn:
        # Return empty if table doesn't exist yet (fresh install)
        if not await _table_exists(conn, "checkpoints"):
            return []

        if agent_name:
            query = """
                SELECT thread_id,
                       json_extract(metadata, '$.agent_name') as agent_name,
                       MAX(json_extract(metadata, '$.updated_at')) as updated_at
                FROM checkpoints
                WHERE json_extract(metadata, '$.agent_name') = ?
                GROUP BY thread_id
                ORDER BY updated_at DESC
                LIMIT ?
            """
            params: tuple = (agent_name, limit)
        else:
            query = """
                SELECT thread_id,
                       json_extract(metadata, '$.agent_name') as agent_name,
                       MAX(json_extract(metadata, '$.updated_at')) as updated_at
                FROM checkpoints
                GROUP BY thread_id
                ORDER BY updated_at DESC
                LIMIT ?
            """
            params = (limit,)

        async with conn.execute(query, params) as cursor:
            rows = await cursor.fetchall()
            return [{"thread_id": r[0], "agent_name": r[1], "updated_at": r[2]} for r in rows]


async def get_most_recent(agent_name: str | None = None) -> str | None:
    """Get most recent thread_id, optionally filtered by agent."""
    db_path = str(get_db_path())
    async with aiosqlite.connect(db_path, timeout=30.0) as conn:
        if not await _table_exists(conn, "checkpoints"):
            return None

        if agent_name:
            query = """
                SELECT thread_id FROM checkpoints
                WHERE json_extract(metadata, '$.agent_name') = ?
                ORDER BY checkpoint_id DESC
                LIMIT 1
            """
            params: tuple = (agent_name,)
        else:
            query = "SELECT thread_id FROM checkpoints ORDER BY checkpoint_id DESC LIMIT 1"
            params = ()

        async with conn.execute(query, params) as cursor:
            row = await cursor.fetchone()
            return row[0] if row else None


async def get_thread_agent(thread_id: str) -> str | None:
    """Get agent_name for a thread."""
    db_path = str(get_db_path())
    async with aiosqlite.connect(db_path, timeout=30.0) as conn:
        if not await _table_exists(conn, "checkpoints"):
            return None

        query = """
            SELECT json_extract(metadata, '$.agent_name')
            FROM checkpoints
            WHERE thread_id = ?
            LIMIT 1
        """
        async with conn.execute(query, (thread_id,)) as cursor:
            row = await cursor.fetchone()
            return row[0] if row else None


async def thread_exists(thread_id: str) -> bool:
    """Check if a thread exists in checkpoints."""
    db_path = str(get_db_path())
    async with aiosqlite.connect(db_path, timeout=30.0) as conn:
        if not await _table_exists(conn, "checkpoints"):
            return False

        query = "SELECT 1 FROM checkpoints WHERE thread_id = ? LIMIT 1"
        async with conn.execute(query, (thread_id,)) as cursor:
            row = await cursor.fetchone()
            return row is not None


async def delete_thread(thread_id: str) -> bool:
    """Delete thread checkpoints. Returns True if deleted."""
    db_path = str(get_db_path())
    async with aiosqlite.connect(db_path, timeout=30.0) as conn:
        if not await _table_exists(conn, "checkpoints"):
            return False

        cursor = await conn.execute("DELETE FROM checkpoints WHERE thread_id = ?", (thread_id,))
        deleted = cursor.rowcount > 0
        if await _table_exists(conn, "writes"):
            await conn.execute("DELETE FROM writes WHERE thread_id = ?", (thread_id,))
        await conn.commit()
        return deleted


@asynccontextmanager
async def get_checkpointer() -> AsyncIterator[AsyncSqliteSaver]:
    """Get AsyncSqliteSaver for the global database."""
    async with AsyncSqliteSaver.from_conn_string(str(get_db_path())) as checkpointer:
        yield checkpointer


@asynccontextmanager
async def get_store() -> AsyncIterator[AsyncSqliteStore]:
    """Get AsyncSqliteStore for persistent cross-thread memories."""
    async with AsyncSqliteStore.from_conn_string(str(get_store_db_path())) as store:
        yield store


async def list_threads_command(
    agent_name: str | None = None,
    limit: int = 20,
) -> None:
    """CLI handler for: ieum threads list."""
    threads = await list_threads(agent_name, limit=limit)

    if not threads:
        if agent_name:
            console.print(f"[yellow]No threads found for agent '{agent_name}'.[/yellow]")
        else:
            console.print("[yellow]No threads found.[/yellow]")
        console.print("[dim]Start a conversation with: ieum[/dim]")
        return

    title = f"Threads for '{agent_name}'" if agent_name else "All Threads"

    table = Table(title=title, show_header=True, header_style=f"bold {COLORS['primary']}")
    table.add_column("Thread ID", style="bold")
    table.add_column("Agent")
    table.add_column("Last Used", style="dim")

    for t in threads:
        table.add_row(
            t["thread_id"],
            t["agent_name"] or "unknown",
            _format_timestamp(t.get("updated_at")),
        )

    console.print()
    console.print(table)
    console.print()


async def delete_thread_command(thread_id: str) -> None:
    """CLI handler for: ieum threads delete."""
    deleted = await delete_thread(thread_id)

    if deleted:
        console.print(f"[green]Thread '{thread_id}' deleted.[/green]")
    else:
        console.print(f"[red]Thread '{thread_id}' not found.[/red]")
