from __future__ import annotations

import re
from collections.abc import Mapping
from datetime import date, datetime
from enum import StrEnum
from typing import Any
from zoneinfo import ZoneInfo

from pydantic import BaseModel, ConfigDict, Field


class DateSource(StrEnum):
    CLINICAL_EVENT = "clinical_event"
    DOCUMENT = "document"
    ANALYSIS = "analysis"
    INGESTION = "ingestion"
    UNKNOWN = "unknown"


class DatePrecision(StrEnum):
    DATETIME = "datetime"
    DATE = "date"
    MONTH = "month"
    YEAR = "year"
    UNKNOWN = "unknown"


DATE_SOURCE_LABELS = {
    DateSource.CLINICAL_EVENT: "Evento clínico",
    DateSource.DOCUMENT: "Documento",
    DateSource.ANALYSIS: "Análisis/disponibilidad",
    DateSource.INGESTION: "Ingreso al sistema",
    DateSource.UNKNOWN: "Sin fuente identificada",
}
DATE_PRECISION_LABELS = {
    DatePrecision.DATETIME: "Fecha y hora",
    DatePrecision.DATE: "Fecha",
    DatePrecision.MONTH: "Mes",
    DatePrecision.YEAR: "Año",
    DatePrecision.UNKNOWN: "Sin precisión",
}

_PENDING_ASSOCIATION_STATUSES = frozenset(
    {
        "pending_canonical_review",
        "pendiente_revision",
        "ambiguous",
        "ocr_required",
        "fuzzy_candidate",
    }
)
_PENDING_ASSOCIATION_METHODS = frozenset({"ambiguous", "ocr_required", "fuzzy_candidate", "not_found"})
_TRAJECTORY_VALUE_KEYS = (
    "resultado",
    "result",
    "resultado_clinico",
    "valor_resultado",
    "resultado_examen",
)
_TRAJECTORY_STAGE_KEYS = ("clinical_stage", "stage", "etapa_clinica", "etapa", "phase")
_TRAJECTORY_STATE_KEYS = ("trajectory_status", "estado_trayectoria", "result_status")


class ClinicalTrajectory(BaseModel):
    """Proyección auditable de un artefacto a través de sus ocurrencias."""

    model_config = ConfigDict(extra="forbid", frozen=True)

    trajectory_id: str
    canonical_id: str = ""
    canonical_term: str = ""
    occurrences: list[dict[str, Any]]
    source_ids: list[str] = Field(default_factory=list)
    clinical_stages: list[str] = Field(default_factory=list)
    states: list[str] = Field(default_factory=list)
    evidence_ids: list[str] = Field(default_factory=list)
    evidence_count: int = 0
    confidence_score: float | None = None
    requires_review: bool = False
    review_reasons: list[str] = Field(default_factory=list)


class EffectiveDate(BaseModel):
    """Fecha utilizable para cronología, conservando cómo fue obtenida."""

    model_config = ConfigDict(extra="forbid", frozen=True)

    effective_date: str | None = None
    date_source: DateSource = DateSource.UNKNOWN
    date_precision: DatePrecision = DatePrecision.UNKNOWN
    warning: str | None = None

    @property
    def is_dated(self) -> bool:
        return self.effective_date is not None

    @property
    def source_label(self) -> str:
        return DATE_SOURCE_LABELS[self.date_source]

    @property
    def precision_label(self) -> str:
        return DATE_PRECISION_LABELS[self.date_precision]

    @property
    def display_date(self) -> str:
        return self.effective_date or "Sin fecha"


