"""
Annotation format converters for SLTK.

Provides converters from various raw annotation formats to the
SLTK unified annotation format.
"""

from __future__ import annotations

import csv
import json
import logging
import re
from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path

logger = logging.getLogger(__name__)

from sltk.data.core import Sample, Segment, SegmentList
from sltk.io.annotations import (
    TIER_CAPTION,
    TIER_GLOSS,
    TIER_GLOSS_LH,
    TIER_GLOSS_RH,
    TIER_MOUTHING,
    TIER_SUBTITLE,
    TIER_TRANSLATION,
    write_unified_annotation,
)

# Registry of converters
CONVERTERS: dict[str, Callable] = {}


def register_converter(name: str):
    """Decorator to register a converter function."""

    def decorator(func):
        CONVERTERS[name] = func
        return func

    return decorator


def get_converter(name: str) -> Callable | None:
    """Get a converter function by name."""
    return CONVERTERS.get(name)


def list_converters() -> list[str]:
    """List all registered converter names."""
    return list(CONVERTERS.keys())


# =============================================================================
# ELAN Converter (.eaf)
# =============================================================================

# Standard tier mappings for BSLCP
BSLCP_TIER_MAPPING = {
    "RH-IDgloss": TIER_GLOSS_RH,
    "LH-IDgloss": TIER_GLOSS_LH,
    "Sign Boundaries": "sign-boundary",
    "mouthing": TIER_MOUTHING,
    "Mouthing": TIER_MOUTHING,
}


@register_converter("elan")
def convert_elan(
    eaf_path: str | Path,
    output_path: str | Path,
    dataset: str,
    sample_id: str | None = None,
    split: str | None = None,
    signer_id: str | None = None,
    video_relative_path: str | None = None,
    tier_mapping: dict[str, str] | None = None,
    gloss_tiers: list[str] | None = None,
) -> Path:
    """
    Convert an ELAN .eaf file to unified annotation format.

    Args:
        eaf_path: Path to .eaf file
        output_path: Output JSON file path
        dataset: Dataset name
        sample_id: Sample ID (defaults to filename stem)
        split: Data split
        signer_id: Signer identifier
        video_relative_path: Relative path to video
        tier_mapping: Mapping from ELAN tier names to standard tier names
        gloss_tiers: Tier names to extract glosses from (for gloss list)

    Returns:
        Path to output file
    """
    from sltk.io.elan import get_eaf_metadata, read_eaf

    eaf_path = Path(eaf_path)

    if sample_id is None:
        sample_id = eaf_path.stem

    # Use default tier mapping if not provided
    if tier_mapping is None:
        tier_mapping = BSLCP_TIER_MAPPING

    # Read ELAN file
    segments = read_eaf(eaf_path)
    metadata = get_eaf_metadata(eaf_path)

    # Map tier names
    mapped_segments = []
    for seg in segments:
        new_tier = tier_mapping.get(seg.tier, seg.tier)
        mapped_segments.append(
            Segment(
                start=seg.start,
                end=seg.end,
                label=seg.label,
                confidence=seg.confidence,
                tier=new_tier,
                metadata=seg.metadata,
            )
        )

    # Extract glosses from specified tiers
    glosses = None
    if gloss_tiers is None:
        # Default: use right-hand and left-hand gloss tiers
        gloss_tiers = [TIER_GLOSS_RH, TIER_GLOSS_LH, TIER_GLOSS]

    gloss_segments = [s for s in mapped_segments if s.tier in gloss_tiers and s.label]
    if gloss_segments:
        # Sort by start time and extract labels
        gloss_segments.sort(key=lambda s: s.start)
        glosses = [s.label for s in gloss_segments if s.label is not None]

    # Calculate duration from segments
    duration_sec = None
    if mapped_segments:
        duration_sec = max(s.end for s in mapped_segments)

    # Build sample
    sample = Sample(
        id=sample_id,
        segments=SegmentList(mapped_segments),
        glosses=glosses,
        signer_id=signer_id,
        dataset=dataset,
        split=split,
        metadata={
            "elan_tiers": metadata.get("tiers", []),
            "elan_author": metadata.get("author"),
        },
    )

    return write_unified_annotation(
        sample,
        output_path,
        source_format="elan",
        source_path=str(eaf_path),
        video_relative_path=video_relative_path,
        duration_sec=duration_sec,
    )


# =============================================================================
# WLASL JSON Converter
# =============================================================================


@register_converter("wlasl_json")
def convert_wlasl_json(
    json_path: str | Path,
    output_dir: str | Path,
    dataset: str = "wlasl",
    split_mapping: dict[str, str] | None = None,
) -> list[Path]:
    """
    Convert WLASL JSON annotations to unified format.

    The WLASL JSON has nested structure with glosses containing video instances.

    Args:
        json_path: Path to WLASL JSON file (e.g., WLASL_v0.3.json)
        output_dir: Output directory for JSON files
        dataset: Dataset name
        split_mapping: Optional mapping to override splits

    Returns:
        List of output file paths
    """
    json_path = Path(json_path)
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    with open(json_path, encoding="utf-8") as f:
        data = json.load(f)

    output_files = []

    for gloss_entry in data:
        gloss = gloss_entry.get("gloss", "")

        for instance in gloss_entry.get("instances", []):
            video_id = instance.get("video_id", "")
            sample_id = f"{gloss}_{video_id}" if video_id else gloss

            # Get split
            split = instance.get("split")
            if split_mapping and split in split_mapping:
                split = split_mapping[split]

            # Parse timing info
            start = instance.get("frame_start", 0)
            end = instance.get("frame_end")
            fps = instance.get("fps", 25)

            segments = []
            if start is not None and end is not None and fps:
                start_sec = start / fps
                end_sec = end / fps
                segments.append(
                    Segment(
                        start=start_sec,
                        end=end_sec,
                        label=gloss,
                        tier=TIER_GLOSS,
                    )
                )

            # Build sample
            sample = Sample(
                id=sample_id,
                segments=SegmentList(segments) if segments else None,
                glosses=[gloss],
                signer_id=instance.get("signer_id"),
                dataset=dataset,
                split=split,
                metadata={
                    "video_id": video_id,
                    "url": instance.get("url"),
                    "bbox": instance.get("bbox"),
                    "fps": fps,
                    "source": instance.get("source"),
                    "variation_id": instance.get("variation_id"),
                },
            )

            # Determine video relative path
            video_relative = None
            if video_id:
                video_relative = f"videos/{video_id}.mp4"

            output_path = output_dir / f"{sample_id}.json"
            write_unified_annotation(
                sample,
                output_path,
                source_format="wlasl_json",
                source_path=str(json_path),
                video_relative_path=video_relative,
                fps=fps,
            )
            output_files.append(output_path)

    return output_files


