from flask import Flask, request, send_file, render_template_string, jsonify
import subprocess
import os
import uuid
import json
import re
import threading
import queue
import time
from datetime import datetime
from pathlib import Path
from typing import Any

app = Flask(__name__)

# ================= CONFIGURACOES =================
VIDEO_DIR = Path("/var/project/videos/drige")
TMP_THUMBS = Path("/tmp/thumbs")
CUT_DIR = Path("/var/www/project/cortes")
SNAPSHOT_DIR = Path("/var/www/project/snapshots")
QUEUE_FILE = Path("/tmp/video_queue.json")
ALLOWED_VIDEO_EXT = {".mp4", ".mkv", ".avi", ".mov"}

TMP_THUMBS.mkdir(parents=True, exist_ok=True)
CUT_DIR.mkdir(parents=True, exist_ok=True)
SNAPSHOT_DIR.mkdir(parents=True, exist_ok=True)


def now_iso() -> str:
    return datetime.now().isoformat(timespec="seconds")


def error(message: str, status: int = 400):
    return jsonify({"error": message}), status


def secure_subpath(base: Path, *parts: str) -> Path:
    candidate = (base / Path(*parts)).resolve()
    base_resolved = base.resolve()
    if not str(candidate).startswith(str(base_resolved) + os.sep) and candidate != base_resolved:
        raise ValueError("Caminho invalido")
    return candidate


def parse_bool(value: Any) -> bool:
    if isinstance(value, bool):
        return value
    return str(value).lower() in {"1", "true", "yes", "on"}


def list_subdirs(base: Path):
    if not base.exists():
        return []
    return sorted([d.name for d in base.iterdir() if d.is_dir()])


def sanitize_name(raw: str, fallback: str) -> str:
    cleaned = "".join(c for c in raw if c.isalnum() or c in "._- ").strip()
    return (cleaned or fallback)[:120]


class ConversionQueue:
    def __init__(self, max_workers: int = 3):
        self.queue = queue.Queue()
        self.jobs: dict[str, dict[str, Any]] = {}
        self.completed_jobs: dict[str, dict[str, Any]] = {}
        self.failed_jobs: dict[str, dict[str, Any]] = {}
        self.max_workers = max_workers
        self.lock = threading.Lock()
        self.load_state()
        for _ in range(max_workers):
            threading.Thread(target=self.worker, daemon=True).start()

    def load_state(self):
        if not QUEUE_FILE.exists():
            return
        try:
            with QUEUE_FILE.open("r", encoding="utf-8") as f:
                data = json.load(f)
            self.completed_jobs = data.get("completed", {})
            self.failed_jobs = data.get("failed", {})
        except Exception:
            # Arquivo corrompido nao deve derrubar servico
            self.completed_jobs = {}
            self.failed_jobs = {}

    def save_state(self):
        payload = {"completed": self.completed_jobs, "failed": self.failed_jobs}
        tmp = QUEUE_FILE.with_suffix(".tmp")
        with tmp.open("w", encoding="utf-8") as f:
            json.dump(payload, f, ensure_ascii=False)
        os.replace(tmp, QUEUE_FILE)

    def clear_history(self):
        with self.lock:
            self.completed_jobs = {}
            self.failed_jobs = {}
            self.save_state()

    def get_all_status(self):
        with self.lock:
            jobs = list(self.jobs.values())
            queued = [j for j in jobs if j["status"] == "queued"]
            processing = [j for j in jobs if j["status"] == "processing"]
            completed = list(self.completed_jobs.values())[::-1]
            failed = list(self.failed_jobs.values())[::-1]
            return {
                "queued": queued,
                "processing": processing,
                "completed": completed,
                "failed": failed,
            }

    def add_job(self, job_id: str, job_data: dict[str, Any]):
        with self.lock:
            payload = dict(job_data)
            payload["id"] = job_id
            payload["status"] = "queued"
            payload["progress"] = 0
            payload["created_at"] = now_iso()
            self.jobs[job_id] = payload
            self.queue.put(job_id)
        return job_id

    def worker(self):
        while True:
            job_id = self.queue.get()
            if job_id is None:
                break
            try:
                with self.lock:
                    job = self.jobs.get(job_id)
                    if not job:
                        continue
                    job["status"] = "processing"
                self.process_conversion(job_id)
                with self.lock:
                    job = self.jobs.pop(job_id, None)
                    if job:
                        job["status"] = "completed"
                        job["finished_at"] = now_iso()
                        self.completed_jobs[job_id] = job
                        self.save_state()
            except Exception as exc:
                with self.lock:
                    job = self.jobs.pop(job_id, {"id": job_id})
                    job["status"] = "failed"
                    job["finished_at"] = now_iso()
                    job["error"] = str(exc)
                    self.failed_jobs[job_id] = job
                    self.save_state()
            finally:
                self.queue.task_done()

    def process_conversion(self, job_id: str):
        with self.lock:
            job = self.jobs[job_id]

        src_file = secure_subpath(VIDEO_DIR, job["pasta"], job["file"])
        if not src_file.exists():
            raise FileNotFoundError("Arquivo fonte nao encontrado")

        dest_dir = CUT_DIR if not job.get("pasta_corte") else secure_subpath(CUT_DIR, job["pasta_corte"])
        dest_dir.mkdir(parents=True, exist_ok=True)

        safe_name = sanitize_name(job.get("custom_name") or "", f"cut_{job_id[:8]}")
        output_path = dest_dir / f"{safe_name}.mp4"

        cmd = [
            "ffmpeg",
            "-y",
            "-nostdin",
            "-progress",
            "pipe:1",
            "-ss",
            str(job["start"]),
            "-t",
            str(job["duration"]),
            "-i",
            str(src_file),
        ]

        vf_filters = []
        if job.get("mode") == "tiktok":
            vf_filters.append("crop=ih*(9/16):ih")

        if vf_filters:
            cmd.extend(["-vf", ",".join(vf_filters)])

        cmd.extend(["-c:v", "libx264", "-preset", "ultrafast"])
        cmd.extend(["-crf", "28" if job.get("mode") == "whatsapp" else "22"])

        if job.get("mute"):
            cmd.append("-an")
        else:
            cmd.extend(["-c:a", "aac", "-b:a", "128k"])

        cmd.extend(["-movflags", "+faststart", str(output_path)])

        process = subprocess.Popen(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            bufsize=1,
        )

        try:
            while True:
                line = process.stdout.readline() if process.stdout else ""
                if not line:
                    break
                if "out_time_ms" in line:
                    match = re.search(r"out_time_ms=(\d+)", line)
                    if match:
                        current_s = int(match.group(1)) / 1_000_000.0
                        progress = min(int((current_s / job["duration"]) * 100), 100)
                        with self.lock:
                            if job_id in self.jobs:
                                self.jobs[job_id]["progress"] = progress
            _, stderr = process.communicate()
        finally:
            if process.stdout:
                process.stdout.close()
            if process.stderr:
                process.stderr.close()

        if process.returncode != 0:
            raise RuntimeError(f"Erro FFmpeg: {(stderr or '').strip()[-250:]}")

        with self.lock:
            if job_id in self.jobs:
                self.jobs[job_id]["output_path"] = str(output_path)
                self.jobs[job_id]["progress"] = 100