_BOGOTA_TZ = ZoneInfo("America/Bogota")
_EMPTY_DATE_TOKENS = {
    "",
    "-",
    "n/a",
    "na",
    "no aplica",
    "no especificado",
    "sin fecha",
    "desconocido",
    "unknown",
}
_ISO_DATETIME_PATTERN = re.compile(
    r"(?<!\w)(?P<value>\d{4}-\d{2}-\d{2}[T ]\d{1,2}:\d{2}"
    r"(?::\d{2}(?:\.\d+)?)?(?:\s*(?:Z|[+-]\d{2}:?\d{2}))?)(?!\w)",
    re.IGNORECASE,
)
_ISO_DATE_PATTERN = re.compile(r"(?<!\w)(?P<value>\d{4}-\d{2}-\d{2})(?!\w)")
_SHORT_DATE_PATTERN = re.compile(
    r"(?<!\d)(?P<day>\d{1,2})[/-](?P<month>\d{1,2})[/-](?P<year>\d{2,4})(?!\d)"
)
_NUMERIC_MONTH_PATTERN = re.compile(r"(?<!\d)(?P<month>\d{1,2})[/-](?P<year>\d{4})(?!\d)")
_YEAR_MONTH_PATTERN = re.compile(r"(?<!\d)(?P<year>\d{4})-(?P<month>\d{1,2})(?![\d-])")
_YEAR_PATTERN = re.compile(r"(?<!\d)(?P<year>\d{4})(?!\d)")
_TIME_PATTERN = re.compile(
    r"(?<!\d)(?P<hour>\d{1,2}):(?P<minute>\d{2})(?::(?P<second>\d{2}))?"
    r"\s*(?P<meridiem>[ap])?\.?\s*m?\.?"
    r"(?!\d)",
    re.IGNORECASE,
)
_SPANISH_MONTHS = {
    "enero": 1,
    "febrero": 2,
    "marzo": 3,
    "abril": 4,
    "mayo": 5,
    "junio": 6,
    "julio": 7,
    "agosto": 8,
    "septiembre": 9,
    "setiembre": 9,
    "octubre": 10,
    "noviembre": 11,
    "diciembre": 12,
}
_SPANISH_MONTH_PATTERN = re.compile(
    r"(?<!\w)(?:(?P<day>\d{1,2})\s+(?:de\s+)?|)"
    r"(?P<month>enero|febrero|marzo|abril|mayo|junio|julio|agosto|"
    r"septiembre|setiembre|octubre|noviembre|diciembre)"
    r"(?:\s+de)?\s+(?P<year>\d{4})(?!\d)",
    re.IGNORECASE,
)


def _clean(value: Any) -> str:
    return re.sub(r"\s+", " ", str(value or "")).strip()


def _is_empty(value: Any) -> bool:
    return _clean(value).casefold() in _EMPTY_DATE_TOKENS


def _normalize_year(value: str) -> int:
    year = int(value)
    return year + 2000 if year < 100 else year


def _serialize_datetime(value: datetime) -> str:
    if value.tzinfo is None:
        value = value.replace(tzinfo=_BOGOTA_TZ)
    normalized = value.astimezone(_BOGOTA_TZ).replace(microsecond=0)
    timespec = "seconds" if normalized.second else "minutes"
    return normalized.isoformat(timespec=timespec)


def _time_near_date(value: str, start: int, end: int) -> tuple[int, int, int] | None:
    candidates = [
        match
        for match in _TIME_PATTERN.finditer(value)
        if abs(match.start() - end) <= 24 or abs(start - match.end()) <= 24
    ]
    if not candidates:
        return None
    match = min(candidates, key=lambda item: min(abs(item.start() - end), abs(start - item.end())))
    hour = int(match.group("hour"))
    minute = int(match.group("minute"))
    second = int(match.group("second") or 0)
    meridiem = str(match.group("meridiem") or "").casefold()
    if meridiem == "p" and hour < 12:
        hour += 12
    if meridiem == "a" and hour == 12:
        hour = 0
    return hour, minute, second


def _date_result(
    parsed: date,
    *,
    time_parts: tuple[int, int, int] | None,
) -> tuple[str, DatePrecision] | None:
    if time_parts is None:
        return parsed.isoformat(), DatePrecision.DATE
    try:
        value = datetime(parsed.year, parsed.month, parsed.day, *time_parts)
        return _serialize_datetime(value), DatePrecision.DATETIME
    except ValueError:
        return None


