"""Official LicensesOS API client for Python."""

from __future__ import annotations

import re
import time
from typing import Any
from urllib.parse import quote

import httpx

from licensesos.errors import ApiError, NetworkError
from licensesos.results.activation import ActivationResult
from licensesos.results.activations import ActivationsResult
from licensesos.results.deactivation import DeactivationResult
from licensesos.results.heartbeat import HeartbeatResult
from licensesos.results.usage import UsageResult
from licensesos.results.validation import ValidationResult

DEFAULT_BASE_URL = "https://api.licenseos.com"
DEFAULT_TIMEOUT = 30.0


class LicensesOSClient:
    """Official LicensesOS API client for Python.

    Uses ``httpx`` for HTTP requests with automatic JSON serialization,
    authentication header injection, and response envelope unwrapping.

    Supports use as a context manager::

        with LicensesOSClient("your-api-key") as client:
            result = client.validate("LICENSE-KEY")
    """

    def __init__(
        self,
        api_key: str,
        *,
        base_url: str = DEFAULT_BASE_URL,
        timeout: float = DEFAULT_TIMEOUT,
    ) -> None:
        if not api_key:
            raise ValueError("api_key is required")

        self._api_key = api_key
        self._base_url = base_url.rstrip("/")
        self._client = httpx.Client(
            base_url=self._base_url,
            headers={
                "Authorization": f"Bearer {api_key}",
                "Content-Type": "application/json",
                "Accept": "application/json",
            },
            timeout=httpx.Timeout(timeout),
        )

    # -----------------------------------------------------------------------
    # Context manager
    # -----------------------------------------------------------------------

    def __enter__(self) -> LicensesOSClient:
        return self

    def __exit__(self, *args: Any) -> None:
        self.close()

    def close(self) -> None:
        """Close the underlying HTTP client."""
        self._client.close()

    # -----------------------------------------------------------------------
    # Public API methods
    # -----------------------------------------------------------------------

    def validate(
        self,
        license_key: str,
        identifier: str | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> ValidationResult:
        """Validate a license key, optionally with an identifier (domain/device).

        The validation endpoint always returns HTTP 200, even for invalid
        licenses. Check ``result.is_valid()`` to determine validity.
        """
        payload: dict[str, Any] = {"license_key": license_key}

        if identifier is not None:
            payload["identifier"] = LicensesOSClient.normalize_domain(identifier)

        if metadata is not None and len(metadata) > 0:
            payload["metadata"] = metadata

        data = self._request("POST", "/api/v1/licenses/validate", json=payload)

        return ValidationResult(
            valid=data.get("valid", False),
            license=data.get("license"),
            activated=data.get("activated"),
            activation=data.get("activation"),
            offline_token=data.get("offline_token"),
            error=data.get("error"),
        )

    def activate(
        self,
        license_key: str,
        identifier: str,
        *,
        label: str | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> ActivationResult:
        """Activate a license for an identifier."""
        payload: dict[str, Any] = {
            "license_key": license_key,
            "identifier": LicensesOSClient.normalize_domain(identifier),
        }

        if label is not None:
            payload["label"] = label

        if metadata is not None and len(metadata) > 0:
            payload["metadata"] = metadata

        data = self._request("POST", "/api/v1/licenses/activate", json=payload)

        return ActivationResult(
            activation=data["activation"],
            license=data["license"],
        )

    def deactivate(
        self,
        license_key: str,
        identifier: str,
    ) -> DeactivationResult:
        """Deactivate a license for an identifier."""
        payload = {
            "license_key": license_key,
            "identifier": identifier,
        }

        data = self._request("POST", "/api/v1/licenses/deactivate", json=payload)

        return DeactivationResult(deactivated=data.get("deactivated", False))

    def list_activations(self, license_key: str) -> ActivationsResult:
        """List all activations for a license."""
        path = f"/api/v1/licenses/{quote(license_key, safe='')}/activations"
        data = self._request("GET", path)

        return ActivationsResult(
            activations=data.get("activations", []),
            count=data.get("count", 0),
            limit=data.get("limit"),
        )

    def heartbeat(
        self,
        license_key: str,
        identifier: str,
    ) -> HeartbeatResult:
        """Send a heartbeat check-in for an activation."""
        payload = {
            "license_key": license_key,
            "identifier": identifier,
        }

        data = self._request("POST", "/api/v1/licenses/heartbeat", json=payload)

        return HeartbeatResult(
            activation=data["activation"],
            next_check_in_at=data["next_check_in_at"],
        )

    def increment_usage(
        self,
        license_key: str,
        amount: int = 1,
    ) -> UsageResult:
        """Increment usage count for a license."""
        payload: dict[str, Any] = {"license_key": license_key}

        if amount != 1:
            payload["amount"] = amount

        data = self._request("POST", "/api/v1/licenses/usage/increment", json=payload)

        return UsageResult(usage=data["usage"])

    # -----------------------------------------------------------------------
    # Static helpers
    # -----------------------------------------------------------------------

    @staticmethod
    def normalize_domain(input_str: str) -> str:
        """Normalize a domain/URL to a consistent format.

        - Lowercases and strips whitespace
        - Strips protocol (``http://`` or ``https://``)
        - Strips path, port, and ``www.`` prefix
        - Encodes internationalized domain names (IDNA)

        Example::

            >>> LicensesOSClient.normalize_domain("https://WWW.Example.Com:8080/path")
            'example.com'
        """
        domain = input_str.lower().strip()

        # Remove protocol
        domain = re.sub(r"^https?://", "", domain)

        # Remove path and everything after
        domain = re.sub(r"/.*$", "", domain)

        # Remove port
        domain = re.sub(r":\d+$", "", domain)

        # Remove www. prefix
        domain = re.sub(r"^www\.", "", domain)

        # IDNA encode for internationalized domain names
        try:
            domain = domain.encode("idna").decode("ascii")
        except (UnicodeError, UnicodeDecodeError):
            pass

        return domain

    @staticmethod
    def should_allow_premium(
        state: dict[str, Any],
        grace_ttl: float = 172_800,
    ) -> bool:
        """Determine if premium features should be allowed based on cached state.

        Useful for graceful degradation when the API is unreachable. Cache the
        validation status and a Unix timestamp, then call this to decide if the
        cache is still fresh enough to trust.

        Args:
            state: Dictionary with ``status`` (license status string) and
                ``cached_at`` (Unix timestamp in seconds).
            grace_ttl: Grace period in seconds beyond the 12-hour cache TTL.
                Defaults to 172800 (48 hours).

        Returns:
            True if premium access should be granted based on the cached state.
        """
        status = state.get("status")
        cached_at = state.get("cached_at", 0)

        if status != "active":
            return False

        cache_ttl = 43_200  # 12 hours
        max_age = cache_ttl + grace_ttl
        age = int(time.time()) - cached_at

        return age <= max_age

    # -----------------------------------------------------------------------
    # Private HTTP layer
    # -----------------------------------------------------------------------

    def _request(
        self,
        method: str,
        path: str,
        json: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        """Make an authenticated HTTP request to the LicensesOS API.

        - Unwraps ``{"success": true, "data": T}`` envelope, returning ``T``.
        - Raises ``ApiError`` for non-2xx responses with error bodies.
        - Wraps connection errors in ``NetworkError``.
        """
        try:
            response = self._client.request(method, path, json=json)
        except httpx.TimeoutException as exc:
            raise NetworkError(
                f"Request to {path} timed out",
                cause=exc,
            ) from exc
        except httpx.HTTPError as exc:
            raise NetworkError(
                f"Failed to connect to LicensesOS API: {exc}",
                cause=exc,
            ) from exc

        body: dict[str, Any] = response.json()

        if not response.is_success:
            error = body.get("error", {})
            error_code = error.get("code", "UNKNOWN_ERROR") if isinstance(error, dict) else "UNKNOWN_ERROR"
            error_message = (
                error.get("message", f"API request failed with status {response.status_code}")
                if isinstance(error, dict)
                else f"API request failed with status {response.status_code}"
            )
            raise ApiError(error_message, error_code, response.status_code)

        # Unwrap the envelope
        if "data" in body:
            return body["data"]

        # Fallback: some endpoints may return data at the top level
        return body
