"""indiaopt exception hierarchy.

Every public exception traces back to :class:`IndiaOptError` so callers can
catch either the specific subclass or the base class, depending on how much
granularity they need.

Hierarchy::

    IndiaOptError
    ├── FetchError
    │   ├── NetworkError
    │   ├── RateLimitError
    │   └── ParseError
    ├── CircuitOpenError
    ├── FetchTimeoutError
    ├── ConfigurationError
    └── ValidationError
"""

from __future__ import annotations

from typing import Any


class IndiaOptError(Exception):
    """Base exception for all indiaopt errors.

    All exceptions raised by this library are subclasses of
    :class:`IndiaOptError`, so you can use a single broad ``except``
    clause when desired::

        try:
            result = await client.fetch_option_chain("NIFTY")
        except IndiaOptError as exc:
            logger.error("indiaopt error: %s", exc)
    """

    def __init__(
        self,
        message: str,
        *,
        context: dict[str, Any] | None = None,
        recovery: str | None = None,
    ) -> None:
        super().__init__(message)
        self.context: dict[str, Any] = context or {}
        self.recovery: str | None = recovery

    def __str__(self) -> str:
        parts = [super().__str__()]
        if self.recovery:
            parts.append(f"  ➜ Recovery: {self.recovery}")
        if self.context:
            ctx = ", ".join(f"{k}={v!r}" for k, v in self.context.items())
            parts.append(f"  ➜ Context: {ctx}")
        return "\n".join(parts)


# ── Fetch Errors ─────────────────────────────────────────────────────────────


class FetchError(IndiaOptError):
    """Raised when a data fetch operation fails after all retry attempts.

    Attributes:
        symbol:      The trading symbol that failed.
        exchange:    The exchange name (``"NSE"`` or ``"BSE"``).
        status_code: HTTP status code of the last response (if any).
        attempts:    Number of attempts made before giving up.
    """

    def __init__(
        self,
        message: str,
        *,
        symbol: str,
        exchange: str = "NSE",
        status_code: int | None = None,
        attempts: int = 0,
        context: dict[str, Any] | None = None,
        recovery: str | None = None,
    ) -> None:
        ctx = {
            "symbol": symbol,
            "exchange": exchange,
            "attempts": attempts,
            **({"status_code": status_code} if status_code is not None else {}),
            **(context or {}),
        }
        super().__init__(message, context=ctx, recovery=recovery)
        self.symbol = symbol
        self.exchange = exchange
        self.status_code = status_code
        self.attempts = attempts


class NetworkError(FetchError):
    """Raised when a low-level network error occurs (connection refused, DNS, etc.).

    Example::

        except NetworkError as exc:
            print(f"Network problem for {exc.symbol}: {exc}")
    """


class RateLimitError(FetchError):
    """Raised when the exchange rate-limits the client (HTTP 429 / 403).

    Includes a ``retry_after`` hint when the exchange provides a
    ``Retry-After`` header.

    Attributes:
        retry_after: Suggested wait time in seconds, if known.
    """

    def __init__(
        self,
        message: str,
        *,
        symbol: str,
        exchange: str = "NSE",
        status_code: int | None = None,
        attempts: int = 0,
        retry_after: float | None = None,
        context: dict[str, Any] | None = None,
        recovery: str | None = None,
    ) -> None:
        super().__init__(
            message,
            symbol=symbol,
            exchange=exchange,
            status_code=status_code,
            attempts=attempts,
            context=context,
            recovery=recovery or (
                f"Wait {retry_after:.0f}s before retrying."
                if retry_after is not None
                else "Back off and retry after a delay."
            ),
        )
        self.retry_after = retry_after


class ParseError(FetchError):
    """Raised when the exchange response cannot be parsed.

    Typically caused by the exchange returning a redirect, CAPTCHA page,
    or a malformed JSON payload.

    Attributes:
        raw_preview: First 200 chars of the raw response body for debugging.
    """

    def __init__(
        self,
        message: str,
        *,
        symbol: str,
        exchange: str = "NSE",
        attempts: int = 0,
        raw_preview: str = "",
        context: dict[str, Any] | None = None,
    ) -> None:
        super().__init__(
            message,
            symbol=symbol,
            exchange=exchange,
            attempts=attempts,
            context=context,
            recovery="Check if the exchange is returning HTML instead of JSON (CAPTCHA / maintenance).",
        )
        self.raw_preview = raw_preview


# ── Circuit Breaker ───────────────────────────────────────────────────────────


class CircuitOpenError(IndiaOptError):
    """Raised when the circuit breaker is open for a symbol.

    The circuit opens after :attr:`~indiaopt.config.settings.Settings.circuit_failure_threshold`
    consecutive failures and remains open for
    :attr:`~indiaopt.config.settings.Settings.circuit_backoff_seconds`.

    Attributes:
        symbol:         The trading symbol whose circuit is open.
        open_until:     Monotonic timestamp when the circuit will close.
        failures:       Number of consecutive failures that opened the circuit.
    """

    def __init__(
        self,
        symbol: str,
        *,
        open_until: float,
        failures: int,
        exchange: str = "NSE",
    ) -> None:
        import time

        remaining = max(0.0, open_until - time.monotonic())
        super().__init__(
            f"Circuit breaker OPEN for {exchange}/{symbol} "
            f"({failures} consecutive failures). Retrying in {remaining:.0f}s.",
            context={
                "symbol": symbol,
                "exchange": exchange,
                "failures": failures,
                "retry_in_seconds": round(remaining),
            },
            recovery=f"Wait {remaining:.0f}s or reset the circuit manually via client.reset_circuit(symbol).",
        )
        self.symbol = symbol
        self.open_until = open_until
        self.failures = failures
        self.exchange = exchange


# ── Timeout ───────────────────────────────────────────────────────────────────


class FetchTimeoutError(IndiaOptError):
    """Raised when a fetch operation exceeds the configured timeout.

    Attributes:
        symbol:     The trading symbol that timed out.
        timeout_s:  The timeout threshold in seconds.
    """

    def __init__(self, symbol: str, *, timeout_s: float, exchange: str = "NSE") -> None:
        super().__init__(
            f"Fetch timed out after {timeout_s:.1f}s for {exchange}/{symbol}.",
            context={"symbol": symbol, "exchange": exchange, "timeout_s": timeout_s},
            recovery="Increase Settings.fetch_timeout or check network connectivity.",
        )
        self.symbol = symbol
        self.timeout_s = timeout_s
        self.exchange = exchange


# ── Configuration & Validation ────────────────────────────────────────────────


class ConfigurationError(IndiaOptError):
    """Raised when the library is configured incorrectly.

    Example::

        except ConfigurationError as exc:
            print(f"Fix your config: {exc}")
    """


class ValidationError(IndiaOptError):
    """Raised when caller-supplied input fails validation.

    Attributes:
        field:  The name of the invalid field.
        value:  The value that failed validation.
    """

    def __init__(
        self,
        message: str,
        *,
        field: str,
        value: object,
        recovery: str | None = None,
    ) -> None:
        super().__init__(
            message,
            context={"field": field, "value": value},
            recovery=recovery,
        )
        self.field = field
        self.value = value


__all__ = [
    "CircuitOpenError",
    "ConfigurationError",
    "FetchError",
    "FetchTimeoutError",
    "IndiaOptError",
    "NetworkError",
    "ParseError",
    "RateLimitError",
    "ValidationError",
]