def normalize_date_value(value: Any) -> tuple[str, DatePrecision] | None:
    """Normaliza fechas completas o parciales sin completar datos faltantes."""
    if isinstance(value, datetime):
        return _serialize_datetime(value), DatePrecision.DATETIME
    if isinstance(value, date):
        return value.isoformat(), DatePrecision.DATE

    cleaned = _clean(value)
    if _is_empty(cleaned):
        return None

    iso_datetime = _ISO_DATETIME_PATTERN.search(cleaned)
    if iso_datetime:
        raw = iso_datetime.group("value").replace("z", "+00:00").replace("Z", "+00:00")
        try:
            return _serialize_datetime(datetime.fromisoformat(raw)), DatePrecision.DATETIME
        except ValueError:
            return None

    iso_date = _ISO_DATE_PATTERN.search(cleaned)
    if iso_date:
        try:
            return date.fromisoformat(iso_date.group("value")).isoformat(), DatePrecision.DATE
        except ValueError:
            return None

    short_date = _SHORT_DATE_PATTERN.search(cleaned)
    if short_date:
        try:
            parsed = date(
                _normalize_year(short_date.group("year")),
                int(short_date.group("month")),
                int(short_date.group("day")),
            )
        except ValueError:
            return None
        return _date_result(parsed, time_parts=_time_near_date(cleaned, short_date.start(), short_date.end()))

    long_date = _SPANISH_MONTH_PATTERN.search(cleaned)
    if long_date:
        try:
            parsed = date(
                int(long_date.group("year")),
                _SPANISH_MONTHS[long_date.group("month").casefold()],
                int(long_date.group("day") or 1),
            )
        except ValueError:
            return None
        if long_date.group("day"):
            return _date_result(parsed, time_parts=_time_near_date(cleaned, long_date.start(), long_date.end()))
        return parsed.strftime("%Y-%m"), DatePrecision.MONTH

    year_month = _YEAR_MONTH_PATTERN.search(cleaned)
    if year_month:
        try:
            parsed = date(int(year_month.group("year")), int(year_month.group("month")), 1)
        except ValueError:
            return None
        return parsed.strftime("%Y-%m"), DatePrecision.MONTH

    numeric_month = _NUMERIC_MONTH_PATTERN.search(cleaned)
    if numeric_month:
        try:
            parsed = date(int(numeric_month.group("year")), int(numeric_month.group("month")), 1)
        except ValueError:
            return None
        return parsed.strftime("%Y-%m"), DatePrecision.MONTH

    year = _YEAR_PATTERN.search(cleaned)
    if year:
        return year.group("year"), DatePrecision.YEAR
    return None


_CLINICAL_KEYS = (
    "clinical_date",
    "fecha_clinica",
    "event_date",
    "fecha_evento",
    "fecha_procedimiento",
    "fecha_estudio",
    "fecha_examen",
    "fecha_servicio",
    "fecha_ingreso",
)
_DOCUMENT_KEYS = ("document_date", "fecha_documento", "fecha_documental", "fecha_emision")
_ANALYSIS_KEYS = ("analysis_date", "fecha_analisis", "available_date", "fecha_disponibilidad")
_INGESTION_KEYS = ("ingestion_date", "fecha_ingreso_sistema", "uploaded_at", "created_at")
_STRUCTURED_CLINICAL_KEYS = {
    "historia_clinica": ("fecha_ingreso", "fi"),
    "quirurgico": ("fecha_procedimiento", "fp"),
    "radiologia": ("fecha_estudio", "fe"),
    "laboratorio": ("fecha_examen", "fe"),
    "prescripcion": ("fecha", "fe"),
}


def _first_value(document: Mapping[str, Any], keys: tuple[str, ...]) -> Any:
    for key in keys:
        value = document.get(key)
        if value is not None and not _is_empty(value):
            return value
    return None


def _structured_clinical_value(document: Mapping[str, Any], structured: Mapping[str, Any]) -> Any:
    document_type = str(
        document.get("tipo_documento")
        or document.get("document_type")
        or structured.get("tipo_documento")
        or structured.get("document_type")
        or structured.get("dt")
        or ""
    ).strip()
    value = _first_value(structured, _STRUCTURED_CLINICAL_KEYS.get(document_type, ()))
    if value is not None:
        return value
    if document_type == "factura":
        patient = structured.get("paciente") or structured.get("pc")
        if isinstance(patient, Mapping):
            value = _first_value(patient, ("fecha_ingreso", "fi"))
            if value is not None:
                return value
    return None


def _structured_document_value(document: Mapping[str, Any], structured: Mapping[str, Any]) -> Any:
    document_type = str(
        document.get("tipo_documento")
        or document.get("document_type")
        or structured.get("tipo_documento")
        or structured.get("document_type")
        or structured.get("dt")
        or ""
    ).strip()
    if document_type != "factura":
        return None
    invoice_data = structured.get("datos_factura") or structured.get("df")
    if isinstance(invoice_data, Mapping):
        return _first_value(invoice_data, ("fecha_emision", "fe"))
    return None


