"""Persist background bash task records.

Sibling to run_state_store.py: a SQLite table tracking bg shell tasks the
agent spawns via bash(run_in_background=true). The store survives backend
restarts so polling can be resumed; the actual remote process is owned by
nohup on the Jupyter terminal and is independent of this store.
"""

from __future__ import annotations

import sqlite3
import time
from dataclasses import dataclass
from pathlib import Path
from threading import RLock
from typing import Any


ACTIVE_BG_STATUSES = {'running'}
TERMINAL_BG_STATUSES = {'completed', 'failed', 'cancelled'}


@dataclass(frozen=True)
class BgTaskSpec:
    """Payload an agent-side handler hands to the backend on registration."""

    task_id: str
    account_key: str
    session_id: str
    run_id: str
    pid: int
    task_dir: str
    output_path: str
    pid_path: str
    exit_code_path: str
    command: str
    started_at: float
    wait_for_completion: bool


@dataclass(frozen=True)
class BgTaskStatus:
    """Snapshot the agent reads back via bash_status."""

    task_id: str
    status: str
    pid: int
    started_at: float
    finished_at: float | None
    exit_code: int | None
    output_path: str
    output_preview: str
    auto_resumed: bool


class BashBgStore:
    def __init__(self, db_path: Path) -> None:
        self.db_path = db_path
        self._lock = RLock()

    def record_start(self, spec: BgTaskSpec) -> None:
        now = time.time()
        with self._connect() as conn:
            conn.execute(
                """
                insert into bash_bg_tasks (
                    task_id, account_key, session_id, run_id, pid,
                    task_dir, output_path, pid_path, exit_code_path,
                    command, status, started_at, updated_at,
                    wait_for_completion, auto_resumed
                )
                values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', ?, ?, ?, 0)
                on conflict(task_id) do nothing
                """,
                (
                    spec.task_id,
                    spec.account_key,
                    spec.session_id,
                    spec.run_id,
                    spec.pid,
                    spec.task_dir,
                    spec.output_path,
                    spec.pid_path,
                    spec.exit_code_path,
                    spec.command,
                    spec.started_at,
                    now,
                    1 if spec.wait_for_completion else 0,
                ),
            )

    def mark_completed(
        self,
        task_id: str,
        *,
        exit_code: int,
        finished_at: float | None = None,
    ) -> None:
        finished = finished_at if finished_at is not None else time.time()
        status = 'completed' if exit_code == 0 else 'failed'
        with self._connect() as conn:
            conn.execute(
                """
                update bash_bg_tasks
                set status = ?, exit_code = ?, finished_at = ?, updated_at = ?
                where task_id = ? and status = 'running'
                """,
                (status, exit_code, finished, finished, task_id),
            )

    def mark_killed(self, task_id: str) -> None:
        now = time.time()
        with self._connect() as conn:
            conn.execute(
                """
                update bash_bg_tasks
                set status = 'cancelled', finished_at = ?, updated_at = ?
                where task_id = ? and status = 'running'
                """,
                (now, now, task_id),
            )

    def mark_auto_resumed(self, task_id: str) -> bool:
        """Set auto_resumed=1 atomically; return True if we won the race."""
        with self._connect() as conn:
            cursor = conn.execute(
                """
                update bash_bg_tasks
                set auto_resumed = 1, updated_at = ?
                where task_id = ? and auto_resumed = 0
                """,
                (time.time(), task_id),
            )
            return cursor.rowcount > 0

    def list_active(self) -> list[dict[str, Any]]:
        with self._connect() as conn:
            rows = conn.execute(
                "select * from bash_bg_tasks where status = 'running'"
            ).fetchall()
            return [dict(row) for row in rows]

    def list_for_session(
        self,
        account_key: str,
        session_id: str,
    ) -> list[dict[str, Any]]:
        with self._connect() as conn:
            rows = conn.execute(
                """
                select * from bash_bg_tasks
                where account_key = ? and session_id = ?
                order by started_at desc
                """,
                (account_key, session_id),
            ).fetchall()
            return [dict(row) for row in rows]

    def list_active_for_session(
        self,
        account_key: str,
        session_id: str,
    ) -> list[dict[str, Any]]:
        with self._connect() as conn:
            rows = conn.execute(
                """
                select * from bash_bg_tasks
                where account_key = ?
                  and session_id = ?
                  and status = 'running'
                order by started_at desc
                """,
                (account_key, session_id),
            ).fetchall()
            return [dict(row) for row in rows]

    def get(self, task_id: str) -> dict[str, Any] | None:
        with self._connect() as conn:
            row = conn.execute(
                "select * from bash_bg_tasks where task_id = ?",
                (task_id,),
            ).fetchone()
            return dict(row) if row is not None else None

    def _connect(self) -> sqlite3.Connection:
        with self._lock:
            self.db_path.parent.mkdir(parents=True, exist_ok=True)
            conn = sqlite3.connect(self.db_path, timeout=10)
            conn.row_factory = sqlite3.Row
            self._init_schema(conn)
            return conn

    def _init_schema(self, conn: sqlite3.Connection) -> None:
        conn.execute('pragma journal_mode=wal')
        conn.execute(
            """
            create table if not exists bash_bg_tasks (
                task_id text primary key,
                account_key text not null,
                session_id text not null,
                run_id text not null default '',
                pid integer not null,
                task_dir text not null,
                output_path text not null,
                pid_path text not null,
                exit_code_path text not null,
                command text not null,
                status text not null,
                started_at real not null,
                finished_at real,
                exit_code integer,
                updated_at real not null,
                wait_for_completion integer not null default 1,
                auto_resumed integer not null default 0
            )
            """
        )
        conn.execute(
            """
            create index if not exists idx_bash_bg_session
            on bash_bg_tasks(account_key, session_id, started_at)
            """
        )
        conn.execute(
            """
            create index if not exists idx_bash_bg_active
            on bash_bg_tasks(status)
            """
        )
