import json
import os
import shutil
import subprocess
import tempfile
import threading
import uuid
from concurrent.futures import ThreadPoolExecutor
from datetime import date, datetime, timedelta
from pathlib import Path

import requests
from django.conf import settings
from django.core.files import File
from django.db import close_old_connections
from django.db.models import Q

from learn.models import Live_Lecture


_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="zoom-recording")
_status_lock = threading.Lock()
_JOB_ID_PATTERN = __import__("re").compile(r"^[a-f0-9]{32}$")


def spool_root():
    configured = getattr(settings, "ZOOM_RECORDING_SPOOL_ROOT", "")
    return Path(configured or os.path.join(tempfile.gettempdir(), "cet-zoom-recording-jobs"))


def _job_dir(job_id):
    if not _JOB_ID_PATTERN.fullmatch(str(job_id or "")):
        raise ValueError("Invalid Zoom recording job id.")
    return spool_root() / job_id


def _status_path(job_id):
    return _job_dir(job_id) / "status.json"


def _write_status(job_id, status):
    job_dir = _job_dir(job_id)
    job_dir.mkdir(parents=True, exist_ok=True)
    status["updated_at"] = datetime.utcnow().isoformat() + "Z"
    temporary = _status_path(job_id).with_suffix(".tmp")
    with _status_lock:
        temporary.write_text(json.dumps(status), encoding="utf-8")
        os.replace(str(temporary), str(_status_path(job_id)))


def read_zoom_recording_job(job_id):
    path = _status_path(job_id)
    if not path.exists():
        return None
    with _status_lock:
        return json.loads(path.read_text(encoding="utf-8"))


def _active_lock_path():
    return spool_root() / "active.lock"


