"""
HTTP client for the AiVibe universal platform API (api.aivibe.cloud).

Handles tenant lookup, plan validation, and credit management via REST API
calls, replacing direct DynamoDB access. Uses httpx with Bearer auth
and tenacity for retry logic.
"""

from __future__ import annotations

import time
from datetime import UTC, datetime
from typing import TYPE_CHECKING, Any
from uuid import uuid4

import httpx
from tenacity import (
    retry,
    retry_if_exception,
    retry_if_result,
    stop_after_attempt,
    wait_exponential,
)

from aicippy import __version__
from aicippy.platform.models import TenantInfo, TenantRole
from aicippy.utils.logging import get_logger

if TYPE_CHECKING:
    from collections.abc import Callable

logger = get_logger(__name__)

# HTTP status codes that should trigger a retry
_RETRYABLE_STATUS_CODES = frozenset({500, 502, 503, 504})

# Buffer in seconds before token expiry to trigger proactive refresh
_TOKEN_REFRESH_BUFFER_SECONDS = 300  # 5 minutes

# Default Cognito token lifetime (standard 1 hour)
_DEFAULT_TOKEN_LIFETIME_SECONDS = 3600.0


def _should_retry(exc: BaseException) -> bool:
    """Determine if an exception warrants a retry.

    Returns True for:
    - httpx.ConnectError (network connectivity issues)
    - httpx.TimeoutException (request timeouts)
    - httpx.HTTPStatusError with 500, 502, 503, or 504 status codes
    """
    if isinstance(exc, (httpx.ConnectError, httpx.TimeoutException)):
        return True
    if isinstance(exc, httpx.HTTPStatusError):
        return exc.response.status_code in _RETRYABLE_STATUS_CODES
    return False


def _is_server_error_response(result: object) -> bool:
    """Check if a result is an httpx.Response with a 5xx status code.

    This is used as a retry_if_result predicate. Since decorated methods
    may return dicts, ints, TenantInfo, or booleans (not just Response
    objects), we must guard against non-Response return values.
    """
    if isinstance(result, httpx.Response):
        return result.status_code >= 500
    return False


# Retry config: 3 attempts with exponential backoff (1s, 2s, 4s)
_RETRY_KWARGS: dict[str, Any] = {
    "stop": stop_after_attempt(3),
    "wait": wait_exponential(multiplier=1, min=1, max=4),
    "retry": (
        retry_if_exception(lambda e: _should_retry(e)) | retry_if_result(_is_server_error_response)
    ),
    "reraise": True,
}


