"""Exception hierarchy for ``mgf-apiprobe``.

See: ``DESIGN.md`` §3.2 (Finding shape) and ``HANDOFF.md`` §3.2 (the
runtime-dep inversion: subclass ``mgf.common.exceptions.AppError``,
NOT define a parallel hierarchy).

These exceptions are for **suite-logic / harness errors** — the probe
harness itself failed (bad config, transport error, auth flow couldn't
resolve a token). They are NOT the way provider misbehavior surfaces;
that is a :class:`~mgf.apiprobe.core.finding.Finding`. The split is
load-bearing:

  - The probe couldn't run at all → exception (e.g., DNS failure,
    bad config).
  - The probe ran and the response was wrong → ``Finding``.

A 401 from an authenticated endpoint produces a Finding with
``RootCause.AUTH``. An :class:`AuthError` is raised when the AuthFlow
couldn't even produce a token to send (e.g., the env var holding the
bearer token is unset).
"""

from __future__ import annotations

from mgf.common.exceptions import AppError, ConfigError, OperationError

# ---------------------------------------------------------------------------
# Root — the public ``except`` handle.
# ---------------------------------------------------------------------------


class ApiprobeError(AppError):
    """Base class for every exception raised by ``mgf-apiprobe``.

    Subclasses :class:`mgf.common.exceptions.AppError` (rule EH-01).
    Consumers writing ``except ApiprobeError:`` catch every harness
    failure from this library; ``except AppError:`` catches both
    apiprobe and ``mgf-common``-level failures.
    """


# ---------------------------------------------------------------------------
# Configuration / construction-time errors. Subclass ConfigError so
# the existing CLI exit-code translator (mgf.common.cli.run_and_translate)
# routes these to EXIT_CONFIG_ERROR without per-class wiring.
# ---------------------------------------------------------------------------


class ApiprobeConfigError(ConfigError, ApiprobeError):
    """Raised when a probe / suite / settings construction is invalid.

    Examples: a probe declares ``expect_schema(X)`` but ``X`` is not a
    pydantic model; a suite is constructed with two probes sharing the
    same ``id``; a settings YAML has the wrong shape.
    """


class SchemaError(ApiprobeConfigError):
    """Raised when a schema source can't be parsed or applied.

    v0.1 covers Pydantic schemas only. OpenAPI +
    JSON Schema land later; their ingestion errors are subclasses of
    this.
    """


class MatcherError(ApiprobeConfigError):
    """Raised when a matcher is applied to an incompatible value.

    Distinct from "the matcher said no" (that produces a Finding).
    Raised when the value can't even be coerced to the type the matcher
    operates on (e.g., ``is_uuid`` applied to an int).
    """


# ---------------------------------------------------------------------------
# Operation-time errors. Subclass OperationError so the CLI translator
# routes to EXIT_OPERATION_ERROR.
# ---------------------------------------------------------------------------


class TransportError(OperationError, ApiprobeError):
    """Raised when the underlying HTTP transport could not produce a response.

    Wraps connection errors, DNS failures, TLS handshake failures, and
    timeouts — anything where the probe never got a real response. A
    transport error short-circuits the suite for that probe; downstream
    checks don't run on a non-existent response. The harness emits a
    ``Finding`` with ``RootCause.NETWORK`` so the failure is visible in
    the report alongside provider-side findings.
    """

    def __init__(self, url: str, cause: str) -> None:
        self.url = url
        self.cause = cause
        super().__init__(
            f"transport failed for {url}: {cause}. "
            f"Verify the host is reachable and the URL is correct, then retry "
            f"(see APIPROBE_MAX_RETRIES / Probe.retry)."
        )


class AuthError(OperationError, ApiprobeError):
    """Raised when an :class:`AuthFlow` cannot produce credentials to send.

    Distinct from a 401/403 response (those produce ``Finding`` with
    ``RootCause.AUTH``). Raised when the flow itself failed before the
    request was sent — env var unset, vault key missing, OAuth2 refresh
    rejected (when OAuth2 lands in v0.6).
    """

    def __init__(self, flow_name: str, reason: str) -> None:
        self.flow_name = flow_name
        self.reason = reason
        super().__init__(
            f"auth flow {flow_name!r} failed: {reason}. "
            f"Check the flow's credentials + endpoint and that any prior steps succeeded."
        )


class SuiteLogicError(OperationError, ApiprobeError):
    """Raised when the suite or a scenario hits a state corruption.

    Scenarios land in v0.2. v0.1 raises this for
    e.g. a duplicate probe id discovered at run time.
    """


__all__ = [
    "ApiprobeConfigError",
    "ApiprobeError",
    "AuthError",
    "MatcherError",
    "SchemaError",
    "SuiteLogicError",
    "TransportError",
]
