"""Robust parsing and validation for Wan/ComfyUI video outputs."""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Iterable


@dataclass(frozen=True)
class WanOutputItem:
    filename: str
    subfolder: str
    type: str


@dataclass(frozen=True)
class WanRunResult:
    exit_code: int
    recovered: bool
    outputs: list[Path]
    stderr: str = ""


@dataclass(frozen=True)
class WanRunGateResult:
    allowed: bool
    reason: str
    warnings: tuple[str, ...] = ()


def parse_wan_history_outputs(history: dict[str, Any]) -> list[WanOutputItem]:
    """Extract only real ComfyUI output file objects from history.

    ComfyUI SaveVideo histories may include sibling lists such as
    ``"animated": [true]``. Those booleans are metadata, not files. The old
    extraction path treated every list item as a file object and crashed with
    ``AttributeError: 'bool' object has no attribute 'get'`` after the GPU job
    had actually finished. This parser is intentionally strict for dict items
    and intentionally ignores bool/null metadata.
    """

    outputs: list[WanOutputItem] = []
    node_outputs = history.get("outputs")
    if not isinstance(node_outputs, dict):
        raise ValueError("history missing outputs object")

    for node_id, node_data in node_outputs.items():
        if not isinstance(node_data, dict):
            continue
        for output_kind, items in node_data.items():
            if not isinstance(items, list):
                continue
            for item in items:
                if item is None or isinstance(item, bool):
                    continue
                if not isinstance(item, dict):
                    raise ValueError(f"invalid output item type at node {node_id}/{output_kind}: {type(item).__name__}")
                filename = item.get("filename")
                subfolder = item.get("subfolder", "")
                item_type = item.get("type", "output")
                if not filename:
                    raise ValueError(f"missing filename at node {node_id}/{output_kind}")
                if not isinstance(filename, str) or not isinstance(subfolder, str) or not isinstance(item_type, str):
                    raise ValueError(f"invalid filename/subfolder/type at node {node_id}/{output_kind}")
                outputs.append(WanOutputItem(filename=filename, subfolder=subfolder, type=item_type))
    if not outputs:
        raise ValueError("history contained no video output files")
    return outputs


def validate_wan_run_success(
    result: WanRunResult,
    *,
    expected_final_seconds: float,
    probe: Callable[[Path], tuple[float, float]],
    min_duration_ratio_for_full_final: float = 0.45,
    min_fps: float = 10.0,
) -> WanRunGateResult:
    if result.exit_code != 0 and not result.recovered:
        return WanRunGateResult(False, f"exit code {result.exit_code} without explicit recovery")
    if not result.outputs:
        return WanRunGateResult(False, "no_wan_video_outputs")

    warnings: list[str] = []
    for output in result.outputs:
        if not output.exists():
            return WanRunGateResult(False, f"missing output file: {output}")
        duration, fps = probe(output)
        if duration <= 0:
            return WanRunGateResult(False, "invalid_wan_duration")
        if fps < min_fps:
            return WanRunGateResult(False, "invalid_wan_fps")
        if expected_final_seconds >= 20 and duration < expected_final_seconds * min_duration_ratio_for_full_final:
            return WanRunGateResult(False, "wan_clip_too_short_for_final")
        if duration < expected_final_seconds:
            warnings.append("wan_clip_shorter_than_target; composition must not look looped")

    if result.exit_code != 0 and result.recovered:
        return WanRunGateResult(True, "wan_run_valid_recovered", tuple(warnings))
    return WanRunGateResult(True, "wan_run_valid", tuple(warnings))
