from __future__ import annotations

import json
import mimetypes
import re
import shutil
import subprocess
import time
import uuid
from pathlib import Path

import imageio_ffmpeg
from mutagen.mp4 import MP4

from django.conf import settings
from django.http import Http404, JsonResponse, StreamingHttpResponse
from django.shortcuts import render
from django.urls import reverse
from django.views.decorators.http import require_GET, require_POST


CHUNK_SIZE = 1024 * 1024
MAX_UPLOAD_SIZE = 100 * 1024 * 1024  # 100 MB


def _tmp_root() -> Path:
    root = Path(settings.SPLITTER_TMP_DIR)
    root.mkdir(parents=True, exist_ok=True)
    return root


def _cleanup_stale() -> None:
    root = _tmp_root()
    now = time.time()
    max_age = settings.SPLITTER_STALE_SECONDS

    for item in root.iterdir():
        if not item.is_dir():
            continue
        try:
            if now - item.stat().st_mtime > max_age:
                shutil.rmtree(item, ignore_errors=True)
        except FileNotFoundError:
            pass


def _safe_stem(filename: str) -> str:
    stem = Path(filename).stem
    stem = re.sub(r"[^A-Za-z0-9._ -]+", "_", stem)
    stem = re.sub(r"\s+", "_", stem).strip("._")
    return stem[:120] or "audio"


def _seconds_to_label(total_seconds: int) -> str:
    h = total_seconds // 3600
    m = (total_seconds % 3600) // 60
    s = total_seconds % 60
    return f"{h:02d}-{m:02d}-{s:02d}"


def _ffmpeg_bin() -> str:
    # imageio-ffmpeg menyediakan binary FFmpeg sendiri melalui wheel Python.
    return imageio_ffmpeg.get_ffmpeg_exe()


def _probe_duration(path: Path) -> float:
    # Tidak membutuhkan ffprobe. Mutagen membaca metadata/durasi MP4/M4A.
    audio = MP4(str(path))
    if audio.info is None or audio.info.length is None:
        raise RuntimeError("Durasi file tidak dapat dibaca.")
    return float(audio.info.length)


def _run_ffmpeg(cmd: list[str]) -> None:
    result = subprocess.run(
        cmd,
        stdout=subprocess.DEVNULL,
        stderr=subprocess.PIPE,
        text=True,
        timeout=3600,
        check=False,
    )

    if result.returncode != 0:
        raise RuntimeError(
            result.stderr[-4000:].strip() or "FFmpeg gagal memproses file."
        )


def _split_m4a(
    input_path: Path,
    output_1: Path,
    output_2: Path,
    split_seconds: int,
) -> None:
    ffmpeg = _ffmpeg_bin()

    # Bagian 1: awal -> titik potong.
    cmd1 = [
        ffmpeg,
        "-hide_banner",
        "-loglevel", "error",
        "-y",
        "-i", str(input_path),
        "-t", str(split_seconds),
        "-map", "0:a:0",
        "-map_metadata", "0",
        "-c", "copy",
        "-avoid_negative_ts", "make_zero",
        "-movflags", "+faststart",
        str(output_1),
    ]

    # Bagian 2: titik potong -> akhir.
    cmd2 = [
        ffmpeg,
        "-hide_banner",
        "-loglevel", "error",
        "-y",
        "-ss", str(split_seconds),
        "-i", str(input_path),
        "-map", "0:a:0",
        "-map_metadata", "0",
        "-c", "copy",
        "-avoid_negative_ts", "make_zero",
        "-movflags", "+faststart",
        str(output_2),
    ]

    _run_ffmpeg(cmd1)
    _run_ffmpeg(cmd2)


def _stream_file(path: Path):
    with path.open("rb") as f:
        while True:
            data = f.read(CHUNK_SIZE)
            if not data:
                break
            yield data


@require_GET
def index(request):
    _cleanup_stale()
    return render(request, "splitter/index.html")


