import asyncio
from asyncio import subprocess
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=subprocess.PIPE, stderr=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)


async def _ffprobe_dimensions_path(video_path: str) -> Tuple[int, int, int]:
    ffprobe = _resolve_bin(FFPROBE_BIN)
    cmd = [
        ffprobe,
        "-v",
        "error",
        "-select_streams",
        "v:0",
        "-show_entries",
        "stream=width,height,rotation:stream_tags=rotate",
        "-of",
        "json",
        video_path,
    ]
    proc = await asyncio.create_subprocess_exec(
        *cmd, stdout=subprocess.PIPE, stderr=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)


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)

        # Calculate crop
        target_ar = 9 / 16
        if rot in (90, 270):
            w, h = h, w
        current_ar = w / h
        if current_ar > target_ar:
            # Too wide, crop sides
            new_w = h * target_ar
            crop = f"crop={int(new_w)}:{h}:{(w - new_w) / 2}:0"
        else:
            # Too tall, crop top/bottom
            new_h = w / target_ar
            crop = f"crop={w}:{int(new_h)}:0:{(h - new_h) / 2}"

        # Scale down if too large
        scale = "scale='if(gt(iw,1080),1080,iw)':-1"

        cmd = [
            ffmpeg,
            "-y",
            "-i",
            tmp_in.name,
            "-vf",
            f"{crop},{scale}",
            "-c:v",
            "libx264",
            "-crf",
            "23",
            "-preset",
            "veryfast",
            "-maxrate",
            "12M",
            "-bufsize",
            "12M",
            tmp_out.name,
        ]
        proc = await asyncio.create_subprocess_exec(
            *cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
        )
        out, 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)


async def validate_mp4(video_path: str) -> bool:
    """
    Validate if the given file is an MP4.
    """
    if not os.path.exists(video_path):
        return False

    ffprobe = _resolve_bin(FFPROBE_BIN)
    cmd = [
        ffprobe,
        "-v",
        "error",
        "-show_entries",
        "format=format_name",
        "-of",
        "default=nw=1:nk=1",
        video_path,
    ]
    try:
        proc = await asyncio.create_subprocess_exec(
            *cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
        )
        out, _ = await proc.communicate()
        if proc.returncode != 0:
            return False

        format_name = out.decode("utf-8").strip().lower()
        return "mp4" in format_name
    except Exception:
        return False


async def get_video_duration(video_path: str) -> float:
    """
    Get the duration of a video file in seconds using ffprobe.
    """
    if not os.path.exists(video_path):
        return 0.0

    ffprobe = _resolve_bin(FFPROBE_BIN)
    dur_cmd = [
        ffprobe,
        "-v",
        "error",
        "-show_entries",
        "format=duration",
        "-of",
        "json",
        video_path,
    ]
    try:
        proc = await asyncio.create_subprocess_exec(
            *dur_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
        )
        out, _ = await proc.communicate()
        if proc.returncode != 0:
            return 0.0

        data = json.loads(out.decode("utf-8") or "{}")
        return float(data.get("format", {}).get("duration", 0))
    except Exception:
        return 0.0


async def generate_thumbnail(video_path: str, thumbnail_path: str) -> bool:
    """
    Generate a thumbnail from the middle of the video.
    """
    if not os.path.exists(video_path):
        return False

    ffmpeg = _resolve_bin(FFMPEG_BIN)
    try:
        duration = await get_video_duration(video_path)
        if duration <= 0:
            return False

        seek_time = duration / 2

        # Extract frame
        cmd = [
            ffmpeg,
            "-y",
            "-ss",
            str(seek_time),
            "-i",
            video_path,
            "-vframes",
            "1",
            "-q:v",
            "2",
            thumbnail_path,
        ]
        proc = await asyncio.create_subprocess_exec(
            *cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
        )
        await proc.communicate()
        return proc.returncode == 0
    except Exception:
        return False
