"""ORCH-019: bug-fast-track — a cheaper/shorter pipeline route for bug-fix tasks.

Leaf module — pure, unit-testable logic over the config flags + the proven Plane
label apparatus (``labels.has_label`` -> ``plane_sync``, ORCH-089). Mirrors the
leaf pattern of ``src/labels.py`` / ``src/serial_gate.py``: imports only
``config`` (and lazily ``labels`` / ``db`` / ``qg.checks``), never
``stage_engine`` / ``launcher``.

What it decides (ADR-001):
  * Whether the bug-fast-track is in scope for a repo (``bug_fast_track_applies``)
    — a LOCAL, network-free check evaluated FIRST.
  * Whether a given Plane issue carries the ``Bug`` label (``is_bug_task``) — the
    only network call, made ONLY after ``applies()`` is True, so a disabled
    kill-switch costs zero network and yields zero regression (AC-6).
  * Whether a task's stored track skips the ``architecture`` stage
    (``skips_architecture``) — a pure predicate over the DB-stored ``track``,
    read in the hot ``advance_stage`` path WITHOUT any network call (NFR-4).

never-raise contract (BR-6/AC-6, fail-safe to the FULL cycle): every public
function degrades to "full cycle" on ANY error / ambiguity / Plane
unavailability / disabled flag. There is NO fail-open here — the conservative
default is always the full pipeline (with ``architecture``), so an error can
never silently skip a stage.
"""
from __future__ import annotations

import logging

from .config import settings

logger = logging.getLogger("orchestrator.bug_fast_track")


# ---------------------------------------------------------------------------
# Scope / kill-switch (mirrors _auto_label_applies / serial_gate_applies)
# ---------------------------------------------------------------------------
def bug_fast_track_applies(repo: str) -> bool:
    """Whether the bug-fast-track is REAL for ``repo`` (ADR-001 D6 / AC-6).

      * ``bug_fast_track_enabled=False`` -> always False (kill-switch; start and
        routing are 1:1 as before ORCH-019, and — crucially — ``has_label`` is
        never consulted, so no new network call on start, AC-6).
      * ``bug_fast_track_repos`` (CSV) non-empty -> real only for the listed repos.
      * empty CSV -> self-hosting only (``orchestrator``) — the safe default (the
        track is first burnt in on the orchestrator itself, where the `Bug` label
        is guaranteed to exist; enduro opts in via an explicit CSV entry).
    Checked FIRST (local, network-free); never raises -> False on error (degrade
    to "full cycle", which matches the kill-switch-off behaviour).
    """
    try:
        if not getattr(settings, "bug_fast_track_enabled", False):
            return False
        raw = (getattr(settings, "bug_fast_track_repos", "") or "").strip()
        if raw:
            allowed = {r.strip().lower() for r in raw.split(",") if r.strip()}
            return (repo or "").strip().lower() in allowed
        # Lazy import keeps this module a leaf (avoids importing qg at load).
        from .qg.checks import is_self_hosting_repo
        return is_self_hosting_repo(repo)
    except Exception as e:  # noqa: BLE001 - never-raise -> full cycle
        logger.warning("bug_fast_track_applies error for %s: %s", repo, e)
        return False


# ---------------------------------------------------------------------------
# Classification (the ONLY network call; ADR-001 D1)
# ---------------------------------------------------------------------------
def is_bug_task(work_item_id: str, project_id: str | None = None) -> bool:
    """True iff the issue carries the configured ``Bug`` label (Plane API source).

    ``bug_fast_track_applies`` is assumed already True (checked by the caller —
    the gate idiom ``applies(repo) and is_bug_task(...)`` short-circuits before any
    network call when the kill-switch is off). Delegates to the proven
    ``labels.has_label`` (fetch_issue_labels + get_project_labels, normalization,
    TTL-cache, source-of-truth = Plane API, not the webhook payload).

    Any error / ambiguity / Plane unavailability -> **False** (fail-safe -> full
    cycle, never silently fast-track on doubt).
    """
    try:
        label = (getattr(settings, "bug_fast_track_label", "") or "").strip()
        if not label:
            return False
        from . import labels
        return bool(labels.has_label(work_item_id, label, project_id))
    except Exception as e:  # noqa: BLE001 - never-raise -> full cycle
        logger.warning(
            "is_bug_task error for %s -> fail-safe (full cycle): %s", work_item_id, e
        )
        return False


# ---------------------------------------------------------------------------
# Routing predicate (pure, DB-backed; hot path — NO network, NFR-4) — ADR-001 D3
# ---------------------------------------------------------------------------
def skips_architecture(track: str | None) -> bool:
    """Whether a task with stored ``track`` skips the ``architecture`` stage.

    Pure predicate (no I/O): True iff the kill-switch is on AND ``track == 'bug'``.
    Used by ``advance_stage`` on the analysis-exit edge to map
    ``analysis -> architecture`` to ``analysis -> development`` for a bug task.
    A disabled flag -> always False (1:1 prior routing); any error -> False
    (fail-safe -> full cycle).
    """
    try:
        if not getattr(settings, "bug_fast_track_enabled", False):
            return False
        return (track or "").strip().lower() == "bug"
    except Exception as e:  # noqa: BLE001 - never-raise -> full cycle
        logger.warning("skips_architecture error for track=%r: %s", track, e)
        return False


# ---------------------------------------------------------------------------
# Observability snapshot for GET /queue (ADR-001 D7)
# ---------------------------------------------------------------------------
def snapshot() -> dict:
    """Read-only bug-fast-track summary for GET /queue (additive block). never-raise.

    Surfaces the flags + a savings metric derived from the existing telemetry: the
    count of tasks on the bug track and the number of ``architecture`` agent runs
    those tasks structurally skipped (one per bug task = ``est_saved_architecture_runs``).
    Any error -> a minimal dict with the flags (never crashes the endpoint).
    """
    try:
        enabled = bool(getattr(settings, "bug_fast_track_enabled", False))
    except Exception:  # noqa: BLE001
        enabled = False
    try:
        label = getattr(settings, "bug_fast_track_label", "Bug") or "Bug"
    except Exception:  # noqa: BLE001
        label = "Bug"
    try:
        repos_cfg = getattr(settings, "bug_fast_track_repos", "") or ""
    except Exception:  # noqa: BLE001
        repos_cfg = ""
    active_bug_tasks = 0
    total_bug_tasks = 0
    try:
        from . import db
        conn = db.get_db()
        try:
            # ORCH-090 terminal set {done,cancelled}: "active" = not terminal.
            row = conn.execute(
                "SELECT "
                "  COUNT(*) AS total, "
                "  SUM(CASE WHEN stage NOT IN ('done','cancelled') THEN 1 ELSE 0 END) AS active "
                "FROM tasks WHERE track = 'bug'"
            ).fetchone()
            if row:
                total_bug_tasks = int(row["total"] or 0)
                active_bug_tasks = int(row["active"] or 0)
        finally:
            conn.close()
    except Exception as e:  # noqa: BLE001
        logger.warning("bug_fast_track snapshot count error: %s", e)
    return {
        "enabled": enabled,
        "label": label,
        "repos": repos_cfg,
        "active_bug_tasks": active_bug_tasks,
        "total_bug_tasks": total_bug_tasks,
        # Each bug task skips exactly one `architecture` stage (one architect agent
        # run + ADR). This is the structural savings the track buys (FR-7 / AC-7).
        "est_saved_architecture_runs": total_bug_tasks,
    }
