from __future__ import annotations

import json
import os
import shutil
import subprocess
import tempfile
from pathlib import Path
from typing import Literal, Sequence

from .presets import PRESETS

Preset = Literal["vorticity", "speed", "comparison"]
AxisOrder = Literal["tyx", "txy", "yx", "xy"]


def _executable(value: str | Path | None, environment: str, fallback: Path | str) -> str:
    configured = str(value or os.environ.get(environment) or fallback)
    path = Path(configured).expanduser()
    if path.parent != Path("."):
        path = path.resolve()
        if not path.is_file() or not os.access(path, os.X_OK):
            raise FileNotFoundError(f"executable not found: {path}")
        return str(path)
    resolved = shutil.which(configured)
    if not resolved:
        raise FileNotFoundError(f"executable not found on PATH: {configured}")
    return resolved


def plot(
    source: str | Path | Sequence[str | Path],
    output: str | Path,
    *,
    preset: Preset = "vorticity",
    reference: str | Path | Sequence[str | Path] | None = None,
    axis_order: AxisOrder = "tyx",
    scalar_range: tuple[float, float] | None = None,
    frame: int = 0,
    fps: int = 10,
    field_names: tuple[str, str] = ("ux", "uy"),
    show_colorbar: bool = True,
    pvpython: str | Path | None = None,
    ffmpeg: str | Path | None = None,
    timeout: float = 1800,
) -> Path:
    """Render an NPZ field or Legacy Tecplot series with one ParaView subprocess.

    NPZ files must contain velocity arrays named by ``field_names``. Their axis
    order is explicit: new CelerisLab eval files use ``tyx`` and Legacy
    CCD/OID files use ``txy``. A source sequence is treated as Tecplot files.
    ``output`` ending in ``.mp4`` renders every frame; otherwise one PNG frame
    is rendered.
    """
    if preset not in PRESETS:
        raise ValueError(f"unknown preset {preset!r}; choose from {sorted(PRESETS)}")
    if reference is not None and preset != "comparison":
        raise ValueError("reference is only used by the comparison preset")
    if preset == "comparison" and reference is None:
        raise ValueError("comparison requires reference")
    if axis_order not in {"tyx", "txy", "yx", "xy"}:
        raise ValueError("axis_order must be tyx, txy, yx, or xy")
    if scalar_range is not None and scalar_range[0] >= scalar_range[1]:
        raise ValueError("scalar_range must satisfy low < high")

    root = Path(__file__).resolve().parents[2]
    pvpython_exe = _executable(pvpython, "PV_PLOT_PVPYTHON", root / "ParaView" / "bin" / "pvpython")
    ffmpeg_exe = _executable(ffmpeg, "PV_PLOT_FFMPEG", "ffmpeg") if Path(output).suffix.lower() == ".mp4" else None

    def paths(value):
        values = value if isinstance(value, (list, tuple)) else [value]
        resolved = [str(Path(item).expanduser().resolve()) for item in values]
        missing = [item for item in resolved if not Path(item).is_file()]
        if missing:
            raise FileNotFoundError(f"input file not found: {missing[0]}")
        return resolved

    sources = paths(source)
    references = paths(reference) if reference is not None else []
    source_format = "npz" if len(sources) == 1 and Path(sources[0]).suffix.lower() == ".npz" else "tecplot"
    if source_format == "npz" and len(sources) != 1:
        raise ValueError("NPZ input accepts one archive containing all frames")

    target = Path(output).expanduser().resolve()
    target.parent.mkdir(parents=True, exist_ok=True)
    config = {
        "source": sources,
        "reference": references,
        "format": source_format,
        "axis_order": axis_order,
        "fields": list(field_names),
        "preset": preset,
        "range": list(scalar_range or PRESETS[preset]["range"]),
        "colorbar": PRESETS[preset]["colorbar"] if show_colorbar else None,
        "frame": int(frame),
        "fps": int(fps),
        "output": str(target),
        "ffmpeg": ffmpeg_exe,
    }
    worker = Path(__file__).with_name("render.py")
    with tempfile.NamedTemporaryFile("w", suffix=".json", encoding="utf-8", delete=False) as stream:
        json.dump(config, stream)
        config_path = Path(stream.name)
    try:
        completed = subprocess.run(
            [pvpython_exe, str(worker), str(config_path)],
            text=True,
            capture_output=True,
            timeout=timeout,
        )
    finally:
        config_path.unlink(missing_ok=True)
    if completed.returncode:
        raise RuntimeError((completed.stderr or completed.stdout)[-4000:])
    if not target.is_file():
        raise RuntimeError(f"ParaView reported success but did not create {target}")
    return target
