"""Observability checks — request tracing + rate-limit hygiene.

See: ``DESIGN.md`` §7.6 (hygiene) — observability cohabits hygiene
in the catalogue but is its own ship-when-needed concern in v0.1.

3 checks, all ``RootCause.PROVIDER_OBSERVABILITY``:

  - ``MissingRequestId``         — no ``X-Request-Id`` (or any of the common variants)
  - ``MissingRateLimitHeaders``  — 429 response without ``X-RateLimit-*`` / ``Retry-After``
  - ``MissingServerHeader``      — no ``Server`` (or ``Via``) — providers should identify themselves
"""

from __future__ import annotations

from typing import TYPE_CHECKING, ClassVar

from mgf.apiprobe.core.check import Check
from mgf.apiprobe.core.finding import RootCause, Severity

if TYPE_CHECKING:
    from collections.abc import Iterable

    from mgf.apiprobe.core.context import Context
    from mgf.apiprobe.core.finding import Finding
    from mgf.apiprobe.core.probe import Probe
    from mgf.apiprobe.core.response import Response


# The headers we consider "request id-ish". Different ecosystems
# settled on different names; any one is acceptable.
_REQUEST_ID_HEADERS: tuple[str, ...] = (
    "X-Request-Id",
    "X-Request-ID",
    "Request-Id",
    "X-Correlation-Id",
    "X-Amz-Request-Id",  # AWS
    "Cf-Ray",  # Cloudflare
)

# Headers that indicate rate-limiting context.
_RATE_LIMIT_HEADERS: tuple[str, ...] = (
    "X-RateLimit-Limit",
    "X-RateLimit-Remaining",
    "X-RateLimit-Reset",
    "RateLimit-Limit",
    "RateLimit-Remaining",
    "Retry-After",
)


class MissingRequestId(Check):
    """No X-Request-Id (or equivalent)."""

    code: ClassVar[str] = "observability.headers.missing-request-id"
    severity_default: ClassVar[Severity] = Severity.WARN
    root_cause_default: ClassVar[RootCause] = RootCause.PROVIDER_OBSERVABILITY

    def run(
        self,
        probe: Probe,
        response: Response,
        ctx: Context,
    ) -> Iterable[Finding]:
        if any(response.header(h) for h in _REQUEST_ID_HEADERS):
            return
        yield self.finding(
            probe_id=probe.probe_id,
            message=(
                f"Response carries no request id (none of: {', '.join(_REQUEST_ID_HEADERS)})."
            ),
            evidence={
                "url": response.url,
                "headers_checked": list(_REQUEST_ID_HEADERS),
            },
            remediation=(
                "Provider should include a request id on every response "
                "so client-side and provider-side logs can be correlated. "
                "Recommended header name: X-Request-Id."
            ),
            threat_refs=("OWASP-API-9",),
        )


class MissingRateLimitHeaders(Check):
    """429 response without rate-limit context headers.

    Fires only on 429 — a non-throttled response doesn't need
    rate-limit headers (though many providers include them anyway,
    which is good practice and out of scope for this check).
    """

    code: ClassVar[str] = "observability.headers.missing-rate-limit"
    severity_default: ClassVar[Severity] = Severity.WARN
    root_cause_default: ClassVar[RootCause] = RootCause.PROVIDER_OBSERVABILITY

    def run(
        self,
        probe: Probe,
        response: Response,
        ctx: Context,
    ) -> Iterable[Finding]:
        if response.status != 429:
            return
        if any(response.header(h) for h in _RATE_LIMIT_HEADERS):
            return
        yield self.finding(
            probe_id=probe.probe_id,
            severity=Severity.WARN,
            root_cause=RootCause.RATE_LIMIT,
            message=(
                "429 response without rate-limit headers. Client cannot back off intelligently."
            ),
            evidence={
                "url": response.url,
                "headers_checked": list(_RATE_LIMIT_HEADERS),
            },
            remediation=(
                "Provider should set Retry-After on every 429 response. "
                "X-RateLimit-{Limit,Remaining,Reset} are also recommended "
                "for proactive client back-off."
            ),
        )


class MissingServerHeader(Check):
    """No ``Server`` or ``Via`` header — provider isn't self-identifying."""

    code: ClassVar[str] = "observability.headers.missing-server"
    severity_default: ClassVar[Severity] = Severity.INFO
    root_cause_default: ClassVar[RootCause] = RootCause.PROVIDER_OBSERVABILITY

    def run(
        self,
        probe: Probe,
        response: Response,
        ctx: Context,
    ) -> Iterable[Finding]:
        if response.header("Server") or response.header("Via"):
            return
        yield self.finding(
            probe_id=probe.probe_id,
            message="Response carries no Server (or Via) header.",
            evidence={"url": response.url},
            remediation=(
                "Provider may be hiding its identity for security reasons "
                "(legitimate); if not, set a Server header so traffic "
                "fingerprinting works."
            ),
        )


__all__ = [
    "MissingRateLimitHeaders",
    "MissingRequestId",
    "MissingServerHeader",
]