# =============================================================================
# MS-ASL JSON Converter
# =============================================================================


@register_converter("msasl_json")
def convert_msasl_json(
    json_path: str | Path,
    output_dir: str | Path,
    dataset: str = "ms_asl",
) -> list[Path]:
    """
    Convert MS-ASL JSON annotations to unified format.

    MS-ASL has flat array structure with one entry per video clip.

    Args:
        json_path: Path to MS-ASL JSON file
        output_dir: Output directory
        dataset: Dataset name

    Returns:
        List of output file paths
    """
    json_path = Path(json_path)
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    with open(json_path, encoding="utf-8") as f:
        data = json.load(f)

    output_files = []

    for entry in data:
        sample_id = entry.get("clean_text", "") + "_" + str(entry.get("video_id", ""))
        sample_id = _sanitize_filename(sample_id)

        gloss = entry.get("clean_text", entry.get("text", ""))

        # Get timing
        start = entry.get("start_time", 0)
        end = entry.get("end_time")

        segments = []
        if start is not None and end is not None:
            segments.append(
                Segment(
                    start=float(start),
                    end=float(end),
                    label=gloss,
                    tier=TIER_GLOSS,
                )
            )

        sample = Sample(
            id=sample_id,
            segments=SegmentList(segments) if segments else None,
            glosses=[gloss] if gloss else None,
            signer_id=entry.get("signer_id"),
            dataset=dataset,
            split=entry.get("split"),
            metadata={
                "url": entry.get("url"),
                "label": entry.get("label"),
                "height": entry.get("height"),
                "width": entry.get("width"),
                "text": entry.get("text"),
            },
        )

        output_path = output_dir / f"{sample_id}.json"
        write_unified_annotation(
            sample,
            output_path,
            source_format="msasl_json",
            source_path=str(json_path),
        )
        output_files.append(output_path)

    return output_files


# =============================================================================
# ASLDict/BSLDict JSON Converter
# =============================================================================


@register_converter("asldict_json")
def convert_asldict_json(
    json_path: str | Path,
    output_dir: str | Path,
    dataset: str = "asldict",
) -> list[Path]:
    """
    Convert ASLDict/BSLDict JSON to unified format.

    ASLDict has word entries with multiple video sources per word.

    Args:
        json_path: Path to metadata.json
        output_dir: Output directory
        dataset: Dataset name

    Returns:
        List of output file paths
    """
    json_path = Path(json_path)
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    with open(json_path, encoding="utf-8") as f:
        data = json.load(f)

    output_files = []

    for word_entry in data:
        word = word_entry.get("word", "")

        for entry in word_entry.get("entries", []):
            entry_id = entry.get("entry_id", "")
            sample_id = _sanitize_filename(entry_id) if entry_id else _sanitize_filename(word)

            sample = Sample(
                id=sample_id,
                glosses=[word],
                dataset=dataset,
                metadata={
                    "word": word,
                    "entry_word": entry.get("entry_word"),
                    "entry_source": entry.get("entry_source"),
                    "video_link": entry.get("entry_video_link"),
                    "word_page_link": word_entry.get("word_page_link"),
                },
            )

            # Video path from entry_id or video_link
            video_relative = None
            video_link = entry.get("entry_video_link", "")
            if video_link:
                # Extract filename from URL
                video_filename = video_link.split("/")[-1]
                video_relative = f"videos/{video_filename}"

            output_path = output_dir / f"{sample_id}.json"
            write_unified_annotation(
                sample,
                output_path,
                source_format="asldict_json",
                source_path=str(json_path),
                video_relative_path=video_relative,
            )
            output_files.append(output_path)

    return output_files


# =============================================================================
# How2Sign CSV Converter
# =============================================================================