conversion_queue = ConversionQueue(max_workers=3)

thumb_generation_guard: set[str] = set()
thumb_generation_lock = threading.Lock()

# Template menor e funcional para manter o backend autocontido
TEMPLATE = """
<!DOCTYPE html>
<html lang="pt-br">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>Studio Mobile V4</title>
  <style>
    body { font-family: sans-serif; max-width: 900px; margin: 20px auto; padding: 0 12px; }
    .row { display: flex; gap: 8px; margin-bottom: 10px; flex-wrap: wrap; }
    input, select, button { padding: 8px; }
    #queueList div { border: 1px solid #ddd; padding: 6px; margin-bottom: 6px; }
  </style>
</head>
<body>
  <h2>Studio Mobile V4</h2>
  <div class="row">
    <select id="pasta">{% for p in pastas %}<option value="{{p}}">{{p}}</option>{% endfor %}</select>
    <select id="videoSelect"></select>
  </div>
  <div class="row">
    <input id="start" type="number" min="0" value="0" />
    <input id="duration" type="number" min="1" value="30" />
    <input id="customName" type="text" placeholder="Nome do corte" />
  </div>
  <div class="row">
    <select id="mode">
      <option value="normal">normal</option>
      <option value="tiktok">tiktok</option>
      <option value="whatsapp">whatsapp</option>
    </select>
    <label><input id="mute" type="checkbox" /> sem audio</label>
    <select id="pastaCorte"><option value="">Raiz</option>{% for pc in pastas_corte %}<option value="{{pc}}">{{pc}}</option>{% endfor %}</select>
    <button onclick="addQueue()">Adicionar fila</button>
  </div>
  <h3>Fila</h3>
  <div id="queueList"></div>
<script>
async function loadVideos() {
  const pasta = document.getElementById('pasta').value;
  const res = await fetch(`/videos_list?pasta=${encodeURIComponent(pasta)}`);
  const list = await res.json();
  const sel = document.getElementById('videoSelect');
  sel.innerHTML = list.map(v => `<option value="${v}">${v}</option>`).join('');
}

async function addQueue() {
  const body = {
    pasta: document.getElementById('pasta').value,
    file: document.getElementById('videoSelect').value,
    start: Number(document.getElementById('start').value || 0),
    duration: Number(document.getElementById('duration').value || 30),
    pasta_corte: document.getElementById('pastaCorte').value,
    mode: document.getElementById('mode').value,
    mute: document.getElementById('mute').checked,
    custom_name: document.getElementById('customName').value,
  };
  const res = await fetch('/add_to_queue', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)});
  if (!res.ok) {
    const e = await res.json();
    alert(e.error || 'erro');
  }
}

setInterval(async () => {
  const res = await fetch('/queue_status');
  if (!res.ok) return;
  const data = await res.json();
  const all = [...data.processing, ...data.queued, ...data.completed, ...data.failed];
  document.getElementById('queueList').innerHTML = all.map(j => `<div>${j.status} - ${j.file || j.id} (${j.progress || 0}%) ${j.status==='completed' ? `<a href="/download_job/${j.id}">download</a>` : ''}</div>`).join('');
}, 1200);

window.onload = loadVideos;
document.getElementById('pasta').addEventListener('change', loadVideos);
</script>
</body>
</html>
"""


