"""
ELAN .eaf file reading and writing.

ELAN is the standard annotation tool for sign language linguistics.
This module provides seamless import/export of SLTK data structures.

References:
    - ELAN: https://archive.mpi.nl/tla/elan
    - EAF format: https://www.mpi.nl/tools/elan/EAF_Annotation_Format_3.0_and_ELAN.pdf
"""

import logging
from datetime import datetime
from pathlib import Path
from typing import Any
from xml.etree import ElementTree as ET

from sltk.data.core import Segment, SegmentList
from sltk.exceptions import SLTKFileNotFoundError

_logger = logging.getLogger(__name__)

# Use defusedxml for safe XML parsing (prevents XML bomb / billion laughs attacks).
# Falls back to stdlib with a warning if defusedxml is not installed.
try:
    from defusedxml import ElementTree as SafeET
    from defusedxml import minidom as safe_minidom
except ImportError:
    _logger.warning(
        "defusedxml is not installed. XML parsing will use stdlib xml.etree.ElementTree, "
        "which is vulnerable to XML bomb attacks. Install defusedxml: pip install defusedxml"
    )
    SafeET = ET  # type: ignore[assignment,misc]
    from xml.dom import minidom as safe_minidom  # type: ignore[no-redef]


def read_eaf(
    path: str | Path,
    tiers: list[str] | None = None,
    include_empty: bool = False,
) -> SegmentList:
    """
    Read segments from an ELAN .eaf file.

    Args:
        path: Path to .eaf file
        tiers: Optional list of tier names to load. If None, loads all tiers.
        include_empty: Whether to include annotations with empty values

    Returns:
        SegmentList containing all annotations

    Example:
        >>> segments = read_eaf("annotations.eaf")
        >>> gloss_segments = segments.filter_by_tier("Gloss")
        >>> print(f"Found {len(gloss_segments)} glosses")
    """
    path = Path(path)

    if not path.exists():
        raise SLTKFileNotFoundError(str(path))

    tree = SafeET.parse(path)
    root = tree.getroot()

    # Build time slot mapping (ms -> seconds)
    time_slots: dict[str, float] = {}
    for ts in root.findall(".//TIME_SLOT"):
        ts_id = ts.get("TIME_SLOT_ID")
        ts_value = ts.get("TIME_VALUE")
        if ts_id and ts_value:
            time_slots[ts_id] = int(ts_value) / 1000.0  # ms to seconds

    # Extract annotations
    segments = []
    for tier in root.findall(".//TIER"):
        tier_id = tier.get("TIER_ID")

        if tier_id is None:
            continue

        if tiers is not None and tier_id not in tiers:
            continue

        # Handle alignable annotations (time-aligned)
        for annotation in tier.findall(".//ALIGNABLE_ANNOTATION"):
            start_slot = annotation.get("TIME_SLOT_REF1")
            end_slot = annotation.get("TIME_SLOT_REF2")

            if start_slot not in time_slots or end_slot not in time_slots:
                continue

            value_elem = annotation.find("ANNOTATION_VALUE")
            value = ""
            if value_elem is not None and value_elem.text:
                value = value_elem.text.strip()

            if not include_empty and not value:
                continue

            segments.append(
                Segment(
                    start=time_slots[start_slot],
                    end=time_slots[end_slot],
                    label=value if value else None,
                    tier=tier_id,
                    metadata={
                        "annotation_id": annotation.get("ANNOTATION_ID"),
                    },
                )
            )

    return SegmentList(segments)


