#!/usr/bin/env python3
"""Pipeline-status oracle: determine PR phase in 6-phase PR pipeline.

Reads facts from GitHub (gh CLI), git, and memory files to deterministically
derive the current pipeline phase of a PR — no state file, like ``git status``
for the PR pipeline. CI gate via ``gh pr view --json statusCheckRollup`` (read-only):
CI ❌ blocks MERGE (transitive guard — first ❌ phase blocks all subsequent phases).

Usage:
    python3 config/scripts/pipeline-status.py <PR_NUMBER>   # single PR status
    python3 config/scripts/pipeline-status.py               # table of open PRs

Six phases:
    1. ISSUE      — GitHub issue exists and linked via Closes/Fixes #N
    2. IMPLEMENT  — PR exists + PR body has 4 required headings
    3. CI         — all checks on PR head SHA completed & success (statusCheckRollup)
    4. REVIEW     — APPROVE found in PR comments
    5. MERGE      — PR state is MERGED
    6. MEMORY     — PR#N distilled into repos/{host}/{org}/{repo}.md
"""

from __future__ import annotations

import functools
import json
import os
import re
import subprocess
import sys
import time
from dataclasses import dataclass
from enum import StrEnum
from pathlib import Path


def _resolve_repo_root() -> Path:
    """Resolve repo root via git (cwd-aware), fallback to script location."""
    result = subprocess.run(
        ["git", "rev-parse", "--show-toplevel"], capture_output=True, text=True, check=False
    )
    if result.returncode == 0 and result.stdout.strip():
        return Path(result.stdout.strip()).resolve()
    return Path(__file__).resolve().parent.parent.parent


REPO_ROOT = _resolve_repo_root()
_MEMORY_BASE = os.environ.get(
    "OPENCODE_MEMORY_DIR",
    str(REPO_ROOT / "app_data" / "opencode-memory"),
)
MEMORY_DIR = Path(_MEMORY_BASE) / "repos"

PHASE_NAMES = ["ISSUE", "IMPLEMENT", "CI", "REVIEW", "MERGE", "MEMORY"]

CI_WAIT_TIMEOUT = 300
CI_POLL_INTERVAL = 10
CI_NO_RUNS_RETRY = 3
CI_NO_RUNS_INTERVAL = 5

CLOSURE_RE = re.compile(r"(?:Closes|Fixes|Resolves)\s+#(\d+)", re.IGNORECASE)
REVIEW_APPROVE_RE = re.compile(
    r"## Code Review Summary.*?###\s*Verdict:\s*APPROVE\b",
    re.IGNORECASE | re.DOTALL,
)
REVIEW_VERDICT_RE = re.compile(
    r"## Code Review Summary.*?###\s*Verdict:\s*(\w+)",
    re.IGNORECASE | re.DOTALL,
)
JSON_FIELD_RE = re.compile(r'"(\w+)"\s*:\s*"?([^",}]*)"?', re.IGNORECASE)


class PhaseStatus(StrEnum):
    """Phase check result."""

    DONE = "DONE"
    NOT_DONE = "NOT_DONE"
    AMBIGUOUS = "AMBIGUOUS"


@dataclass(frozen=True)
class PhaseResult:
    """Result of a single phase check."""

    status: PhaseStatus
    detail: str


def run_cmd(args: list[str]) -> tuple[int, str, str]:
    """Run a command, return (returncode, stdout, stderr)."""
    result = subprocess.run(args, capture_output=True, text=True, check=False)
    return result.returncode, result.stdout, result.stderr


@dataclass(frozen=True)
class CiPollConfig:
    """CI polling parameters.

    Priority: CLI flag > env var > constant in code.
    """

    wait_timeout: int
    poll_interval: int


def _env_int(name: str, default: int) -> int:
    """Read int from env var, fallback to default on missing/invalid."""
    raw = os.environ.get(name)
    if raw is None:
        return default
    try:
        return int(raw)
    except ValueError:
        return default


