import json
import logging
import os
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, Optional, Union

import validators
from pydantic import BaseModel, Field, field_validator, model_validator
from pydantic_settings import SettingsConfigDict
from starlette.datastructures import FormData

from chanjo2.constants import (
    AMBIGUOUS_SAMPLES_INPUT,
    DEFAULT_COMPLETENESS_LEVELS,
    GENE_LISTS_NOT_SUPPORTED_MSG,
    WRONG_COVERAGE_FILE_MSG,
)

LOG = logging.getLogger("uvicorn.access")


def default_report_coverage_levels() -> List[int]:
    """Sets the coverage thresholds to be used for report metrics whenever a request doesn't contain 'completeness_thresholds' values."""
    if os.getenv("REPORT_COVERAGE_LEVELS"):
        return json.loads(os.getenv("REPORT_COVERAGE_LEVELS"))
    return DEFAULT_COMPLETENESS_LEVELS


class Builds(str, Enum):
    build_37 = "GRCh37"
    build_38 = "GRCh38"

    @staticmethod
    def get_enum_values() -> List[str]:
        """Returns the values of the available genome builds."""
        return [member.value for member in Builds]


class IntervalType(str, Enum):
    GENES = "genes"
    TRANSCRIPTS = "transcripts"
    EXONS = "exons"
    CUSTOM = "custom_intervals"


class TranscriptTag(str, Enum):
    REFSEQ_MRNA = "refseq_mrna"
    REFSEQ_NCRNA = "refseq_ncrna"
    REFSEQ_MANE_SELECT = "refseq_mane_select"
    REFSEQ_MANE_PLUS_CLINICAL = "refseq_mane_plus_clinical"


class Sex(str, Enum):
    FEMALE = "female"
    MALE = "male"
    UNKNOWN = "unknown"


class CaseBase(BaseModel):
    display_name: Optional[str] = None
    name: str


class CaseCreate(CaseBase):
    pass


class SampleBase(BaseModel):
    coverage_file_path: str
    display_name: str
    track_name: str
    name: str

    @field_validator("coverage_file_path", mode="before")
    def validate_coverage_path(cls, value: str) -> Any:
        if not Path(value).is_file() and not validators.url(value):
            raise ValueError(WRONG_COVERAGE_FILE_MSG)

        return value


class SampleCreate(SampleBase):
    case_name: str


class Sample(SampleBase):
    created_at: datetime
    id: int
    model_config = SettingsConfigDict(from_attributes=True)


class Case(CaseBase):
    id: int
    samples: List = []
    model_config = SettingsConfigDict(from_attributes=True)


class IntervalBase(BaseModel):
    chromosome: str
    start: int
    stop: int


class Interval(IntervalBase):
    id: int


class GeneBase(IntervalBase):
    build: Builds
    ensembl_id: str
    hgnc_id: Optional[int]
    hgnc_symbol: Optional[str]


class GeneQuery(BaseModel):
    build: Builds
    ensembl_ids: Optional[List[str]] = None
    hgnc_ids: Optional[List[int]] = None
    hgnc_symbols: Optional[List[str]] = None
    limit: Optional[int] = 100


class GeneIntervalQuery(GeneQuery):
    ensembl_gene_ids: Optional[List[str]] = None


class Gene(IntervalBase):
    id: int


class TranscriptBase(IntervalBase):
    build: Builds
    ensembl_id: str
    ensembl_gene_id: str
    refseq_mrna: Optional[str] = None
    refseq_mrna_pred: Optional[str] = None
    refseq_ncrna: Optional[str] = None
    refseq_mane_select: Optional[str] = None
    refseq_mane_plus_clinical: Optional[str] = None


class Transcript(TranscriptBase):
    id: int


class ExonBase(IntervalBase):
    build: Builds
    ensembl_id: str
    ensembl_gene_id: str
    ensembl_transcript_id: str


class Exon(IntervalBase):
    id: int


class IntervalCoverage(BaseModel):
    mean_coverage: float
    completeness: Optional[Dict] = Field(default_factory=dict)
    interval_id: Optional[str] = None
    interval_type: Optional[IntervalType] = IntervalType.CUSTOM


class GeneCoverage(IntervalCoverage):
    inner_intervals: List[IntervalCoverage] = []  # Transcripts or exons
    hgnc_id: Optional[int] = None
    hgnc_symbol: Optional[str] = None
    ensembl_gene_id: Optional[str] = None


class FileCoverageBaseQuery(BaseModel):
    coverage_file_path: str


class FileCoverageQuery(FileCoverageBaseQuery):
    chromosome: str
    start: Optional[int] = None
    end: Optional[int] = None
    completeness_thresholds: Optional[List[int]] = []


class FileCoverageIntervalsFileQuery(FileCoverageBaseQuery):
    intervals_bed_path: str
    completeness_thresholds: Optional[List[int]] = []


class SampleGeneIntervalQuery(BaseModel):
    build: Builds
    completeness_thresholds: Optional[List[int]] = []
    ensembl_gene_ids: Optional[List[str]] = None
    hgnc_gene_ids: Optional[List[int]] = None
    hgnc_gene_symbols: Optional[List[str]] = None
    samples: Optional[List[str]] = None
    case: Optional[str] = None

    @model_validator(mode="before")
    def check_genes_lists(cls, values: dict):
        nr_provided_gene_lists: int = 0
        for gene_list in [
            values.get("ensembl_gene_ids"),
            values.get("hgnc_gene_ids"),
            values.get("hgnc_gene_symbols"),
        ]:
            if gene_list:
                nr_provided_gene_lists += 1
        if nr_provided_gene_lists != 1:
            raise ValueError(GENE_LISTS_NOT_SUPPORTED_MSG)
        return values

    @model_validator(mode="before")
    def check_sample_input(cls, values: dict):
        case = values.get("case")
        samples = values.get("samples")
        if case and samples:
            raise ValueError(AMBIGUOUS_SAMPLES_INPUT)

        return values


