"""
Modèle Pydantic pour les mutations génomiques
"""

from typing import Literal, Optional

from pydantic import BaseModel, field_validator, model_validator

from .db_connection import DbConnection


def chrom_to_int(chrom_str: str) -> Optional[int]:
    """Convertit chr1-chr22, chrX, chrY en 1-24"""
    chrom_str = str(chrom_str).upper().replace("chr", "")
    if chrom_str == "X":
        return 23
    elif chrom_str == "Y":
        return 24
    elif chrom_str in ["M", "MT"]:
        return None
    else:
        try:
            return int(chrom_str)
        except ValueError:
            return None


def int_to_chrom(chrom_int: int) -> str:
    """Convertit 1-24 en chr1-chr22, chrX, chrY"""
    if chrom_int == 23:
        return "chrX"
    elif chrom_int == 24:
        return "chrY"
    else:
        return f"chr{chrom_int}"


class Mutation(BaseModel):
    """
    Modèle pour une mutation génomique

    Attributs:
        id: ID de la mutation dans la DB (auto-généré)
        chrom: Chromosome (1-22, 23=X, 24=Y)
        pos: Position génomique
        ref: Allèle de référence
        alt: Allèle alternatif

    Validation:
        - Soit id est fourni
        - Soit (chrom, pos, ref, alt) sont tous fournis
        - Soit les deux

    Usage:
        # Avec ID seulement (lazy load des attributs)
        mutation = Mutation(id=123)
        mutation.fetch_attributes_from_db()

        # Avec coordonnées (lazy load de l'ID)
        mutation = Mutation(chrom=17, pos=7577548, ref="C", alt="T")
        mutation.fetch_id_from_db()

        # Avec tout
        mutation = Mutation(id=123, chrom=17, pos=7577548, ref="C", alt="T")
    """

    id: Optional[int] = None
    chrom: Optional[int] = None
    pos: Optional[int] = None
    ref: Optional[str] = None
    alt: Optional[str] = None

    @field_validator("chrom", mode="before")
    @classmethod
    def normalize_chrom(cls, v):
        return chrom_to_int(v)

    @model_validator(mode="after")
    def validate_mutation(self):
        """
        Valide qu'on a soit id, soit (chrom, pos, ref, alt), soit les deux
        """
        has_id = self.id is not None
        has_coords = all(
            [
                self.chrom is not None,
                self.pos is not None,
                self.ref is not None,
                self.alt is not None,
            ]
        )

        if not has_id and not has_coords:
            raise ValueError("Must provide either 'id' or all of (chrom, pos, ref, alt)")

        # Vérifier que si on a des coordonnées partielles, elles sont complètes
        coord_fields = [self.chrom, self.pos, self.ref, self.alt]
        partial_coords = any(f is not None for f in coord_fields)

        if partial_coords and not has_coords:
            raise ValueError("If providing coordinates, must provide all of (chrom, pos, ref, alt)")

        return self

    @property
    def state(self) -> Literal["full", "miss_id", "miss_attributes"]:
        """
        Retourne l'état de la mutation

        Returns:
            - "full": id et attributs présents
            - "miss_id": attributs présents, id manquant
            - "miss_attributes": id présent, attributs manquants
        """
        has_id = self.id is not None
        has_coords = all(
            [
                self.chrom is not None,
                self.pos is not None,
                self.ref is not None,
                self.alt is not None,
            ]
        )

        if has_id and has_coords:
            return "full"
        elif has_coords:
            return "miss_id"
        else:
            return "miss_attributes"

    def fetch_id_from_db(self) -> Optional[int]:
        """
        Récupère l'ID de la mutation depuis la DB via (chrom, pos, ref, alt)
        Met à jour self.id si trouvé

        Returns:
            ID de la mutation ou None si non trouvée

        Raises:
            ValueError: Si les coordonnées ne sont pas complètes
        """
        if self.state == "miss_attributes":
            raise ValueError("Cannot fetch ID: coordinates (chrom, pos, ref, alt) are required")

        conn = DbConnection.get_connection()
        cursor = conn.cursor()

        cursor.execute(
            """
            SELECT id FROM mutations
            WHERE chrom=? AND pos=? AND ref=? AND alt=?
        """,
            (self.chrom, self.pos, self.ref, self.alt),
        )

        result = cursor.fetchone()
        if result:
            self.id = result["id"]
            return self.id

        return None

    def fetch_attributes_from_db(self) -> bool:
        """
        Récupère les attributs de la mutation depuis la DB via l'ID
        Met à jour (chrom, pos, ref, alt) si trouvés

        Returns:
            True si trouvé, False sinon

        Raises:
            ValueError: Si l'ID n'est pas fourni
        """
        if self.id is None:
            raise ValueError("Cannot fetch attributes: id is required")

        conn = DbConnection.get_connection()
        cursor = conn.cursor()

        cursor.execute(
            """
            SELECT chrom, pos, ref, alt FROM mutations
            WHERE id=?
        """,
            (self.id,),
        )

        result = cursor.fetchone()
        if result:
            self.chrom = result["chrom"]
            self.pos = result["pos"]
            self.ref = result["ref"]
            self.alt = result["alt"]
            return True

        return False

    def ensure_in_db(self, annotate: bool = True) -> int:
        """
        S'assure que la mutation existe dans la DB (crée si nécessaire)
        Met à jour self.id

        Args:
            annotate: Si True, annote automatiquement avec le contexte génomique

        Returns:
            ID de la mutation

        Raises:
            ValueError: Si les coordonnées ne sont pas complètes
        """
        if self.state == "miss_attributes":
            raise ValueError("Cannot ensure in DB: coordinates (chrom, pos, ref, alt) are required")

        # Si on a déjà l'ID, on vérifie qu'il existe
        if self.id is not None:
            conn = DbConnection.get_connection()
            cursor = conn.cursor()
            cursor.execute("SELECT id FROM mutations WHERE id=?", (self.id,))
            if cursor.fetchone():
                return self.id

        # Sinon, chercher par coordonnées
        existing_id = self.fetch_id_from_db()
        if existing_id is not None:
            return existing_id

        # Si pas trouvé, créer
        conn = DbConnection.get_connection()
        cursor = conn.cursor()

        cursor.execute(
            """
            INSERT INTO mutations (chrom, pos, ref, alt)
            VALUES (?, ?, ?, ?)
        """,
            (self.chrom, self.pos, self.ref, self.alt),
        )

        self.id = cursor.lastrowid

        # Annoter si demandé
        if annotate:
            self._annotate_mutation()

        conn.commit()
        return self.id

    def _annotate_mutation(self) -> None:
        """
        Annote la mutation avec le contexte génomique
        (méthode interne, appelée par ensure_in_db)
        """
        if self.id is None or self.chrom is None or self.pos is None:
            return

        conn = DbConnection.get_connection()
        cursor = conn.cursor()

        # Supprimer les anciennes annotations
        cursor.execute("DELETE FROM mutation_annotations WHERE mutation_id = ?", (self.id,))

        # Trouver les features qui chevauchent
        cursor.execute(
            """
            SELECT gf.id
            FROM genomic_features gf
            JOIN genes g ON gf.gene_id = g.id
            WHERE g.chrom = ? AND gf.feature_start <= ? AND gf.feature_end >= ?
        """,
            (self.chrom, self.pos, self.pos),
        )

        feature_ids = [row["id"] for row in cursor.fetchall()]

        if feature_ids:
            for feature_id in feature_ids:
                cursor.execute(
                    """
                    INSERT INTO mutation_annotations (mutation_id, feature_id)
                    VALUES (?, ?)
                """,
                    (self.id, feature_id),
                )
        else:
            # Intergenic
            cursor.execute(
                """
                INSERT INTO mutation_annotations (mutation_id, feature_id)
                VALUES (?, NULL)
            """,
                (self.id,),
            )

    @classmethod
    def fetch_ids_from_db_batch(cls, mutations: list["Mutation"]) -> None:
        """
        Récupère les IDs pour un batch de mutations (modifie en place)

        Args:
            mutations: Liste de mutations (doivent avoir chrom, pos, ref, alt)

        Raises:
            ValueError: Si une mutation n'a pas de coordonnées complètes
        """
        conn = DbConnection.get_connection()
        cursor = conn.cursor()

        for mutation in mutations:
            if mutation.state == "miss_attributes":
                raise ValueError(f"Mutation {mutation} missing coordinates")

            cursor.execute(
                """
                SELECT id FROM mutations
                WHERE chrom=? AND pos=? AND ref=? AND alt=?
            """,
                (mutation.chrom, mutation.pos, mutation.ref, mutation.alt),
            )

            result = cursor.fetchone()
            if result:
                mutation.id = result["id"]

    @classmethod
    def fetch_attributes_from_db_batch(cls, mutations: list["Mutation"]) -> None:
        """
        Récupère les attributs pour un batch de mutations (modifie en place)

        Args:
            mutations: Liste de mutations (doivent avoir id)

        Raises:
            ValueError: Si une mutation n'a pas d'ID
        """
        conn = DbConnection.get_connection()
        cursor = conn.cursor()

        for mutation in mutations:
            if mutation.id is None:
                raise ValueError(f"Mutation {mutation} missing id")

            cursor.execute(
                """
                SELECT chrom, pos, ref, alt FROM mutations
                WHERE id=?
            """,
                (mutation.id,),
            )

            result = cursor.fetchone()
            if result:
                mutation.chrom = result["chrom"]
                mutation.pos = result["pos"]
                mutation.ref = result["ref"]
                mutation.alt = result["alt"]

    @classmethod
    def ensure_in_db_batch(cls, mutations: list["Mutation"], annotate: bool = True) -> None:
        """
        S'assure que toutes les mutations existent dans la DB (crée si nécessaire)
        Modifie les mutations en place pour ajouter les IDs

        Args:
            mutations: Liste de mutations (doivent avoir chrom, pos, ref, alt)
            annotate: Si True, annote automatiquement avec le contexte génomique

        Raises:
            ValueError: Si une mutation n'a pas de coordonnées complètes
        """
        # D'abord, essayer de récupérer les IDs existants
        cls.fetch_ids_from_db_batch(mutations)

        # Créer les mutations qui n'existent pas
        conn = DbConnection.get_connection()
        cursor = conn.cursor()

        for mutation in mutations:
            if mutation.id is None:
                # Créer la mutation
                cursor.execute(
                    """
                    INSERT INTO mutations (chrom, pos, ref, alt)
                    VALUES (?, ?, ?, ?)
                """,
                    (mutation.chrom, mutation.pos, mutation.ref, mutation.alt),
                )

                mutation.id = cursor.lastrowid

                # Annoter si demandé
                if annotate:
                    mutation._annotate_mutation()

        conn.commit()

    def __repr__(self) -> str:
        if self.state == "full":
            chrom_str = int_to_chrom(self.chrom)
            return f"Mutation(id={self.id}, {chrom_str}:{self.pos} {self.ref}>{self.alt})"
        elif self.state == "miss_id":
            chrom_str = int_to_chrom(self.chrom)
            return f"Mutation({chrom_str}:{self.pos} {self.ref}>{self.alt}, id=?)"
        else:
            return f"Mutation(id={self.id}, coords=?)"
