# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Paul Chen / axoviq.com
from __future__ import annotations
from typing import NoReturn

"""Synthadoc error code registry.

Every user-facing error carries a short, stable code so that errors can be
searched, documented, and handled programmatically.

Format: <CATEGORY>-<NNN>

Categories
----------
SRV    Server lifecycle errors (not running, port conflict, HTTP errors)
WIKI   Wiki filesystem errors (not found, invalid structure, not writable)
CFG    Configuration / environment errors (missing API key, bad provider)
SKILL  Skill dispatch and execution errors (not found, missing dep, blocked)
INGEST Ingest source errors (file not found, empty, wrong type)
JOB    Job management errors (not found)
"""

# ── Server ────────────────────────────────────────────────────────────────────
SRV_NOT_RUNNING  = "ERR-SRV-001"   # No server listening for the requested wiki
SRV_PORT_IN_USE  = "ERR-SRV-002"   # Port already bound by another process
SRV_HTTP_ERROR   = "ERR-SRV-003"   # Server returned a 4xx/5xx response
SRV_BG_CRASH     = "ERR-SRV-004"   # Background server process exited immediately

# ── Wiki ──────────────────────────────────────────────────────────────────────
WIKI_NOT_FOUND       = "ERR-WIKI-001"  # Wiki root directory does not exist
WIKI_INVALID         = "ERR-WIKI-002"  # Directory exists but missing wiki/ subfolder
WIKI_NOT_WRITABLE    = "ERR-WIKI-003"  # wiki/ directory is not writable
WIKI_ALREADY_EXISTS  = "ERR-WIKI-004"  # Install target already exists on disk
WIKI_DEMO_NOT_FOUND  = "ERR-WIKI-005"  # Unknown demo template name
WIKI_NOT_REGISTERED  = "ERR-WIKI-006"  # Name not in ~/.synthadoc/wikis.json

# ── Config / Environment ──────────────────────────────────────────────────────
CFG_MISSING_API_KEY  = "ERR-CFG-001"   # Required env var (API key) not set
CFG_UNKNOWN_PROVIDER = "ERR-CFG-002"   # Provider name not recognised

# ── Skills ────────────────────────────────────────────────────────────────────
SKILL_NOT_FOUND   = "ERR-SKILL-001"  # No skill matched the source string
SKILL_MISSING_DEP = "ERR-SKILL-002"  # Required pip package not installed
SKILL_URL_BLOCKED = "ERR-SKILL-003"  # URL returned 403 (bot/paywall protection)
SKILL_WEB_NO_KEY  = "ERR-SKILL-004"  # TAVILY_API_KEY not set for web search


class DomainBlockedException(Exception):
    """Raised by the URL skill when a domain returns a bot-blocking response.

    The orchestrator catches this, auto-adds the domain to
    ``.synthadoc/blocked_domains.json``, records an audit event, and
    permanently fails the job (no retry).
    """
    def __init__(self, domain: str, url: str, status_code: int) -> None:
        self.domain = domain
        self.url = url
        self.status_code = status_code
        super().__init__(
            f"[{SKILL_URL_BLOCKED}] Domain auto-blocked ({status_code}): {domain} — "
            f"site blocked automated access. Future URLs from this domain will be skipped."
        )

# ── Provider ──────────────────────────────────────────────────────────────────
PROVIDER_DAILY_QUOTA = "ERR-PROV-001"  # Daily API quota exhausted for today
CODING_TOOL_QUOTA    = "ERR-PROV-002"  # Coding tool CLI usage quota exhausted


class DailyQuotaExhaustedException(Exception):
    """Raised when a provider's daily request quota is exhausted.

    Unlike per-minute rate limits, daily quotas do not reset for hours.
    The orchestrator catches this and permanently fails the job rather than
    requeuing it — retrying today is futile.
    """
    def __init__(self, provider: str) -> None:
        self.provider = provider
        super().__init__(
            f"[{PROVIDER_DAILY_QUOTA}] Daily quota exhausted for {provider} — "
            f"no retry possible until quota resets (typically midnight UTC). "
            f"Upgrade to a paid API key or switch providers."
        )


class CodingToolQuotaExhaustedException(Exception):
    """Raised when a coding tool CLI provider (Claude Code, Opencode) hits its usage quota.

    Unlike per-minute rate limits, coding tool quotas require waiting hours before resetting.
    The orchestrator permanently fails the job rather than requeuing.
    """
    def __init__(self, tool_name: str) -> None:
        super().__init__(
            f"[{CODING_TOOL_QUOTA}] {tool_name} usage quota exhausted — "
            f"wait for quota to reset, then retry the job. "
            f"Or switch provider temporarily: synthadoc serve -w <wiki> --provider anthropic"
        )


# ── Ingest ────────────────────────────────────────────────────────────────────
INGEST_NOT_FOUND  = "ERR-INGEST-001"  # Source file or directory not found
INGEST_EMPTY      = "ERR-INGEST-002"  # Source file exists but is empty
INGEST_NOT_DIR    = "ERR-INGEST-003"  # --batch target exists but is not a directory

# ── Query ─────────────────────────────────────────────────────────────────────
QUERY_TIMEOUT = "ERR-QUERY-001"  # LLM synthesis timed out; retry the query

# ── Jobs ──────────────────────────────────────────────────────────────────────
JOB_NOT_FOUND = "ERR-JOB-001"   # Job ID does not exist in jobs.db

# ── Agent ─────────────────────────────────────────────────────────────────────
AGENT_FAILED = "ERR-AGENT-001"  # LLM agent call failed (empty response, bad JSON, timeout)


def cli_error(code: str, message: str, hint: str = "") -> NoReturn:
    """Print a categorised error and exit with code 1.

    Only call from CLI-layer code. Agents and skills raise standard Python
    exceptions (with the error code embedded in the message string).

    Parameters
    ----------
    code:
        One of the constants defined in this module, e.g. ``SRV_NOT_RUNNING``.
    message:
        Human-readable description of what went wrong.
    hint:
        Optional follow-up text (suggested fix, next step).
    """
    import typer
    lines = [f"\n[{code}] {message}"]
    if hint:
        lines.append(hint)
    lines.append("")
    typer.echo("\n".join(lines), err=True)
    raise typer.Exit(1)
