from datetime import datetime, timedelta, timezone
import json
from lclazy import LazyLoader

try:
    from lchttp.json import json_dumps
except ImportError:
    from json import dumps as json_dumps
import logging
import secrets
from typing import TYPE_CHECKING
from .typehints import CID_STR, PasetoFooter, PasetoPayload, Subject

if TYPE_CHECKING:
    from aspen_crypto.keys import FacadePrivateKey, FacadePublicKey
    import pyseto
else:
    pyseto = LazyLoader('pyseto', globals(), 'pyseto')

logger = logging.getLogger(__name__)
TIME_LEEWAY = 20


# ---------------------------------------------------------------------------
class PasetoToken:
    """
    Represents a Paseto Token that's either been constructed
    by our code or has been verified to be valid.
    """

    auth_method = 'Paseto'
    version: int = 4
    subject: Subject = Subject.Any
    user_cid: CID_STR = ''

    def __init__(
        self,
        private_key: 'FacadePrivateKey',
        user_cid: CID_STR = '',
        email: str = '',
        subject: Subject | None = None,
        subject_cid: CID_STR = '',
        verification_cid: CID_STR = '',
        expires: int = 90,
    ):
        self.private_key: 'FacadePrivateKey' = private_key
        self.public_key: 'FacadePublicKey' = private_key.public_key
        self.user_cid = user_cid or self.user_cid
        self.email: str = email
        self.subject: Subject = subject or self.subject

        # The CID of the record for which this API call is being made.
        # Only used in limited situations, such as constraining Odoo querysets.
        self.subject_cid: CID_STR = subject_cid

        # TODO - remove this and rely only on the kid
        self.verification_cid: CID_STR = verification_cid
        self.expires = expires

    def __repr__(self) -> str:
        name = self.__class__.__name__
        return (
            f"{name}('<private_key>', {self.auth_method!r}, "
            f"{self.user_cid!r}, {self.email!r}, {self.expires!r})"
        )

    def __str__(self) -> str:
        return f"{self.auth_method} <token>"

    def auth_header(self) -> str:
        """
        Create an HTTP Authorization header.
        """
        return f"{self.auth_method} {self.sign().decode()}"

    def payload(self) -> PasetoPayload:
        """
        Return data for Paseto token payload.
        """
        now = datetime.now(tz=timezone.utc)
        payload = PasetoPayload(
            nonce=secrets.token_urlsafe(nbytes=8),
            exp=(now + timedelta(seconds=self.expires)).isoformat(timespec="seconds"),
            nbf=(now - timedelta(seconds=TIME_LEEWAY)).isoformat(timespec="seconds"),
        )

        if self.user_cid:
            payload['user_cid'] = self.user_cid
        elif self.email:
            payload['email'] = self.email

        if self.subject:
            payload['sub'] = self.subject
        if self.subject_cid:
            payload['sub_cid'] = self.subject_cid

        return payload

    def footer(self) -> PasetoFooter:
        """
        Insert keys into the footer to enable server-side
        verification, such as lookup of Public Keys to verify
        token signing.
        """
        if self.verification_cid:
            return PasetoFooter(cid=self.verification_cid)

        return PasetoFooter(kid=self.private_key.public_key.to_paserk_id(self.version))

    def sign(self) -> bytes:
        """
        Create and return signed authentication Paseto token
        """
        paseto = pyseto.Paseto.new(
            exp=self.expires,
            include_iat=True,
            leeway=TIME_LEEWAY,
        )

        return paseto.encode(
            key=self.private_key.as_paseto(self.version),
            payload=json_dumps(self.payload()),
            footer=json_dumps(self.footer()),
            serializer=json,
        )


# ---------------------------------------------------------------------------
class V1PasetoToken(PasetoToken):
    version = 1


class V2PasetoToken(PasetoToken):
    version = 2


class V3PasetoToken(PasetoToken):
    version = 3


class PasetoRegistrationToken(PasetoToken):
    auth_method = 'Pinned_Paseto'


V4PasetoToken = PasetoToken
V4PasetoRegistrationToken = PasetoRegistrationToken


__all__ = (
    'PasetoToken',
    'PasetoRegistrationToken',
    'V1PasetoToken',
    'V2PasetoToken',
    'V3PasetoToken',
    'V4PasetoToken',
    'V4PasetoRegistrationToken',
)