@register_converter("how2sign_csv")
def convert_how2sign_csv(
    csv_path: str | Path,
    output_dir: str | Path,
    dataset: str = "how2sign",
    split: str | None = None,
) -> list[Path]:
    """
    Convert How2Sign CSV annotations to unified format.

    How2Sign CSV has columns: VIDEO_ID, VIDEO_NAME, SENTENCE_ID, SENTENCE_NAME,
    START_REALIGNED, END_REALIGNED, SENTENCE

    Args:
        csv_path: Path to CSV file
        output_dir: Output directory
        dataset: Dataset name
        split: Data split (inferred from filename if not provided)

    Returns:
        List of output file paths
    """
    csv_path = Path(csv_path)
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    # Infer split from filename
    if split is None:
        filename = csv_path.stem.lower()
        if "train" in filename:
            split = "train"
        elif "test" in filename:
            split = "test"
        elif "val" in filename or "dev" in filename:
            split = "val"

    output_files = []

    with open(csv_path, encoding="utf-8") as f:
        # Detect delimiter
        csv_sample = f.read(4096)
        f.seek(0)

        dialect = csv.Sniffer().sniff(csv_sample, delimiters="\t,")
        reader = csv.DictReader(f, dialect=dialect)

        for row in reader:
            # Handle different column name variations
            video_id = row.get("VIDEO_ID", row.get("video_id", ""))
            sentence_id = row.get("SENTENCE_ID", row.get("sentence_id", ""))
            sentence_name = row.get("SENTENCE_NAME", row.get("sentence_name", ""))
            start = row.get("START_REALIGNED", row.get("start", row.get("START", "")))
            end = row.get("END_REALIGNED", row.get("end", row.get("END", "")))
            sentence = row.get("SENTENCE", row.get("sentence", row.get("translation", "")))

            sample_id = sentence_id if sentence_id else f"{video_id}_{sentence_name}"
            sample_id = _sanitize_filename(sample_id)

            segments = []
            duration_sec = None

            try:
                start_sec = float(start) if start else 0
                end_sec = float(end) if end else None
                if end_sec is not None:
                    segments.append(
                        Segment(
                            start=start_sec,
                            end=end_sec,
                            label=sentence,
                            tier=TIER_TRANSLATION,
                        )
                    )
                    duration_sec = end_sec
            except (ValueError, TypeError) as e:
                logger.debug(
                    "Could not parse timing for sample %s: %s (start=%r, end=%r)",
                    sample_id,
                    e,
                    start,
                    end,
                )

            sample = Sample(
                id=sample_id,
                segments=SegmentList(segments) if segments else None,
                text=sentence if sentence else None,
                dataset=dataset,
                split=split,
                metadata={
                    "video_id": video_id,
                    "sentence_id": sentence_id,
                    "video_name": row.get("VIDEO_NAME", row.get("video_name", "")),
                },
            )

            # Video relative path
            video_name = row.get("VIDEO_NAME", row.get("video_name", ""))
            video_relative = f"videos/{video_name}.mp4" if video_name else None

            output_path = output_dir / f"{sample_id}.json"
            write_unified_annotation(
                sample,
                output_path,
                source_format="how2sign_csv",
                source_path=str(csv_path),
                video_relative_path=video_relative,
                duration_sec=duration_sec,
            )
            output_files.append(output_path)

    return output_files


# =============================================================================
# ASL Citizen CSV Converter
# =============================================================================


@register_converter("asl_citizen_csv")
def convert_asl_citizen_csv(
    csv_path: str | Path,
    output_dir: str | Path,
    dataset: str = "asl_citizen",
    split: str | None = None,
) -> list[Path]:
    """
    Convert ASL Citizen CSV annotations to unified format.

    Args:
        csv_path: Path to CSV file
        output_dir: Output directory
        dataset: Dataset name
        split: Data split

    Returns:
        List of output file paths
    """
    csv_path = Path(csv_path)
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    # Infer split from filename
    if split is None:
        filename = csv_path.stem.lower()
        if "train" in filename:
            split = "train"
        elif "test" in filename:
            split = "test"
        elif "val" in filename or "dev" in filename:
            split = "val"

    output_files = []

    with open(csv_path, encoding="utf-8") as f:
        reader = csv.DictReader(f)

        for row in reader:
            # Extract fields (ASL Citizen specific column names)
            video_id = row.get("video_id", row.get("id", ""))
            gloss = row.get("gloss", row.get("label", row.get("word", "")))
            signer_id = row.get("signer_id", row.get("participant_id", ""))

            sample_id = (
                _sanitize_filename(video_id)
                if video_id
                else _sanitize_filename(f"{gloss}_{signer_id}")
            )

            sample = Sample(
                id=sample_id,
                glosses=[gloss] if gloss else None,
                signer_id=signer_id if signer_id else None,
                dataset=dataset,
                split=split,
                metadata={
                    k: v for k, v in row.items() if k not in {"video_id", "gloss", "signer_id"}
                },
            )

            video_relative = f"videos/{video_id}.mp4" if video_id else None

            output_path = output_dir / f"{sample_id}.json"
            write_unified_annotation(
                sample,
                output_path,
                source_format="asl_citizen_csv",
                source_path=str(csv_path),
                video_relative_path=video_relative,
            )
            output_files.append(output_path)

    return output_files


# =============================================================================
# BOBSL Converter (TSV + VTT + Mouthings + Dict Spottings)
# =============================================================================


def _load_bobsl_mouthings_json(mouthings_path: Path) -> dict[str, list[dict]]:
    """
    Load BOBSL mouthings from JSON format.

    The mouthings.json has structure:
    {
        "train": {
            "word": {
                "names": ["video_id1", ...],      # Video IDs
                "global_times": [501.276, ...],   # Center time in seconds
                "probs": [0.93872, ...]           # Confidence scores
            },
            ...
        },
        "val": { ... },
        "public_test": { ... }
    }

    Returns dict mapping video_id -> list of mouthing segments
    """
    import json

    mouthings_by_video: dict[str, list[dict]] = {}

    with open(mouthings_path) as f:
        data = json.load(f)

    # Estimated duration for mouthing segments (center +/- half)
    MOUTHING_HALF_DURATION = 0.25  # 0.5 second total

    # Process all splits
    for split_name in ["train", "val", "public_test", "test"]:
        split_data = data.get(split_name, {})
        if not split_data:
            continue

        for word, word_data in split_data.items():
            names = word_data.get("names", [])
            times = word_data.get("global_times", [])
            probs = word_data.get("probs", [])

            # All lists should be same length
            for video_id, center_time, prob in zip(names, times, probs):
                if video_id not in mouthings_by_video:
                    mouthings_by_video[video_id] = []

                mouthings_by_video[video_id].append(
                    {
                        "label": word,
                        "start": max(0, center_time - MOUTHING_HALF_DURATION),
                        "end": center_time + MOUTHING_HALF_DURATION,
                        "confidence": prob,
                        "split": split_name,
                    }
                )

    # Sort each video's mouthings by start time
    for video_id in mouthings_by_video:
        mouthings_by_video[video_id].sort(key=lambda x: x["start"])

    return mouthings_by_video


