"""
Core client implementation for Táillí License Manager.

This module provides the LicenseClient class for interacting with the
License Manager API, managing seats, and handling API keys.
"""

from __future__ import annotations

import threading
import time
import logging
from contextlib import contextmanager
from dataclasses import dataclass, field
from typing import Optional, Dict, Any, Generator
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError
import json

logger = logging.getLogger("tailli_license")


class LicenseError(Exception):
    """Base exception for license-related errors."""
    pass


class SeatLimitError(LicenseError):
    """Raised when all seats are in use."""
    pass


class LicenseExpiredError(LicenseError):
    """Raised when the license has expired."""
    pass


class ConnectionError(LicenseError):
    """Raised when unable to connect to the License Manager."""
    pass


@dataclass
class Seat:
    """
    Represents an allocated seat in the license system.

    A seat gives you permission to use a licensed product. Seats are
    limited resources - only a certain number can be active at once.

    Attributes:
        seat_id: Unique identifier for this seat allocation.
        token: Authentication token for this seat (refreshed automatically).
        product_id: The product this seat is for.
        expires_at: When the current token expires (ISO 8601 format).

    Example:
        with client.seat() as seat:
            print(f"Using seat {seat.seat_id}")
            # Your licensed code here
    """
    seat_id: str
    token: str
    product_id: str
    expires_at: str
    _client: "LicenseClient" = field(repr=False)
    _refresh_thread: Optional[threading.Thread] = field(default=None, repr=False)
    _stop_refresh: threading.Event = field(default_factory=threading.Event, repr=False)
    _released: bool = field(default=False, repr=False)

    def release(self) -> None:
        """
        Release this seat back to the pool.

        After releasing, this seat can be used by another user. You should
        always release seats when you're done with them.

        This is called automatically when using the context manager:
            with client.seat() as seat:
                pass  # seat released automatically here

        Raises:
            LicenseError: If the release fails.
        """
        if self._released:
            return

        self._stop_refresh.set()
        if self._refresh_thread and self._refresh_thread.is_alive():
            self._refresh_thread.join(timeout=2.0)

        try:
            self._client._post("/license/release", {"seatId": self.seat_id})
            self._released = True
            logger.info(f"Released seat {self.seat_id}")
        except Exception as e:
            logger.warning(f"Failed to release seat {self.seat_id}: {e}")
            raise LicenseError(f"Failed to release seat: {e}") from e

    def refresh(self) -> None:
        """
        Refresh the seat token to extend its validity.

        This is called automatically in the background when using
        auto_refresh=True (the default). You typically don't need
        to call this manually.

        Raises:
            LicenseError: If the refresh fails.
        """
        try:
            response = self._client._post("/license/refresh", {"token": self.token})
            self.token = response["token"]
            self.expires_at = response.get("expiresAt", self.expires_at)
            logger.debug(f"Refreshed seat {self.seat_id}, expires at {self.expires_at}")
        except Exception as e:
            logger.error(f"Failed to refresh seat {self.seat_id}: {e}")
            raise LicenseError(f"Failed to refresh seat: {e}") from e

    def _start_auto_refresh(self, interval_seconds: float = 30.0) -> None:
        """Start background thread to refresh token before expiry."""
        def refresh_loop():
            while not self._stop_refresh.wait(interval_seconds):
                if self._released:
                    break
                try:
                    self.refresh()
                except Exception as e:
                    logger.error(f"Auto-refresh failed: {e}")
                    break

        self._refresh_thread = threading.Thread(target=refresh_loop, daemon=True)
        self._refresh_thread.start()
        logger.debug(f"Started auto-refresh for seat {self.seat_id}")