def project_effective_date(
    element: Mapping[str, Any] | None = None,
    *,
    clinical_date: Any = None,
    document_date: Any = None,
    analysis_date: Any = None,
    ingestion_date: Any = None,
) -> EffectiveDate:
    """Proyecta la fecha efectiva usando la precedencia definida por PRI-002."""
    document = element if isinstance(element, Mapping) else {}

    projected_value = document.get("effective_date")
    if projected_value is not None and not _is_empty(projected_value):
        normalized = normalize_date_value(projected_value)
        if normalized:
            _, inferred_precision = normalized
            try:
                precision = DatePrecision(document.get("date_precision", inferred_precision))
            except ValueError:
                precision = inferred_precision
            try:
                source = DateSource(document.get("date_source", DateSource.UNKNOWN))
            except ValueError:
                source = DateSource.UNKNOWN
            return EffectiveDate(
                effective_date=normalized[0],
                date_source=source,
                date_precision=precision,
                warning=document.get("date_warning") or document.get("warning"),
            )

    structured = document.get("analysis_structured")
    structured_document = structured if isinstance(structured, Mapping) else {}

    clinical_value = clinical_date or _first_value(document, _CLINICAL_KEYS)
    if clinical_value is None:
        clinical_value = _structured_clinical_value(document, structured_document)
    document_value = document_date or _first_value(document, _DOCUMENT_KEYS)
    if document_value is None:
        document_value = _structured_document_value(document, structured_document)
    analysis_value = analysis_date or _first_value(document, _ANALYSIS_KEYS)
    ingestion_value = ingestion_date or _first_value(document, _INGESTION_KEYS)

    for source, value in (
        (DateSource.CLINICAL_EVENT, clinical_value),
        (DateSource.DOCUMENT, document_value),
        (DateSource.ANALYSIS, analysis_value),
        (DateSource.INGESTION, ingestion_value),
    ):
        normalized = normalize_date_value(value)
        if normalized:
            effective_date, precision = normalized
            return EffectiveDate(
                effective_date=effective_date,
                date_source=source,
                date_precision=precision,
            )

    return EffectiveDate(
        warning="No se encontró una fecha utilizable; el elemento requiere revisión de trazabilidad.",
    )


_DOCUMENT_TYPE_KEYS = (
    "document_type",
    "tipo_documento",
    "document_type_label",
    "fuente",
    "source",
    "tipo",
)
_SOURCE_POSITION_KEYS = (
    "source_position",
    "source_index",
    "origen_position",
    "origen_index",
    "indice_origen",
    "position",
    "index",
)
_IDENTIFIER_KEYS = (
    "key",
    "item_id",
    "identifier",
    "document_uid",
    "document_id",
    "_id",
    "id",
)
_PRECISION_ORDER = {
    DatePrecision.DATETIME: 0,
    DatePrecision.DATE: 1,
    DatePrecision.MONTH: 2,
    DatePrecision.YEAR: 3,
    DatePrecision.UNKNOWN: 4,
}


def _first_text(mapping: Mapping[str, Any], keys: tuple[str, ...]) -> str:
    for key in keys:
        value = mapping.get(key)
        if value is not None and _clean(value):
            return _clean(value).casefold()
    return ""


def _source_position(mapping: Mapping[str, Any]) -> int | None:
    for key in _SOURCE_POSITION_KEYS:
        value = mapping.get(key)
        if value is None or isinstance(value, bool):
            continue
        try:
            return int(value)
        except (TypeError, ValueError):
            continue
    return None


def _effective_date_sort_value(effective: EffectiveDate) -> tuple[int, int, int, int, int, int, int]:
    value = effective.effective_date
    precision = effective.date_precision
    if not value:
        return (9999, 99, 99, 99, 99, 99, _PRECISION_ORDER[DatePrecision.UNKNOWN])
    try:
        if precision == DatePrecision.DATETIME:
            parsed = datetime.fromisoformat(value)
            return (
                parsed.year,
                parsed.month,
                parsed.day,
                parsed.hour,
                parsed.minute,
                parsed.second,
                _PRECISION_ORDER[precision],
            )
        if precision == DatePrecision.DATE:
            parsed_date = date.fromisoformat(value)
            return (parsed_date.year, parsed_date.month, parsed_date.day, 0, 0, 0, _PRECISION_ORDER[precision])
        if precision == DatePrecision.MONTH:
            parsed_month = date.fromisoformat(f"{value}-01")
            return (parsed_month.year, parsed_month.month, 0, 0, 0, 0, _PRECISION_ORDER[precision])
        if precision == DatePrecision.YEAR:
            return (int(value), 0, 0, 0, 0, 0, _PRECISION_ORDER[precision])
    except (TypeError, ValueError):
        pass
    return (9999, 99, 99, 99, 99, 99, _PRECISION_ORDER[DatePrecision.UNKNOWN])