def _load_ci_config(argv: list[str] | None = None) -> CiPollConfig:
    """Build CiPollConfig from CLI flags + env vars + constants.

    Priority: CLI flag > env var > constant. CLI flags ``--ci-wait-timeout N``
    and ``--ci-poll-interval N`` parsed manually from ``argv`` (last wins).
    """
    args = argv if argv is not None else sys.argv[1:]
    cli_timeout: int | None = None
    cli_interval: int | None = None
    i = 0
    while i < len(args):
        if args[i] == "--ci-wait-timeout" and i + 1 < len(args):
            try:
                cli_timeout = int(args[i + 1])
            except ValueError:
                cli_timeout = None
            i += 2
            continue
        if args[i] == "--ci-poll-interval" and i + 1 < len(args):
            try:
                cli_interval = int(args[i + 1])
            except ValueError:
                cli_interval = None
            i += 2
            continue
        i += 1

    timeout = (
        cli_timeout
        if cli_timeout is not None
        else _env_int("OPENCODE_CI_WAIT_TIMEOUT", CI_WAIT_TIMEOUT)
    )
    interval = (
        cli_interval
        if cli_interval is not None
        else _env_int("OPENCODE_CI_POLL_INTERVAL", CI_POLL_INTERVAL)
    )
    return CiPollConfig(wait_timeout=timeout, poll_interval=interval)


def parse_remote_url(url: str) -> tuple[str, str, str]:
    """Parse git remote URL into (host, org, repo).

    Supports both HTTPS and SSH formats:
        https://github.com/org/repo.git   -> (github.com, org, repo)
        git@github.com:org/repo.git        -> (github.com, org, repo)
    """
    ssh_match = re.match(r"git@([^:]+):([^/]+)/(.+?)(?:\.git)?$", url)
    if ssh_match:
        return ssh_match.group(1), ssh_match.group(2), ssh_match.group(3)
    # `(?:[^/@]*@)?` optionally skips `user:password@` userinfo before host.
    # Needed because `git config url.insteadOf` rewrites `https://github.com/`
    # to `https://x-access-token:TOKEN@github.com/`, and `git remote get-url
    # origin` returns the rewritten URL (see ADR-022 / ADR-023).
    https_match = re.match(r"https?://(?:[^/@]*@)?([^/]+)/([^/]+)/(.+?)(?:\.git)?$", url)
    if https_match:
        return https_match.group(1), https_match.group(2), https_match.group(3)
    raise ValueError(f"Cannot parse remote URL: {url}")


def _resolve_memory_base() -> tuple[Path, str]:
    """Derive (memory_dir, repo_name) from ``git remote get-url origin``."""
    rc, out, err = run_cmd(["git", "remote", "get-url", "origin"])
    if rc != 0:
        raise RuntimeError(f"Cannot get git remote URL: {err.strip()}")
    host, org, repo = parse_remote_url(out.strip())
    return MEMORY_DIR / host / org, repo


def get_memory_files() -> list[Path]:
    """All ``repo*.md`` sorted by mtime descending (newest first)."""
    base_dir, repo = _resolve_memory_base()
    if not base_dir.exists():
        return []
    rot_pattern = re.compile(rf"^{re.escape(repo)}(-\d+)?$")
    files = [f for f in base_dir.glob("*.md") if rot_pattern.fullmatch(f.stem)]
    return sorted(files, key=lambda p: p.stat().st_mtime, reverse=True)


@functools.cache
def get_repo_full_name() -> str:
    """Return ``org/repo`` from git remote (cached, one call per run).

    Used for GitHub Actions API URL: ``repos/{org}/{repo}/actions/runs``.
    Cached via ``functools.cache`` (one git call per process); tests reset
    via ``get_repo_full_name.cache_clear()``.
    """
    rc, out, err = run_cmd(["git", "remote", "get-url", "origin"])
    if rc != 0:
        raise RuntimeError(f"Cannot get git remote URL: {err.strip()}")
    _host, org, repo = parse_remote_url(out.strip())
    return f"{org}/{repo}"


def extract_json_field(json_str: str, field: str) -> str | None:
    """Extract a string field value from JSON (simple regex, no json import)."""
    match = re.search(rf'"{field}"\s*:\s*"([^"]*)"', json_str)
    return match.group(1) if match else None


def check_gh_auth() -> str | None:
    """Check if gh CLI is authenticated. Returns error message or None."""
    rc, _, err = run_cmd(["gh", "auth", "status"])
    if rc != 0:
        return f"gh CLI не авторизован: {err.strip()}"
    return None


