"""Export functions for CSV, JSON, and ELAN (.eaf) annotation files."""

import csv
import datetime
import json
import logging
import re
import xml.etree.ElementTree as ET
from dataclasses import asdict
from pathlib import Path
from xml.dom import minidom

from ..constants import FPS
from ..models import BlinkEvent, NMSEvent

logger = logging.getLogger(__name__)


def derive_participant_id(h5_path: str, media_path: str = "") -> str:
    """Auto-derive participant ID from h5 or media filename."""
    for p in [h5_path, media_path]:
        if not p:
            continue
        stem = Path(p).stem
        stem = re.sub(r"_nlf$", "", stem)
        m = re.match(r"^([A-Z]+\d+)", stem)
        if m:
            return m.group(1)
    return Path(h5_path).stem


ELAN_CV_DEFS = {
    "CV_BLINK": ["blink"],
    "CV_HEAD_NOD": ["nod", "single nod"],
    "CV_HEAD_SHAKE": ["shake", "single shake"],
    "CV_HEAD_TILT": ["tilt-left", "tilt-right"],
    "CV_MOUTH": ["mouth open", "mouth movement", "lip movement"],
    "CV_EYEBROW_RAISE": ["eyebrow raise"],
    "CV_EYEBROW_FURROW": ["eyebrow furrow"],
    "CV_GAZE": [
        "gaze up",
        "gaze down",
        "gaze left",
        "gaze right",
        "gaze up-left",
        "gaze up-right",
        "gaze down-left",
        "gaze down-right",
        "gaze shift",
    ],
    "CV_EYE_SQUINT": ["eye squint"],
}

TIER_CV_MAP = {
    "BLINK": "CV_BLINK",
    "HEAD-NOD": "CV_HEAD_NOD",
    "HEAD-SHAKE": "CV_HEAD_SHAKE",
    "HEAD-TILT": "CV_HEAD_TILT",
    "MOUTH-MOVEMENT": "CV_MOUTH",
    "EYEBROW-RAISE": "CV_EYEBROW_RAISE",
    "EYEBROW-FURROW": "CV_EYEBROW_FURROW",
    "EYE-GAZE": "CV_GAZE",
    "EYE-SQUINT": "CV_EYE_SQUINT",
}


def clean_label(tier: str, raw_label: str) -> str:
    """Map internal NMS event labels to clean CV-compliant annotation values."""
    label_lower = raw_label.lower().strip()
    _LABEL_MAP = {
        "blink": "blink",
        "nod": "nod",
        "single nod": "single nod",
        "shake": "shake",
        "single shake": "single shake",
        "tilt-left": "tilt-left",
        "tilt-right": "tilt-right",
        "tilt left": "tilt-left",
        "tilt right": "tilt-right",
        "mouth open": "mouth movement",
        "mouth-open": "mouth movement",
        "mouth movement": "mouth movement",
        "lip movement": "mouth movement",
        "eyebrow raise": "eyebrow raise",
        "eyebrow-raise": "eyebrow raise",
        "eyebrow furrow": "eyebrow furrow",
        "eyebrow-furrow": "eyebrow furrow",
        "gaze up": "gaze up",
        "gaze down": "gaze down",
        "gaze left": "gaze left",
        "gaze right": "gaze right",
        "gaze up-left": "gaze up-left",
        "gaze up-right": "gaze up-right",
        "gaze down-left": "gaze down-left",
        "gaze down-right": "gaze down-right",
        "gaze shift": "gaze shift",
        "eye squint": "eye squint",
    }
    if label_lower in _LABEL_MAP:
        return _LABEL_MAP[label_lower]
    cleaned = re.sub(r"\s*(conf|prom|prominence|confidence)=\S*", "", raw_label).strip()
    return cleaned if cleaned else raw_label


def export_csv(blinks: list[BlinkEvent], path: str, nms_events: list[NMSEvent] | None = None):
    """Write blink and NMS events to CSV."""
    if not blinks and not nms_events:
        return
    total = 0
    with open(path, "w", newline="") as f:
        if blinks:
            writer = csv.DictWriter(f, fieldnames=list(asdict(blinks[0]).keys()))
            writer.writeheader()
            for b in blinks:
                writer.writerow(asdict(b))
            total += len(blinks)
        if nms_events:
            nms_fields = list(asdict(nms_events[0]).keys())
            if not blinks:
                writer = csv.DictWriter(f, fieldnames=nms_fields)
                writer.writeheader()
            else:
                f.write("\n")
                writer = csv.DictWriter(f, fieldnames=nms_fields)
                writer.writeheader()
            for e in nms_events:
                writer.writerow(asdict(e))
            total += len(nms_events)
    logger.info("Exported %d events to %s", total, path)


def export_json(blinks: list[BlinkEvent], path: str, nms_events: list[NMSEvent] | None = None):
    """Write blink and NMS events to JSON."""
    data = {"blinks": [asdict(b) for b in blinks]}
    if nms_events:
        data["nms_events"] = [asdict(e) for e in nms_events]
    with open(path, "w") as f:
        json.dump(data, f, indent=2)
    total = len(blinks) + (len(nms_events) if nms_events else 0)
    logger.info("Exported %d events to %s", total, path)