def _load_bobsl_dict_spottings_json(spottings_path: Path) -> dict[str, list[dict]]:
    """
    Load BOBSL dictionary sign spottings from JSON format.

    Similar structure to mouthings but for sign spottings from dictionary.

    Returns dict mapping video_id -> list of gloss segments
    """
    import json

    spottings_by_video: dict[str, list[dict]] = {}

    with open(spottings_path) as f:
        data = json.load(f)

    # Sign duration estimate (slightly longer than mouthings)
    SIGN_HALF_DURATION = 0.4  # 0.8 second total

    # Process all splits
    for split_name in ["train", "val", "public_test", "test"]:
        split_data = data.get(split_name, {})
        if not split_data:
            continue

        for gloss, gloss_data in split_data.items():
            names = gloss_data.get("names", [])
            times = gloss_data.get("global_times", [])
            probs = gloss_data.get("probs", [])

            for video_id, center_time, prob in zip(names, times, probs):
                if video_id not in spottings_by_video:
                    spottings_by_video[video_id] = []

                spottings_by_video[video_id].append(
                    {
                        "label": gloss.upper(),  # Normalize glosses to uppercase
                        "start": max(0, center_time - SIGN_HALF_DURATION),
                        "end": center_time + SIGN_HALF_DURATION,
                        "confidence": prob,
                        "split": split_name,
                    }
                )

    # Sort by start time
    for video_id in spottings_by_video:
        spottings_by_video[video_id].sort(key=lambda x: x["start"])

    return spottings_by_video


@register_converter("bobsl")
def convert_bobsl(
    metadata_path: str | Path,
    subtitles_dir: str | Path,
    output_dir: str | Path,
    dataset: str = "bobsl",
    mouthings_path: str | Path | None = None,
    dict_spottings_path: str | Path | None = None,
    min_confidence: float = 0.0,
) -> list[Path]:
    """
    Convert BOBSL annotations to unified format.

    BOBSL uses:
    - TSV/JSON for video metadata
    - VTT files for subtitles (English translations)
    - JSON file for mouthing annotations (detected mouth shapes)
    - JSON file for dictionary sign spottings (detected signs)

    Args:
        metadata_path: Path to metadata file (TSV or JSON with video list)
        subtitles_dir: Directory containing .vtt subtitle files
        output_dir: Output directory
        dataset: Dataset name
        mouthings_path: Optional path to mouthings.json file
        dict_spottings_path: Optional path to dict_spottings.json file
        min_confidence: Minimum confidence threshold for mouthings/spottings (0.0-1.0)

    Returns:
        List of output file paths
    """
    metadata_path = Path(metadata_path)
    subtitles_dir = Path(subtitles_dir)
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    # Load mouthings if provided (JSON format)
    mouthings_by_video: dict[str, list[dict]] = {}
    if mouthings_path:
        mouthings_path = Path(mouthings_path)
        if mouthings_path.exists() and mouthings_path.suffix == ".json":
            mouthings_by_video = _load_bobsl_mouthings_json(mouthings_path)

    # Load dict spottings if provided (JSON format)
    spottings_by_video: dict[str, list[dict]] = {}
    if dict_spottings_path:
        dict_spottings_path = Path(dict_spottings_path)
        if dict_spottings_path.exists() and dict_spottings_path.suffix == ".json":
            spottings_by_video = _load_bobsl_dict_spottings_json(dict_spottings_path)

    output_files = []

    # Find all VTT files
    vtt_files = list(subtitles_dir.glob("**/*.vtt"))

    for vtt_path in vtt_files:
        video_id = vtt_path.stem
        sample_id = _sanitize_filename(video_id)

        # Parse VTT subtitles
        segments = _parse_vtt(vtt_path, tier=TIER_SUBTITLE)

        # Add mouthings for this video
        if video_id in mouthings_by_video:
            for m in mouthings_by_video[video_id]:
                if m.get("confidence", 1.0) >= min_confidence:
                    segments.append(
                        Segment(
                            start=m["start"],
                            end=m["end"],
                            label=m["label"],
                            tier=TIER_MOUTHING,
                            confidence=m.get("confidence"),
                        )
                    )

        # Add dict spottings (as gloss tier) for this video
        if video_id in spottings_by_video:
            for s in spottings_by_video[video_id]:
                if s.get("confidence", 1.0) >= min_confidence:
                    segments.append(
                        Segment(
                            start=s["start"],
                            end=s["end"],
                            label=s["label"],
                            tier=TIER_GLOSS,  # Sign spottings are glosses
                            confidence=s.get("confidence"),
                        )
                    )

        # Sort all segments by start time
        segments.sort(key=lambda seg: (seg.start, seg.tier))

        # Calculate duration
        duration_sec = None
        if segments:
            duration_sec = max(seg.end for seg in segments)

        # Determine split from mouthings/spottings metadata
        split = None
        if video_id in mouthings_by_video and mouthings_by_video[video_id]:
            split = mouthings_by_video[video_id][0].get("split")
        elif video_id in spottings_by_video and spottings_by_video[video_id]:
            split = spottings_by_video[video_id][0].get("split")

        sample = Sample(
            id=sample_id,
            segments=SegmentList(segments) if segments else None,
            dataset=dataset,
            split=split,
            metadata={"video_id": video_id},
        )

        video_relative = f"videos/{video_id}.mp4"

        output_path = output_dir / f"{sample_id}.json"
        write_unified_annotation(
            sample,
            output_path,
            source_format="bobsl",
            source_path=str(vtt_path),
            video_relative_path=video_relative,
            duration_sec=duration_sec,
        )
        output_files.append(output_path)

    return output_files