def check_issue(pr_number: int) -> PhaseResult:
    """Phase 1: ISSUE — issue exists and linked via Closes/Fixes #N."""
    rc, out, _ = run_cmd(
        ["gh", "pr", "view", str(pr_number), "--json", "body", "--repo", get_repo_full_name()]
    )
    if rc != 0:
        return PhaseResult(PhaseStatus.NOT_DONE, f"PR #{pr_number} не существует")

    matches = CLOSURE_RE.findall(out)
    if not matches:
        return PhaseResult(PhaseStatus.NOT_DONE, "Closes/Fixes #N не найден в body")

    issue_numbers = list({int(m) for m in matches})
    if len(issue_numbers) > 1:
        return PhaseResult(
            PhaseStatus.AMBIGUOUS,
            f"несколько issue в body: {', '.join(f'#{n}' for n in issue_numbers)}",
        )

    issue_num = issue_numbers[0]
    rc2, _, err2 = run_cmd(["gh", "issue", "view", str(issue_num), "--repo", get_repo_full_name()])
    if rc2 != 0:
        return PhaseResult(
            PhaseStatus.NOT_DONE,
            f"issue #{issue_num} не существует: {err2.strip()}",
        )

    return PhaseResult(PhaseStatus.DONE, f"#{issue_num} связан через Closes #{issue_num}")


def check_implement(pr_number: int) -> PhaseResult:
    """Phase 2: IMPLEMENT — PR exists + PR body has 4 required headings."""
    rc, out, _ = run_cmd(
        ["gh", "pr", "view", str(pr_number), "--json", "body", "--repo", get_repo_full_name()]
    )
    if rc != 0:
        return PhaseResult(PhaseStatus.NOT_DONE, f"PR #{pr_number} не существует")
    try:
        pr_body = json.loads(out).get("body", "") or ""
    except (json.JSONDecodeError, ValueError):
        return PhaseResult(
            PhaseStatus.NOT_DONE, f"PR #{pr_number}: не удалось распарсить JSON body"
        )
    required = ["## Что сделано", "## Почему", "## Watch out", "## Pending"]
    missing = [h for h in required if h not in pr_body]
    if missing:
        return PhaseResult(
            PhaseStatus.NOT_DONE, f"PR body не содержит heading'и: {', '.join(missing)}"
        )
    return PhaseResult(PhaseStatus.DONE, "PR body: 4 heading'а валидны")


def check_ci(pr_number: int) -> PhaseResult:
    """Phase 3: CI — all checks on PR head SHA completed & success.

    Uses ``gh pr view --json statusCheckRollup`` which aggregates ALL
    workflows for the PR head SHA (CI, CI (always), ADR check, etc.).
    This handles docs-only PRs where ``ci.yml`` has ``paths-ignore`` and
    only ``always-ci.yml`` runs.
    """
    config = _load_ci_config()
    return _run_ci_loop(pr_number, config)


def _run_ci_loop(pr_number: int, config: CiPollConfig) -> PhaseResult:
    """Initial CI query + edge-case dispatch + delegate to poll/no-checks helpers."""
    kind, json_str, err = _query_ci_rollup(pr_number)
    if kind == "error":
        return PhaseResult(PhaseStatus.AMBIGUOUS, err)
    if kind == "no_checks":
        return _retry_no_checks(pr_number, config)
    return _classify_rollup_with_poll(pr_number, config, json_str)


def _retry_no_checks(pr_number: int, config: CiPollConfig) -> PhaseResult:
    """Retry CI query when no checks registered yet (CI may lag after push).

    Up to ``CI_NO_RUNS_RETRY`` total attempts, sleeping ``CI_NO_RUNS_INTERVAL``
    between attempts. On success → classify/poll; exhausted → AMBIGUOUS.
    """
    for attempt in range(CI_NO_RUNS_RETRY):
        if attempt > 0:
            time.sleep(CI_NO_RUNS_INTERVAL)
        kind, json_str, err = _query_ci_rollup(pr_number)
        if kind == "error":
            return PhaseResult(PhaseStatus.AMBIGUOUS, err)
        if kind == "rollup":
            return _classify_rollup_with_poll(pr_number, config, json_str)
    return PhaseResult(
        PhaseStatus.AMBIGUOUS,
        f"нет CI checks на PR #{pr_number} — возможна проблема триггера",
    )


