"""
IamClient — IAM V2 API wrapper.

Usage:
    client = IamClient(
        iam_base_url="http://iamapi.easecation.net",
        client_id="YOUR_APP_CLIENT_ID",
        client_secret="YOUR_APP_CLIENT_SECRET",
    )
    tokens = client.exchange_oauth_code(code, redirect_uri)
    result = client.verify_token(access_token)
"""
from __future__ import annotations

import time
from typing import Any

import requests
from requests import Response

from .types import (
    ApiKeyVerifyData,
    HrBootstrapData,
    OAuthTokenData,
    RefreshTokenData,
    UserBootstrapData,
    VerifyTokenData,
)


class IamError(Exception):
    """IAM API or verification error."""

    pass


class IamClient:
    APP_TOKEN_CACHE_TTL_MS_DEFAULT = 55 * 60 * 1000  # 55 min
    PERMISSION_CACHE_TTL_MS_DEFAULT = 5 * 60 * 1000  # 5 min
    TIMEOUT_MS_DEFAULT = 5000

    def __init__(
        self,
        *,
        iam_base_url: str,
        client_id: str,
        client_secret: str,
        app_token_cache_ttl_ms: int | None = None,
        permission_cache_ttl_ms: int | None = None,
        timeout_ms: int | None = None,
    ):
        self._iam_base_url = iam_base_url.rstrip("/")
        self._client_id = client_id
        self._client_secret = client_secret
        self._app_token_cache_ttl_ms = app_token_cache_ttl_ms or self.APP_TOKEN_CACHE_TTL_MS_DEFAULT
        self._permission_cache_ttl_ms = permission_cache_ttl_ms or self.PERMISSION_CACHE_TTL_MS_DEFAULT
        self._timeout_s = (timeout_ms or self.TIMEOUT_MS_DEFAULT) / 1000.0

        self._session = requests.Session()
        self._session.headers["Content-Type"] = "application/json"

        self._cached_app_token: tuple[str, float] | None = None
        self._cached_v1_app_token: tuple[str, float] | None = None
        self._verify_cache: dict[str, tuple[VerifyTokenData, float]] = {}
        self._api_key_cache: dict[str, tuple[ApiKeyVerifyData, float]] = {}
        self._app_session_verify_cache: dict[str, tuple[bool, float]] = {}

    def _request(
        self,
        method: str,
        path: str,
        *,
        json: dict[str, Any] | None = None,
        headers: dict[str, str] | None = None,
    ) -> Response:
        url = f"{self._iam_base_url}{path}"
        try:
            response = self._session.request(
                method,
                url,
                json=json,
                headers=headers or {},
                timeout=self._timeout_s,
            )
            response.raise_for_status()
            return response
        except requests.RequestException as exc:
            raise IamError(f"IAM: {method} {path} failed: {exc}") from exc

    def _post(self, path: str, json: dict[str, Any], headers: dict[str, str] | None = None) -> dict[str, Any]:
        return self._request("POST", path, json=json, headers=headers).json()

    def _get(self, path: str) -> dict[str, Any]:
        return self._request("GET", path).json()

    def get_app_session_token(self) -> str:
        now_ms = time.time() * 1000
        if self._cached_app_token and now_ms < self._cached_app_token[1]:
            return self._cached_app_token[0]

        data = self._post(
            "/api/open/v2/token",
            {"client_id": self._client_id, "client_secret": self._client_secret},
        )
        if not data.get("success") or not data.get("data", {}).get("applicationSessionToken"):
            raise IamError(f"IAM: Failed to get app token: {data.get('message') or data.get('code')}")

        token = data["data"]["applicationSessionToken"]
        self._cached_app_token = (token, now_ms + self._app_token_cache_ttl_ms)
        return token

    def invalidate_app_token(self) -> None:
        self._cached_app_token = None

    def get_v1_app_session_token(self) -> str:
        now_ms = time.time() * 1000
        if self._cached_v1_app_token and now_ms < self._cached_v1_app_token[1]:
            return self._cached_v1_app_token[0]

        data = self._post(
            "/api/open/v1/token",
            {"client_id": self._client_id, "client_secret": self._client_secret},
        )
        if not data.get("success") or not data.get("data", {}).get("applicationSessionToken"):
            raise IamError(f"IAM: Failed to get V1 app token: {data.get('message') or data.get('code')}")

        token = data["data"]["applicationSessionToken"]
        self._cached_v1_app_token = (token, now_ms + self._app_token_cache_ttl_ms)
        return token

    def invalidate_v1_app_token(self) -> None:
        self._cached_v1_app_token = None

    def exchange_oauth_code(self, code: str, redirect_uri: str) -> OAuthTokenData:
        app_token = self.get_app_session_token()
        data = self._post(
            "/api/open/v2/oauth/token",
            {"code": code, "redirect_uri": redirect_uri},
            headers={"X-App-Session-Token": app_token},
        )
        if not data.get("success") or not data.get("data"):
            raise IamError(f"IAM: OAuth code exchange failed: {data.get('message') or data.get('code')}")
        return data["data"]

    def verify_token(self, access_token: str) -> VerifyTokenData:
        now_ms = time.time() * 1000
        cached = self._verify_cache.get(access_token)
        if cached and now_ms < cached[1]:
            return cached[0]

        app_token = self.get_app_session_token()
        data = self._post(
            "/api/open/v2/verify",
            {"access_token": access_token},
            headers={"X-App-Session-Token": app_token},
        )
        if not data.get("success") or not data.get("data"):
            self._verify_cache.pop(access_token, None)
            raise IamError(f"IAM: Token verification failed: {data.get('message') or data.get('code')}")

        result = data["data"]
        exp_ms = result["exp"] * 1000
        cache_expires = min(now_ms + self._permission_cache_ttl_ms, exp_ms - 1000)
        if cache_expires > now_ms:
            self._verify_cache[access_token] = (result, cache_expires)
        return result

    def evict_from_verify_cache(self, access_token: str) -> None:
        self._verify_cache.pop(access_token, None)

    def refresh_token(self, refresh_token: str) -> RefreshTokenData:
        app_token = self.get_app_session_token()
        data = self._post(
            "/api/open/v2/refresh",
            {"refresh_token": refresh_token},
            headers={"X-App-Session-Token": app_token},
        )
        if not data.get("success") or not data.get("data"):
            raise IamError(f"IAM: Token refresh failed: {data.get('message') or data.get('code')}")
        return data["data"]

    def revoke_token(self, refresh_token: str) -> bool:
        app_token = self.get_app_session_token()
        data = self._post(
            "/api/open/v2/revoke",
            {"refresh_token": refresh_token},
            headers={"X-App-Session-Token": app_token},
        )
        return bool(data.get("success") and data.get("data", {}).get("revoked"))

    def get_jwks(self) -> JwksData:
        return self._get("/api/open/v2/jwks")

    def get_user_profile(self, access_token: str) -> UserProfileData:
        app_token = self.get_app_session_token()
        data = self._post(
            "/api/open/v2/user/profile",
            {"access_token": access_token},
            headers={"X-App-Session-Token": app_token},
        )
        if not data.get("success") or not data.get("data"):
            raise IamError(f"IAM: getUserProfile failed: {data.get('message') or data.get('code')}")
        return data["data"]

    def get_user_bootstrap(
        self,
        access_token: str,
        data_codes: list[str],
        target_application_id: int | None = None,
    ) -> UserBootstrapData:
        app_token = self.get_app_session_token()
        body: dict[str, Any] = {"access_token": access_token, "data_codes": data_codes}
        if target_application_id is not None:
            body["target_application_id"] = target_application_id

        data = self._post(
            "/api/open/v2/user/bootstrap",
            body,
            headers={"X-App-Session-Token": app_token},
        )
        if not data.get("success") or not data.get("data"):
            raise IamError(f"IAM: getUserBootstrap failed: {data.get('message') or data.get('code')}")
        return data["data"]

    def get_hr_bootstrap_data(self, access_token: str, data_app_id: int = 1) -> HrBootstrapData:
        bootstrap = self.get_user_bootstrap(
            access_token,
            ["group", "ECID", "serverAdmin", "serverAdminDue"],
            data_app_id,
        )
        user_data = bootstrap.get("user_data") or {}
        hr_group_raw = user_data.get("group")
        if hr_group_raw is None:
            raise IamError("Failed to get user group")
        try:
            hr_group = int(hr_group_raw)
        except (ValueError, TypeError):
            raise IamError("Invalid user group")

        server_admin = user_data.get("serverAdmin")
        return {
            "uid": bootstrap["uid"],
            "name": bootstrap["name"],
            "email": bootstrap["email"],
            "hrGroup": hr_group,
            "ECID": user_data.get("ECID"),
            "serverAdmin": int(server_admin) if server_admin is not None else None,
            "serverAdminDue": user_data.get("serverAdminDue"),
        }

    def verify_iam_callback_token(self, iam_token: str, source_application_id: int | None = None) -> bool:
        app_token = self.get_v1_app_session_token()
        body: dict[str, Any] = {"iam_token": iam_token}
        if source_application_id is not None:
            body["source_application_id"] = source_application_id

        data = self._post(
            "/api/open/v1/verify-iam-token",
            body,
            headers={"X-App-Session-Token": app_token},
        )
        return data.get("data", {}).get("valid") is True

    def get_user_permissions(self, user_id: int, target_application_id: int | None = None) -> list[str]:
        app_token = self.get_app_session_token()
        body: dict[str, Any] = {"user_id": user_id}
        if target_application_id is not None:
            body["target_application_id"] = target_application_id

        data = self._post(
            "/api/open/v2/user/permissions",
            body,
            headers={"X-App-Session-Token": app_token},
        )
        if not data.get("success") or not data.get("data"):
            raise IamError(f"IAM: getUserPermissions failed: {data.get('message') or data.get('code')}")
        return data["data"]["permissions"]

    def verify_api_key(self, key_value: str) -> ApiKeyVerifyData:
        now_ms = time.time() * 1000
        cached = self._api_key_cache.get(key_value)
        if cached and now_ms < cached[1]:
            return cached[0]

        app_token = self.get_app_session_token()
        data = self._post(
            "/api/internal/apikeys/verify",
            {"key": key_value},
            headers={"X-App-Session-Token": app_token},
        )
        if not data.get("success") or not data.get("data"):
            self._api_key_cache.pop(key_value, None)
            raise IamError(f"IAM: API key verification failed: {data.get('message') or data.get('code')}")
        result = data["data"]
        if not result.get("valid"):
            self._api_key_cache.pop(key_value, None)
            raise IamError("IAM: API key verification failed: invalid key")

        self._api_key_cache[key_value] = (result, now_ms + self._permission_cache_ttl_ms)
        return result

    def evict_api_key_from_cache(self, key_value: str) -> None:
        self._api_key_cache.pop(key_value, None)

    def clear_api_key_cache(self) -> None:
        self._api_key_cache.clear()

    def verify_app_session_token(self, app_session_token: str) -> bool:
        now_ms = time.time() * 1000
        cached = self._app_session_verify_cache.get(app_session_token)
        if cached and now_ms < cached[1]:
            return cached[0]

        url = f"{self._iam_base_url}/api/open/v2/verify"
        try:
            resp = self._session.post(
                url,
                json={"access_token": "__app_session_probe__"},
                headers={"X-App-Session-Token": app_session_token},
                timeout=self._timeout_s,
            )
        except requests.RequestException as exc:
            raise IamError(f"IAM: App session verification failed: {exc}") from exc
        if resp.status_code in (401, 403):
            self._app_session_verify_cache[app_session_token] = (False, now_ms + 10 * 1000)
            return False
        if resp.status_code >= 500:
            raise IamError(f"IAM: App session verification failed: status={resp.status_code}")

        self._app_session_verify_cache[app_session_token] = (True, now_ms + 30 * 1000)
        return True

    def evict_app_session_verify_cache(self, app_session_token: str) -> None:
        self._app_session_verify_cache.pop(app_session_token, None)

    def clear_app_session_verify_cache(self) -> None:
        self._app_session_verify_cache.clear()
