from __future__ import annotations

from datetime import datetime
from keyword import iskeyword
from typing import Any, ClassVar, Sequence, Type, TypeVar, Union

import annotated_types
from dateutil.parser import isoparse
from pydantic import PlainSerializer, RootModel, StringConstraints
from typing_extensions import Annotated, Literal

from .constants import DOI_REGEX, SI_UNIT_REGEX
from .io import FileSource as FileSource
from .io import ImportantFileSource as ImportantFileSource
from .io import PermissiveFileSource as PermissiveFileSource
from .io import RelativeFilePath as RelativeFilePath
from .io_basics import AbsoluteDirectory as AbsoluteDirectory
from .io_basics import AbsoluteFilePath as AbsoluteFilePath
from .io_basics import FileName as FileName
from .io_basics import Sha256 as Sha256
from .license_id import DeprecatedLicenseId as DeprecatedLicenseId
from .license_id import LicenseId as LicenseId
from .url import HttpUrl as HttpUrl
from .validated_string import ValidatedString
from .validator_annotations import AfterValidator, BeforeValidator
from .version_type import Version as Version

S = TypeVar("S", bound=Sequence[Any])
NotEmpty = Annotated[S, annotated_types.MinLen(1)]


def _validate_identifier(s: str) -> str:
    if not s.isidentifier():
        raise ValueError(
            f"'{s}' is not a valid (Python) identifier, see"
            + " https://docs.python.org/3/reference/lexical_analysis.html#identifiers"
            + " for details."
        )

    return s


def _validate_is_not_keyword(s: str) -> str:
    if iskeyword(s):
        raise ValueError(f"'{s}' is a Python keyword and not allowed here.")

    return s


def _validate_datetime(dt: Union[datetime, str, Any]) -> datetime:
    if isinstance(dt, datetime):
        return dt
    elif isinstance(dt, str):
        return isoparse(dt)

    raise ValueError(f"'{dt}' not a string or datetime.")


def _validate_orcid_id(orcid_id: str):
    if len(orcid_id) == 19 and all(orcid_id[idx] == "-" for idx in [4, 9, 14]):
        check = 0
        for n in orcid_id[:4] + orcid_id[5:9] + orcid_id[10:14] + orcid_id[15:]:
            # adapted from stdnum.iso7064.mod_11_2.checksum()
            check = (2 * check + int(10 if n == "X" else n)) % 11
        if check == 1:
            return orcid_id  # valid

    raise ValueError(
        f"'{orcid_id} is not a valid ORCID iD in hyphenated groups of 4 digits."
    )


# TODO follow up on https://github.com/pydantic/pydantic/issues/8964
# to remove _serialize_datetime
def _serialize_datetime_json(dt: datetime) -> str:
    return dt.isoformat()


class Datetime(
    RootModel[
        Annotated[
            datetime,
            BeforeValidator(_validate_datetime),
            PlainSerializer(_serialize_datetime_json, when_used="json-unless-none"),
        ]
    ]
):
    """Timestamp in [ISO 8601](#https://en.wikipedia.org/wiki/ISO_8601) format
    with a few restrictions listed [here](https://docs.python.org/3/library/datetime.html#datetime.datetime.fromisoformat).
    """


class Doi(ValidatedString):
    root_model: ClassVar[Type[RootModel[Any]]] = RootModel[
        Annotated[str, StringConstraints(pattern=DOI_REGEX)]
    ]


FormatVersionPlaceholder = Literal["latest", "discover"]
IdentifierAnno = Annotated[
    NotEmpty[str],
    AfterValidator(_validate_identifier),
    AfterValidator(_validate_is_not_keyword),
]


class Identifier(ValidatedString):
    root_model: ClassVar[Type[RootModel[Any]]] = RootModel[IdentifierAnno]


LowerCaseIdentifierAnno = Annotated[IdentifierAnno, annotated_types.LowerCase]


class LowerCaseIdentifier(ValidatedString):
    root_model: ClassVar[Type[RootModel[Any]]] = RootModel[LowerCaseIdentifierAnno]


class OrcidId(ValidatedString):
    """an ORCID identifier"""

    root_model: ClassVar[Type[RootModel[Any]]] = RootModel[
        Annotated[str, AfterValidator(_validate_orcid_id)]
    ]


class SiUnit(ValidatedString):
    """Si unit"""

    root_model: ClassVar[Type[RootModel[Any]]] = RootModel[
        Annotated[
            str,
            StringConstraints(min_length=1, pattern=SI_UNIT_REGEX),
            BeforeValidator(
                lambda s: (
                    s.replace("×", "·").replace("*", "·").replace(" ", "·")
                    if isinstance(s, str)
                    else s
                )
            ),
        ]
    ]