def _chronology_sort_key(item: Any, original_position: int) -> tuple[Any, ...]:
    mapping = dict(item) if isinstance(item, Mapping) else {}
    if "effective_date" not in mapping and "fecha_efectiva" in mapping:
        mapping["effective_date"] = mapping.get("fecha_efectiva")
        mapping["date_source"] = mapping.get("fuente_fecha")
        mapping["date_precision"] = mapping.get("precision_fecha")
    if "key" not in mapping and "identificador" in mapping:
        mapping["key"] = mapping.get("identificador")
    if "document_type" not in mapping and "tipo_documento" in mapping:
        mapping["document_type"] = mapping.get("tipo_documento")
    effective = project_effective_date(mapping)
    position = _source_position(mapping)
    return (
        not effective.is_dated,
        _effective_date_sort_value(effective),
        _first_text(mapping, _DOCUMENT_TYPE_KEYS),
        position is None,
        position if position is not None else 0,
        _first_text(mapping, _IDENTIFIER_KEYS),
        original_position,
    )


def project_chronological_item(
    item: Mapping[str, Any],
    *,
    clinical_date: Any = None,
    document_date: Any = None,
    analysis_date: Any = None,
    ingestion_date: Any = None,
) -> dict[str, Any]:
    """Copia un elemento con metadatos de fecha para sus salidas cronológicas."""
    candidate = dict(item)
    if "effective_date" not in candidate and "fecha_efectiva" in candidate:
        candidate["effective_date"] = candidate.get("fecha_efectiva")
    if "date_source" not in candidate and "fuente_fecha" in candidate:
        candidate["date_source"] = candidate.get("fuente_fecha")
    if "date_precision" not in candidate and "precision_fecha" in candidate:
        candidate["date_precision"] = candidate.get("precision_fecha")
    if "date_warning" not in candidate and "advertencia_fecha" in candidate:
        candidate["date_warning"] = candidate.get("advertencia_fecha")
    projected = project_effective_date(
        candidate,
        clinical_date=clinical_date,
        document_date=document_date,
        analysis_date=analysis_date,
        ingestion_date=ingestion_date,
    )
    return dict(item) | {
        "effective_date": projected.effective_date,
        "date_source": projected.date_source.value,
        "date_precision": projected.date_precision.value,
        "date_warning": projected.warning,
    }


def sort_chronological_items(items: Any) -> list[Any]:
    """Ordena antiguo→reciente con desempate reproducible y sin mutar la fuente."""
    if not isinstance(items, list):
        return []
    return [
        project_chronological_item(item) if isinstance(item, Mapping) else item
        for _, item in sorted(
            enumerate(items),
            key=lambda indexed: _chronology_sort_key(indexed[1], indexed[0]),
        )
    ]


def _trajectory_text(value: Any) -> str:
    return _clean(value)


def _trajectory_normalized(value: Any) -> str:
    return re.sub(r"\s+", " ", _trajectory_text(value)).casefold()


def _unique_trajectory_texts(values: Any) -> list[str]:
    result: list[str] = []
    seen: set[str] = set()
    for value in values or []:
        text = _trajectory_text(value)
        normalized = text.casefold()
        if text and normalized not in seen:
            seen.add(normalized)
            result.append(text)
    return result


def _confirmed_canonical_id(item: Mapping[str, Any]) -> str:
    canonical_id = _trajectory_text(item.get("canonical_id"))
    status = _trajectory_normalized(item.get("status"))
    method = _trajectory_normalized(item.get("match_method"))
    if not canonical_id or status in _PENDING_ASSOCIATION_STATUSES or method in _PENDING_ASSOCIATION_METHODS:
        return ""
    return canonical_id


def _trajectory_source_id(item: Mapping[str, Any]) -> str:
    for key in ("document_uid", "document_id", "source_hash", "filename", "document_type"):
        value = _trajectory_text(item.get(key))
        if value:
            return value
    return ""


