"""Lightweight HTTP client for the Skytale API server.

Uses only stdlib (``urllib``) to avoid adding external dependencies.
All methods authenticate with the API key directly — the API server
accepts both ``sk_live_*`` keys and JWTs in the Authorization header.

Usage::

    from skytale_sdk._api import SkytaleAPI

    api = SkytaleAPI("https://api.skytale.sh", "sk_live_...")
    api.register_channel("org/ns/svc")
    resp = api.create_invite("org/ns/svc")
    print(resp["token"])  # "skt_inv_..."
"""

from __future__ import annotations

import json
import logging
import random
import time
import urllib.error
import urllib.parse
import urllib.request
from typing import Any, Dict, Optional

logger = logging.getLogger(__name__)


class _RedactedStr(str):
    """String subclass that redacts its value in repr/str for logging safety."""

    def __repr__(self):
        return "'***REDACTED***'"

    def __format__(self, format_spec):
        if format_spec:
            return super().__format__(format_spec)
        return "***REDACTED***"


def _validate_api_url(api_url: str) -> None:
    """Require HTTPS for remote API URLs.

    Parses the URL properly to prevent bypasses like
    ``http://localhost.evil.com`` or ``http://localhost@evil.com``.
    Allows ``http://`` only for ``localhost``, ``127.0.0.1``, and ``::1``
    (local development).  Raises ``ValueError`` for non-HTTPS remote URLs.
    """
    from urllib.parse import urlparse

    parsed = urlparse(api_url)
    if parsed.scheme == "https":
        return
    if parsed.scheme == "http" and parsed.hostname in ("localhost", "127.0.0.1", "::1"):
        return
    raise ValueError(
        f"API URL must use HTTPS (got {parsed.scheme}://{parsed.hostname}). "
        "HTTP is only allowed for localhost/127.0.0.1."
    )


