import re
from enum import Enum
from typing import Annotated

from pydantic import BaseModel, PositiveInt
from pydantic.functional_validators import AfterValidator

from primal_page.bedfiles import BedfileVersion

INFO_SCHEMA = "v2.0.0"

SCHEMENAME_PATTERN = r"^[a-z0-9][a-z0-9-]*[a-z0-9]$"
VERSION_PATTERN = r"^v\d+\.\d+\.\d+$"
DOI_PATTERN = r"^(https:\/\/|dx.)*(doi.org\/|doi:)10.\d+\/.*$"


class PrimerClass(Enum):
    PRIMERSCHEMES = "primerschemes"


class SchemeStatus(Enum):
    WITHDRAWN = "withdrawn"
    DEPRECATED = "deprecated"
    AUTOGENERATED = "autogenerated"
    DRAFT = "draft"
    TESTING = "tested"
    VALIDATED = "validated"


def validate_schemeversion(version: str) -> str:
    if not re.match(VERSION_PATTERN, version):
        raise ValueError(
            f"Invalid version: {version}. Must match be in form of v(int).(int).(int)"
        )
    return version


def validate_schemename(schemename: str) -> str:
    if not re.match(SCHEMENAME_PATTERN, schemename):
        raise ValueError(
            f"Invalid schemename: {schemename}. Must only contain a-z, 0-9, and -. Cannot start or end with -"
        )
    return schemename


def not_empty(x: list | set | str) -> list | set | str:
    if len(x) == 0:
        raise ValueError("Cannot be empty")
    return x


IUPACAmbiguousDNA = {
    "A",
    "G",
    "K",
    "Y",
    "B",
    "S",
    "N",
    "H",
    "C",
    "W",
    "D",
    "R",
    "M",
    "T",
    "V",
}


class Collection(Enum):
    # Authors
    ARTIC = "ARTIC"
    MODJADJI = "MODJADJI"
    QUICK_LAB = "QUICK-LAB"
    COMMUNITY = "COMMUNITY"
    # Uses
    WASTE_WATER = "WASTE-WATER"
    CLINAL_ISOLATES = "CLINAL-ISOLATES"
    # Coverage types
    WHOLE_GENOME = "WHOLE-GENOME"
    PANEL = "PANEL"
    MULTI_TARGET = "MULTI-TARGET"


class Links(BaseModel):
    protocals: list[str] = []
    validation: list[str] = []
    homepage: list[str] = []
    vendors: list[str] = []
    misc: list[str] = []

    def getattr(self, link_type: str):
        """
        Get the list of links
        Raises:
        -   AttributeError if the link_type is not in the model_fields
        """
        return getattr(self, link_type)

    def append_link(
        self,
        link_type: str,
        link: str,
    ):
        """
        Add a link to the correct list
        Raises:
        -   AttributeError if the link_type is not in the model_fields
        """
        # Append the link to the correct list
        getattr(self, link_type).append(link)

    def remove_link(
        self,
        link_type: str,
        link: str,
    ):
        """
        Remove a the first occourance of the link from the correct list
        Raises:
        -   ValueError if the link is not in the list
        -   AttributeError if the link_type is not in the model_fields
        """
        getattr(self, link_type).remove(link)


class Info(BaseModel):
    ampliconsize: PositiveInt
    schemeversion: Annotated[str, AfterValidator(validate_schemeversion)]
    schemename: Annotated[str, AfterValidator(validate_schemename)]
    primer_bed_md5: str
    reference_fasta_md5: str
    status: SchemeStatus
    citations: set[str]
    authors: Annotated[list[str], AfterValidator(not_empty)]
    algorithmversion: str
    species: Annotated[set[int | str], AfterValidator(not_empty)]
    license: str = "CC BY-SA 4.0"
    primerclass: PrimerClass = PrimerClass.PRIMERSCHEMES
    infoschema: str = INFO_SCHEMA
    articbedversion: BedfileVersion
    collections: set[Collection] = set()
    links: Links = Links()
    # Add the optional fields
    description: str | None = None
    derivedfrom: str | None = None
    contactinfo: str | None = None

    def add_collection(self, collection: Collection):
        """Add a collection to the collections set"""
        self.collections.add(collection)

    def remove_collection(self, collection: Collection):
        """
        Remove a collection from the collections set.
        Raises: KeyError if the collection is not in the set
        """
        self.collections.remove(collection)

    def add_citation(self, citation: str):
        """Add a citation to the citations set"""
        self.citations.add(citation)

    def remove_citation(self, citation: str):
        """
        Remove a citation from the citations set.
        Raises: KeyError if the citation is not in the set
        """
        self.citations.remove(citation)

    def add_author(self, author: str, author_index: int | None):
        """Add an author to the authors list"""
        if author not in self.authors:
            if author_index is None:
                self.authors.append(author)
            else:
                # Insert is safe, will append if index is out of range
                self.authors.insert(author_index, author)

    def remove_author(self, author: str):
        """
        Remove an author from the authors list.
        Raises: ValueError if the author is not in the list
        """
        self.authors.remove(author)

    def reorder_authors(self, new_order: list[int]):
        """
        Reorder the authors list
        Raises:
            ValueError if duplicate indexes are found
            IndexError if the index is out of range
        """
        if len(new_order) != len(set(new_order)):
            raise ValueError("Duplicate indexes found")
            # Append authors in the new order

        new_authors = []
        for new_index in new_order:
            if new_index >= len(self.authors) or new_index < 0:
                raise IndexError(f"{new_index} is out of range")
            new_authors.append(self.authors[new_index])

        # Append any authors not in the new order
        for index, author in enumerate(self.authors):
            if index not in new_order:
                new_authors.append(author)

        self.authors = new_authors

    def change_primerclass(self, new_class: PrimerClass):
        """Change the primerclass"""
        self.primerclass = new_class

    def change_status(self, new_status: SchemeStatus):
        """Change the status"""
        self.status = new_status

    def change_license(self, new_license: str):
        """Change the license"""
        self.license = new_license

    def change_derivedfrom(self, new_derivedfrom: str):
        """Change the derivedfrom"""
        self.derivedfrom = new_derivedfrom

    def change_description(self, new_description: str):
        """Change the description"""
        self.description = new_description

    def change_contactinfo(self, new_contactinfo: str | None):
        """Change the contactinfo"""
        self.contactinfo = new_contactinfo


if __name__ == "__main__":
    info = Info(
        ampliconsize=400,
        schemeversion="v0.0.0",
        schemename="test",
        primer_bed_md5="hello",
        reference_fasta_md5="world",
        status=SchemeStatus.DRAFT,
        citations=set(),
        authors=["artic"],
        algorithmversion="test",
        species=set(["sars-cov-2"]),
        articbedversion=BedfileVersion.V3,
        collections=set(),
        links=Links(),
    )

    info_json = info.model_dump_json()
    print(info_json)

    info2 = Info.model_validate_json(info_json)