def export_elan(
    blinks: list[BlinkEvent],
    path: str,
    media_path: str = "",
    fps: float = FPS,
    nms_events: list[NMSEvent] | None = None,
    participant_id: str | None = None,
    h5_path: str = "",
):
    """Write annotations to an ELAN (.eaf) file with BSL corpus conventions."""
    if not blinks and not nms_events:
        return

    pid = participant_id or derive_participant_id(h5_path, media_path)

    doc = ET.Element(
        "ANNOTATION_DOCUMENT",
        {
            "AUTHOR": "nms_annotator",
            "DATE": datetime.datetime.now().isoformat(),
            "FORMAT": "3.0",
            "VERSION": "3.0",
            "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
            "xsi:noNamespaceSchemaLocation": "http://www.mpi.nl/tools/elan/EAFv3.0.xsd",
        },
    )

    header = ET.SubElement(
        doc,
        "HEADER",
        {
            "MEDIA_FILE": "",
            "TIME_UNITS": "milliseconds",
        },
    )
    if media_path:
        ext = Path(media_path).suffix.lower()
        mime = {".mp4": "video/mp4", ".mov": "video/quicktime", ".avi": "video/x-msvideo"}.get(
            ext, "video/mp4"
        )
        ET.SubElement(
            header,
            "MEDIA_DESCRIPTOR",
            {
                "MEDIA_URL": f"file://{media_path}",
                "MIME_TYPE": mime,
                "RELATIVE_MEDIA_URL": "",
            },
        )

    all_annotations = []

    for b in blinks:
        start_ms = int(b.start_frame * 1000.0 / fps)
        end_ms = int(b.end_frame * 1000.0 / fps)
        all_annotations.append(("BLINK", start_ms, end_ms, "blink"))

    if nms_events:
        for e in nms_events:
            start_ms = int(e.start_frame * 1000.0 / fps)
            end_ms = int(e.end_frame * 1000.0 / fps)
            label = clean_label(e.tier, e.label)
            all_annotations.append((e.tier, start_ms, end_ms, label))

    time_order = ET.SubElement(doc, "TIME_ORDER")
    for i, (_, start_ms, end_ms, _) in enumerate(all_annotations):
        ET.SubElement(
            time_order,
            "TIME_SLOT",
            {
                "TIME_SLOT_ID": f"ts{2 * i + 1}",
                "TIME_VALUE": str(start_ms),
            },
        )
        ET.SubElement(
            time_order,
            "TIME_SLOT",
            {
                "TIME_SLOT_ID": f"ts{2 * i + 2}",
                "TIME_VALUE": str(end_ms),
            },
        )

    tier_order = [
        "BLINK",
        "HEAD-NOD",
        "HEAD-SHAKE",
        "HEAD-TILT",
        "MOUTH-MOVEMENT",
        "EYEBROW-RAISE",
        "EYEBROW-FURROW",
        "EYE-GAZE",
        "EYE-SQUINT",
    ]
    tier_annotations: dict[str, list[int]] = {}
    for i, (tier_id, _, _, _) in enumerate(all_annotations):
        tier_annotations.setdefault(tier_id, []).append(i)

    ann_counter = 0
    for tier_id in tier_order:
        if tier_id not in tier_annotations:
            continue
        full_tier_id = f"{pid}_{tier_id}"
        cv_ref = TIER_CV_MAP.get(tier_id, "")
        tier_attrs = {
            "LINGUISTIC_TYPE_REF": "nms_event_lt",
            "PARTICIPANT": pid,
            "TIER_ID": full_tier_id,
        }
        if cv_ref:
            tier_attrs["DEFAULT_LOCALE"] = "sgn"
        tier = ET.SubElement(doc, "TIER", tier_attrs)
        for idx in tier_annotations[tier_id]:
            ann_counter += 1
            _, _, _, label = all_annotations[idx]
            ann = ET.SubElement(tier, "ANNOTATION")
            alignable = ET.SubElement(
                ann,
                "ALIGNABLE_ANNOTATION",
                {
                    "ANNOTATION_ID": f"a{ann_counter}",
                    "CVE_REF": (
                        f"{cv_ref}_{label.replace(' ', '_').replace('-', '_')}" if cv_ref else ""
                    ),
                    "TIME_SLOT_REF1": f"ts{2 * idx + 1}",
                    "TIME_SLOT_REF2": f"ts{2 * idx + 2}",
                },
            )
            value = ET.SubElement(alignable, "ANNOTATION_VALUE")
            value.text = label

    ET.SubElement(
        doc,
        "LINGUISTIC_TYPE",
        {
            "GRAPHIC_REFERENCES": "false",
            "LINGUISTIC_TYPE_ID": "nms_event_lt",
            "TIME_ALIGNABLE": "true",
        },
    )

    ET.SubElement(
        doc,
        "LOCALE",
        {
            "COUNTRY_CODE": "GB",
            "LANGUAGE_CODE": "sgn",
        },
    )

    for cv_id, entries in ELAN_CV_DEFS.items():
        tier_for_cv = [t for t, cv in TIER_CV_MAP.items() if cv == cv_id]
        if not any(t in tier_annotations for t in tier_for_cv):
            continue
        cv = ET.SubElement(doc, "CONTROLLED_VOCABULARY", {"CV_ID": cv_id})
        ET.SubElement(
            cv,
            "DESCRIPTION",
            {
                "LANG_REF": "sgn",
            },
        ).text = f"NMS labels for {cv_id.replace('CV_', '').replace('_', ' ').lower()}"
        for entry_val in entries:
            cve_id = f"{cv_id}_{entry_val.replace(' ', '_').replace('-', '_')}"
            cve = ET.SubElement(cv, "CV_ENTRY_ML", {"CVE_ID": cve_id})
            ET.SubElement(
                cve,
                "CVE_VALUE",
                {
                    "DESCRIPTION": entry_val,
                    "LANG_REF": "sgn",
                },
            ).text = entry_val

    rough = ET.tostring(doc, encoding="unicode", xml_declaration=False)
    parsed = minidom.parseString(rough)
    pretty = parsed.toprettyxml(indent="  ", encoding="UTF-8")

    with open(path, "wb") as f:
        f.write(pretty)

    logger.info("Exported ELAN to %s", path)
