from typing import Callable, Dict, Any, Optional

import httpx
import jwt
from async_lru import alru_cache
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from fastapi import Depends, Cookie
from fastapi import Request, Response, status
from fastapi.exceptions import HTTPException
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jwt import PyJWK
from starlette.responses import JSONResponse

from sirius.common import is_test_environment, get_environmental_secret
from sirius.constants import SiriusEnvironmentSecretKey
from sirius.exceptions import ApplicationException


async def get_private_key() -> str:
    generate_private_key: Callable[[], str] = lambda: rsa.generate_private_key(public_exponent=65537, key_size=2048).private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.NoEncryption()
    ).decode("utf-8")

    if is_test_environment():
        return generate_private_key()

    try:
        return await get_environmental_secret(SiriusEnvironmentSecretKey.PRIVATE_KEY)
    except ApplicationException:
        return await get_environmental_secret(SiriusEnvironmentSecretKey.PRIVATE_KEY, generate_private_key())


@alru_cache(maxsize=50, ttl=86_400)  # 24 hour cache
async def get_public_key() -> str:
    private_key_string: str = await get_private_key()
    return (serialization.load_pem_private_key(private_key_string.encode("utf-8"), password=None)
            .public_key()
            .public_bytes(encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo).decode("utf-8"))


@alru_cache(maxsize=50, ttl=86_400)  # 24 hour cache
async def get_jwks_client(oidc_config_url: str) -> jwt.PyJWKClient:
    from sirius.http_requests import AsyncHTTPSession
    oidc_config: Dict[str, Any] = (await AsyncHTTPSession(oidc_config_url).get(oidc_config_url)).data
    return jwt.PyJWKClient(oidc_config.get("jwks_uri"))


async def is_token_valid(token: str) -> bool:
    if not token:
        return False

    tenant_id: str = await get_environmental_secret(SiriusEnvironmentSecretKey.MICROSOFT_TENANT_ID)
    audience: str = await get_environmental_secret(SiriusEnvironmentSecretKey.MICROSOFT_CLIENT_ID)
    issuer: str = f"https://sts.windows.net/{tenant_id}/" if is_application_generated_token(token) else f"https://login.microsoftonline.com/{tenant_id}/v2.0"
    oidc_config_url: str = f"https://login.microsoftonline.com/{tenant_id}/v2.0/.well-known/openid-configuration"
    signing_key: PyJWK = (await get_jwks_client(oidc_config_url)).get_signing_key(jwt.get_unverified_header(token)["kid"])

    try:
        jwt.decode(
            token,
            signing_key.key,
            algorithms=["RS256"],
            audience=audience,
            issuer=issuer,
            verify=True
        )
    except Exception:
        return False

    return True


@alru_cache(maxsize=50, ttl=1800)  # 30 minute cache
async def get_access_token() -> str:
    tenant_id: str = await get_environmental_secret(SiriusEnvironmentSecretKey.MICROSOFT_TENANT_ID)
    client_id: str = await get_environmental_secret(SiriusEnvironmentSecretKey.MICROSOFT_CLIENT_ID)
    client_secret: str = await get_environmental_secret(SiriusEnvironmentSecretKey.MICROSOFT_CLIENT_SECRET)

    token_url: str = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"

    payload: Dict[str, str] = {
        "grant_type": "client_credentials",
        "client_id": client_id,
        "client_secret": client_secret,
        "scope": f"{client_id}/.default"
    }

    async with httpx.AsyncClient() as client:
        response: httpx._models.Response = await client.post(token_url, data=payload)
        response.raise_for_status()
        access_token: str = response.json()["access_token"]

    return access_token


async def get_central_finite_curve_authentication_headers() -> Dict[str, str]:
    return {"Authorization": f"Bearer {await get_access_token()}"}


def is_application_generated_token(token: str) -> bool:
    return "sts.windows.net" in jwt.decode(token, options={"verify_signature": False}).get("iss")


async def verify_token(header_token: HTTPAuthorizationCredentials = Depends(HTTPBearer(auto_error=False)), cookie_token: Optional[str] = Cookie(None, alias="Authorization")) -> None:
    token: str | None = cookie_token.replace("Bearer ", "") if cookie_token else header_token.credentials if header_token else None
    if not await is_token_valid(token):
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, headers={"WWW-Authenticate": "Bearer"}, )


async def redirect_unauthorized_handler(request: Request, exc: HTTPException) -> Response:
    return JSONResponse(
        status_code=exc.status_code,
        content={"message": exc.detail},
        headers={"WWW-Authenticate": "Bearer"} if exc.status_code == status.HTTP_401_UNAUTHORIZED else None,
    )
