"""持久化保存会话运行状态。

这个模块只保存“运行事实”：当前 run 的状态、开始/结束时间、阶段说明和
活动事件。它不负责取消进程；取消仍由内存里的 RunManager 持有进程句柄来做。
前端刷新、切换会话或服务短暂重启后，可以通过这里恢复可靠的展示状态。
"""

from __future__ import annotations

import json
import sqlite3
import time
from pathlib import Path
from threading import RLock
from typing import Any


ACTIVE_RUN_STATUSES = {'queued', 'running'}
TERMINAL_RUN_STATUSES = {'completed', 'failed', 'cancelled', 'interrupted'}


class RunStateStore:
    """SQLite backed run-state store.

    数据库放在 `.port_sessions/run_state.db`，用 account_key/session_id 做隔离。
    SQLite 足够支撑这个场景：写入量小，读多写少，并且可以避免前端回放依赖
    后端进程内存。
    """

    def __init__(self, db_path: Path, *, max_events_per_run: int = 240) -> None:
        self.db_path = db_path
        self.max_events_per_run = max_events_per_run
        self._lock = RLock()

    def start(
        self,
        *,
        run_id: str,
        account_key: str,
        session_id: str,
        pending_prompt: str,
        started_at: float,
        status: str = 'queued',
    ) -> None:
        now = time.time()
        with self._connect() as conn:
            conn.execute(
                """
                insert into run_states (
                    run_id, account_key, session_id, status, started_at,
                    updated_at, pending_prompt, cancellable
                )
                values (?, ?, ?, ?, ?, ?, ?, ?)
                on conflict(run_id) do update set
                    account_key=excluded.account_key,
                    session_id=excluded.session_id,
                    status=excluded.status,
                    started_at=excluded.started_at,
                    updated_at=excluded.updated_at,
                    pending_prompt=excluded.pending_prompt,
                    cancellable=excluded.cancellable
                """,
                (
                    run_id,
                    account_key,
                    session_id,
                    status,
                    started_at,
                    now,
                    pending_prompt,
                    1 if status in ACTIVE_RUN_STATUSES else 0,
                ),
            )

    def update(
        self,
        run_id: str,
        *,
        status: str | None = None,
        stage: str | None = None,
        error: str | None = None,
        cancellable: bool | None = None,
        elapsed_ms: int | None = None,
        finished_at: float | None = None,
    ) -> None:
        assignments: list[str] = ['updated_at = ?']
        values: list[Any] = [time.time()]
        if status is not None:
            assignments.append('status = ?')
            values.append(status)
            if cancellable is None:
                cancellable = status in ACTIVE_RUN_STATUSES
        if stage is not None:
            assignments.append('current_stage = ?')
            values.append(stage)
        if error is not None:
            assignments.append('error = ?')
            values.append(error)
        if cancellable is not None:
            assignments.append('cancellable = ?')
            values.append(1 if cancellable else 0)
        if elapsed_ms is not None:
            assignments.append('elapsed_ms = ?')
            values.append(max(0, int(elapsed_ms)))
        if finished_at is not None:
            assignments.append('finished_at = ?')
            values.append(finished_at)
        values.append(run_id)
        with self._connect() as conn:
            conn.execute(
                f"update run_states set {', '.join(assignments)} where run_id = ?",
                values,
            )

    def finish(
        self,
        run_id: str,
        *,
        status: str,
        elapsed_ms: int | None = None,
        stage: str | None = None,
        error: str | None = None,
    ) -> None:
        self.update(
            run_id,
            status=status,
            stage=stage,
            error=error,
            cancellable=False,
            elapsed_ms=elapsed_ms,
            finished_at=time.time(),
        )

    def finish_active_for_session(
        self,
        account_key: str,
        session_id: str,
        *,
        status: str,
        stage: str | None = None,
    ) -> list[str]:
        now = time.time()
        with self._connect() as conn:
            rows = conn.execute(
                """
                select run_id, started_at
                from run_states
                where account_key = ?
                  and session_id = ?
                  and status in ('queued', 'running')
                """,
                (account_key, session_id),
            ).fetchall()
            run_ids: list[str] = []
            for row in rows:
                run_ids.append(row['run_id'])
                started_at = row['started_at']
                elapsed_ms = (
                    max(0, int((now - float(started_at)) * 1000))
                    if isinstance(started_at, (int, float))
                    else None
                )
                conn.execute(
                    """
                    update run_states
                    set status = ?,
                        current_stage = coalesce(?, current_stage),
                        cancellable = 0,
                        elapsed_ms = coalesce(?, elapsed_ms),
                        finished_at = ?,
                        updated_at = ?
                    where run_id = ?
                    """,
                    (status, stage, elapsed_ms, now, now, row['run_id']),
                )
            return run_ids

    def record_event(self, run_id: str, event: dict[str, Any]) -> None:
        recorded_at = event.get('recorded_at')
        if not isinstance(recorded_at, (int, float)):
            recorded_at = time.time()
            event = {**event, 'recorded_at': recorded_at}
        event_json = json.dumps(event, ensure_ascii=False, sort_keys=True)
        with self._connect() as conn:
            conn.execute(
                """
                insert into run_events (run_id, recorded_at, event_json)
                values (?, ?, ?)
                """,
                (run_id, float(recorded_at), event_json),
            )
            conn.execute(
                """
                delete from run_events
                where run_id = ?
                  and id not in (
                    select id
                    from run_events
                    where run_id = ?
                    order by id desc
                    limit ?
                  )
                """,
                (run_id, run_id, self.max_events_per_run),
            )

    def snapshot_latest(self, account_key: str, session_id: str) -> dict[str, Any] | None:
        with self._connect() as conn:
            row = conn.execute(
                """
                select *
                from run_states
                where account_key = ? and session_id = ?
                order by updated_at desc, started_at desc
                limit 1
                """,
                (account_key, session_id),
            ).fetchone()
            if row is None:
                return None
            return self._snapshot_from_row(conn, row)

    def snapshot_run(self, run_id: str) -> dict[str, Any] | None:
        with self._connect() as conn:
            row = conn.execute(
                "select * from run_states where run_id = ?",
                (run_id,),
            ).fetchone()
            if row is None:
                return None
            return self._snapshot_from_row(conn, row)

    def mark_interrupted_if_active(
        self,
        run_id: str,
        *,
        stage: str = '上次运行已中断，后台没有正在执行的进程',
    ) -> dict[str, Any] | None:
        snapshot = self.snapshot_run(run_id)
        if snapshot is None:
            return None
        if snapshot.get('status') not in ACTIVE_RUN_STATUSES:
            return snapshot
        started_at = snapshot.get('started_at')
        elapsed_ms = None
        if isinstance(started_at, (int, float)):
            elapsed_ms = max(0, int((time.time() - float(started_at)) * 1000))
        self.finish(
            run_id,
            status='interrupted',
            elapsed_ms=elapsed_ms,
            stage=stage,
        )
        return self.snapshot_run(run_id)

    def _snapshot_from_row(
        self,
        conn: sqlite3.Connection,
        row: sqlite3.Row,
    ) -> dict[str, Any]:
        event_rows = conn.execute(
            """
            select event_json
            from run_events
            where run_id = ?
            order by id asc
            """,
            (row['run_id'],),
        ).fetchall()
        events: list[dict[str, Any]] = []
        for event_row in event_rows:
            try:
                event = json.loads(event_row['event_json'])
            except (TypeError, json.JSONDecodeError):
                continue
            if isinstance(event, dict):
                events.append(event)
        return {
            'run_id': row['run_id'],
            'session_id': row['session_id'],
            'status': row['status'],
            'current_stage': row['current_stage'] or '',
            'started_at': row['started_at'],
            'updated_at': row['updated_at'],
            'finished_at': row['finished_at'],
            'elapsed_ms': row['elapsed_ms'],
            'pending_prompt': row['pending_prompt'] or '',
            'error': row['error'] or '',
            'cancellable': bool(row['cancellable']),
            'events': events,
        }

    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 run_states (
                run_id text primary key,
                account_key text not null,
                session_id text not null,
                status text not null,
                started_at real not null,
                updated_at real not null,
                finished_at real,
                elapsed_ms integer,
                pending_prompt text default '',
                current_stage text default '',
                error text default '',
                cancellable integer not null default 0
            )
            """
        )
        conn.execute(
            """
            create index if not exists idx_run_states_session
            on run_states(account_key, session_id, updated_at)
            """
        )
        conn.execute(
            """
            create table if not exists run_events (
                id integer primary key autoincrement,
                run_id text not null,
                recorded_at real not null,
                event_json text not null
            )
            """
        )
        conn.execute(
            """
            create index if not exists idx_run_events_run_id
            on run_events(run_id, id)
            """
        )
