from __future__ import annotations

import csv
import math
from datetime import datetime, timezone
from pathlib import Path
from typing import Any


SESSION_COLUMNS = (
    "sourceId",
    "selectionIndex",
    "displayName",
    "captureDateRaw",
    "durationSeconds",
    "transcriptPath",
    "hasVideo",
    "hasAudio",
)


def _capture_date(value: str) -> tuple[datetime, bool] | None:
    text = value.strip()
    if not text:
        return None
    if text.endswith(("Z", "z")):
        text = text[:-1] + "+00:00"
    try:
        parsed = datetime.fromisoformat(text)
    except ValueError:
        return None
    aware = parsed.tzinfo is not None and parsed.utcoffset() is not None
    return parsed, aware


def _bool(value: str, label: str) -> bool:
    normalized = value.strip().casefold()
    if normalized in {"true", "1", "yes"}:
        return True
    if normalized in {"false", "0", "no"}:
        return False
    raise ValueError(f"{label} must be true or false.")


def load_session_table(path: Path) -> dict[str, Any]:
    with path.open("r", encoding="utf-8", newline="") as handle:
        reader = csv.DictReader(handle, dialect="excel-tab")
        if tuple(reader.fieldnames or ()) != SESSION_COLUMNS:
            raise ValueError("Session table header does not match the frozen contract.")
        rows = list(reader)
    if not rows:
        raise ValueError("Session table must contain at least one source.")

    parsed: list[tuple[dict[str, Any], tuple[datetime, bool] | None]] = []
    source_ids: set[str] = set()
    for position, row in enumerate(rows):
        source_id = row["sourceId"].strip()
        if not source_id or source_id in source_ids:
            raise ValueError("Session source IDs must be non-empty and unique.")
        source_ids.add(source_id)
        try:
            selection_index = int(row["selectionIndex"])
            duration_seconds = float(row["durationSeconds"])
        except ValueError as error:
            raise ValueError(f"Session source {source_id} has invalid numeric fields.") from error
        duration_ms = (
            int(round(duration_seconds * 1000))
            if math.isfinite(duration_seconds)
            else 0
        )
        if selection_index != position or duration_ms <= 0:
            raise ValueError(
                "Session rows must use contiguous selection order and positive duration."
            )
        transcript_path = row["transcriptPath"].strip()
        if not transcript_path:
            raise ValueError(f"Session source {source_id} has no transcript path.")
        parsed.append(({
            "sourceId": source_id,
            "selectionIndex": selection_index,
            "displayName": row["displayName"].strip() or source_id,
            "captureDateRaw": row["captureDateRaw"].strip(),
            "durationMs": duration_ms,
            "transcriptPath": transcript_path,
            "hasVideo": _bool(row["hasVideo"], f"{source_id}.hasVideo"),
            "hasAudio": _bool(row["hasAudio"], f"{source_id}.hasAudio"),
        }, _capture_date(row["captureDateRaw"])))
        if not parsed[-1][0]["hasVideo"] and not parsed[-1][0]["hasAudio"]:
            raise ValueError(f"Session source {source_id} has neither audio nor video.")

    dates = [item[1] for item in parsed]
    awareness = {item[1] for item in dates if item is not None}
    coherent_dates = all(item is not None for item in dates) and len(awareness) == 1
    if coherent_dates:
        aware = next(iter(awareness))
        parsed.sort(key=lambda item: (
            item[1][0].astimezone(timezone.utc) if aware else item[1][0],  # type: ignore[index]
            item[0]["selectionIndex"],
            item[0]["sourceId"],
        ))
        order_mode = "CAPTURE_DATE"
        warning_codes: list[str] = []
    else:
        order_mode = "PREMIERE_SELECTION_API"
        warning_codes = [
            "CAPTURE_DATE_UNAVAILABLE"
            if any(item is None for item in dates)
            else "CAPTURE_DATE_TIMEZONE_MIXED"
        ]
    sources = []
    for source_order, (source, _) in enumerate(parsed):
        sources.append({
            key: value
            for key, value in source.items()
            if key not in {"selectionIndex", "captureDateRaw"}
        } | {"sourceOrder": source_order})
    return {
        "contractVersion": 1,
        "orderMode": order_mode,
        "orderWarningCodes": warning_codes,
        "sources": sources,
    }