def write_eaf(
    segments: SegmentList,
    output_path: str | Path,
    video_path: str | Path | None = None,
    author: str = "SLTK",
    tier_types: dict[str, str] | None = None,
) -> Path:
    """
    Write segments to an ELAN .eaf file.

    Args:
        segments: SegmentList to export
        output_path: Output .eaf file path
        video_path: Optional path to linked video file
        author: Author name for ELAN file metadata
        tier_types: Optional mapping of tier names to linguistic types

    Returns:
        Path to created .eaf file

    Example:
        >>> segments = SegmentList([
        ...     Segment(0.0, 1.5, "HELLO", tier="Gloss"),
        ...     Segment(1.5, 3.0, "WORLD", tier="Gloss"),
        ... ])
        >>> write_eaf(segments, "output.eaf", video_path="source.mp4")
    """
    output_path = Path(output_path)
    output_path.parent.mkdir(parents=True, exist_ok=True)

    # Create root element
    root = ET.Element("ANNOTATION_DOCUMENT")
    root.set("AUTHOR", author)
    root.set("DATE", datetime.now().strftime("%Y-%m-%dT%H:%M:%S+00:00"))
    root.set("FORMAT", "3.0")
    root.set("VERSION", "3.0")
    root.set("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
    root.set(
        "xsi:noNamespaceSchemaLocation",
        "http://www.mpi.nl/tools/elan/EAFv3.0.xsd",
    )

    # Add header
    header = ET.SubElement(root, "HEADER")
    header.set("MEDIA_FILE", "")
    header.set("TIME_UNITS", "milliseconds")

    if video_path is not None:
        video_path = Path(video_path)
        media_desc = ET.SubElement(header, "MEDIA_DESCRIPTOR")
        media_desc.set("MEDIA_URL", f"file:///{video_path.absolute()}")
        media_desc.set("MIME_TYPE", _get_mime_type(video_path))
        media_desc.set("RELATIVE_MEDIA_URL", f"./{video_path.name}")

    # Build time slots
    time_order = ET.SubElement(root, "TIME_ORDER")
    time_slot_map: dict[float, str] = {}  # time_sec -> slot_id
    slot_counter = 1

    # Collect all unique times
    all_times = set()
    for seg in segments:
        all_times.add(seg.start)
        all_times.add(seg.end)

    for time_sec in sorted(all_times):
        slot_id = f"ts{slot_counter}"
        time_slot_map[time_sec] = slot_id

        ts_elem = ET.SubElement(time_order, "TIME_SLOT")
        ts_elem.set("TIME_SLOT_ID", slot_id)
        ts_elem.set("TIME_VALUE", str(int(time_sec * 1000)))  # seconds to ms

        slot_counter += 1

    # Group segments by tier
    tier_segments: dict[str, list[Segment]] = {}
    for seg in segments:
        if seg.tier not in tier_segments:
            tier_segments[seg.tier] = []
        tier_segments[seg.tier].append(seg)

    # Create tiers and annotations
    annotation_counter = 1
    for tier_id, tier_segs in tier_segments.items():
        tier_elem = ET.SubElement(root, "TIER")
        ling_type = "default-lt"
        if tier_types and tier_id in tier_types:
            ling_type = tier_types[tier_id]
        tier_elem.set("LINGUISTIC_TYPE_REF", ling_type)
        tier_elem.set("TIER_ID", tier_id)

        for seg in sorted(tier_segs, key=lambda s: s.start):
            ann_elem = ET.SubElement(tier_elem, "ANNOTATION")
            align_ann = ET.SubElement(ann_elem, "ALIGNABLE_ANNOTATION")
            align_ann.set("ANNOTATION_ID", f"a{annotation_counter}")
            align_ann.set("TIME_SLOT_REF1", time_slot_map[seg.start])
            align_ann.set("TIME_SLOT_REF2", time_slot_map[seg.end])

            value_elem = ET.SubElement(align_ann, "ANNOTATION_VALUE")
            value_elem.text = seg.label or ""

            annotation_counter += 1

    # Add linguistic type
    ling_type_elem = ET.SubElement(root, "LINGUISTIC_TYPE")
    ling_type_elem.set("GRAPHIC_REFERENCES", "false")
    ling_type_elem.set("LINGUISTIC_TYPE_ID", "default-lt")
    ling_type_elem.set("TIME_ALIGNABLE", "true")

    # Write with pretty formatting
    xml_str = ET.tostring(root, encoding="unicode")
    pretty_xml = safe_minidom.parseString(xml_str).toprettyxml(indent="    ")

    # Remove extra blank lines from minidom output
    lines = [line for line in pretty_xml.split("\n") if line.strip()]
    pretty_xml = "\n".join(lines)

    with open(output_path, "w", encoding="utf-8") as f:
        f.write(pretty_xml)

    return output_path


def get_eaf_tiers(path: str | Path) -> list[str]:
    """
    Get list of tier names from an ELAN file without loading all annotations.

    Args:
        path: Path to .eaf file

    Returns:
        List of tier names
    """
    path = Path(path)
    tree = SafeET.parse(path)
    root = tree.getroot()

    tiers = []
    for tier in root.findall(".//TIER"):
        tier_id = tier.get("TIER_ID")
        if tier_id:
            tiers.append(tier_id)

    return tiers


def get_eaf_metadata(path: str | Path) -> dict[str, Any]:
    """
    Get metadata from an ELAN file.

    Args:
        path: Path to .eaf file

    Returns:
        Dictionary with metadata (author, date, media files, etc.)
    """
    path = Path(path)
    tree = SafeET.parse(path)
    root = tree.getroot()

    metadata = {
        "author": root.get("AUTHOR"),
        "date": root.get("DATE"),
        "format": root.get("FORMAT"),
        "version": root.get("VERSION"),
        "tiers": get_eaf_tiers(path),
        "media_files": [],
    }

    # Get linked media files
    for media_desc in root.findall(".//MEDIA_DESCRIPTOR"):
        metadata["media_files"].append(
            {
                "url": media_desc.get("MEDIA_URL"),
                "mime_type": media_desc.get("MIME_TYPE"),
                "relative_url": media_desc.get("RELATIVE_MEDIA_URL"),
            }
        )

    return metadata


def add_tier_to_eaf(
    source_path: str | Path,
    segments: SegmentList,
    output_path: str | Path,
    tier_name: str = "AI_Segmentation",
    linguistic_type: str = "default-lt",
) -> Path:
    """
    Add a new tier to an existing ELAN file.

    This function reads an existing ELAN file, adds a new tier with the
    provided segments, and writes to a new output file. The original file
    is never modified.

    Args:
        source_path: Path to existing .eaf file
        segments: SegmentList with annotations to add as new tier
        output_path: Path for new .eaf file (must be different from source)
        tier_name: Name for the new tier (default: "AI_Segmentation")
        linguistic_type: Linguistic type reference for the tier

    Returns:
        Path to the created .eaf file

    Raises:
        SLTKFileNotFoundError: If source file doesn't exist
        ValueError: If output_path is same as source_path

    Example:
        >>> ai_segments = SegmentList([
        ...     Segment(0.5, 2.0, "SIGN_1", tier="AI_Segmentation"),
        ...     Segment(2.5, 4.0, "SIGN_2", tier="AI_Segmentation"),
        ... ])
        >>> add_tier_to_eaf(
        ...     "original.eaf",
        ...     ai_segments,
        ...     "original_with_ai.eaf",
        ...     tier_name="AI_Segmentation_20240101"
        ... )
    """
    source_path = Path(source_path)
    output_path = Path(output_path)

    if not source_path.exists():
        raise SLTKFileNotFoundError(str(source_path))

    # Prevent overwriting source file
    if source_path.resolve() == output_path.resolve():
        raise ValueError(
            f"Output path cannot be the same as source path: {source_path}. "
            "Please specify a different output file to preserve the original."
        )

    # Parse existing file
    tree = SafeET.parse(source_path)
    root = tree.getroot()

    # Get existing time slots and find the highest slot number
    time_order = root.find(".//TIME_ORDER")
    if time_order is None:
        raise ValueError("Invalid ELAN file: missing TIME_ORDER element")

    existing_slots: dict[str, int] = {}  # slot_id -> time_ms
    max_slot_num = 0
    for ts in time_order.findall("TIME_SLOT"):
        ts_id = ts.get("TIME_SLOT_ID", "")
        ts_value = ts.get("TIME_VALUE")
        if ts_id and ts_value:
            existing_slots[ts_id] = int(ts_value)
            # Extract number from slot id like "ts123"
            if ts_id.startswith("ts"):
                try:
                    slot_num = int(ts_id[2:])
                    max_slot_num = max(max_slot_num, slot_num)
                except ValueError:
                    pass

    # Build reverse mapping: time_ms -> slot_id
    time_to_slot: dict[int, str] = {v: k for k, v in existing_slots.items()}

    # Find highest annotation ID
    max_ann_num = 0
    for ann in root.findall(".//ALIGNABLE_ANNOTATION"):
        ann_id = ann.get("ANNOTATION_ID", "")
        if ann_id.startswith("a"):
            try:
                ann_num = int(ann_id[1:])
                max_ann_num = max(max_ann_num, ann_num)
            except ValueError:
                pass

    # Collect new time slots needed and create them
    slot_counter = max_slot_num + 1
    new_time_slot_map: dict[int, str] = {}  # time_ms -> slot_id

    for seg in segments:
        start_ms = int(seg.start * 1000)
        end_ms = int(seg.end * 1000)

        for time_ms in [start_ms, end_ms]:
            if time_ms not in time_to_slot and time_ms not in new_time_slot_map:
                slot_id = f"ts{slot_counter}"
                new_time_slot_map[time_ms] = slot_id

                ts_elem = ET.SubElement(time_order, "TIME_SLOT")
                ts_elem.set("TIME_SLOT_ID", slot_id)
                ts_elem.set("TIME_VALUE", str(time_ms))

                slot_counter += 1

    # Merge the slot mappings
    full_time_to_slot = {**time_to_slot, **new_time_slot_map}

    # Check if the tier already exists
    existing_tier_names = [t.get("TIER_ID") for t in root.findall(".//TIER")]
    if tier_name in existing_tier_names:
        # Generate unique name with timestamp
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        tier_name = f"{tier_name}_{timestamp}"

    # Find the position to insert the new tier (after existing tiers, before LINGUISTIC_TYPE)
    tiers = root.findall(".//TIER")
    if tiers:
        insert_index = list(root).index(tiers[-1]) + 1
    else:
        # Insert after TIME_ORDER
        insert_index = list(root).index(time_order) + 1

    # Create new tier element
    tier_elem = ET.Element("TIER")
    tier_elem.set("LINGUISTIC_TYPE_REF", linguistic_type)
    tier_elem.set("TIER_ID", tier_name)

    # Add annotations to the tier
    annotation_counter = max_ann_num + 1
    for seg in sorted(segments, key=lambda s: s.start):
        start_ms = int(seg.start * 1000)
        end_ms = int(seg.end * 1000)

        ann_elem = ET.SubElement(tier_elem, "ANNOTATION")
        align_ann = ET.SubElement(ann_elem, "ALIGNABLE_ANNOTATION")
        align_ann.set("ANNOTATION_ID", f"a{annotation_counter}")
        align_ann.set("TIME_SLOT_REF1", full_time_to_slot[start_ms])
        align_ann.set("TIME_SLOT_REF2", full_time_to_slot[end_ms])

        value_elem = ET.SubElement(align_ann, "ANNOTATION_VALUE")
        value_elem.text = seg.label or ""

        annotation_counter += 1

    # Insert the tier at the correct position
    root.insert(insert_index, tier_elem)

    # Ensure the linguistic type exists
    ling_types = [lt.get("LINGUISTIC_TYPE_ID") for lt in root.findall(".//LINGUISTIC_TYPE")]
    if linguistic_type not in ling_types:
        # Find position for LINGUISTIC_TYPE (at the end, before any CONSTRAINT elements)
        constraints = root.findall(".//CONSTRAINT")
        if constraints:
            ling_insert_index = list(root).index(constraints[0])
        else:
            ling_insert_index = len(root)

        ling_type_elem = ET.Element("LINGUISTIC_TYPE")
        ling_type_elem.set("GRAPHIC_REFERENCES", "false")
        ling_type_elem.set("LINGUISTIC_TYPE_ID", linguistic_type)
        ling_type_elem.set("TIME_ALIGNABLE", "true")
        root.insert(ling_insert_index, ling_type_elem)

    # Write output file with pretty formatting
    output_path.parent.mkdir(parents=True, exist_ok=True)

    xml_str = ET.tostring(root, encoding="unicode")
    pretty_xml = safe_minidom.parseString(xml_str).toprettyxml(indent="    ")

    # Remove extra blank lines from minidom output
    lines = [line for line in pretty_xml.split("\n") if line.strip()]
    pretty_xml = "\n".join(lines)

    with open(output_path, "w", encoding="utf-8") as f:
        f.write(pretty_xml)

    return output_path


def _get_mime_type(path: Path) -> str:
    """Get MIME type from file extension."""
    suffix = path.suffix.lower()
    mime_types = {
        ".mp4": "video/mp4",
        ".avi": "video/x-msvideo",
        ".mov": "video/quicktime",
        ".webm": "video/webm",
        ".mkv": "video/x-matroska",
        ".wav": "audio/x-wav",
        ".mp3": "audio/mpeg",
    }
    return mime_types.get(suffix, "video/mp4")


# Alias for naming consistency
list_elan_tiers = get_eaf_tiers


def _generate_safe_output_path(source_path: Path, suffix: str = "AI") -> Path:
    """
    Generate a safe output path that doesn't overwrite the original file.

    Creates versioned filenames like: original_AI_v1.eaf, original_AI_v2.eaf, etc.

    Args:
        source_path: Original file path
        suffix: Suffix to add before version number

    Returns:
        New path that doesn't exist yet

    Raises:
        RuntimeError: If too many versions already exist (safety limit)
    """
    import re

    source_path = Path(source_path)
    parent = source_path.parent
    stem = source_path.stem

    # Remove existing version suffix if present (e.g., "_AI_v1" -> stem without it)
    version_pattern = rf"_{suffix}_v\d+$"
    clean_stem = re.sub(version_pattern, "", stem)

    version = 1
    while True:
        new_name = f"{clean_stem}_{suffix}_v{version}.eaf"
        new_path = parent / new_name
        if not new_path.exists():
            return new_path
        version += 1
        # Safety limit to prevent infinite loops
        if version > 10000:
            raise RuntimeError(f"Too many versions exist for {source_path}")


def _generate_default_tier_name() -> str:
    """Generate default tier name with timestamp."""
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    return f"AI_Segmentation_{timestamp}"


def _validate_eaf_xml(path: Path) -> bool:
    """
    Validate that an EAF file has valid XML structure.

    Args:
        path: Path to EAF file

    Returns:
        True if valid, False otherwise
    """
    try:
        tree = SafeET.parse(path)
        root = tree.getroot()
        # Check for required ELAN elements
        if root.tag != "ANNOTATION_DOCUMENT":
            return False
        if root.find(".//TIME_ORDER") is None:
            return False
        return True
    except ET.ParseError:
        return False
    except Exception:
        return False


def merge_tiers(
    source_eaf: str | Path,
    new_segments: SegmentList | list[Segment],
    tier_name: str | None = None,
    output_path: str | Path | None = None,
) -> Path:
    """
    Merge new segments into an ELAN file, creating a new tier.

    This is a convenience wrapper around add_tier_to_eaf() that:
    - Automatically generates a unique output path if not provided
    - Generates a default tier name with timestamp if not provided
    - Assigns all segments to the specified tier

    SAFETY: This function NEVER overwrites the original file.

    Args:
        source_eaf: Path to existing .eaf file
        new_segments: Segments to add (tier attribute will be set to tier_name)
        tier_name: Name for the merged tier. If None, generates "AI_Segmentation_[timestamp]"
        output_path: Optional explicit output path. If None, generates versioned name.

    Returns:
        Path to the newly created .eaf file

    Raises:
        FileNotFoundError: If source file doesn't exist
        FileExistsError: If explicit output_path already exists

    Example:
        >>> ai_segments = SegmentList([
        ...     Segment(0.0, 1.0, "HELLO"),
        ...     Segment(1.2, 2.0, "WORLD"),
        ... ])
        >>> new_file = merge_tiers("video.eaf", ai_segments, "AI_Predictions")
        >>> print(new_file)  # video_AI_v1.eaf
    """
    source_eaf = Path(source_eaf)

    if not source_eaf.exists():
        raise SLTKFileNotFoundError(str(source_eaf))

    # Generate tier name if not provided
    if tier_name is None:
        tier_name = _generate_default_tier_name()

    # Convert to SegmentList if needed
    if isinstance(new_segments, list):
        new_segments = SegmentList(new_segments)

    # Create segments with the unified tier name
    unified_segments = SegmentList(
        [
            Segment(
                start=seg.start,
                end=seg.end,
                label=seg.label,
                confidence=seg.confidence,
                tier=tier_name,
                metadata=seg.metadata.copy() if seg.metadata else {},
            )
            for seg in new_segments
        ]
    )

    # Determine output path
    if output_path is None:
        output_path = _generate_safe_output_path(source_eaf)
    else:
        output_path = Path(output_path)
        if output_path.exists():
            raise FileExistsError(
                f"Output path {output_path} already exists. "
                "This function never overwrites files for safety."
            )
        # Also prevent overwriting source
        if output_path.resolve() == source_eaf.resolve():
            raise ValueError(
                f"Output path cannot be the same as source path: {source_eaf}. "
                "Please specify a different output file to preserve the original."
            )

    return add_tier_to_eaf(
        source_path=source_eaf,
        segments=unified_segments,
        output_path=output_path,
        tier_name=tier_name,
    )


def get_eaf_tier_info(path: str | Path) -> list[dict[str, Any]]:
    """
    Get detailed information about tiers in an ELAN file.

    Args:
        path: Path to .eaf file

    Returns:
        List of dicts with tier information:
        - tier_id: Tier name
        - linguistic_type: Linguistic type reference
        - annotation_count: Number of annotations
        - parent_ref: Parent tier reference (if any)
        - participant: Participant attribute (if any)

    Example:
        >>> tier_info = get_eaf_tier_info("annotations.eaf")
        >>> for tier in tier_info:
        ...     print(f"{tier['tier_id']}: {tier['annotation_count']} annotations")
    """
    path = Path(path)

    if not path.exists():
        raise SLTKFileNotFoundError(str(path))

    tree = SafeET.parse(path)
    root = tree.getroot()

    tier_info = []
    for tier in root.findall(".//TIER"):
        tier_id = tier.get("TIER_ID")
        if tier_id:
            info = {
                "tier_id": tier_id,
                "linguistic_type": tier.get("LINGUISTIC_TYPE_REF"),
                "annotation_count": len(tier.findall(".//ALIGNABLE_ANNOTATION")),
                "parent_ref": tier.get("PARENT_REF"),
                "participant": tier.get("PARTICIPANT"),
            }
            tier_info.append(info)

    return tier_info


def safe_add_tier(
    eaf_path: str | Path,
    tier_name: str | None,
    segments: SegmentList | list[Segment],
    output_path: str | Path | None = None,
) -> Path:
    """
    Safely add a new tier with segments to an existing ELAN file.

    This is the recommended function for adding AI-generated segmentations.
    It provides maximum safety by:
    - NEVER overwriting the original file
    - Generating versioned output filenames automatically
    - Validating XML structure before and after modification
    - Generating default tier names with timestamps

    Args:
        eaf_path: Path to existing .eaf file
        tier_name: Name for the new tier. If None, generates "AI_Segmentation_[timestamp]"
        segments: Segments to add to the new tier
        output_path: Optional explicit output path. If None, generates versioned name.

    Returns:
        Path to the newly created .eaf file

    Raises:
        FileNotFoundError: If source file doesn't exist
        FileExistsError: If explicit output_path already exists
        RuntimeError: If XML validation fails

    Example:
        >>> new_segments = SegmentList([
        ...     Segment(0.5, 1.2, "SIGN1"),
        ...     Segment(1.5, 2.0, "SIGN2"),
        ... ])
        >>> new_path = safe_add_tier("annotations.eaf", "AI_Predictions", new_segments)
        >>> print(new_path)  # annotations_AI_v1.eaf
    """
    eaf_path = Path(eaf_path)

    if not eaf_path.exists():
        raise SLTKFileNotFoundError(str(eaf_path))

    # Validate source file first
    if not _validate_eaf_xml(eaf_path):
        raise RuntimeError(f"Source EAF file has invalid XML structure: {eaf_path}")

    # Generate default tier name if not provided
    if tier_name is None:
        tier_name = _generate_default_tier_name()

    # Convert to SegmentList if needed
    if isinstance(segments, list):
        segments = SegmentList(segments)

    # Determine output path
    if output_path is None:
        output_path = _generate_safe_output_path(eaf_path)
    else:
        output_path = Path(output_path)
        if output_path.exists():
            raise FileExistsError(
                f"Output path {output_path} already exists. "
                "This function never overwrites files for safety."
            )

    # Use the core add_tier_to_eaf function
    result_path = add_tier_to_eaf(
        source_path=eaf_path,
        segments=segments,
        output_path=output_path,
        tier_name=tier_name,
    )

    # Validate the output file
    if not _validate_eaf_xml(result_path):
        # Remove invalid file
        result_path.unlink(missing_ok=True)
        raise RuntimeError("Generated EAF file failed XML validation")

    return result_path