def _acquire_active_lock(job_id):
    root = spool_root()
    root.mkdir(parents=True, exist_ok=True)
    path = _active_lock_path()
    stale_seconds = int(getattr(settings, "ZOOM_RECORDING_LOCK_TIMEOUT", 21600))

    if path.exists() and (datetime.now().timestamp() - path.stat().st_mtime) > stale_seconds:
        try:
            path.unlink()
        except OSError:
            pass

    try:
        descriptor = os.open(str(path), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
    except FileExistsError:
        try:
            active_job_id = path.read_text(encoding="utf-8").strip()
            return False, read_zoom_recording_job(active_job_id)
        except (OSError, ValueError):
            return False, None

    with os.fdopen(descriptor, "w") as lock_file:
        lock_file.write(job_id)
    return True, None


def _release_active_lock(job_id):
    path = _active_lock_path()
    try:
        if path.read_text(encoding="utf-8").strip() == job_id:
            path.unlink()
    except OSError:
        pass


def queue_zoom_recording_job():
    job_id = uuid.uuid4().hex
    acquired, active_status = _acquire_active_lock(job_id)
    if not acquired:
        return active_status or {
            "state": "processing",
            "message": "A Zoom recording job is already running.",
        }, False

    status = {
        "job_id": job_id,
        "state": "queued",
        "total": 0,
        "processed": 0,
        "uploaded": 0,
        "skipped": 0,
        "failed": 0,
        "errors": [],
        "created_at": datetime.utcnow().isoformat() + "Z",
    }
    _write_status(job_id, status)
    try:
        _executor.submit(_process_zoom_recordings, job_id)
    except Exception:
        _release_active_lock(job_id)
        raise
    return status, True


def _zoom_access_token(zoom_key):
    response = requests.post(
        "https://zoom.us/oauth/token",
        params={
            "grant_type": "account_credentials",
            "account_id": zoom_key.account_id,
        },
        auth=(zoom_key.client_id, zoom_key.client_secret),
        timeout=(10, 30),
    )
    response.raise_for_status()
    return response.json()["access_token"]


def _zoom_recordings(meeting_id, access_token):
    response = requests.get(
        "https://api.zoom.us/v2/meetings/{0}/recordings".format(meeting_id),
        headers={"Authorization": "Bearer {0}".format(access_token)},
        timeout=(10, 30),
    )
    if response.status_code == 404:
        return []
    if response.status_code >= 400:
        try:
            if response.json().get("code") == 3301:
                return []
        except ValueError:
            pass
    response.raise_for_status()
    files = response.json().get("recording_files", [])
    mp4_files = [
        item for item in files
        if str(item.get("file_type") or item.get("file_extension") or "").upper() == "MP4"
        and item.get("download_url")
        and str(item.get("status") or "completed").lower() == "completed"
    ]
    # Prefer the main/largest MP4 when Zoom provides gallery and speaker views.
    return sorted(mp4_files, key=lambda item: int(item.get("file_size") or 0), reverse=True)


def _ffmpeg_binary():
    configured = getattr(settings, "FFMPEG_BINARY", "")
    if configured and os.path.isfile(configured) and os.access(configured, os.X_OK):
        return configured
    return shutil.which("ffmpeg")


def _compress(source, destination):
    binary = _ffmpeg_binary()
    if not binary:
        raise RuntimeError("FFmpeg is not installed or FFMPEG_BINARY is not configured.")
    result = subprocess.run(
        [
            binary, "-y", "-i", source,
            "-map_metadata", "-1",
            "-c:v", "libx264", "-preset", "fast", "-crf", "28",
            "-pix_fmt", "yuv420p",
            "-c:a", "aac", "-b:a", "96k",
            "-movflags", "+faststart",
            destination,
        ],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.PIPE,
        timeout=int(getattr(settings, "ZOOM_RECORDING_FFMPEG_TIMEOUT", 7200)),
        check=False,
    )
    if result.returncode != 0:
        details = result.stderr.decode("utf-8", errors="replace")[-1500:]
        raise RuntimeError("FFmpeg compression failed: {0}".format(details))


def _download(download_url, access_token, destination):
    separator = "&" if "?" in download_url else "?"
    url = "{0}{1}access_token={2}".format(download_url, separator, access_token)
    with requests.get(url, stream=True, timeout=(30, 180)) as response:
        response.raise_for_status()
        with open(destination, "wb") as output:
            for chunk in response.iter_content(chunk_size=1024 * 1024):
                if chunk:
                    output.write(chunk)


def _process_zoom_recordings(job_id):
    close_old_connections()
    status = read_zoom_recording_job(job_id)
    status["state"] = "processing"
    _write_status(job_id, status)
    job_dir = _job_dir(job_id)

    try:
        today = date.today()
        candidate_limit = int(getattr(settings, "ZOOM_RECORDING_CANDIDATE_LIMIT", 100))
        upload_limit = int(getattr(settings, "ZOOM_RECORDING_UPLOADS_PER_JOB", 1))
        lectures = list(
            Live_Lecture.objects.filter(
                Q(videos="") | Q(videos__isnull=True),
                lacture_date__range=[today - timedelta(days=3), today],
                zoom_key__isnull=False,
            ).exclude(meeting_id__isnull=True).exclude(meeting_id="")
            .select_related("zoom_key")
            .order_by("-lacture_date", "start_time")[:candidate_limit]
        )
        status["total"] = len(lectures)
        _write_status(job_id, status)

        for lecture in lectures:
            if status["uploaded"] >= upload_limit:
                break
            status["current_lecture_id"] = lecture.id
            status["current_lecture_title"] = lecture.title
            _write_status(job_id, status)
            input_path = job_dir / ("{0}-input.mp4".format(lecture.id))
            output_path = job_dir / ("{0}-compressed.mp4".format(lecture.id))

            try:
                access_token = _zoom_access_token(lecture.zoom_key)
                recordings = _zoom_recordings(lecture.meeting_id, access_token)
                if not recordings:
                    status["skipped"] += 1
                    continue

                _download(recordings[0]["download_url"], access_token, str(input_path))
                _compress(str(input_path), str(output_path))
                with output_path.open("rb") as compressed:
                    lecture.videos.save(
                        "{0}.mp4".format(uuid.uuid4().hex),
                        File(compressed),
                        save=True,
                    )
                status["uploaded"] += 1
            except Exception as exc:
                status["failed"] += 1
                status["errors"].append({
                    "lecture_id": lecture.id,
                    "lecture_title": lecture.title,
                    "error": str(exc),
                })
            finally:
                status["processed"] += 1
                for path in (input_path, output_path):
                    try:
                        path.unlink()
                    except OSError:
                        pass
                _write_status(job_id, status)

        status["state"] = "completed" if status["failed"] == 0 else "completed_with_errors"
        status.pop("current_lecture_id", None)
        status.pop("current_lecture_title", None)
    except Exception as exc:
        status["state"] = "failed"
        status["errors"].append({"error": str(exc)})
    finally:
        status["finished_at"] = datetime.utcnow().isoformat() + "Z"
        _write_status(job_id, status)
        _release_active_lock(job_id)
        close_old_connections()
