"""Read-only global factory visibility for the systray.

Identity comparison follows ``adw_modules.control`` (recorded argv subsequence
contract). This module never mutates the trace database.
"""

from __future__ import annotations

import os
import shlex
import sqlite3
from dataclasses import dataclass
from enum import Enum
from pathlib import Path

DEFAULT_DB_PATH = Path.home() / ".local" / "state" / "overdeck" / "factory" / "sssf.db"
DEFAULT_DECK_BASE_URL = "http://127.0.0.1:31337"
FACTORY_DECK_ROUTE = "/factory"
REQUIRED_TABLES = ("sessions", "processes")


class FactoryAvailability(str, Enum):
    AVAILABLE = "available"
    UNAVAILABLE = "unavailable"


class FactoryActivity(str, Enum):
    LIVE = "live"
    IDLE = "idle"


@dataclass(frozen=True)
class FactoryLiveRun:
    adw_id: str
    repo: str | None


@dataclass(frozen=True)
class FactoryPendingDecision:
    decision_id: str
    adw_id: str
    question: str
    created_at: str


@dataclass(frozen=True)
class FactoryStatusSnapshot:
    availability: FactoryAvailability
    activity: FactoryActivity
    live_runs: tuple[FactoryLiveRun, ...]
    db_path: str
    db_present: bool
    pending_decisions: tuple[FactoryPendingDecision, ...] = ()
    all_pending_decision_ids: tuple[str, ...] = ()

    @classmethod
    def idle_safe(
        cls,
        *,
        db_path: str = str(DEFAULT_DB_PATH),
        db_present: bool = False,
    ) -> FactoryStatusSnapshot:
        return cls(
            availability=FactoryAvailability.UNAVAILABLE if not db_present else FactoryAvailability.AVAILABLE,
            activity=FactoryActivity.IDLE,
            live_runs=(),
            db_path=db_path,
            db_present=db_present,
        )

    @classmethod
    def unavailable(cls, *, db_path: str = str(DEFAULT_DB_PATH)) -> FactoryStatusSnapshot:
        return cls(
            availability=FactoryAvailability.UNAVAILABLE,
            activity=FactoryActivity.IDLE,
            live_runs=(),
            db_path=db_path,
            db_present=False,
        )


def deck_base_url() -> str:
    return os.environ.get("OVERDECK_WEB_URL", DEFAULT_DECK_BASE_URL).rstrip("/")


def deck_factory_url() -> str:
    return f"{deck_base_url()}{FACTORY_DECK_ROUTE}"


def _is_path_like(token: str) -> bool:
    return "/" in token or token.startswith(".")


def _resolve_token(token: str) -> str:
    if not _is_path_like(token):
        return token
    try:
        return str(Path(token).resolve())
    except OSError:
        return token


def _is_python_interpreter(token: str) -> bool:
    name = Path(token).name
    return name == "python" or name.startswith("python3")


def recorded_command_argv(recorded_command: str) -> list[str]:
    text = recorded_command.strip()
    if not text:
        return []
    try:
        return shlex.split(text)
    except ValueError:
        return []


def command_tokens_for_match(tokens: list[str]) -> list[str]:
    return [_resolve_token(token) for token in tokens]


def proc_argv_for_match(proc_argv: list[str]) -> list[str]:
    return command_tokens_for_match(proc_argv)


def _proc_argv_match_candidates(proc_argv: list[str]) -> list[list[str]]:
    resolved = proc_argv_for_match(proc_argv)
    candidates = [resolved]
    if len(proc_argv) >= 2 and _is_python_interpreter(proc_argv[0]) and _is_path_like(proc_argv[1]):
        stripped = command_tokens_for_match(proc_argv[2:])
        if stripped != resolved:
            candidates.append(stripped)
    return candidates


def _recorded_live_pairs(recorded: list[str], live: list[str]) -> list[tuple[list[str], list[str]]]:
    pairs = [(recorded, live)]
    if (
        recorded
        and live
        and _is_python_interpreter(recorded[0])
        and _is_python_interpreter(live[0])
    ):
        pairs.append((recorded[1:], live[1:]))
    return pairs


def _subsequence_match(recorded: list[str], live: list[str]) -> bool:
    if live == recorded:
        return True
    m = len(recorded)
    if m > len(live):
        return False
    for i in range(len(live) - m + 1):
        if live[i : i + m] == recorded:
            return True
    return False


def argv_matches_recorded_subsequence(proc_argv: list[str], recorded_command: str) -> bool:
    recorded = command_tokens_for_match(recorded_command_argv(recorded_command))
    if not recorded:
        return False
    for live in _proc_argv_match_candidates(proc_argv):
        for recorded_cmp, live_cmp in _recorded_live_pairs(recorded, live):
            if _subsequence_match(recorded_cmp, live_cmp):
                return True
    return False


def read_proc_argv(pid: int) -> list[str] | None:
    path = Path(f"/proc/{pid}/cmdline")
    if not path.exists():
        return None
    raw = path.read_bytes()
    if not raw:
        return None
    parts = [part.decode(errors="replace") for part in raw.split(b"\0")]
    while parts and not parts[-1]:
        parts.pop()
    return parts or None


