"""Upload helpers: spool a multipart file to a temp path, probe its duration.

Kept separate from the routes so both the sync endpoint and the job endpoint
reuse the exact same spooling + duration guard.
"""
from __future__ import annotations

import tempfile
from pathlib import Path

from fastapi import HTTPException, UploadFile
from loguru import logger

_CHUNK = 1024 * 1024  # 1 MB


async def spool_upload(file: UploadFile, max_bytes: int, data_dir: Path) -> Path:
    """Stream an upload to a temp file under data_dir/uploads, enforcing a hard
    byte cap (413 on overflow). Returns the temp path; caller must delete it."""
    uploads = data_dir / "uploads"
    uploads.mkdir(parents=True, exist_ok=True)
    # Sanitize the suffix: keep only a short, safe extension (mkstemp fixes the
    # directory, so no traversal is possible, but a pathological filename could
    # otherwise raise OSError or inject odd characters into the temp name).
    raw_suffix = Path(file.filename or "audio").suffix
    suffix = "".join(c for c in raw_suffix if c.isalnum() or c == ".")[:16] or ".bin"
    try:
        fd, tmp_name = tempfile.mkstemp(dir=str(uploads), suffix=suffix)
    except OSError:
        raise HTTPException(status_code=400, detail="Invalid upload")
    tmp = Path(tmp_name)
    written = 0
    try:
        with open(fd, "wb") as out:
            while True:
                chunk = await file.read(_CHUNK)
                if not chunk:
                    break
                written += len(chunk)
                if written > max_bytes:
                    raise HTTPException(
                        status_code=413,
                        detail=f"Upload exceeds the {max_bytes // (1024*1024)} MB limit",
                    )
                out.write(chunk)
    except HTTPException:
        _safe_unlink(tmp)
        raise
    except Exception as e:
        _safe_unlink(tmp)
        logger.warning("Upload read failed: {}", e)
        raise HTTPException(status_code=400, detail="Could not read upload")
    if written == 0:
        _safe_unlink(tmp)
        raise HTTPException(status_code=400, detail="Empty upload")
    return tmp


def probe_duration_s(path: Path) -> float | None:
    """Best-effort media duration in seconds via PyAV container metadata.
    Returns None when it cannot be determined (caller then declines to gate)."""
    try:
        import av  # bundled (faster-whisper uses it to decode)
    except Exception:  # noqa: BLE001
        return None
    try:
        with av.open(str(path)) as container:
            if container.duration is not None:
                return float(container.duration) / 1_000_000.0  # av.time_base
            # Fall back to the longest audio stream's duration.
            best = 0.0
            for stream in container.streams:
                if stream.type == "audio" and stream.duration and stream.time_base:
                    best = max(best, float(stream.duration * stream.time_base))
            return best or None
    except Exception as e:  # noqa: BLE001
        logger.debug("Duration probe failed for {}: {}", path, e)
        return None


def _safe_unlink(path: Path) -> None:
    try:
        if path and path.exists():
            path.unlink()
    except OSError:
        pass
