import re
from datetime import date
from typing import TYPE_CHECKING, Annotated, Any

import ftfy
from pycpfcnpj import cpfcnpj
from pydantic import BaseModel, Field, GetCoreSchemaHandler, GetJsonSchemaHandler
from pydantic.json_schema import JsonSchemaValue
from pydantic_core import CoreSchema, core_schema
from pydantic_extra_types.payment import PaymentCardNumber

if TYPE_CHECKING:
    NameStr = Annotated[str, ...]
else:

    class NameStr(str):
        """
        >>> class User(BaseModel):
        ...     name: NameStr


        >>> User(name='S&#x00E9;rgio R Siqueira ').name
        'Sérgio R Siqueira'

        >>> User(name=' Sérgio R  Siqueira ').name
        'Sérgio R Siqueira'


        >>> User(name='Siqueira')  # doctest: +ELLIPSIS
        Traceback (most recent call last):
        ...
        pydantic_core._pydantic_core.ValidationError: 1 validation error for User
        ...
        """

        @classmethod
        def __get_pydantic_core_schema__(
            cls, _source: type[Any], _handler: GetCoreSchemaHandler
        ) -> CoreSchema:
            return core_schema.no_info_after_validator_function(
                cls._validate, core_schema.str_schema()
            )

        @classmethod
        def _validate(cls, __input_value: str) -> str:
            name = ftfy.fix_text(__input_value.strip())
            name = ' '.join(name.split())

            if ' ' not in name:
                raise ValueError('Invalid name.')

            return name

        @classmethod
        def __get_pydantic_json_schema__(
            cls, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
        ) -> JsonSchemaValue:
            field_schema = handler(core_schema)
            field_schema.update(type='string', format='name')
            return field_schema


class PaymentCardValidation:
    """
    >>> class CreditCard(BaseModel):
    ...     exp: PaymentCardValidation


    >>> CreditCard(exp='20/23')
    Traceback (most recent call last):
    ...
    pydantic_core._pydantic_core.ValidationError: 1 validation error for CreditCard
    ...

    >>> CreditCard(exp='12/23')
    CreditCard(exp=datetime.date(2023, 12, 1))

    >>> CreditCard(exp='12/2024')
    CreditCard(exp=datetime.date(2024, 12, 1))

    >>> CreditCard(exp='2024-12-02')
    CreditCard(exp=datetime.date(2024, 12, 2))
    """

    @classmethod
    def __get_pydantic_core_schema__(
        cls, _source: type[Any], _handler: GetCoreSchemaHandler
    ) -> CoreSchema:
        return core_schema.no_info_after_validator_function(
            cls._validate, core_schema.str_schema()
        )

    @classmethod
    def _validate(cls, __input_value: str) -> date:
        if '/' in __input_value:
            month, year = __input_value.split('/')
            return date(int(f'20{year[-2:]}'), int(month), 1)

        try:
            return date.fromisoformat(__input_value)
        except Exception:
            raise ValueError('Invalid card expiration date.')


class CreditCard(BaseModel):
    name: NameStr
    number: PaymentCardNumber
    cvv: str = Field(..., min_length=3)
    exp: PaymentCardValidation

    @property
    def brand(self) -> str:
        return self.number.brand

    @property
    def last4(self) -> str:
        return self.number.last4

    @property
    def first_name(self) -> str:
        first_name, _ = self.name.split(' ', 1)
        return first_name

    @property
    def last_name(self) -> str:
        _, last_name = self.name.split(' ', 1)
        return last_name


class CpfCnpj:
    @classmethod
    def __get_pydantic_core_schema__(
        cls, _source: type[Any], _handler: GetCoreSchemaHandler
    ) -> CoreSchema:
        return core_schema.no_info_after_validator_function(
            cls._validate, core_schema.str_schema()
        )

    @classmethod
    def _validate(cls, __input_value: str) -> str | None:
        if __input_value == '':
            return None

        cpfcnpj_ = re.sub(r'\D', '', __input_value)

        if not cpfcnpj.validate(cpfcnpj_):
            raise ValueError(f'Invalid {cls.__name__}.')

        return cpfcnpj_

    @classmethod
    def __get_pydantic_json_schema__(
        cls, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
    ) -> JsonSchemaValue:
        field_schema = handler(core_schema)
        field_schema.update(
            type='string',
            format=cls.__name__.lower().removesuffix('str'),
        )
        return field_schema


if TYPE_CHECKING:
    CpfStr = Annotated[str, ...]
    CnpjStr = Annotated[str, ...]
else:

    class CpfStr(CpfCnpj):
        pass

    class CnpjStr(CpfCnpj):
        pass


if __name__ == '__main__':
    import doctest

    doctest.testmod(optionflags=doctest.ELLIPSIS)
