"""Authentication related functions."""

import logging
import os
import ssl
import subprocess
from datetime import datetime, timedelta
from pathlib import Path
from typing import Literal

import certifi
import jwt
import msgspec.json
from jwt import PyJWTError
from msgspec import Struct
from pytz import UTC

from slices_clientlib_core.oauth2.config import settings
from slices_clientlib_core.oauth2.token import AuthJWT, OAuthToken

from .config import clear_cache, config_dir_location
from .const import AUTH_FILENAME, JWT_SUPPORTED_ALGORITHMS
from .exceptions import AuthenticationError, ConfigurationError

logger = logging.getLogger(__name__)

auth_file_location = config_dir_location / AUTH_FILENAME


_GLOBAL_JWKS_CLIENT = None


def _get_jwks_client():
    global _GLOBAL_JWKS_CLIENT  # noqa: PLW0603 one time init using global state
    if _GLOBAL_JWKS_CLIENT is None:
        # we import here because this is a slow import
        from .jwk_cache import CachingPyJWKClient  # noqa: PLC0415 we put import here to reduce startup time

        ssl_context = ssl.create_default_context(cafile=certifi.where())
        # jwks_client = jwt.PyJWKClient(settings.jwks_url, ssl_context=ssl_context)
        # we set cache_jwk_set because we want to disable caching of PyJWKClient as we replace it with our own caching
        _GLOBAL_JWKS_CLIENT = CachingPyJWKClient(settings.jwks_url, ssl_context=ssl_context, cache_jwk_set=False)
    return _GLOBAL_JWKS_CLIENT


class AuthConfig(Struct):
    """Auth config file contents."""

    version: Literal["0.1"]
    oauth_token: OAuthToken


def save_auth_token(auth_token: OAuthToken):
    """Save authentication token."""

    auth_config: AuthConfig = AuthConfig(
        version="0.1",
        oauth_token=auth_token,
    )

    config_dir_location.mkdir(exist_ok=True)
    with auth_file_location.open("wb") as f:
        f.write(msgspec.json.encode(auth_config))

    auth_file_location.chmod(0o600)  # set secure file permissions


def parse_auth_token(auth_token: OAuthToken) -> AuthJWT:
    """Parse auth token into an AuthJWT object."""
    jwt_token = auth_token.access_token
    signing_key = _get_jwks_client().get_signing_key_from_jwt(jwt_token)

    return jwt.decode(
        jwt_token,
        signing_key.key,
        algorithms=JWT_SUPPORTED_ALGORITHMS,
        audience=settings.audience,
        leeway=15,
        # do not verify iat and nbf claims, as sometimes there is a clock skew between the local system and actual time.
        # This can cause ImmatureSignatureError, which confuses the user.
        options={"verify_iat": False, "verify_nbf": False},
    )


def delete_auth_token():
    """Delete authentication token."""
    auth_file_location.unlink(missing_ok=True)

    # Also clear caches
    clear_cache()


def refresh_auth_token(refresh_token: str) -> OAuthToken:
    """Refresh the authentication token."""

    # import here, because this is a slow import
    from slices_clientlib_core.oauth2.flow import RefreshFlow  # noqa: PLC0415 we put import here to reduce startup time

    new_auth_token = RefreshFlow().perform_refresh(refresh_token)

    save_auth_token(new_auth_token)

    return new_auth_token


MINIMUM_LIFETIME_FOR_FRESH_TOKEN = timedelta(minutes=1)


def ensure_auth_token_is_fresh(auth_token: OAuthToken) -> OAuthToken:
    """Ensure the authentication token is fresh."""

    if datetime.fromtimestamp(auth_token.expires_at, UTC) < (datetime.now(UTC) - MINIMUM_LIFETIME_FOR_FRESH_TOKEN):
        if auth_token.refresh_token:
            return refresh_auth_token(auth_token.refresh_token)

        raise AuthenticationError("Token expired and no refresh token available")

    return auth_token


AUTH_TOKEN_LOAD_EXPIRE_LEEWAY_S = 90
AUTH_TOKEN_LOAD_EXPIRE_LEEWAY = timedelta(seconds=AUTH_TOKEN_LOAD_EXPIRE_LEEWAY_S)


def load_auth_token() -> OAuthToken | None:
    """Load authentication token from file."""

    # Check if we have an external token provider
    if auth_token_provider := os.environ.get("SLICES_AUTH_TOKEN_PROVIDER"):
        try:
            return msgspec.json.decode(subprocess.check_output(auth_token_provider), type=OAuthToken)  # noqa: S603
        except subprocess.CalledProcessError as e:
            raise ConfigurationError("Could not retrieve authentication token from external provider") from e
    if not auth_file_location.exists():
        return None

    try:
        with Path(auth_file_location).open() as f:
            auth_file_text = f.read()
            auth_file_contents = msgspec.json.decode(auth_file_text, type=AuthConfig)
    except (msgspec.DecodeError, FileNotFoundError):
        logger.exception("Could not decode authentication token file")
        return None

    if not auth_file_contents:
        logger.error("Could not load authentication token")
        return None

    # sanity checks
    if not isinstance(auth_file_contents, AuthConfig) or not hasattr(auth_file_contents, "version"):
        logger.error("Problem processing auth file contents")
        logger.error("  Debug Info:")
        logger.error("    correct_type=%s", isinstance(auth_file_contents, AuthConfig))
        logger.error("    has_version=%s", hasattr(auth_file_contents, "version"))
        logger.error("    auth_file_location=%s", auth_file_location)
        logger.error("    type=%s", type(auth_file_contents))
        logger.error("    auth_file_text=%s", auth_file_text)
        logger.error("    auth_file_contents=%s", auth_file_contents)
        return None

    if auth_file_contents.version != "0.1":
        logger.warning("Unexpected auth file version: %s", auth_file_contents.version)

    auth_token: OAuthToken = auth_file_contents.oauth_token

    if auth_token.refresh_token and datetime.fromtimestamp(auth_token.expires_at, tz=UTC) < (
        datetime.now(UTC) + AUTH_TOKEN_LOAD_EXPIRE_LEEWAY
    ):
        auth_token = refresh_auth_token(auth_token.refresh_token)

    return auth_token


class CredentialsBrokenError(Exception):
    """Problem loading credentials."""


def load_auth_token_and_user() -> tuple[OAuthToken | None, AuthJWT | None]:
    """Load auth token and parse user info from it."""

    from slices_clientlib_core.util.disable_authlib_warning import disable_authlib_warning

    disable_authlib_warning()
    from authlib.common.errors import AuthlibBaseError

    try:
        auth_token = load_auth_token()
        user_info = parse_auth_token(auth_token) if auth_token else None
    except (AuthlibBaseError, PyJWTError):
        try:
            delete_auth_token()
        except:  # noqa E722 ignore failures deleting token
            pass
        finally:
            raise CredentialsBrokenError("Something is wrong with your user credentials")
    else:
        return auth_token, user_info