class SkytaleAPI:
    """HTTP client for Skytale API channel and invite operations.

    Args:
        api_url: Base URL of the API server (e.g. ``https://api.skytale.sh``).
        api_key: API key for authentication (``sk_live_...``).
    """

    def __init__(self, api_url: str, api_key: str, max_retries: int = 2) -> None:
        _validate_api_url(api_url)
        self._base = api_url.rstrip("/")
        self._api_key = _RedactedStr(api_key)
        self._max_retries = max_retries

    def _request(
        self,
        method: str,
        path: str,
        body: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """Send an HTTP request with retry and return the parsed JSON response.

        Retries on transient failures (408, 429, 500, 502, 503, 504) and
        connection errors with exponential backoff.  Respects ``Retry-After``
        header when present and ≤ 60 s.

        Args:
            method: HTTP method (GET, POST, etc.).
            path: URL path (appended to the base URL).
            body: Optional JSON body for POST requests.

        Returns:
            Parsed JSON response as a dict.

        Raises:
            SkytaleError: On non-retryable HTTP errors.
            TransportError: On connection failures after all retries.
        """
        url = f"{self._base}{path}"
        data = json.dumps(body).encode("utf-8") if body else None
        retryable_codes = {408, 429, 500, 502, 503, 504}
        last_exc: Optional[Exception] = None

        for attempt in range(1 + self._max_retries):
            req = urllib.request.Request(url, data=data, method=method)
            req.add_header("Authorization", f"Bearer {self._api_key}")
            req.add_header("Content-Type", "application/json")
            req.add_header("Accept", "application/json")

            try:
                with urllib.request.urlopen(req, timeout=30) as resp:
                    return json.loads(resp.read())
            except urllib.error.HTTPError as exc:
                if exc.code in retryable_codes and attempt < self._max_retries:
                    retry_after = exc.headers.get("Retry-After") if exc.headers else None
                    delay = self._backoff_delay(attempt, retry_after)
                    logger.warning(
                        "request to %s failed (%d), retrying in %.1fs (attempt %d/%d)",
                        path, exc.code, delay, attempt + 1, self._max_retries,
                    )
                    time.sleep(delay)
                    last_exc = exc
                    continue

                try:
                    error_body = json.loads(exc.read())
                    msg = error_body.get("error", str(exc))
                    code = error_body.get("code", "")
                except (json.JSONDecodeError, UnicodeDecodeError):
                    msg = str(exc)
                    code = ""
                raise _api_error(exc.code, code, msg, url) from None
            except urllib.error.URLError as exc:
                if attempt < self._max_retries:
                    delay = self._backoff_delay(attempt)
                    logger.warning(
                        "connection to %s failed (%s), retrying in %.1fs (attempt %d/%d)",
                        path, exc.reason, delay, attempt + 1, self._max_retries,
                    )
                    time.sleep(delay)
                    last_exc = exc
                    continue

                from skytale_sdk.errors import TransportError

                raise TransportError(
                    f"unable to reach API at {url}: {exc.reason}. "
                    "Check your network connection and https://status.skytale.sh",
                    code="connection_error",
                ) from None

        # Should not reach here, but satisfy type checker
        from skytale_sdk.errors import TransportError
        raise TransportError(
            f"request to {url} failed after {self._max_retries} retries",
            code="max_retries_exceeded",
        ) from last_exc

    @staticmethod
    def _backoff_delay(attempt: int, retry_after: Optional[str] = None) -> float:
        """Calculate backoff delay with jitter.

        Args:
            attempt: Zero-based attempt number.
            retry_after: Optional ``Retry-After`` header value (seconds).

        Returns:
            Delay in seconds.
        """
        if retry_after is not None:
            try:
                ra = float(retry_after)
                if 0 < ra <= 60:
                    return ra
            except (ValueError, OverflowError):
                pass
        base = min(0.5 * (2 ** attempt), 5.0)
        jitter = 0.75 + 0.25 * random.random()
        return base * jitter

    # ------------------------------------------------------------------
    # Channel registration
    # ------------------------------------------------------------------

    def register_channel(self, name: str) -> Dict[str, Any]:
        """Register a channel with the API (idempotent).

        Args:
            name: Channel name in ``org/namespace/service`` format.

        Returns:
            ``{"id": "uuid", "name": "org/ns/svc"}``
        """
        return self._request("POST", "/v1/channels", {"name": name})

    # ------------------------------------------------------------------
    # Invite tokens
    # ------------------------------------------------------------------

    def create_invite(
        self,
        channel: str,
        max_uses: int = 1,
        ttl_seconds: int = 3600,
    ) -> Dict[str, Any]:
        """Create an invite token for a channel.

        Args:
            channel: Channel name.
            max_uses: Maximum number of times the token can be used.
            ttl_seconds: Token lifetime in seconds.

        Returns:
            ``{"token": "skt_inv_...", "expires_at": "..."}``
        """
        return self._request(
            "POST",
            "/v1/channels/invites",
            {
                "channel": channel,
                "max_uses": max_uses,
                "ttl_seconds": ttl_seconds,
            },
        )

    # ------------------------------------------------------------------
    # Join flow
    # ------------------------------------------------------------------

    def join(
        self,
        channel: str,
        token: str,
        key_package_b64: str,
        identity: str,
    ) -> Dict[str, Any]:
        """Submit a join request with an invite token and key package.

        Args:
            channel: Channel name to join.
            token: Invite token (``skt_inv_...``).
            key_package_b64: Base64-encoded MLS key package.
            identity: Agent identity string.

        Returns:
            ``{"request_id": "uuid"}``
        """
        return self._request(
            "POST",
            "/v1/channels/join",
            {
                "channel": channel,
                "token": token,
                "key_package": key_package_b64,
                "identity": identity,
            },
        )

    def get_pending(self, channel: str) -> Dict[str, Any]:
        """Get pending join requests for a channel.

        Args:
            channel: Channel name.

        Returns:
            ``{"requests": [{"id": "...", "identity": "...",
            "key_package": "...", "created_at": "..."}]}``
        """
        encoded = urllib.parse.quote(channel, safe="")
        return self._request("GET", f"/v1/channels/pending?channel={encoded}")

    def submit_welcome(self, request_id: str, welcome_b64: str) -> Dict[str, Any]:
        """Submit an MLS Welcome message for a pending join request.

        Args:
            request_id: UUID of the join request.
            welcome_b64: Base64-encoded MLS Welcome message.

        Returns:
            ``{"status": "completed"}``
        """
        return self._request(
            "POST",
            "/v1/channels/welcome",
            {"request_id": request_id, "welcome": welcome_b64},
        )

    def get_welcome(self, request_id: str) -> Dict[str, Any]:
        """Poll for the Welcome message (called by the joining agent).

        Args:
            request_id: UUID of the join request.

        Returns:
            ``{"status": "pending"}`` or
            ``{"status": "completed", "welcome": "<base64>"}``
        """
        return self._request("GET", f"/v1/channels/welcome/{request_id}")


def _api_error(http_code: int, error_code: str, message: str, url: str = "") -> Exception:
    """Map an API error to a typed exception with actionable guidance."""
    from skytale_sdk.errors import (
        AuthError,
        ChannelError,
        QuotaExceededError,
        SkytaleError,
        TransportError,
    )

    if error_code == "unauthorized" or http_code == 401:
        return AuthError(
            "unable to authenticate: API key is invalid or expired. "
            "Verify SKYTALE_API_KEY is set correctly, or generate a new key "
            "with `skytale keys create`.",
            code="unauthorized",
            http_status=http_code,
        )
    if error_code == "quota_exceeded" or http_code == 429:
        return QuotaExceededError(
            "unable to send: monthly message quota exceeded. "
            "Upgrade your plan at https://skytale.sh/billing or wait for "
            "the next billing cycle.",
            code="quota_exceeded",
            http_status=http_code,
        )
    if error_code == "not_found" or http_code == 404:
        return ChannelError(
            f"unable to find resource: {message}. "
            "Verify the channel name uses org/namespace/service format.",
            code="not_found",
            http_status=http_code,
        )
    if error_code in ("bad_request", "conflict"):
        return ChannelError(
            f"invalid request: {message}",
            code=error_code,
            http_status=http_code,
        )
    if http_code >= 500:
        return TransportError(
            f"API server error ({http_code}): {message}. "
            "Check https://status.skytale.sh for service status.",
            code="server_error",
            http_status=http_code,
        )
    return SkytaleError(
        f"API error ({http_code}): {message}",
        code=error_code or "api_error",
        http_status=http_code,
    )