class LicenseClient:
    """
    Client for interacting with the Táillí License Manager.

    This is your main entry point for managing seats and API keys. Create
    one client per License Manager instance you want to connect to.

    Args:
        base_url: URL of the License Manager (e.g., "http://localhost:8787").
        product_id: Your product identifier (must match the activated license).
        timeout: Request timeout in seconds (default: 30).

    Example:
        # Create a client
        client = LicenseClient(
            base_url="http://localhost:8787",
            product_id="my-awesome-product"
        )

        # Check license status
        status = client.status()
        print(f"Seats available: {status['seatsAvailable']}")

        # Use a seat
        with client.seat() as seat:
            # Do licensed work
            pass
    """

    def __init__(
        self,
        base_url: str,
        product_id: str,
        timeout: float = 30.0,
    ):
        self.base_url = base_url.rstrip("/")
        self.product_id = product_id
        self.timeout = timeout

    def _request(self, method: str, path: str, data: Optional[Dict] = None) -> Dict[str, Any]:
        """Make an HTTP request to the License Manager."""
        url = f"{self.base_url}{path}"
        headers = {"Content-Type": "application/json"}

        body = json.dumps(data).encode("utf-8") if data else None
        req = Request(url, data=body, headers=headers, method=method)

        try:
            with urlopen(req, timeout=self.timeout) as response:
                return json.loads(response.read().decode("utf-8"))
        except HTTPError as e:
            try:
                error_body = json.loads(e.read().decode("utf-8"))
                error_msg = error_body.get("error", str(e))
            except:
                error_msg = str(e)

            if e.code == 429:
                raise SeatLimitError(error_msg)
            elif e.code == 410:
                raise LicenseExpiredError(error_msg)
            else:
                raise LicenseError(f"Request failed ({e.code}): {error_msg}") from e
        except URLError as e:
            raise ConnectionError(f"Cannot connect to License Manager at {self.base_url}: {e}") from e

    def _get(self, path: str, params: Optional[Dict] = None) -> Dict[str, Any]:
        """Make a GET request."""
        if params:
            query = "&".join(f"{k}={v}" for k, v in params.items())
            path = f"{path}?{query}"
        return self._request("GET", path)

    def _post(self, path: str, data: Dict) -> Dict[str, Any]:
        """Make a POST request."""
        return self._request("POST", path, data)

    def status(self) -> Dict[str, Any]:
        """
        Get the current license status for your product.

        Returns information about your license including seat limits,
        current usage, expiration, and enabled features.

        Returns:
            Dict containing:
                - productId: Your product ID
                - seats: Maximum seats allowed
                - seatsInUse: Currently allocated seats
                - seatsAvailable: Seats available for allocation
                - expiry: License expiration date (ISO 8601)
                - features: List of enabled features
                - valid: Whether the license is currently valid

        Raises:
            LicenseError: If the product is not licensed.
            LicenseExpiredError: If the license has expired.
            ConnectionError: If unable to reach the License Manager.

        Example:
            status = client.status()
            if status['seatsAvailable'] > 0:
                print("Seats available!")
            else:
                print(f"All {status['seats']} seats in use")
        """
        return self._get("/license/status", {"productId": self.product_id})

    def allocate(
        self,
        context: Optional[Dict[str, Any]] = None,
        auto_refresh: bool = True,
        refresh_interval: float = 30.0,
    ) -> Seat:
        """
        Allocate a seat for your product.

        This reserves one of your available seats. The seat comes with a
        token that must be refreshed periodically (handled automatically
        by default).

        Args:
            context: Optional metadata about this allocation (e.g., username,
                     workstation ID). Stored for auditing purposes.
            auto_refresh: If True (default), automatically refresh the token
                          in the background to prevent expiry.
            refresh_interval: How often to refresh in seconds (default: 30).

        Returns:
            A Seat object representing your allocation.

        Raises:
            SeatLimitError: If all seats are currently in use.
            LicenseExpiredError: If the license has expired.
            LicenseError: For other allocation failures.

        Example:
            # Simple allocation
            seat = client.allocate()
            try:
                # Use the seat
                pass
            finally:
                seat.release()

            # With context for auditing
            seat = client.allocate(context={
                "user": "alice",
                "workstation": "ws-042"
            })
        """
        data = {"productId": self.product_id}
        if context:
            data["context"] = context

        response = self._post("/license/allocate", data)

        seat = Seat(
            seat_id=response["seatId"],
            token=response["token"],
            product_id=self.product_id,
            expires_at=response.get("expiresAt", ""),
            _client=self,
        )

        if auto_refresh:
            seat._start_auto_refresh(refresh_interval)

        logger.info(f"Allocated seat {seat.seat_id} for {self.product_id}")
        return seat

    @contextmanager
    def seat(
        self,
        context: Optional[Dict[str, Any]] = None,
        auto_refresh: bool = True,
    ) -> Generator[Seat, None, None]:
        """
        Context manager for using a seat with automatic cleanup.

        This is the recommended way to use seats. The seat is automatically
        released when you exit the context, even if an exception occurs.

        Args:
            context: Optional metadata about this allocation.
            auto_refresh: If True (default), keep the token fresh automatically.

        Yields:
            A Seat object for the duration of the context.

        Raises:
            SeatLimitError: If all seats are in use.
            LicenseExpiredError: If the license has expired.

        Example:
            with client.seat() as seat:
                print(f"Got seat {seat.seat_id}")
                # Do your licensed work here
                do_something_that_requires_license()
            # Seat is automatically released here

            # With context for auditing
            with client.seat(context={"user": "bob"}) as seat:
                process_data()
        """
        seat = self.allocate(context=context, auto_refresh=auto_refresh)
        try:
            yield seat
        finally:
            seat.release()

    def verify_key(self, token: str) -> Dict[str, Any]:
        """
        Verify a user API key (JWT).

        Use this to validate tokens issued by the License Manager.

        Args:
            token: The JWT token to verify.

        Returns:
            Dict containing the decoded token payload if valid.

        Raises:
            LicenseError: If the token is invalid or expired.

        Example:
            try:
                payload = client.verify_key(user_token)
                print(f"Valid token for: {payload['sub']}")
            except LicenseError:
                print("Invalid token!")
        """
        response = self._post("/v1/verify", {"token": token})
        if not response.get("valid"):
            raise LicenseError(response.get("error", "Token verification failed"))
        return response.get("payload", {})

    def get_entitlements(self) -> Dict[str, Any]:
        """
        Get the entitlements for your product license.

        Returns detailed information about what your license permits,
        including features, scopes, and key limits.

        Returns:
            Dict containing:
                - features: List of enabled feature flags
                - scopes: List of allowed API scopes
                - maxKeys: Maximum user keys allowed
                - keysIssued: Number of keys currently issued
                - keysAvailable: Keys available for creation
                - expiry: License expiration date

        Example:
            ent = client.get_entitlements()
            if "premium-feature" in ent['features']:
                enable_premium_feature()
        """
        return self._get("/v1/entitlements", {"productId": self.product_id})