def validate_video_payload(data: dict[str, Any]):
    required = {"pasta", "file", "start", "duration"}
    missing = [k for k in required if k not in data]
    if missing:
        raise ValueError(f"Campos obrigatorios ausentes: {', '.join(missing)}")

    pasta = str(data["pasta"]).strip()
    file_name = str(data["file"]).strip()

    if not pasta or "/" in pasta or "\\" in pasta:
        raise ValueError("Pasta invalida")

    if not file_name:
        raise ValueError("Arquivo invalido")

    ext = Path(file_name).suffix.lower()
    if ext not in ALLOWED_VIDEO_EXT:
        raise ValueError("Extensao de arquivo nao permitida")

    start = float(data["start"])
    duration = float(data["duration"])

    if start < 0:
        raise ValueError("Inicio deve ser >= 0")

    if duration <= 0 or duration > 60 * 60 * 6:
        raise ValueError("Duracao invalida")

    mode = str(data.get("mode", "normal")).strip().lower()
    if mode not in {"normal", "tiktok", "whatsapp"}:
        raise ValueError("Mode invalido")

    pasta_corte = str(data.get("pasta_corte", "")).strip()
    if pasta_corte and ("/" in pasta_corte or "\\" in pasta_corte):
        raise ValueError("Destino invalido")

    return {
        "pasta": pasta,
        "file": file_name,
        "start": start,
        "duration": duration,
        "pasta_corte": pasta_corte,
        "mode": mode,
        "mute": parse_bool(data.get("mute", False)),
        "custom_name": str(data.get("custom_name", "")).strip(),
    }


# ================= ROTAS FLASK =================

@app.route("/")
def index():
    pastas = list_subdirs(VIDEO_DIR)
    pastas_corte = list_subdirs(CUT_DIR)
    return render_template_string(TEMPLATE, pastas=pastas, pastas_corte=pastas_corte)


@app.route("/videos_list")
def videos_list():
    pasta = (request.args.get("pasta") or "").strip()
    if not pasta:
        return jsonify([])

    try:
        dir_path = secure_subpath(VIDEO_DIR, pasta)
    except ValueError:
        return jsonify([])

    if not dir_path.exists() or not dir_path.is_dir():
        return jsonify([])

    videos = [
        f.name for f in dir_path.iterdir() if f.is_file() and f.suffix.lower() in ALLOWED_VIDEO_EXT
    ]
    return jsonify(sorted(videos))


@app.route("/video")
def video_stream():
    pasta = (request.args.get("pasta") or "").strip()
    file_name = (request.args.get("file") or "").strip()
    if not pasta or not file_name:
        return error("Parametros invalidos", 400)

    try:
        path = secure_subpath(VIDEO_DIR, pasta, file_name)
    except ValueError:
        return error("Caminho invalido", 400)

    if not path.exists() or not path.is_file():
        return error("Arquivo nao encontrado", 404)

    return send_file(path)


@app.route("/take_snapshot", methods=["POST"])
def take_snapshot():
    data = request.get_json(silent=True) or {}
    try:
        pasta = str(data.get("pasta", "")).strip()
        file_name = str(data.get("file", "")).strip()
        at = float(data.get("time", 0))
        if at < 0:
            raise ValueError("time invalido")

        src = secure_subpath(VIDEO_DIR, pasta, file_name)
        if not src.exists():
            return error("Arquivo nao encontrado", 404)

        name = f"snap_{int(time.time())}_{uuid.uuid4().hex[:6]}.jpg"
        out = SNAPSHOT_DIR / name

        proc = subprocess.run(
            [
                "ffmpeg",
                "-y",
                "-ss",
                str(at),
                "-i",
                str(src),
                "-frames:v",
                "1",
                "-q:v",
                "2",
                str(out),
            ],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
        )
        if proc.returncode != 0:
            return error("Falha ao gerar snapshot", 500)

        return jsonify({"success": True, "name": name})
    except ValueError as exc:
        return error(str(exc), 400)