## Coverage and  overview report - related models ###


class ReportQuerySample(BaseModel):
    name: str
    coverage_file_path: Optional[str] = None
    case_name: Optional[str] = None
    analysis_date: Optional[datetime] = datetime.now()


class ReportQuery(BaseModel):
    build: Builds
    completeness_thresholds: Optional[List[int]] = default_report_coverage_levels()
    ensembl_gene_ids: Optional[List[str]] = None
    hgnc_gene_ids: Optional[List[int]] = None
    hgnc_gene_symbols: Optional[List[str]] = None
    interval_type: IntervalType
    default_level: int = 10
    panel_name: Optional[str] = "Custom panel"
    case_display_name: Optional[str] = None
    samples: List[ReportQuerySample]

    @staticmethod
    def comma_sep_values_to_list(
        comma_sep_values: Optional[str], items_format: Union[str, int]
    ) -> Optional[List[Union[str, int]]]:
        """Helper function that formats list of strings or integers passed by a form as comma separated values."""
        if comma_sep_values is None:
            return
        if items_format == str:
            return [item.strip() for item in comma_sep_values.split(",")]
        else:
            return [int(item.strip()) for item in comma_sep_values.split(",")]

    @staticmethod
    def set_query_genes(form_data: FormData):
        """Helper function that collects form data from report page requests and sets the right gene IDs/values in the query."""
        query_genes = {
            "ensembl_gene_ids": [],
            "hgnc_gene_ids": [],
            "hgnc_gene_symbols": [],
        }

        for gene_ids_key in query_genes.keys():
            if bool(form_data.get(gene_ids_key)) is False:
                continue
            id_values: List[Union[str, int]] = [
                item.strip() for item in form_data.get(gene_ids_key).split(",")
            ]
            for value in id_values:
                if value.isdigit():
                    query_genes["hgnc_gene_ids"].append(value)
                elif value.startswith("ENSG"):
                    query_genes["ensembl_gene_ids"].append(value)
                else:
                    query_genes["hgnc_gene_symbols"].append(value)
        return query_genes

    @classmethod
    def as_form(cls, form_data: FormData) -> "ReportQuery":
        query_genes: dict = cls.set_query_genes(form_data)
        report_query: dict = {
            "build": form_data.get("build"),
            "completeness_thresholds": cls.comma_sep_values_to_list(
                comma_sep_values=form_data.get("completeness_thresholds"),
                items_format=int,
            )
            or default_report_coverage_levels(),
            "ensembl_gene_ids": query_genes["ensembl_gene_ids"],
            "hgnc_gene_ids": query_genes["hgnc_gene_ids"],
            "hgnc_gene_symbols": query_genes["hgnc_gene_symbols"],
            "interval_type": form_data.get("interval_type"),
            "default_level": form_data.get("default_level"),
            "panel_name": form_data.get("panel_name"),
            "case_display_name": form_data.get("case_display_name"),
            "samples": form_data.get("samples"),
        }
        return cls(**report_query)

    @field_validator("samples", mode="before")
    def samples_validator(cls, sample_list):
        if isinstance(sample_list, str):
            return json.loads(sample_list.replace("'", '"'))
        return sample_list

    @model_validator(mode="before")
    def check_genes_lists(cls, values: dict):
        nr_provided_gene_lists: int = 0
        for gene_list in [
            values.get("ensembl_gene_ids"),
            values.get("hgnc_gene_ids"),
            values.get("hgnc_gene_symbols"),
        ]:
            if gene_list:
                nr_provided_gene_lists += 1
        if nr_provided_gene_lists != 1:
            raise ValueError(GENE_LISTS_NOT_SUPPORTED_MSG)
        return values


class GeneReportForm(BaseModel):
    build: Builds
    completeness_thresholds: Optional[List[int]] = default_report_coverage_levels()
    hgnc_gene_id: int
    default_level: int = 10
    samples: List[ReportQuerySample]
    interval_type: IntervalType

    @field_validator("samples", mode="before")
    def samples_validator(cls, sample_list):
        if isinstance(sample_list, str):
            return json.loads(sample_list.replace("'", '"'))
        return sample_list

    @field_validator("completeness_thresholds", mode="before")
    def coverage_thresholds_validator(cls, completeness_thresholds: str):
        thresholds: List[str] = completeness_thresholds.split(",")
        return [int(threshold.strip()) for threshold in thresholds]

    @field_validator("interval_type", mode="before")
    def interval_type_validator(cls, interval_type: IntervalType) -> IntervalType:
        """Make sure that gene stats include transcript data even if it's a WGS analysis."""
        if interval_type == IntervalType.GENES:
            return IntervalType.TRANSCRIPTS
        return interval_type


class SampleSexRow(BaseModel):
    sample: str
    case: Optional[str]
    analysis_date: datetime
    predicted_sex: str
    x_coverage: float
    y_coverage: float