def _classify_rollup_with_poll(pr_number: int, config: CiPollConfig, json_str: str) -> PhaseResult:
    """Classify rollup; if in_progress → poll until completed or timeout."""
    result = _classify_rollup(json_str)
    if result.status != PhaseStatus.AMBIGUOUS or "in progress" not in result.detail.lower():
        return result
    elapsed = 0
    while elapsed < config.wait_timeout:
        if elapsed + config.poll_interval > config.wait_timeout:
            break
        time.sleep(config.poll_interval)
        elapsed += config.poll_interval
        kind, json_str_new, err = _query_ci_rollup(pr_number)
        if kind == "error":
            return PhaseResult(PhaseStatus.AMBIGUOUS, err)
        if kind == "no_checks":
            return PhaseResult(
                PhaseStatus.AMBIGUOUS,
                f"нет CI checks на PR #{pr_number} — возможна проблема триггера",
            )
        result = _classify_rollup(json_str_new)
        if result.status != PhaseStatus.AMBIGUOUS or "in progress" not in result.detail.lower():
            return result
    return PhaseResult(
        PhaseStatus.AMBIGUOUS,
        f"CI ещё идёт после {config.wait_timeout}s — проверь вручную: gh pr checks {pr_number}",
    )


def _query_ci_rollup(pr_number: int) -> tuple[str, str, str]:
    """Query PR statusCheckRollup via gh CLI.

    GitHub aggregates ALL checks (CI, CI (always), ADR check) for PR head SHA.
    Return ``(kind, json_str, error)`` where ``kind`` is:
    - ``"error"`` — API call failed (rc != 0).
    - ``"no_checks"`` — rollup array is empty (no checks registered yet).
    - ``"rollup"`` — JSON with statusCheckRollup array, ``json_str`` set.
    """
    rc, out, err = run_cmd(
        [
            "gh",
            "pr",
            "view",
            str(pr_number),
            "--json",
            "statusCheckRollup",
            "--repo",
            get_repo_full_name(),
        ]
    )
    if rc != 0:
        return "error", "", f"PR API error: {err.strip()}"
    json_str = out.strip()
    if not json_str:
        return "no_checks", "", ""
    if re.search(r'"statusCheckRollup"\s*:\s*\[\s*\]', json_str):
        return "no_checks", "", ""
    return "rollup", json_str, ""


def _classify_rollup(json_str: str) -> PhaseResult:
    """Classify CI status from statusCheckRollup JSON.

    All checks COMPLETED + SUCCESS/SKIPPED/NEUTRAL → DONE.
    Any check COMPLETED + non-success conclusion → NOT_DONE.
    Any check IN_PROGRESS/QUEUED/PENDING → AMBIGUOUS (poll).
    """
    statuses = re.findall(r'"status"\s*:\s*"([^"]*)"', json_str, re.IGNORECASE)
    if not statuses:
        return PhaseResult(PhaseStatus.AMBIGUOUS, "no checks found in rollup")

    in_progress = [s for s in statuses if s.upper() in ("IN_PROGRESS", "QUEUED", "PENDING")]
    if in_progress:
        return PhaseResult(PhaseStatus.AMBIGUOUS, "CI in progress")

    conclusions = re.findall(r'"conclusion"\s*:\s*"([^"]*)"', json_str, re.IGNORECASE)
    null_conclusions = re.findall(r'"conclusion"\s*:\s*null', json_str, re.IGNORECASE)

    for c in conclusions:
        if c.upper() not in ("SUCCESS", "SKIPPED", "NEUTRAL"):
            return PhaseResult(PhaseStatus.NOT_DONE, f"CI {c.lower()} — fix needed")

    if null_conclusions:
        return PhaseResult(PhaseStatus.AMBIGUOUS, "CI completed but conclusion missing")

    return PhaseResult(PhaseStatus.DONE, "CI green")


def _extract_comment_bodies(json_str: str) -> list[str]:
    """Extract 'body' fields from gh pr view --json comments output.

    Handles JSON string escaping (\\n, \\", \\\\).
    """
    bodies = []
    for match in re.finditer(r'"body"\s*:\s*"((?:[^"\\]|\\.)*)"', json_str):
        raw = match.group(1)
        body = raw.encode().decode("unicode_escape")
        bodies.append(body)
    return bodies


