"""
Modèles de données du format ISOF v1.0.

Ces dataclasses sont en lecture seule (frozen=True) parce qu'un document
chargé depuis un fichier signé ne devrait jamais être modifié en mémoire —
toute modification rendrait la vérification de signature caduque.
Pour créer des documents, voir isof.writer.
****************************************************************************
ISOF v1.0 format data models.

These dataclasses are read-only (frozen=True) because a document loaded from 
a signed file should never be modified in memory.
Any modification would invalidate the signature verification.

To create documents, see isof.writer.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional


# ---------------------------------------------------------------------------
# Données isotopiques | Isotope Data
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class IsotopeRecord:
    """
    Un rapport isotopique mesuré pour un élément donné.

    Un même échantillon peut avoir plusieurs IsotopeRecord (un par système
    isotopique mesuré, ex. Sr, Pb, Nd séparément).
    *************************************************************************
    An isotope ratio measured for a given element.

    A single sample can have multiple IsotopeRecords (one per measured 
    isotopic system, e.g., Sr, Pb, Nd separately).
    
    """

    element: Optional[str]
    system: Optional[str]          # ex. "87Sr/86Sr", "206Pb/204Pb"
    ratio: Optional[float]
    ratio_2se: Optional[float]     # incertitude 2 sigma externe
    delta_notation: Optional[str]  # ex. "δ¹³C", "δ¹⁸O"
    delta_value: Optional[float]
    delta_2sd: Optional[float]
    standard: Optional[str]        # isotopic standard used as reference
    n_cycles: Optional[int]
    session_date: Optional[str]
    instrument: Optional[str]
    notes: Optional[str]


# ---------------------------------------------------------------------------
# Échantillon | Sample
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class Sample:
    """
    Echantillon avec ses mesures isotopiques.

    Le champ `classification` suit la convention IsoFind :
    'source' = matériau de référence ou origine connue (ex. une mine),
    'fille' = produit ou matériau à tracer (ex. un métal/une pollution)
    Ces valeurs sont libres dans le format, elles ne sont pas vérifiées ici
    pour rester compatible avec des fichiers produits par d'autres outils.
    **************************************************************************
    Sample with its isotopic measurements.

    The `classification` field follows the IsoFind convention:
    'source' = reference material or known origin (e.g., a mine),
    'daughter' = product or material to be traced (e.g., a metal/pollution).
    These values ​​are free-form and are not checked here
    to ensure compatibility with files generated by other tools.
    """

    id: str
    name: Optional[str]
    classification: Optional[str]
    material_type: Optional[str]
    sector: Optional[str]
    project: Optional[str]
    latitude: Optional[float]
    longitude: Optional[float]
    altitude_m: Optional[float]
    collection_date: Optional[str]
    collector: Optional[str]
    description: Optional[str]
    workflow_stage: Optional[str]
    isotope_data: tuple[IsotopeRecord, ...] = field(default_factory=tuple)

    def has_coordinates(self) -> bool:
        return self.latitude is not None and self.longitude is not None

    def elements(self) -> list[str]:
        """
        Éléments mesurés dans cet échantillon, sans doublons, triés.
        Elements measured in this sample, without duplicates, sorted.
        """
        seen = []
        for iso in self.isotope_data:
            if iso.element and iso.element not in seen:
                seen.append(iso.element)
        return sorted(seen)


# ---------------------------------------------------------------------------
# Méthodes de préparation | Preparation method
# ---------------------------------------------------------------------------
    """
    Le format ISOF intègre directement les données de préparation et 
    d'analyse et les lie aux échantillons pour une traçabilité totale 
    de la chaîne d'analyse.
    *************************************************************************
    The ISOF format directly integrates preparation and analysis data and 
    links them to samples for total traceability of the analysis chain.
    """


@dataclass(frozen=True)
class MethodStep:
    num: int
    title: str
    detail: Optional[str]
    final: bool


@dataclass(frozen=True)
class MethodEquipment:
    column: Optional[str]
    resin: Optional[str]
    resin_volume_ml: Optional[float]
    reagents: Optional[str]
    temperature: Optional[str]


@dataclass(frozen=True)
class MethodReference:
    doi: Optional[str]
    citation: Optional[str]


@dataclass(frozen=True)
class Method:
    """
    Protocole de préparation ou d'analyse (digestion, chromatographie, etc.).

    Les méthodes sont identifiées par une clé string dans le document ISOF,
    pas par leur nom, plusieurs méthodes peuvent avoir le même nom mais
    des paramètres différents.
    *************************************************************************
    Preparation or analysis protocol (digestion, chromatography, etc.).

    Methods are identified by a string key in the ISOF document,
    not by their name; several methods may have the same name but
    different parameters.
    
    """

    key: str                       # identifiant interne du fichier ISOF | Internal identification of the ISOF file
    name: str
    type: Optional[str]            # 'digestion' | 'purification' | 'separation' | 'analysis'
    element: Optional[str]
    target_matrices: tuple[str, ...]
    duration: Optional[str]
    duration_hours: Optional[float]
    yield_min_pct: Optional[float]
    yield_max_pct: Optional[float]
    equipment: Optional[MethodEquipment]
    steps: tuple[MethodStep, ...]
    reference: Optional[MethodReference]
    notes: Optional[str]


# ---------------------------------------------------------------------------
# Pipelines
# ---------------------------------------------------------------------------
    """
    Les pipelines représentent les chaînes analytiques complètes (des 
    suites de méthodes dans des ordres précis).
    Ex. Protocole de digestion acide → puis procédure de purification → 
    paramètres analytiques.
    *************************************************************************
    Pipelines represent complete analytical chains (sequences of methods in 
    specific orders).
    Example: Acid digestion protocol → purification procedure → analytical 
    parameters.
    """

@dataclass(frozen=True)
class PipelineStage:
    order: int
    method_key: str
    label: Optional[str]


@dataclass(frozen=True)
class Pipeline:
    """
    Séquence ordonnée de méthodes pour traiter un élément donné.
    Ordered sequence of methods for processing a given element.
    """

    key: str
    name: Optional[str]
    element: Optional[str]
    description: Optional[str]
    stages: tuple[PipelineStage, ...]


# ---------------------------------------------------------------------------
# Rendements de purification | Purification yield
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class PurificationYield:
    """
    Rendement mesuré pour un couple (échantillon, élément).

    La clé du dictionnaire dans le document ISOF est "{sample_id}_{ELEMENT}".
    Elle est décomposée ici pour faciliter les requêtes par échantillon ou par élément.
    **************************************************************************
    Yield measured for a pair (sample, element).

    The dictionary key in the ISOF document is "{sample_id}_{ELEMENT}".
    It is decomposed here to facilitate queries by sample or by element.
    """

    sample_id: str
    element: str
    value_pct: float
    date: Optional[str]
    operator: Optional[str]
    method_key: Optional[str]
    notes: Optional[str]

    @property
    def is_suspicious(self) -> bool:
        """
        Un rendement > 105 % indique une contamination probable.

        Le seuil de 105 % est une convention analytique, en dessous,
        l'écart peut s'expliquer par la variabilité de pesée.
        ***********************************************************************
        A yield > 105% indicates probable contamination.

        The 105% threshold is an analytical convention; below this threshold,
        the discrepancy may be explained by weighing variability.
        """
        return self.value_pct > 105.0


# ---------------------------------------------------------------------------
# Assignations méthode ↔ échantillon | Method assignments ↔ sample
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class Assignment:
    sample_id: str
    method_key: Optional[str]
    pipeline_key: Optional[str]
    assigned_at: Optional[str]
    assigned_by: Optional[str]


# ---------------------------------------------------------------------------
# Métadonnées et provenance | Metadata and provenance
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class CreatedBy:
    software: Optional[str]
    software_version: Optional[str]
    operator: Optional[str]
    organisation: Optional[str]


@dataclass(frozen=True)
class Project:
    name: Optional[str]
    reference: Optional[str]
    client: Optional[str]
    classification: Optional[str]
    notes: Optional[str]


# ---------------------------------------------------------------------------
# Signature
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class Signature:
    """
    Bloc de signature tel que stocké dans le fichier.

    La structure des données est volontairement distinguée (ce fichier)
    du résultat de la vérification (VerificationResult dans signature.py).
    Un fichier peut avoir un bloc signature valide structurellement
    mais dont le hash ne correspond plus aux données.
    **************************************************************************
    Signature block as stored in the file.

    The data structure (this file) is intentionally distinguished from the 
    verification result (VerificationResult in signature.py).
    A file may have a structurally valid signature block but whose hash 
    no longer matches the data.
    """

    level: int                     # 1 = SHA-256 intégrity, 2 = PKI IsoFind
    algorithm: str
    scope: tuple[str, ...]         # covered blocks : 'samples', 'methods', etc.
    hash: Optional[str]            # level 1 only
    signed_at: Optional[str]
    signed_by: Optional[str]       # organization or CN certificate
    contact: Optional[str]
    # Niveau 2 — champs PKI
    certificate_pem: Optional[str] = None
    certificate_chain: Optional[str] = None

    @property
    def signed_at_dt(self) -> Optional[datetime]:
        """
        Conversion en datetime pour les comparaisons et affichages.
        Conversion to datetime for comparisons and displays.
        """
        if not self.signed_at:
            return None
        try:
            return datetime.fromisoformat(self.signed_at.replace("Z", "+00:00"))
        except ValueError:
            return None
