from aspen_crypto.keys import (
    generate_kid,
    load_local_umbrella_api_key,
)
from aspen_crypto.settings import LOCAL_VERIFICATION_KEY_FILE
from console_base.utils import call_once
from lclazy import LazyLoader
from lcrequests.typehints import Subject
from lcrequests.tokens import V4PasetoToken
import logging
from pathlib import Path
from typing import TYPE_CHECKING

from django.conf import settings
from django.utils.translation import gettext_lazy as tr
from rest_framework.exceptions import AuthenticationFailed

if TYPE_CHECKING:
    from .models import EncryptionKey
    import pylogcabin
else:
    pylogcabin = LazyLoader('pylogcabin', globals(), 'pylogcabin')

from .typehints import CidType, KeyData

logger = logging.getLogger(__name__)


def register_key_on_aspen(public_key: 'EncryptionKey', api=None) -> bool:
    """
    Helper method to create the EncryptionKey on the Aspen CA server.
    """
    aspen_api = api or pylogcabin.AspenAPI(signed_token_class=load_aspen_token_class())

    if confirm_key_on_aspen(public_key, api):
        return True

    aspen_api.load('encipher', 'encryptionkey')  # noqa

    data = {
        'cid': str(public_key.cid),
        'user': str(public_key.user.cid),
        'key': public_key.key,
        'name': public_key.name,
        'revocation_date': None,
        'kid': generate_kid(public_key.key),
    }
    try:
        resp = aspen_api.post(post_data=data)
        return resp.ok
    except pylogcabin.exceptions.ConsoleAPIClientError as e:
        logger.error(f'Failed to create Console API key for {public_key}. Error: {e.response.text}')
    except Exception as e:
        logger.exception(e)
    return False


def confirm_key_on_aspen(public_key: 'EncryptionKey', api=None) -> bool:
    """
    Helper method to confirm that the EncryptionKey exists on the Aspen CA server.
    """
    token_class = load_aspen_token_class(public_key.user.cid)
    aspen_api = api or pylogcabin.AspenAPI(signed_token_class=token_class)
    aspen_api.load('aspentls', 'verificationkey')  # noqa

    try:
        return aspen_api.head(cid=public_key.cid).ok
    except pylogcabin.exceptions.ConsoleAPIAuthenticationError:
        msg = 'Invalid credentials; unable to retrieve Verification Key from Aspen.'
        logger.error(msg)
        raise AuthenticationFailed(tr(msg))
    except pylogcabin.exceptions.LCHTTPError as e:
        logger.info('An error occurred while retrieving credentials: %s', e)
        raise AuthenticationFailed(tr('An error occurred while retrieving credentials'))


def retrieve_key_from_aspen(cid: CidType = '', kid: str = '', api=None) -> KeyData:
    """
    The Aspen CA Server is the central repository for all Verification Keys.
    When a token references a kid or cid that isn't in our database, then
    attempt to retrieve it from Aspen to perform the validation.
    """
    aspen_api = api or pylogcabin.AspenAPI(signed_token_class=load_aspen_token_class())
    aspen_api.load('aspentls', 'verificationkey')  # noqa

    query_params = {}
    if cid:
        query_params['cid'] = cid
    if kid:
        query_params['kid'] = kid

    if not query_params:
        raise AuthenticationFailed(tr('Unable to check Aspen - no Key CID or Key ID provided.'))

    try:
        if results := aspen_api.list(params=query_params).get('results', []):
            if len(results) > 1:
                raise AuthenticationFailed(tr('Multiple verification keys returned'))
            return persist_aspen_key(results[0])

        raise AuthenticationFailed(tr('No Verification Key found on Aspen'))

    except pylogcabin.exceptions.ConsoleAPIAuthenticationError:
        msg = 'Invalid credentials; unable to retrieve Verification Key from Aspen.'
        logger.error(msg)
        raise AuthenticationFailed(tr(msg))
    except pylogcabin.exceptions.LCHTTPError as e:
        logger.info('An error occurred while retrieving credentials: %s', e)
        raise AuthenticationFailed(tr('An error occurred while retrieving credentials'))


def persist_aspen_key(key_data: dict) -> KeyData:  # type: ignore[type-arg]
    """
    Save key data from Aspen as new Encryption Key record.

    {
        "pk": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "url": "string",
        "created": "2023-12-18T21:27:00.623Z",
        "modified": "2023-12-18T21:27:00.623Z",
        "cid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "is_active": true,
        "user": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "name": "string",
        "key": "string",
        "kid": "string",
        "status": "active",
        "last_used_on": "2023-12-18T21:27:00.623Z",
        "revocation_date": "2023-12-18T21:27:00.623Z"
    }
    """
    from .api.serializers import EncryptionKeySerializer
    from .models import EncryptionKey

    # Update the existing record if a matching CanonicalID exists.
    ek = EncryptionKey.objects.scid(key_data['cid'])
    ks = EncryptionKeySerializer(data=key_data, instance=ek)

    if not ks.is_valid(raise_exception=True):
        logger.info(f'Unable to verify key data: {key_data}')
        raise AuthenticationFailed(tr('Unable to verify key data from Aspen'))

    key = ks.save()

    return KeyData(
        cid=key.cid,
        kid=key.kid,
        key=key.key,
        subject=key.subject,
        last_used_on=key.last_used_on,
    )


def local_umbrella_key_registered_with_aspen() -> bool:
    """
    Local Umbrella API Key must be saved to disk and
    registered with Aspen before signing API tokens.

    The file should only exist on disk after it's been
    registered with Aspen.
    """
    local_key_file = Path(f'{settings.CONF_DIR}/{LOCAL_VERIFICATION_KEY_FILE}')
    return local_key_file.exists() and local_key_file.stat().st_size > 0


@call_once
def load_local_umbrella_api_key_from_file():
    """
    Wrap the function to cache the lookup.
    """
    return load_local_umbrella_api_key()


def load_aspen_token_class(subject: Subject = Subject.DrawBridge, user_cid: str | None = None):
    """
    Generate a token for Aspen API communications,
    preferring to sign it with the Local Encryption Key.
    """
    key_data = load_local_umbrella_api_key_from_file()

    return V4PasetoToken(
        private_key=key_data.key,
        subject=subject,
        user_cid=user_cid or settings.UMBRELLA_API_USER_CID,
        verification_cid=key_data.cid,
    )