def check_review(pr_number: int) -> PhaseResult:
    """Phase 5: REVIEW — APPROVE found in PR comments from code reviewer.

    Looks for '## Code Review Summary' heading
    with '### Verdict: APPROVE'. Only the latest reviewer comment counts —
    if reviewer changed from APPROVE to REQUEST_CHANGES, NOT_DONE.
    """
    rc, out, _ = run_cmd(
        ["gh", "pr", "view", str(pr_number), "--json", "comments", "--repo", get_repo_full_name()]
    )
    if rc != 0:
        return PhaseResult(PhaseStatus.NOT_DONE, "не удалось получить комментарии PR")

    comment_bodies = _extract_comment_bodies(out)
    if not comment_bodies:
        return PhaseResult(PhaseStatus.NOT_DONE, "нет комментариев PR")

    reviewer_verdict = None
    for body in comment_bodies:
        if REVIEW_VERDICT_RE.search(body):
            match = REVIEW_VERDICT_RE.search(body)
            reviewer_verdict = match.group(1).upper() if match else None

    if reviewer_verdict is None:
        return PhaseResult(PhaseStatus.NOT_DONE, "Code Review Summary не найден в комментариях")

    if reviewer_verdict == "APPROVE":
        return PhaseResult(PhaseStatus.DONE, "APPROVE найден в Code Review Summary")

    return PhaseResult(PhaseStatus.NOT_DONE, f"последний verdict reviewer'а: {reviewer_verdict}")


def check_merge(pr_number: int) -> PhaseResult:
    """Phase 6: MERGE — PR state is MERGED."""
    rc, out, _ = run_cmd(
        ["gh", "pr", "view", str(pr_number), "--json", "state", "--repo", get_repo_full_name()]
    )
    if rc != 0:
        return PhaseResult(PhaseStatus.NOT_DONE, f"PR #{pr_number} не существует")

    state = extract_json_field(out, "state")
    if state is None:
        return PhaseResult(PhaseStatus.AMBIGUOUS, "не удалось распарсить state PR")

    if state == "MERGED":
        return PhaseResult(PhaseStatus.DONE, "merged")

    return PhaseResult(PhaseStatus.NOT_DONE, f"state={state}")


def check_memory(pr_number: int) -> PhaseResult:
    """Phase 7: MEMORY — PR#N distilled into memory file."""
    try:
        files = get_memory_files()
    except (RuntimeError, ValueError) as exc:
        return PhaseResult(PhaseStatus.NOT_DONE, str(exc))

    if not files:
        return PhaseResult(
            PhaseStatus.NOT_DONE,
            "memory files не найдены",
        )

    pattern = f"PR#{pr_number}"
    for f in files:
        if pattern in f.read_text():
            return PhaseResult(PhaseStatus.DONE, f"{pattern} в {f.name}")

    return PhaseResult(
        PhaseStatus.NOT_DONE,
        f"{pattern} не найден в {len(files)} файл(ах)",
    )


def get_pr_title(pr_number: int) -> str:
    """Get PR title via gh CLI."""
    rc, out, _ = run_cmd(
        ["gh", "pr", "view", str(pr_number), "--json", "title", "--repo", get_repo_full_name()]
    )
    if rc != 0:
        return f"PR #{pr_number}"
    title = extract_json_field(out, "title")
    return title if title else f"PR #{pr_number}"


NEXT_ACTIONS: dict[str, str] = {
    "ISSUE": "dispatch subagent (subagent_type=general, template=A) for PR #N",
    "IMPLEMENT": "dispatch subagent (subagent_type=general, template=A) for PR #N",
    "CI": "проверь статус CI вручную (gh run view)",
    "REVIEW": "dispatch subagent (subagent_type=reviewer, template=C) for PR #N",
    "MERGE": "call merge_pr tool with pr_number=N",
    "MEMORY": "dispatch subagent (subagent_type=memory-syncer, template=E) for PR #N",
}

REVIEW_NEXT_REQUEST_CHANGES = (
    "dispatch subagent (subagent_type=general) with prompt 'fix reviewer comments: <list>', "
    "commit, push → re-loop (pipeline_status проверит CI автоматически)"
)
REVIEW_NEXT_NEEDS_DISCUSSION = "уточни вопросы с автором PR (verdict: NEEDS_DISCUSSION)"
REVIEW_NEXT_DEFAULT = NEXT_ACTIONS["REVIEW"]

STATUS_ICONS: dict[PhaseStatus, str] = {
    PhaseStatus.DONE: "✅",
    PhaseStatus.NOT_DONE: "❌",
    PhaseStatus.AMBIGUOUS: "⚠️",
}


def get_next_action(phase_name: str, pr_number: int) -> str:
    """Get NEXT action description for a not-done phase."""
    action = NEXT_ACTIONS.get(phase_name, "уточнить статус")
    return action.replace("N", str(pr_number))