@require_POST
def split_audio(request):
    _cleanup_stale()

    uploaded = request.FILES.get("audio")
    if not uploaded:
        return JsonResponse(
            {"ok": False, "error": "Pilih file M4A terlebih dahulu."},
            status=400,
        )

    if not uploaded.name.lower().endswith(".m4a"):
        return JsonResponse(
            {"ok": False, "error": "File harus berformat .m4a."},
            status=400,
        )

    if uploaded.size > MAX_UPLOAD_SIZE:
        return JsonResponse(
            {
                "ok": False,
                "error": (
                    f"Ukuran file maksimal 100 MB. "
                    f"File yang dipilih sekitar {uploaded.size / 1024 / 1024:.1f} MB."
                ),
            },
            status=400,
        )

    try:
        hours = int(request.POST.get("hours", "0"))
        minutes = int(request.POST.get("minutes", "0"))
        seconds = int(request.POST.get("seconds", "0"))
    except ValueError:
        return JsonResponse(
            {"ok": False, "error": "Jam, menit, dan detik harus berupa angka."},
            status=400,
        )

    if hours < 0 or not 0 <= minutes <= 59 or not 0 <= seconds <= 59:
        return JsonResponse(
            {"ok": False, "error": "Format waktu tidak valid."},
            status=400,
        )

    split_seconds = hours * 3600 + minutes * 60 + seconds
    if split_seconds <= 0:
        return JsonResponse(
            {"ok": False, "error": "Titik potong harus lebih dari 00:00:00."},
            status=400,
        )

    token = uuid.uuid4()
    job_dir = _tmp_root() / str(token)
    job_dir.mkdir(parents=True, exist_ok=False)

    safe_stem = _safe_stem(uploaded.name)
    input_path = job_dir / "input.m4a"

    try:
        with input_path.open("wb") as dst:
            for chunk in uploaded.chunks():
                dst.write(chunk)

        duration = _probe_duration(input_path)

        if split_seconds >= duration:
            raise ValueError(
                f"Titik potong harus lebih kecil dari durasi rekaman "
                f"({int(duration // 3600):02d}:"
                f"{int(duration % 3600 // 60):02d}:"
                f"{int(duration % 60):02d})."
            )

        split_label = _seconds_to_label(split_seconds)
        filename_1 = f"{safe_stem}_01_awal-sampai_{split_label}.m4a"
        filename_2 = f"{safe_stem}_02_dari_{split_label}-sampai-akhir.m4a"

        output_1 = job_dir / filename_1
        output_2 = job_dir / filename_2

        _split_m4a(input_path, output_1, output_2, split_seconds)

        # Input segera dihapus setelah hasil selesai dibuat.
        input_path.unlink(missing_ok=True)

        manifest = {
            "part1": filename_1,
            "part2": filename_2,
            "created": time.time(),
        }
        (job_dir / "manifest.json").write_text(
            json.dumps(manifest),
            encoding="utf-8",
        )

        return JsonResponse(
            {
                "ok": True,
                "duration": duration,
                "downloads": [
                    {
                        "name": filename_1,
                        "url": reverse("splitter:download_part", args=[token, 1]),
                    },
                    {
                        "name": filename_2,
                        "url": reverse("splitter:download_part", args=[token, 2]),
                    },
                ],
            }
        )

    except ValueError as exc:
        shutil.rmtree(job_dir, ignore_errors=True)
        return JsonResponse({"ok": False, "error": str(exc)}, status=400)

    except subprocess.TimeoutExpired:
        shutil.rmtree(job_dir, ignore_errors=True)
        return JsonResponse(
            {"ok": False, "error": "Proses pemotongan terlalu lama dan dihentikan."},
            status=500,
        )

    except Exception as exc:
        shutil.rmtree(job_dir, ignore_errors=True)
        return JsonResponse(
            {"ok": False, "error": f"Gagal memproses file: {exc}"},
            status=500,
        )


@require_GET
def download_part(request, token, part: int):
    _cleanup_stale()

    if part not in (1, 2):
        raise Http404

    job_dir = _tmp_root() / str(token)
    manifest_path = job_dir / "manifest.json"

    if not manifest_path.exists():
        raise Http404("File sudah dihapus atau tautan sudah kedaluwarsa.")

    try:
        manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
        filename = Path(manifest[f"part{part}"]).name
    except Exception as exc:
        raise Http404("Manifest tidak valid.") from exc

    path = job_dir / filename

    if not path.exists():
        raise Http404("File sudah diunduh atau sudah dihapus.")

    size = path.stat().st_size
    content_type = mimetypes.guess_type(filename)[0] or "application/octet-stream"

    response = StreamingHttpResponse(
        _stream_file(path),
        content_type=content_type,
    )
    response["Content-Length"] = str(size)
    response["Content-Disposition"] = f'attachment; filename="{filename}"'
    response["X-Content-Type-Options"] = "nosniff"
    return response