# =============================================================================
# CSL-Daily JSON Converter
# =============================================================================


@register_converter("csl_daily_json")
def convert_csl_daily_json(
    json_path: str | Path,
    output_dir: str | Path,
    dataset: str = "csl_daily",
) -> list[Path]:
    """
    Convert CSL-Daily JSON annotations to unified format.

    Args:
        json_path: Path to JSON file
        output_dir: Output directory
        dataset: Dataset name

    Returns:
        List of output file paths
    """
    json_path = Path(json_path)
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    with open(json_path, encoding="utf-8") as f:
        data = json.load(f)

    output_files = []

    # Handle both dict and list formats
    items = data.items() if isinstance(data, dict) else enumerate(data)

    for key, entry in items:
        if isinstance(entry, dict):
            sample_id = str(entry.get("video_id", entry.get("id", str(key))))
            gloss_list = entry.get("gloss", entry.get("glosses", []))
            text = entry.get("text", entry.get("translation", ""))
            split = entry.get("split")
            signer_id = entry.get("signer_id")
        else:
            sample_id = str(key)
            gloss_list = []
            text = str(entry) if entry else None
            split = None
            signer_id = None

        sample_id = _sanitize_filename(sample_id)

        # Parse glosses
        if isinstance(gloss_list, str):
            gloss_list = gloss_list.split()

        sample = Sample(
            id=sample_id,
            glosses=gloss_list if gloss_list else None,
            text=text if text else None,
            signer_id=signer_id,
            dataset=dataset,
            split=split,
        )

        output_path = output_dir / f"{sample_id}.json"
        write_unified_annotation(
            sample,
            output_path,
            source_format="csl_daily_json",
            source_path=str(json_path),
        )
        output_files.append(output_path)

    return output_files


# =============================================================================
# VTT/SRT Subtitle Converter
# =============================================================================


@register_converter("vtt")
def convert_vtt(
    vtt_path: str | Path,
    output_path: str | Path,
    dataset: str,
    sample_id: str | None = None,
    split: str | None = None,
    tier: str = TIER_SUBTITLE,
) -> Path:
    """
    Convert a VTT or SRT subtitle file to unified format.

    Args:
        vtt_path: Path to .vtt or .srt file
        output_path: Output JSON path
        dataset: Dataset name
        sample_id: Sample ID (defaults to filename stem)
        split: Data split
        tier: Tier name for segments

    Returns:
        Path to output file
    """
    vtt_path = Path(vtt_path)

    if sample_id is None:
        sample_id = vtt_path.stem

    segments = _parse_vtt(vtt_path, tier=tier)

    # Calculate duration
    duration_sec = None
    if segments:
        duration_sec = max(s.end for s in segments)

    # Extract text from segments
    text = " ".join(s.label for s in segments if s.label)

    sample = Sample(
        id=sample_id,
        segments=SegmentList(segments) if segments else None,
        text=text if text else None,
        dataset=dataset,
        split=split,
    )

    return write_unified_annotation(
        sample,
        output_path,
        source_format="vtt",
        source_path=str(vtt_path),
        duration_sec=duration_sec,
    )


def convert_vtt_directory(
    vtt_dir: str | Path,
    output_dir: str | Path,
    dataset: str,
    split: str | None = None,
    tier: str = TIER_SUBTITLE,
) -> list[Path]:
    """
    Convert all VTT/SRT files in a directory to unified format.

    Args:
        vtt_dir: Directory containing .vtt or .srt files
        output_dir: Output directory
        dataset: Dataset name
        split: Data split
        tier: Tier name for segments

    Returns:
        List of output file paths
    """
    vtt_dir = Path(vtt_dir)
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    output_files = []

    for vtt_path in sorted(vtt_dir.glob("**/*.vtt")) + sorted(vtt_dir.glob("**/*.srt")):
        sample_id = _sanitize_filename(vtt_path.stem)
        output_path = output_dir / f"{sample_id}.json"

        convert_vtt(
            vtt_path,
            output_path,
            dataset=dataset,
            sample_id=sample_id,
            split=split,
            tier=tier,
        )
        output_files.append(output_path)

    return output_files


# =============================================================================
# BSLZone Converter
# =============================================================================


@register_converter("bslzone")
def convert_bslzone(
    data_dir: str | Path,
    output_dir: str | Path,
    dataset: str = "bslzone",
) -> list[Path]:
    """
    Convert BSLZone annotations to unified format.

    BSLZone has:
    - VTT subtitle files
    - Scene list text files

    Args:
        data_dir: BSLZone data directory
        output_dir: Output directory
        dataset: Dataset name

    Returns:
        List of output file paths
    """
    data_dir = Path(data_dir)
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    output_files = []

    # Find VTT files
    vtt_dir = data_dir / "subtitles"
    if vtt_dir.exists():
        vtt_outputs = convert_vtt_directory(
            vtt_dir,
            output_dir,
            dataset=dataset,
            tier=TIER_SUBTITLE,
        )
        output_files.extend(vtt_outputs)

    # Also check for scene lists
    scenes_dir = data_dir / "scenes"
    if scenes_dir.exists():
        for scene_file in scenes_dir.glob("*.txt"):
            sample_id = _sanitize_filename(scene_file.stem)
            output_path = output_dir / f"{sample_id}_scenes.json"

            scenes = _parse_scene_list(scene_file)

            sample = Sample(
                id=sample_id,
                segments=SegmentList(scenes) if scenes else None,
                dataset=dataset,
                metadata={"type": "scene_list"},
            )

            write_unified_annotation(
                sample,
                output_path,
                source_format="bslzone_scenes",
                source_path=str(scene_file),
            )
            output_files.append(output_path)

    return output_files