def _open_readonly_db(db_path: Path) -> sqlite3.Connection:
    conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
    conn.execute("PRAGMA busy_timeout = 5000")
    conn.execute("PRAGMA query_only = ON")
    return conn


def _has_required_schema(conn: sqlite3.Connection) -> bool:
    rows = conn.execute(
        "SELECT name FROM sqlite_master WHERE type = 'table'",
    ).fetchall()
    names = {row[0] for row in rows}
    return all(table in names for table in REQUIRED_TABLES)


def _sessions_has_repo_column(conn: sqlite3.Connection) -> bool:
    rows = conn.execute("PRAGMA table_info(sessions)").fetchall()
    return any(row[1] == "repo" for row in rows)


def _sessions_has_archived_column(conn: sqlite3.Connection) -> bool:
    rows = conn.execute("PRAGMA table_info(sessions)").fetchall()
    return any(row[1] == "archived" for row in rows)


def _has_decisions_table(conn: sqlite3.Connection) -> bool:
    rows = conn.execute(
        "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'decisions'",
    ).fetchall()
    return bool(rows)


def _load_pending_decisions(conn: sqlite3.Connection) -> tuple[FactoryPendingDecision, ...]:
    if not _has_decisions_table(conn):
        return ()
    archived_filter = (
        "COALESCE(s.archived, 0) = 0" if _sessions_has_archived_column(conn) else "1 = 1"
    )
    rows = conn.execute(
        "SELECT d.decision_id, d.adw_id, d.question, d.created_at"
        " FROM decisions d INNER JOIN sessions s ON s.adw_id = d.adw_id"
        f" WHERE d.status = 'pending' AND {archived_filter} ORDER BY d.created_at",
    ).fetchall()
    return tuple(
        FactoryPendingDecision(
            decision_id=str(decision_id),
            adw_id=str(adw_id),
            question=str(question or ""),
            created_at=str(created_at or ""),
        )
        for decision_id, adw_id, question, created_at in rows
    )


def _load_all_pending_decision_ids(conn: sqlite3.Connection) -> tuple[str, ...]:
    if not _has_decisions_table(conn):
        return ()
    rows = conn.execute(
        "SELECT decision_id FROM decisions WHERE status = 'pending' ORDER BY created_at",
    ).fetchall()
    return tuple(str(decision_id) for (decision_id,) in rows)


def _session_is_verified_live(
    conn: sqlite3.Connection,
    adw_id: str,
    *,
    read_proc_argv_fn=read_proc_argv,
) -> bool:
    rows = conn.execute(
        "SELECT pid, command FROM processes WHERE adw_id=? AND ended_at IS NULL",
        (adw_id,),
    ).fetchall()
    if not rows:
        return False

    has_verified = False
    for pid, command in rows:
        proc_argv = read_proc_argv_fn(int(pid))
        if proc_argv is None:
            continue
        if argv_matches_recorded_subsequence(proc_argv, command or ""):
            has_verified = True
        else:
            return False
    return has_verified


def load_factory_status(
    db_path: str | Path | None = None,
    *,
    exists_impl=None,
    open_db_impl=None,
    read_proc_argv_fn=read_proc_argv,
) -> FactoryStatusSnapshot:
    path = Path(db_path or DEFAULT_DB_PATH)
    resolved = str(path.resolve())
    exists = exists_impl or (lambda candidate: candidate.exists())
    if not exists(path):
        return FactoryStatusSnapshot.unavailable(db_path=resolved)

    opener = open_db_impl or _open_readonly_db
    try:
        conn = opener(path)
    except (OSError, sqlite3.Error):
        return FactoryStatusSnapshot.unavailable(db_path=resolved)

    try:
        if not _has_required_schema(conn):
            return FactoryStatusSnapshot.unavailable(db_path=resolved)

        has_repo = _sessions_has_repo_column(conn)
        repo_expr = "repo" if has_repo else "NULL AS repo"
        running = conn.execute(
            f"SELECT adw_id, {repo_expr} FROM sessions WHERE status='running' ORDER BY started_at DESC",
        ).fetchall()

        live_runs: list[FactoryLiveRun] = []
        for adw_id, repo in running:
            if not _session_is_verified_live(
                conn,
                str(adw_id),
                read_proc_argv_fn=read_proc_argv_fn,
            ):
                continue
            live_runs.append(FactoryLiveRun(adw_id=str(adw_id), repo=repo))

        activity = FactoryActivity.LIVE if live_runs else FactoryActivity.IDLE
        return FactoryStatusSnapshot(
            availability=FactoryAvailability.AVAILABLE,
            activity=activity,
            live_runs=tuple(live_runs),
            db_path=resolved,
            db_present=True,
            pending_decisions=_load_pending_decisions(conn),
            all_pending_decision_ids=_load_all_pending_decision_ids(conn),
        )
    except sqlite3.Error:
        return FactoryStatusSnapshot.unavailable(db_path=resolved)
    finally:
        conn.close()