def get_next_action_review(result: PhaseResult) -> str:
    """REVIEW-фаза: NEXT зависит от вердикта reviewer'а в result.detail."""
    detail = result.detail.upper()
    if "REQUEST_CHANGES" in detail:
        return REVIEW_NEXT_REQUEST_CHANGES
    if "NEEDS_DISCUSSION" in detail:
        return REVIEW_NEXT_NEEDS_DISCUSSION
    return REVIEW_NEXT_DEFAULT


def run_all_checks(pr_number: int) -> list[PhaseResult]:
    """Run all 6 phase checks, return results in order."""
    return [
        check_issue(pr_number),
        check_implement(pr_number),
        check_ci(pr_number),
        check_review(pr_number),
        check_merge(pr_number),
        check_memory(pr_number),
    ]


def find_current_phase(results: list[PhaseResult]) -> int | None:
    """Return index of first not-done phase, or None if all done."""
    for i, result in enumerate(results):
        if result.status != PhaseStatus.DONE:
            return i
    return None


def format_single_pr(pr_number: int, results: list[PhaseResult]) -> str:
    """Format detailed output for a single PR."""
    title = get_pr_title(pr_number)
    lines = [f"PR #{pr_number}: {title}", ""]

    for i, (name, result) in enumerate(zip(PHASE_NAMES, results, strict=True)):
        icon = STATUS_ICONS[result.status]
        lines.append(f"{icon} {i + 1}. {name:<12} {result.detail}")

    lines.append("")

    current = find_current_phase(results)
    if current is None:
        lines.append("Status: COMPLETE")
    else:
        result = results[current]
        if result.status == PhaseStatus.AMBIGUOUS:
            lines.append(f"AMBIGUOUS: {result.detail}")
            lines.append("NEXT: уточните статус вручную")
        else:
            if PHASE_NAMES[current] == "REVIEW":
                action = get_next_action_review(result)
            else:
                action = get_next_action(PHASE_NAMES[current], pr_number)
            lines.append(f"NEXT: {action}")

    return "\n".join(lines)


def list_open_pr_numbers() -> list[int]:
    """List numbers of all open PRs."""
    rc, out, _ = run_cmd(
        ["gh", "pr", "list", "--state", "open", "--json", "number", "--repo", get_repo_full_name()]
    )
    if rc != 0:
        return []
    numbers = re.findall(r'"number"\s*:\s*(\d+)', out)
    return sorted(int(n) for n in numbers)


def format_pr_row(pr_number: int) -> str:
    """Format a single row for the open-PRs table."""
    results = run_all_checks(pr_number)
    title = get_pr_title(pr_number)
    icon_str = "".join(STATUS_ICONS[r.status] for r in results)

    current = find_current_phase(results)
    if current is None:
        next_action = "COMPLETE"
    elif PHASE_NAMES[current] == "REVIEW":
        next_action = get_next_action_review(results[current])
    else:
        next_action = get_next_action(PHASE_NAMES[current], pr_number)

    short_title = title[:40] + "..." if len(title) > 40 else title
    return f"PR#{pr_number:<5} {short_title:<43} {icon_str}  NEXT: {next_action}"


def format_table(pr_numbers: list[int]) -> str:
    """Format table of all open PRs."""
    if not pr_numbers:
        return "Нет открытых PR"
    rows = [format_pr_row(n) for n in pr_numbers]
    return "\n".join(rows)


def pr_exists(pr_number: int) -> bool:
    """Check if PR exists via gh CLI."""
    rc, _, _ = run_cmd(
        ["gh", "pr", "view", str(pr_number), "--json", "number", "--repo", get_repo_full_name()]
    )
    return rc == 0


def main() -> None:
    """Entry point: parse args and dispatch to single-PR or table mode."""
    auth_error = check_gh_auth()
    if auth_error:
        print(auth_error, file=sys.stderr)
        sys.exit(1)

    if len(sys.argv) > 1:
        try:
            pr_number = int(sys.argv[1])
        except ValueError:
            print(f"Некорректный номер PR: {sys.argv[1]}", file=sys.stderr)
            sys.exit(1)

        if not pr_exists(pr_number):
            print(f"PR #{pr_number} не существует", file=sys.stderr)
            sys.exit(1)

        results = run_all_checks(pr_number)
        print(format_single_pr(pr_number, results))
    else:
        pr_numbers = list_open_pr_numbers()
        print(format_table(pr_numbers))


if __name__ == "__main__":
    main()