# =============================================================================
# Phoenix CSV Converter
# =============================================================================


@register_converter("phoenix_csv")
def convert_phoenix_csv(
    csv_path: str | Path,
    output_dir: str | Path,
    dataset: str = "phoenix",
    split: str | None = None,
) -> list[Path]:
    """
    Convert Phoenix-2014 CSV annotations to unified format.

    Args:
        csv_path: Path to CSV file
        output_dir: Output directory
        dataset: Dataset name
        split: Data split

    Returns:
        List of output file paths
    """
    csv_path = Path(csv_path)
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    # Infer split
    if split is None:
        filename = csv_path.stem.lower()
        if "train" in filename:
            split = "train"
        elif "test" in filename:
            split = "test"
        elif "dev" in filename or "val" in filename:
            split = "val"

    output_files = []

    with open(csv_path, encoding="utf-8") as f:
        reader = csv.DictReader(f, delimiter="|")

        for row in reader:
            sample_id = row.get("id", row.get("name", ""))
            sample_id = _sanitize_filename(sample_id)

            # Parse annotation field
            annotation = row.get("annotation", row.get("orth", ""))
            glosses = annotation.split() if annotation else []

            # Parse translation
            translation = row.get("translation", row.get("de", ""))

            sample = Sample(
                id=sample_id,
                glosses=glosses if glosses else None,
                text=translation if translation else None,
                signer_id=row.get("speaker", row.get("signer")),
                dataset=dataset,
                split=split,
                metadata={
                    k: v
                    for k, v in row.items()
                    if k
                    not in {
                        "id",
                        "name",
                        "annotation",
                        "orth",
                        "translation",
                        "de",
                        "speaker",
                        "signer",
                    }
                },
            )

            # Video path
            video_name = row.get("video", row.get("name", sample_id))
            video_relative = f"videos/{video_name}" if video_name else None

            output_path = output_dir / f"{sample_id}.json"
            write_unified_annotation(
                sample,
                output_path,
                source_format="phoenix_csv",
                source_path=str(csv_path),
                video_relative_path=video_relative,
            )
            output_files.append(output_path)

    return output_files


# =============================================================================
# YTSL-25 TSV Converter
# =============================================================================


@register_converter("ytsl_tsv")
def convert_ytsl_tsv(
    tsv_path: str | Path,
    output_dir: str | Path,
    dataset: str = "ytsl_25",
    split: str | None = None,
) -> list[Path]:
    """
    Convert YTSL-25 TSV annotations to unified format.

    Args:
        tsv_path: Path to TSV file
        output_dir: Output directory
        dataset: Dataset name
        split: Data split

    Returns:
        List of output file paths
    """
    tsv_path = Path(tsv_path)
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    output_files = []

    with open(tsv_path, encoding="utf-8") as f:
        reader = csv.DictReader(f, delimiter="\t")

        for row in reader:
            video_id = row.get("video_id", row.get("id", ""))
            sample_id = _sanitize_filename(video_id)

            # Get caption/text
            caption = row.get("caption", row.get("text", row.get("subtitle", "")))

            # Get language
            language = row.get("language", row.get("lang", ""))

            sample = Sample(
                id=sample_id,
                text=caption if caption else None,
                dataset=dataset,
                split=split,
                metadata={
                    "video_id": video_id,
                    "language": language,
                    **{
                        k: v
                        for k, v in row.items()
                        if k
                        not in {"video_id", "id", "caption", "text", "subtitle", "language", "lang"}
                    },
                },
            )

            video_relative = f"videos/{video_id}.mp4" if video_id else None

            output_path = output_dir / f"{sample_id}.json"
            write_unified_annotation(
                sample,
                output_path,
                source_format="ytsl_tsv",
                source_path=str(tsv_path),
                video_relative_path=video_relative,
            )
            output_files.append(output_path)

    return output_files


# =============================================================================
# YouTube-ASL JSON Caption Converter
# =============================================================================


@register_converter("youtube_asl_json")
def convert_youtube_asl_json(
    json_path: str | Path,
    output_path: str | Path,
    dataset: str = "yt_asl",
    sample_id: str | None = None,
    split: str | None = None,
) -> Path:
    """
    Convert a YouTube-ASL JSON caption file to unified format.

    YouTube-ASL uses a custom JSON format:
    [{"text": "...", "start": 0.22, "duration": 2.08}, ...]

    Args:
        json_path: Path to JSON caption file
        output_path: Output JSON file path
        dataset: Dataset name
        sample_id: Sample ID (defaults to filename stem)
        split: Data split

    Returns:
        Path to output file
    """
    json_path = Path(json_path)
    output_path = Path(output_path)

    if sample_id is None:
        sample_id = json_path.stem

    with open(json_path, encoding="utf-8") as f:
        captions = json.load(f)

    # Convert captions to segments
    segments = []
    for cap in captions:
        start = cap.get("start", 0)
        duration = cap.get("duration", 0)
        end = start + duration
        text = cap.get("text", "")

        if text:
            segments.append(
                Segment(
                    start=start,
                    end=end,
                    label=text,
                    tier=TIER_CAPTION,
                )
            )

    # Calculate duration
    duration_sec = None
    if segments:
        duration_sec = max(s.end for s in segments)

    # Build full text
    text = " ".join(cap.get("text", "") for cap in captions)

    sample = Sample(
        id=sample_id,
        segments=SegmentList(segments) if segments else None,
        text=text if text else None,
        dataset=dataset,
        split=split,
    )

    return write_unified_annotation(
        sample,
        output_path,
        source_format="youtube_asl_json",
        source_path=str(json_path),
        duration_sec=duration_sec,
    )