def _trajectory_stage(item: Mapping[str, Any]) -> str:
    for key in _TRAJECTORY_STAGE_KEYS:
        value = _trajectory_text(item.get(key))
        if value:
            return value
    return ""


def _trajectory_state(item: Mapping[str, Any]) -> str:
    for key in _TRAJECTORY_STATE_KEYS:
        value = _trajectory_text(item.get(key))
        if value:
            return value
    return ""


def _trajectory_review_reasons(items: list[Mapping[str, Any]], *, canonical_id: str) -> list[str]:
    reasons: list[str] = []
    if not canonical_id:
        reasons.append("asociacion_canonica_no_confirmada")
    if (
        any(
            _trajectory_normalized(item.get("status")) in _PENDING_ASSOCIATION_STATUSES
            or _trajectory_normalized(item.get("match_method")) in _PENDING_ASSOCIATION_METHODS
            for item in items
        )
        and "asociacion_canonica_pendiente_revision" not in reasons
    ):
        reasons.append("asociacion_canonica_pendiente_revision")

    for item in items:
        if any(
            item.get(key) is True for key in ("contradictory", "contradiction", "contradictorio", "conflicto")
        ):
            reasons.append("evidencia_contradictoria")
            break

    for key in _TRAJECTORY_VALUE_KEYS + _TRAJECTORY_STATE_KEYS:
        values = _unique_trajectory_texts(item.get(key) for item in items)
        if len(values) > 1:
            reasons.append(f"valores_contradictorios:{key}")
    return _unique_trajectory_texts(reasons)


def build_clinical_trajectories(items: Any) -> list[dict[str, Any]]:
    """Agrupa ocurrencias canónicas sin ocultar candidatos ni conflictos.

    Solo una asociación con ``canonical_id`` y estado/método confirmado puede
    consolidar ocurrencias de distintos documentos. Las demás quedan como
    trayectorias candidatas independientes y revisables.
    """
    if not isinstance(items, list):
        return []

    grouped: dict[str, list[dict[str, Any]]] = {}
    for index, raw_item in enumerate(items):
        if not isinstance(raw_item, Mapping):
            continue
        item = project_chronological_item(dict(raw_item))
        canonical_id = _confirmed_canonical_id(item)
        occurrence_id = _trajectory_text(item.get("occurrence_id") or item.get("key"))
        group_key = f"canonical:{canonical_id}" if canonical_id else f"candidate:{occurrence_id or index}"
        grouped.setdefault(group_key, []).append(item)

    trajectories: list[ClinicalTrajectory] = []
    for group_key, raw_occurrences in grouped.items():
        occurrences = sort_chronological_items(raw_occurrences)
        canonical_id = _confirmed_canonical_id(occurrences[0]) if occurrences else ""
        canonical_term = next(
            (
                _trajectory_text(item.get("canonical_term"))
                for item in occurrences
                if _trajectory_text(item.get("canonical_term"))
            ),
            "",
        )
        reasons = _trajectory_review_reasons(occurrences, canonical_id=canonical_id)
        scores = [
            float(item["match_score"])
            for item in occurrences
            if item.get("match_score") is not None and not isinstance(item.get("match_score"), bool)
        ]
        trajectory = ClinicalTrajectory(
            trajectory_id=f"trajectory:{canonical_id}" if canonical_id else f"trajectory:{group_key}",
            canonical_id=canonical_id,
            canonical_term=canonical_term,
            occurrences=occurrences,
            source_ids=_unique_trajectory_texts(_trajectory_source_id(item) for item in occurrences),
            clinical_stages=_unique_trajectory_texts(_trajectory_stage(item) for item in occurrences),
            states=_unique_trajectory_texts(_trajectory_state(item) for item in occurrences),
            evidence_ids=_unique_trajectory_texts(item.get("occurrence_id") for item in occurrences),
            evidence_count=len(occurrences),
            confidence_score=round(sum(scores) / len(scores), 2) if scores else None,
            requires_review=bool(reasons),
            review_reasons=reasons,
        )
        trajectories.append(trajectory)

    trajectories.sort(
        key=lambda trajectory: _chronology_sort_key(
            trajectory.occurrences[0] if trajectory.occurrences else {},
            0,
        )
    )
    return [trajectory.model_dump(mode="json") for trajectory in trajectories]