@app.route("/thumbs")
def generate_thumbs():
    file_name = (request.args.get("file") or "").strip()
    pasta = (request.args.get("pasta") or "").strip()
    step = request.args.get("step", "60")

    try:
        step_int = int(step)
    except ValueError:
        return error("step invalido", 400)

    if step_int < 10 or step_int > 600:
        return error("step fora do intervalo", 400)

    try:
        src = secure_subpath(VIDEO_DIR, pasta, file_name)
    except ValueError:
        return error("Caminho invalido", 400)

    if not src.exists():
        return error("Arquivo nao encontrado", 404)

    vid_id = uuid.uuid5(uuid.NAMESPACE_URL, f"{pasta}/{file_name}:{step_int}").hex
    out_dir = TMP_THUMBS / vid_id
    out_dir.mkdir(parents=True, exist_ok=True)

    with thumb_generation_lock:
        if vid_id in thumb_generation_guard:
            return jsonify({"status": "already_running", "vid_id": vid_id})
        thumb_generation_guard.add(vid_id)

    def worker():
        try:
            dur_raw = subprocess.check_output(
                [
                    "ffprobe",
                    "-v",
                    "0",
                    "-show_entries",
                    "format=duration",
                    "-of",
                    "default=nw=1:nk=1",
                    str(src),
                ],
                text=True,
            ).strip()
            duration = int(float(dur_raw))

            for t in range(0, max(duration, 1), step_int):
                out = out_dir / f"f_{t:05d}.jpg"
                if out.exists():
                    continue
                subprocess.run(
                    [
                        "ffmpeg",
                        "-y",
                        "-ss",
                        str(t),
                        "-i",
                        str(src),
                        "-frames:v",
                        "1",
                        "-q:v",
                        "8",
                        "-vf",
                        "scale=320:-1",
                        str(out),
                        "-loglevel",
                        "quiet",
                    ]
                )
        finally:
            with thumb_generation_lock:
                thumb_generation_guard.discard(vid_id)

    threading.Thread(target=worker, daemon=True).start()
    return jsonify({"status": "ok", "vid_id": vid_id})


@app.route("/thumbs_list")
def thumbs_list():
    pasta = (request.args.get("pasta") or "").strip()
    file_name = (request.args.get("file") or "").strip()
    step = request.args.get("step", "60")

    try:
        step_int = int(step)
    except ValueError:
        return jsonify([])

    vid_id = uuid.uuid5(uuid.NAMESPACE_URL, f"{pasta}/{file_name}:{step_int}").hex
    out_dir = TMP_THUMBS / vid_id
    if not out_dir.exists():
        return jsonify([])

    items = []
    for f in sorted(out_dir.iterdir()):
        if not f.name.startswith("f_") or not f.name.endswith(".jpg"):
            continue
        try:
            sec = int(f.name[2:7])
        except ValueError:
            continue
        items.append({"sec": sec, "src": f"/thumb/{vid_id}/{f.name}"})

    return jsonify(items)


@app.route("/thumb/<vid>/<name>")
def serve_thumb(vid, name):
    try:
        path = secure_subpath(TMP_THUMBS, vid, name)
    except ValueError:
        return error("Caminho invalido", 400)

    if not path.exists() or not path.is_file():
        return error("Thumb nao encontrada", 404)

    return send_file(path)


@app.route("/add_to_queue", methods=["POST"])
def add_to_queue():
    data = request.get_json(silent=True)
    if not isinstance(data, dict):
        return error("JSON invalido", 400)

    try:
        validated = validate_video_payload(data)
        src = secure_subpath(VIDEO_DIR, validated["pasta"], validated["file"])
        if not src.exists() or not src.is_file():
            return error("Arquivo fonte nao encontrado", 404)
    except ValueError as exc:
        return error(str(exc), 400)

    job_id = uuid.uuid4().hex
    conversion_queue.add_job(job_id, validated)
    return jsonify({"id": job_id})


@app.route("/queue_status")
def queue_status():
    return jsonify(conversion_queue.get_all_status())


@app.route("/clear_queue", methods=["POST"])
def clear_queue():
    conversion_queue.clear_history()
    return jsonify({"success": True})


@app.route("/download_job/<job_id>")
def download_job(job_id):
    job = conversion_queue.completed_jobs.get(job_id)
    if not job:
        return error("Job nao encontrado", 404)

    output_path = Path(job.get("output_path", ""))
    if not output_path.exists():
        return error("Arquivo de saida nao encontrado", 404)

    return send_file(output_path, as_attachment=True)


if __name__ == "__main__":
    app.run("0.0.0.0", 5000, threaded=True)