class PlatformClient:
    """HTTP client for api.aivibe.cloud universal platform.

    Provides methods for tenant lookup, plan validation, and credit
    management via the centralized platform REST API.

    Args:
        base_url: Platform API base URL (e.g. https://api.aivibe.cloud).
        auth_token: Bearer token from Cognito authentication.
        timeout: HTTP request timeout in seconds.
        token_refresher: Optional callable that returns a fresh access token.
            When provided, the client will automatically refresh the token
            before API calls if it is expired or about to expire (within
            5 minutes). If None, the static auth_token is used throughout.
    """

    __slots__ = (
        "_auth_token",
        "_base_url",
        "_client",
        "_token_expires_at",
        "_token_refresher",
    )

    def __init__(
        self,
        base_url: str,
        auth_token: str,
        timeout: float = 30.0,
        token_refresher: Callable[[], str] | None = None,
    ) -> None:
        self._base_url = base_url.rstrip("/")
        self._auth_token = auth_token
        self._token_refresher = token_refresher
        # Default expiry: 1 hour from now (standard Cognito token lifetime)
        self._token_expires_at: float = time.monotonic() + _DEFAULT_TOKEN_LIFETIME_SECONDS
        self._client = httpx.AsyncClient(
            base_url=self._base_url,
            headers={
                "Authorization": f"Bearer {auth_token}",
                "Content-Type": "application/json",
                "Accept": "application/json",
                "User-Agent": f"aicippy-cli/{__version__}",
            },
            timeout=httpx.Timeout(timeout),
        )

    def _ensure_fresh_token(self) -> None:
        """Refresh the auth token if it is expired or about to expire.

        Checks whether the current token will expire within
        _TOKEN_REFRESH_BUFFER_SECONDS. If a token_refresher callable was
        provided and the token needs refreshing, it calls the refresher
        and updates the httpx client's Authorization header.

        If refresh fails, the existing token is kept and a warning is logged.
        """
        if self._token_refresher is None:
            return

        now = time.monotonic()
        if now < (self._token_expires_at - _TOKEN_REFRESH_BUFFER_SECONDS):
            # Token is still fresh enough
            return

        try:
            new_token = self._token_refresher()
            self._auth_token = new_token
            self._client.headers["Authorization"] = f"Bearer {new_token}"
            # Reset expiry to 1 hour from now
            self._token_expires_at = now + 3600.0
            logger.info("platform_client_token_refreshed")
        except Exception as exc:
            logger.warning(
                "platform_client_token_refresh_failed",
                error=str(exc),
            )

    @retry(**_RETRY_KWARGS)
    async def get_tenant(self, email: str) -> TenantInfo:
        """Look up tenant by email.

        GET /api/v1/tenants/by-email/{email}

        Args:
            email: User email address.

        Returns:
            TenantInfo with tenant details.

        Raises:
            httpx.HTTPStatusError: On 4xx/5xx responses.
        """
        self._ensure_fresh_token()
        response = await self._client.get(
            f"/api/v1/tenants/by-email/{email}",
        )
        response.raise_for_status()
        data = response.json()

        created_at_str = data.get("created_at", "")
        try:
            created_at = datetime.fromisoformat(created_at_str.replace("Z", "+00:00"))
        except (ValueError, AttributeError):
            created_at = datetime.now(UTC)

        return TenantInfo(
            tenant_id=data["tenant_id"],
            org_tenant_id=data.get("org_tenant_id", ""),
            email=data["email"],
            name=data.get("name", ""),
            created_at=created_at,
        )

    @retry(**_RETRY_KWARGS)
    async def validate_plan(self, tenant_id: str) -> dict[str, Any]:
        """Get plan details for a tenant.

        GET /api/v1/tenants/{tenant_id}/plan

        Args:
            tenant_id: Tenant identifier.

        Returns:
            Dict with plan, status, credits_remaining, credits_total,
            expiry, is_admin.

        Raises:
            httpx.HTTPStatusError: On 4xx/5xx responses.
        """
        self._ensure_fresh_token()
        response = await self._client.get(
            f"/api/v1/tenants/{tenant_id}/plan",
        )
        response.raise_for_status()
        return response.json()

    @retry(**_RETRY_KWARGS)
    async def deduct_credits(
        self,
        tenant_id: str,
        amount: int,
        source: str,
        description: str,
    ) -> dict[str, Any]:
        """Deduct credits atomically.

        POST /api/v1/tenants/{tenant_id}/credits/deduct

        Args:
            tenant_id: Tenant identifier.
            amount: Number of credits to deduct.
            source: Source application (e.g. "aicippy").
            description: Human-readable description of the deduction.

        Returns:
            Dict with success, credits_remaining, transaction_id, error.

        Raises:
            httpx.HTTPStatusError: On 5xx responses (4xx handled in response).
        """
        self._ensure_fresh_token()
        idempotency_key = str(uuid4())
        response = await self._client.post(
            f"/api/v1/tenants/{tenant_id}/credits/deduct",
            json={
                "amount": amount,
                "source": source,
                "description": description,
            },
            headers={"Idempotency-Key": idempotency_key},
        )
        response.raise_for_status()
        return response.json()

    @retry(**_RETRY_KWARGS)
    async def get_credits(self, tenant_id: str) -> int:
        """Get remaining credit count for a tenant.

        GET /api/v1/tenants/{tenant_id}/credits

        Args:
            tenant_id: Tenant identifier.

        Returns:
            Number of credits remaining.

        Raises:
            httpx.HTTPStatusError: On 4xx/5xx responses.
        """
        self._ensure_fresh_token()
        response = await self._client.get(
            f"/api/v1/tenants/{tenant_id}/credits",
        )
        response.raise_for_status()
        data = response.json()
        return int(data["credits_remaining"])

    async def health_check(self) -> bool:
        """Check platform API health.

        GET /api/v1/health

        Returns:
            True if the platform is healthy, False otherwise.
        """
        try:
            self._ensure_fresh_token()
            response = await self._client.get("/api/v1/health")
            response.raise_for_status()
            data = response.json()
            return data.get("status") == "healthy"
        except Exception as e:
            logger.warning("platform_health_check_failed", error=str(e))
            return False

    @retry(**_RETRY_KWARGS)
    async def get_org(self, org_tenant_id: str) -> dict[str, Any]:
        """Get organization details.

        GET /api/v1/orgs/{org_tenant_id}

        Args:
            org_tenant_id: Organization tenant identifier.

        Returns:
            Dict with org_tenant_id, name, created_at, member_count.

        Raises:
            httpx.HTTPStatusError: On 4xx/5xx responses.
        """
        self._ensure_fresh_token()
        response = await self._client.get(
            f"/api/v1/orgs/{org_tenant_id}",
        )
        response.raise_for_status()
        return response.json()

    @retry(**_RETRY_KWARGS)
    async def get_org_members(self, org_tenant_id: str) -> list[TenantInfo]:
        """Get all members of an organization.

        GET /api/v1/orgs/{org_tenant_id}/members

        Args:
            org_tenant_id: Organization tenant identifier.

        Returns:
            List of TenantInfo for each org member.

        Raises:
            httpx.HTTPStatusError: On 4xx/5xx responses.
        """
        self._ensure_fresh_token()
        response = await self._client.get(
            f"/api/v1/orgs/{org_tenant_id}/members",
        )
        response.raise_for_status()
        data = response.json()

        members: list[TenantInfo] = []
        for member in data.get("members", []):
            created_at_str = member.get("created_at", "")
            try:
                created_at = datetime.fromisoformat(created_at_str.replace("Z", "+00:00"))
            except (ValueError, AttributeError):
                created_at = datetime.now(UTC)

            members.append(
                TenantInfo(
                    tenant_id=member["tenant_id"],
                    org_tenant_id=org_tenant_id,
                    email=member["email"],
                    name=member.get("name", ""),
                    created_at=created_at,
                )
            )
        return members

    @retry(**_RETRY_KWARGS)
    async def get_tenant_roles(self, tenant_id: str) -> list[TenantRole]:
        """Get roles assigned to a tenant.

        GET /api/v1/tenants/{tenant_id}/roles

        Args:
            tenant_id: Tenant identifier.

        Returns:
            List of TenantRole assignments.

        Raises:
            httpx.HTTPStatusError: On 4xx/5xx responses.
        """
        self._ensure_fresh_token()
        response = await self._client.get(
            f"/api/v1/tenants/{tenant_id}/roles",
        )
        response.raise_for_status()
        data = response.json()

        roles: list[TenantRole] = []
        for role_data in data.get("roles", []):
            granted_at_str = role_data.get("granted_at", "")
            try:
                granted_at = datetime.fromisoformat(granted_at_str.replace("Z", "+00:00"))
            except (ValueError, AttributeError):
                granted_at = datetime.now(UTC)

            roles.append(
                TenantRole(
                    tenant_id=tenant_id,
                    role=role_data.get("role", "user"),
                    apps=list(role_data.get("apps", [])),
                    granted_at=granted_at,
                    granted_by=role_data.get("granted_by", ""),
                )
            )
        return roles

    @retry(**_RETRY_KWARGS)
    async def assign_admin_role(
        self,
        tenant_id: str,
        apps: list[str] | None = None,
    ) -> dict[str, Any]:
        """Assign admin role to a tenant for specified apps.

        POST /api/v1/tenants/{tenant_id}/roles

        Args:
            tenant_id: Tenant identifier.
            apps: List of app domains. Use ["*"] for all apps.
                Defaults to ["*"] (admin for all apps).

        Returns:
            Dict with success status and role assignment details.

        Raises:
            httpx.HTTPStatusError: On 4xx/5xx responses.
        """
        self._ensure_fresh_token()
        response = await self._client.post(
            f"/api/v1/tenants/{tenant_id}/roles",
            json={
                "role": "admin",
                "apps": apps or ["*"],
            },
        )
        response.raise_for_status()
        return response.json()

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

    async def __aenter__(self) -> PlatformClient:
        return self

    async def __aexit__(
        self,
        exc_type: type[BaseException] | None,
        exc_val: BaseException | None,
        exc_tb: Any,
    ) -> None:
        await self.close()