def convert_youtube_asl_directory(
    captions_dir: str | Path,
    output_dir: str | Path,
    dataset: str = "yt_asl",
    split: str | None = None,
) -> list[Path]:
    """
    Convert all YouTube-ASL caption files in a directory.

    Args:
        captions_dir: Directory containing JSON caption files
        output_dir: Output directory
        dataset: Dataset name
        split: Data split

    Returns:
        List of output file paths
    """
    captions_dir = Path(captions_dir)
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    output_files = []

    for json_file in sorted(captions_dir.glob("*.json")):
        sample_id = _sanitize_filename(json_file.stem)
        output_path = output_dir / f"{sample_id}.json"

        try:
            convert_youtube_asl_json(
                json_file,
                output_path,
                dataset=dataset,
                sample_id=sample_id,
                split=split,
            )
            output_files.append(output_path)
        except (json.JSONDecodeError, KeyError, ValueError) as e:
            logger.warning(
                "Failed to convert YouTube-ASL JSON file %s: %s",
                json_file.name,
                e,
            )
        except OSError as e:
            logger.warning(
                "IO error converting YouTube-ASL JSON file %s: %s",
                json_file.name,
                e,
            )

    return output_files


# =============================================================================
# Helper Functions
# =============================================================================


def _sanitize_filename(name: str) -> str:
    """Sanitize a string for use as a filename."""
    # Replace problematic characters
    name = re.sub(r'[<>:"/\\|?*]', "_", name)
    # Remove leading/trailing whitespace and dots
    name = name.strip(". ")
    # Limit length
    if len(name) > 200:
        name = name[:200]
    return name or "unknown"


def _parse_vtt(path: Path, tier: str = TIER_SUBTITLE) -> list[Segment]:
    """Parse a VTT or SRT subtitle file into segments."""
    segments = []

    with open(path, encoding="utf-8") as f:
        content = f.read()

    # Detect format
    is_srt = path.suffix.lower() == ".srt" or not content.startswith("WEBVTT")

    # Parse timecodes - regex for HH:MM:SS,mmm --> HH:MM:SS,mmm format
    timecode = r"(\d{2}:\d{2}:\d{2}[,\.]\d{3})"
    if is_srt:
        # SRT format: 00:00:01,000 --> 00:00:04,000
        pattern = rf"{timecode}\s*-->\s*{timecode}\s*\n(.+?)(?=\n\n|\n\d+\n|\Z)"
    else:
        # VTT format: 00:00:01.000 --> 00:00:04.000
        pattern = rf"{timecode}\s*-->\s*{timecode}\s*\n(.+?)(?=\n\n|\Z)"

    matches = re.findall(pattern, content, re.DOTALL)

    for start_str, end_str, text in matches:
        start_sec = _parse_timecode(start_str)
        end_sec = _parse_timecode(end_str)

        # Clean text
        text = text.strip()
        # Remove HTML tags
        text = re.sub(r"<[^>]+>", "", text)
        # Remove speaker labels
        text = re.sub(r"^\[.+?\]\s*", "", text)

        if text and start_sec is not None and end_sec is not None:
            segments.append(
                Segment(
                    start=start_sec,
                    end=end_sec,
                    label=text,
                    tier=tier,
                )
            )

    return segments


def _parse_timecode(timecode: str) -> float | None:
    """Parse a timecode string to seconds."""
    # Handle both , and . as decimal separator
    timecode = timecode.replace(",", ".")

    # Try HH:MM:SS.mmm format
    match = re.match(r"(\d+):(\d+):(\d+)\.(\d+)", timecode)
    if match:
        hours, minutes, seconds, millis = match.groups()
        return int(hours) * 3600 + int(minutes) * 60 + int(seconds) + int(millis) / 1000

    # Try MM:SS.mmm format
    match = re.match(r"(\d+):(\d+)\.(\d+)", timecode)
    if match:
        minutes, seconds, millis = match.groups()
        return int(minutes) * 60 + int(seconds) + int(millis) / 1000

    return None


