import asyncio
import tempfile
import os
import shutil
import json
from typing import Tuple

FFMPEG_BIN = os.environ.get("FFMPEG_BIN", "ffmpeg")
FFPROBE_BIN = os.environ.get("FFPROBE_BIN", "ffprobe")


def _resolve_bin(name: str) -> str:
    path = shutil.which(name)
    if path:
        return path
    raise RuntimeError(
        f"{name} not found. Install it or set an absolute path via env var."
    )


async def _ffprobe_dimensions(video_bytes: bytes) -> Tuple[int, int, int]:
    tmp_in = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4")
    try:
        with open(tmp_in.name, "wb") as f:
            f.write(video_bytes)

        ffprobe = _resolve_bin(FFPROBE_BIN)
        cmd = [
            ffprobe,
            "-v",
            "error",
            "-select_streams",
            "v:0",
            "-show_entries",
            "stream=width,height,rotation:stream_tags=rotate",
            "-of",
            "json",
            tmp_in.name,
        ]
        proc = await asyncio.create_subprocess_exec(
            *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
        )
        out, err = await proc.communicate()
        if proc.returncode != 0:
            raise RuntimeError(
                f"ffprobe failed ({proc.returncode}): {err.decode(errors='ignore')}"
            )

        data = json.loads(out.decode("utf-8") or "{}")
        streams = data.get("streams") or []
        if not streams:
            raise RuntimeError("No video stream found.")
        s0 = streams[0]
        w = int(s0.get("width") or 0)
        h = int(s0.get("height") or 0)

        rot = 0
        tags = s0.get("tags") or {}
        if "rotate" in tags:
            try:
                rot = int(tags["rotate"])
            except (TypeError, ValueError):
                rot = 0
        if "rotation" in s0:
            try:
                rot = int(s0["rotation"])
            except (TypeError, ValueError):
                pass

        rot = ((rot % 360) + 360) % 360
        if rot not in (0, 90, 180, 270):
            rot = 0
        return (w, h, rot)
    finally:
        if os.path.exists(tmp_in.name):
            os.unlink(tmp_in.name)


def _needs_9x16(w: int, h: int, rot: int, tol: float = 0.01) -> bool:
    if rot in (90, 270):
        w, h = h, w
    if w == 0 or h == 0:
        return True
    ar = w / h
    return abs(ar - (9 / 16)) > tol


async def ensure_9x16(video_bytes: bytes) -> bytes:
    """
    If not ~9:16, center-crop to 9:16 and scale to <= 1080x1920. Returns processed bytes.
    """
    w, h, rot = await _ffprobe_dimensions(video_bytes)
    if not _needs_9x16(w, h, rot):
        return video_bytes

    ffmpeg = _resolve_bin(FFMPEG_BIN)
    tmp_in = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4")
    tmp_out = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4")
    try:
        with open(tmp_in.name, "wb") as f:
            f.write(video_bytes)

        vf = (
            "crop="
            "w=if(gt(a\\,0.5625)\\,ih*0.5625\\,iw):"
            "h=if(gt(a\\,0.5625)\\,ih\\,iw/0.5625):"
            "x=(iw-ow)/2:"
            "y=(ih-oh)/2"
            ",scale=min(1080\\,iw):min(1920\\,ih):force_original_aspect_ratio=decrease"
        )

        cmd = [
            ffmpeg,
            "-y",
            "-i",
            tmp_in.name,
            "-vf",
            vf,
            "-c:v",
            "libx264",
            "-preset",
            "veryfast",
            "-profile:v",
            "high",
            "-level",
            "4.0",
            "-pix_fmt",
            "yuv420p",
            "-r",
            "30",
            "-g",
            "60",
            "-x264-params",
            "keyint=60:min-keyint=60:scenecut=0",
            "-movflags",
            "+faststart",
            "-c:a",
            "aac",
            "-b:a",
            "128k",
            "-ar",
            "48000",
            "-ac",
            "2",
            "-b:v",
            "6M",
            "-maxrate",
            "8M",
            "-bufsize",
            "12M",
            tmp_out.name,
        ]
        proc = await asyncio.create_subprocess_exec(
            *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
        )
        _, stderr = await proc.communicate()
        if proc.returncode != 0:
            raise RuntimeError(
                f"ffmpeg crop-to-9x16 failed ({proc.returncode}): {stderr.decode(errors='ignore')}"
            )

        with open(tmp_out.name, "rb") as f:
            return f.read()
    finally:
        for p in (tmp_in.name, tmp_out.name):
            if os.path.exists(p):
                os.unlink(p)