def _parse_scene_list(path: Path) -> list[Segment]:
    """Parse a scene list text file into segments."""
    segments = []

    with open(path, encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if not line:
                continue

            # Try to parse scene markers
            # Format could be: "00:01:30 - Scene 1: Introduction"
            match = re.match(r"(\d{2}:\d{2}:\d{2})\s*[-:]\s*(.+)", line)
            if match:
                start_str, label = match.groups()
                start_sec = _parse_timecode(start_str + ".000")
                if start_sec is not None:
                    segments.append(
                        Segment(
                            start=start_sec,
                            end=start_sec + 1.0,  # Placeholder end time
                            label=label.strip(),
                            tier="scene",
                        )
                    )

    return segments


# =============================================================================
# Batch Conversion
# =============================================================================


@dataclass
class ConversionResult:
    """Result of a conversion operation."""

    dataset: str
    source_format: str
    files_created: int
    output_dir: Path
    errors: list[str]
    duration_sec: float


def convert_dataset(
    dataset: str,
    source_dir: str | Path,
    output_dir: str | Path,
    source_format: str | None = None,
    **kwargs,
) -> ConversionResult:
    """
    Convert a dataset's annotations to unified format.

    Args:
        dataset: Dataset name
        source_dir: Directory containing source annotation files
        output_dir: Output directory for unified annotations
        source_format: Source format (auto-detected if not specified)
        **kwargs: Additional arguments passed to the converter

    Returns:
        ConversionResult with conversion statistics
    """
    import time

    source_dir = Path(source_dir)
    output_dir = Path(output_dir)

    start_time = time.time()
    errors = []
    files_created = 0

    # Auto-detect format if not specified
    if source_format is None:
        source_format = _detect_format(source_dir)
    if source_format is None:
        return ConversionResult(
            dataset=dataset,
            source_format="unknown",
            files_created=0,
            output_dir=output_dir,
            errors=["Could not detect source format"],
            duration_sec=0,
        )

    converter = get_converter(source_format)
    if converter is None:
        return ConversionResult(
            dataset=dataset,
            source_format=source_format,
            files_created=0,
            output_dir=output_dir,
            errors=[f"No converter found for format: {source_format}"],
            duration_sec=0,
        )

    try:
        # Call the appropriate converter
        if source_format == "elan":
            # Process all .eaf files
            for eaf_file in source_dir.glob("**/*.eaf"):
                try:
                    sample_id = eaf_file.stem
                    output_path = output_dir / f"{sample_id}.json"
                    convert_elan(
                        eaf_file,
                        output_path,
                        dataset=dataset,
                        **kwargs,
                    )
                    files_created += 1
                except Exception as e:
                    errors.append(f"{eaf_file.name}: {e}")

        elif source_format in {"wlasl_json", "msasl_json", "asldict_json", "csl_daily_json"}:
            # Find JSON file
            json_files = list(source_dir.glob("*.json"))
            for json_file in json_files:
                try:
                    output_files = converter(json_file, output_dir, dataset=dataset, **kwargs)
                    files_created += len(output_files)
                except Exception as e:
                    errors.append(f"{json_file.name}: {e}")

        elif source_format in {"how2sign_csv", "asl_citizen_csv", "phoenix_csv"}:
            # Process CSV files
            csv_files = list(source_dir.glob("**/*.csv"))
            for csv_file in csv_files:
                try:
                    output_files = converter(csv_file, output_dir, dataset=dataset, **kwargs)
                    files_created += len(output_files)
                except Exception as e:
                    errors.append(f"{csv_file.name}: {e}")

        elif source_format == "ytsl_tsv":
            # Process TSV files
            tsv_files = list(source_dir.glob("**/*.tsv"))
            for tsv_file in tsv_files:
                try:
                    output_files = converter(tsv_file, output_dir, dataset=dataset, **kwargs)
                    files_created += len(output_files)
                except Exception as e:
                    errors.append(f"{tsv_file.name}: {e}")

        elif source_format == "vtt":
            output_files = convert_vtt_directory(
                source_dir,
                output_dir,
                dataset=dataset,
                **kwargs,
            )
            files_created = len(output_files)

        elif source_format == "bobsl":
            # BOBSL needs metadata, subtitles dir, and optional mouthings/spottings
            metadata_path = source_dir / "metadata_public_episodes.tsv"
            if not metadata_path.exists():
                metadata_path = source_dir / "metadata.json"

            # Use subtitles_dir from kwargs, or default location
            subtitles_dir = kwargs.pop("subtitles_dir", None)
            if subtitles_dir is None:
                subtitles_dir = source_dir / "subtitles" / "audio-aligned"
                if not subtitles_dir.exists():
                    subtitles_dir = source_dir / "subtitles" / "raw"

            output_files = convert_bobsl(
                metadata_path,
                subtitles_dir,
                output_dir,
                dataset=dataset,
                **kwargs,
            )
            files_created = len(output_files)

        elif source_format == "bslzone":
            output_files = convert_bslzone(source_dir, output_dir, dataset=dataset, **kwargs)
            files_created = len(output_files)

        else:
            errors.append(f"Unsupported format: {source_format}")

    except Exception as e:
        errors.append(f"Conversion failed: {e}")

    duration = time.time() - start_time

    return ConversionResult(
        dataset=dataset,
        source_format=source_format,
        files_created=files_created,
        output_dir=output_dir,
        errors=errors,
        duration_sec=duration,
    )


def _detect_format(source_dir: Path) -> str | None:
    """Auto-detect annotation format from directory contents."""
    # Check for ELAN files
    if list(source_dir.glob("**/*.eaf")):
        return "elan"

    # Check for VTT/SRT
    if list(source_dir.glob("**/*.vtt")) or list(source_dir.glob("**/*.srt")):
        return "vtt"

    # Check for JSON files
    json_files = list(source_dir.glob("*.json"))
    if json_files:
        # Try to detect JSON type from content
        with open(json_files[0], encoding="utf-8") as f:
            try:
                data = json.load(f)
                if isinstance(data, list) and data:
                    first = data[0]
                    if "gloss" in first and "instances" in first:
                        return "wlasl_json"
                    if "word" in first and "entries" in first:
                        return "asldict_json"
                    if "clean_text" in first or "text" in first:
                        return "msasl_json"
            except (json.JSONDecodeError, KeyError, IndexError):
                pass
        return "csl_daily_json"  # Default JSON format

    # Check for CSV
    csv_files = list(source_dir.glob("**/*.csv"))
    if csv_files:
        # Check column names
        with open(csv_files[0], encoding="utf-8") as f:
            header = f.readline().upper()
            if "SENTENCE" in header or "REALIGNED" in header:
                return "how2sign_csv"
            if "ANNOTATION" in header or "ORTH" in header:
                return "phoenix_csv"
        return "asl_citizen_csv"

    # Check for TSV
    if list(source_dir.glob("**/*.tsv")):
        return "ytsl_tsv"

    return None
