#!/usr/bin/env python3
"""Backend do Video Editor - Flask porta 5054"""

import json
import os
import subprocess
import threading
import time
import uuid
from datetime import datetime, timedelta
from pathlib import Path

from flask import Flask, jsonify, request, send_file, abort, Response
from flask_cors import CORS
from werkzeug.utils import secure_filename

from processor import processar_video

# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------
TEMP_DIR = Path(__file__).parent / "temp"
OUTPUT_DIR = Path(__file__).parent / "output"
MUSIC_DIR = Path(__file__).parent / "music"
UPLOADS_DIR = MUSIC_DIR / "uploads"
UPLOADS_INDEX = UPLOADS_DIR / "index.json"
MANIFEST_PATH = MUSIC_DIR / "manifest.json"

# Upload de música config
UPLOAD_EXTS_PERMITIDAS = {".mp3", ".m4a", ".aac", ".wav", ".ogg"}
UPLOAD_MAX_BYTES = 20 * 1024 * 1024  # 20 MB
UPLOAD_TTL_HORAS = 24

for d in [TEMP_DIR, OUTPUT_DIR, MUSIC_DIR, UPLOADS_DIR]:
    d.mkdir(exist_ok=True, parents=True)

app = Flask(__name__)
CORS(app, origins="*", supports_credentials=True)

# Jobs em memória: job_id -> dict
jobs: dict = {}

# Cache do manifest (recarrega se mtime mudar)
_manifest_cache = {"mtime": 0, "data": None}


def _load_manifest() -> dict:
    """Carrega manifest.json (com cache por mtime)."""
    if not MANIFEST_PATH.exists():
        return {"tracks": [], "moods": []}
    mtime = MANIFEST_PATH.stat().st_mtime
    if _manifest_cache["data"] is None or mtime != _manifest_cache["mtime"]:
        try:
            with open(MANIFEST_PATH, "r", encoding="utf-8") as f:
                _manifest_cache["data"] = json.load(f)
            _manifest_cache["mtime"] = mtime
        except Exception as e:
            print(f"[manifest] erro ao carregar: {e}")
            return {"tracks": [], "moods": []}
    return _manifest_cache["data"]


def _track_by_id(track_id: str) -> dict | None:
    data = _load_manifest()
    for t in data.get("tracks", []):
        if t.get("id") == track_id:
            return t
    return None


# ---------------------------------------------------------------------------
# Uploads de música (registro em disco + cache em memória)
# ---------------------------------------------------------------------------

# Lock para evitar corrida ao gravar o index.json
_uploads_lock = threading.Lock()


def _load_uploads_index() -> dict:
    """Lê o index.json dos uploads (cria vazio se não existe)."""
    if not UPLOADS_INDEX.exists():
        return {}
    try:
        with open(UPLOADS_INDEX, "r", encoding="utf-8") as f:
            return json.load(f)
    except Exception as e:
        print(f"[uploads] erro lendo index: {e}")
        return {}


def _save_uploads_index(data: dict) -> None:
    with _uploads_lock:
        try:
            with open(UPLOADS_INDEX, "w", encoding="utf-8") as f:
                json.dump(data, f, ensure_ascii=False, indent=2)
        except Exception as e:
            print(f"[uploads] erro salvando index: {e}")


def _upload_by_id(track_id: str) -> dict | None:
    """Retorna metadata de um upload pelo id (ex: 'uploaded-<uuid>')."""
    if not track_id or not track_id.startswith("uploaded-"):
        return None
    data = _load_uploads_index()
    return data.get(track_id)


def _ffprobe_duracao(path: str) -> float:
    """Retorna duração em segundos via ffprobe (0 em caso de falha)."""
    try:
        r = subprocess.run(
            ["ffprobe", "-v", "quiet", "-print_format", "json",
             "-show_format", path],
            capture_output=True, text=True
        )
        if r.returncode != 0:
            return 0.0
        info = json.loads(r.stdout or "{}")
        return float(info.get("format", {}).get("duration", 0))
    except Exception as e:
        print(f"[ffprobe] erro: {e}")
        return 0.0


def _limpar_uploads_antigos() -> int:
    """
    Remove uploads com mais de UPLOAD_TTL_HORAS.
    Atualiza o index.json em disco. Retorna quantos removeu.
    """
    cutoff = datetime.utcnow() - timedelta(hours=UPLOAD_TTL_HORAS)
    data = _load_uploads_index()
    if not data:
        # ainda assim varre o diretório por arquivos órfãos antigos
        removidos = 0
        if UPLOADS_DIR.exists():
            for f in UPLOADS_DIR.iterdir():
                if f.is_file() and f.name != "index.json":
                    mtime = datetime.utcfromtimestamp(f.stat().st_mtime)
                    if mtime < cutoff:
                        try:
                            f.unlink()
                            removidos += 1
                        except Exception:
                            pass
        return removidos

    removidos = 0
    novos = {}
    for tid, meta in data.items():
        try:
            criado = datetime.fromisoformat(meta.get("created_at", ""))
        except Exception:
            criado = datetime.utcnow()
        if criado < cutoff:
            # remove arquivo
            p = meta.get("path")
            if p and Path(p).exists():
                try:
                    Path(p).unlink()
                except Exception:
                    pass
            removidos += 1
        else:
            novos[tid] = meta

    if removidos > 0:
        _save_uploads_index(novos)
    return removidos


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def callback_status(job_id: str, status: str, progresso: int, mensagem: str, video_path: str = None):
    if job_id not in jobs:
        return
    jobs[job_id].update({
        "status": status,
        "progresso": progresso,
        "mensagem": mensagem,
        "updated_at": datetime.utcnow().isoformat()
    })
    if video_path:
        jobs[job_id]["video_path"] = video_path


def cleanup_old_files():
    """Remove arquivos temporários e de output com mais de 24 horas."""
    cutoff = datetime.now() - timedelta(hours=24)
    for d in [TEMP_DIR, OUTPUT_DIR]:
        for f in d.iterdir():
            if f.is_file():
                mtime = datetime.fromtimestamp(f.stat().st_mtime)
                if mtime < cutoff:
                    try:
                        f.unlink()
                    except Exception:
                        pass

# Roda cleanup na inicialização
cleanup_old_files()
try:
    _rm = _limpar_uploads_antigos()
    if _rm:
        print(f"[uploads] Limpeza inicial: {_rm} upload(s) expirado(s) removido(s).")
except Exception as _e:
    print(f"[uploads] erro na limpeza inicial: {_e}")

# ---------------------------------------------------------------------------
# Endpoints
# ---------------------------------------------------------------------------

@app.route("/api/health", methods=["GET"])
def health():
    return jsonify({"status": "ok", "timestamp": datetime.utcnow().isoformat()})


@app.route("/api/processar", methods=["POST"])
def processar():
    """Inicia o processamento de um vídeo."""
    if "video" not in request.files:
        return jsonify({"erro": "Arquivo de vídeo não enviado."}), 400

    video_file = request.files["video"]
    if not video_file.filename:
        return jsonify({"erro": "Nome do arquivo inválido."}), 400

    # Valida extensão
    ext = Path(video_file.filename).suffix.lower()
    if ext not in {".mp4", ".mov", ".avi", ".webm", ".mkv"}:
        return jsonify({"erro": f"Formato não suportado: {ext}"}), 400

    modo = request.form.get("modo", "clip")

    # 3 toggles independentes (qualquer combinação aceita)
    def _bool(name, default=True):
        v = request.form.get(name)
        if v is None:
            return default
        return str(v).lower().strip() in {"true", "1", "yes", "on", "sim"}

    # Compat: nome antigo "musica" ainda funciona como sinônimo de "usar_musica"
    usar_musica = _bool("usar_musica", default=_bool("musica", default=True))
    usar_legenda = _bool("usar_legenda", default=True)
    usar_broll = _bool("usar_broll", default=True)

    musica_id = (request.form.get("musica_id") or "").strip() or None

    # Se musica_id foi enviado, valida no manifest OU em uploads
    musica_path_direto = None
    if musica_id:
        if musica_id.startswith("uploaded-"):
            up = _upload_by_id(musica_id)
            if not up:
                return jsonify({"erro": f"musica_id de upload inválido ou expirado: {musica_id}"}), 400
            cand = Path(up.get("path", ""))
            if not cand.exists():
                return jsonify({"erro": "arquivo enviado não encontrado no servidor (pode ter expirado)"}), 500
            musica_path_direto = str(cand)
            usar_musica = True
        else:
            t = _track_by_id(musica_id)
            if not t:
                return jsonify({"erro": f"musica_id inválido: {musica_id}"}), 400
            cand = MUSIC_DIR / t["arquivo"]
            if not cand.exists():
                return jsonify({"erro": f"arquivo da música não encontrado no servidor: {t['arquivo']}"}), 500
            musica_path_direto = str(cand)
            usar_musica = True  # garante coerência

    # Volume da música (0..50, default 12)
    try:
        volume_musica = int(request.form.get("volume_musica", "12"))
    except (TypeError, ValueError):
        volume_musica = 12
    volume_musica = max(0, min(50, volume_musica))

    estilo_legenda = request.form.get("estilo_legenda", "karaoke_ig")
    tamanho_legenda = request.form.get("tamanho_legenda", "medio")
    if tamanho_legenda not in {"pequeno", "medio", "grande"}:
        try:
            int(tamanho_legenda)
        except ValueError:
            tamanho_legenda = "medio"

    segmentos = []
    seg_json = request.form.get("segmentos", "[]")
    try:
        segmentos = json.loads(seg_json)
    except Exception:
        pass

    job_id = str(uuid.uuid4())

    # Salva o vídeo no temp
    video_path = str(TEMP_DIR / f"{job_id}_input{ext}")
    video_file.save(video_path)

    # Registra job
    jobs[job_id] = {
        "status": "fila",
        "progresso": 0,
        "mensagem": "Na fila...",
        "video_path": None,
        "created_at": datetime.utcnow().isoformat(),
        "updated_at": datetime.utcnow().isoformat()
    }

    # Dispara processamento em background
    t = threading.Thread(
        target=processar_video,
        args=(job_id, video_path, modo, segmentos, usar_musica, estilo_legenda, callback_status),
        kwargs={
            "tamanho_legenda": tamanho_legenda,
            "musica_path_direto": musica_path_direto,
            "usar_legenda": usar_legenda,
            "usar_broll": usar_broll,
            "volume_musica": volume_musica,
        },
        daemon=True
    )
    t.start()

    return jsonify({"job_id": job_id})


@app.route("/api/job/<job_id>", methods=["GET"])
def status_job(job_id: str):
    """Retorna status atual de um job."""
    if job_id not in jobs:
        return jsonify({"erro": "Job não encontrado."}), 404

    job = jobs[job_id].copy()
    result = {
        "status": job["status"],
        "progresso": job["progresso"],
        "mensagem": job["mensagem"]
    }

    if job["status"] == "concluido" and job.get("video_path"):
        result["video_url"] = f"/api/download/{job_id}"

    return jsonify(result)


@app.route("/api/download/<job_id>", methods=["GET"])
def download_video(job_id: str):
    """Baixa o vídeo processado."""
    if job_id not in jobs:
        return jsonify({"erro": "Job não encontrado."}), 404

    job = jobs[job_id]
    if job["status"] != "concluido" or not job.get("video_path"):
        return jsonify({"erro": "Vídeo ainda não disponível."}), 404

    video_path = job["video_path"]
    if not Path(video_path).exists():
        return jsonify({"erro": "Arquivo não encontrado no servidor."}), 404

    return send_file(
        video_path,
        mimetype="video/mp4",
        as_attachment=True,
        download_name=f"reel_{job_id[:8]}.mp4"
    )


# ---------------------------------------------------------------------------
# Biblioteca de músicas
# ---------------------------------------------------------------------------

@app.route("/api/musicas", methods=["GET"])
def listar_musicas():
    """Retorna a lista de trilhas do manifest enriquecida com preview_url."""
    data = _load_manifest()
    tracks = []
    for t in data.get("tracks", []):
        arquivo = MUSIC_DIR / t.get("arquivo", "")
        if not arquivo.exists():
            continue
        tracks.append({
            "id": t["id"],
            "nome": t["nome"],
            "mood": t["mood"],
            "duracao": t["duracao"],
            "fonte": t.get("fonte", ""),
            "creditos": t.get("creditos", ""),
            "preview_url": f"/api/musicas/{t['id']}/preview",
        })
    return jsonify({
        "tracks": tracks,
        "moods": data.get("moods", []),
        "fonte": data.get("fonte", ""),
        "licenca": data.get("licenca", ""),
    })


@app.route("/api/musicas/<track_id>/preview", methods=["GET"])
def preview_musica(track_id: str):
    """Serve o MP3 com suporte a Range (necessário pro <audio> HTML5 fazer seek)."""
    arquivo: Path | None = None
    mimetype = "audio/mpeg"
    download_name = f"{track_id}.mp3"

    if track_id.startswith("uploaded-"):
        up = _upload_by_id(track_id)
        if not up:
            abort(404)
        p = Path(up.get("path", ""))
        if not p.exists():
            abort(404)
        arquivo = p
        ext = p.suffix.lower().lstrip(".")
        mime_map = {
            "mp3": "audio/mpeg",
            "m4a": "audio/mp4",
            "aac": "audio/aac",
            "wav": "audio/wav",
            "ogg": "audio/ogg",
        }
        mimetype = mime_map.get(ext, "application/octet-stream")
        download_name = f"{track_id}.{ext}"
    else:
        t = _track_by_id(track_id)
        if not t:
            abort(404)
        cand = MUSIC_DIR / t["arquivo"]
        if not cand.exists():
            abort(404)
        arquivo = cand

    # send_file com conditional=True já lida com Range (HTTP 206)
    resp = send_file(
        str(arquivo),
        mimetype=mimetype,
        conditional=True,
        as_attachment=False,
        download_name=download_name,
    )
    resp.headers["Cache-Control"] = "public, max-age=3600"
    resp.headers["Accept-Ranges"] = "bytes"
    return resp


@app.route("/api/musicas/upload", methods=["POST"])
def upload_musica():
    """
    Aceita upload de uma trilha sonora do usuário.
    multipart/form-data, campo 'arquivo'.
    Salva em music/uploads/<uuid>.<ext> e registra em index.json.
    Retorna metadata pro frontend.
    """
    if "arquivo" not in request.files:
        return jsonify({"erro": "Campo 'arquivo' não enviado."}), 400

    f = request.files["arquivo"]
    if not f or not f.filename:
        return jsonify({"erro": "Arquivo inválido."}), 400

    nome_original = secure_filename(f.filename) or "musica"
    ext = Path(nome_original).suffix.lower()
    if ext not in UPLOAD_EXTS_PERMITIDAS:
        return jsonify({
            "erro": f"Formato não suportado: {ext or '(sem extensão)'}. "
                    f"Aceitos: {', '.join(sorted(UPLOAD_EXTS_PERMITIDAS))}"
        }), 400

    # Valida tamanho (lê stream em chunks pra não estourar memória)
    # Primeiro tenta pelo content_length (rápido). Se faltar, valida ao salvar.
    cl = request.content_length
    if cl and cl > UPLOAD_MAX_BYTES + 8192:  # margem dos headers do multipart
        return jsonify({"erro": f"Arquivo excede o limite de {UPLOAD_MAX_BYTES // (1024*1024)}MB."}), 413

    uid = uuid.uuid4().hex
    upload_id = f"uploaded-{uid}"
    dest = UPLOADS_DIR / f"{uid}{ext}"

    try:
        # Salva em chunks validando tamanho
        total = 0
        with open(dest, "wb") as out:
            while True:
                chunk = f.stream.read(64 * 1024)
                if not chunk:
                    break
                total += len(chunk)
                if total > UPLOAD_MAX_BYTES:
                    out.close()
                    try:
                        dest.unlink()
                    except Exception:
                        pass
                    return jsonify({"erro": f"Arquivo excede o limite de {UPLOAD_MAX_BYTES // (1024*1024)}MB."}), 413
                out.write(chunk)
    except Exception as e:
        try:
            if dest.exists():
                dest.unlink()
        except Exception:
            pass
        return jsonify({"erro": f"Falha ao salvar arquivo: {e}"}), 500

    duracao = _ffprobe_duracao(str(dest))
    if duracao <= 0:
        # Arquivo provavelmente corrompido ou não é áudio válido
        try:
            dest.unlink()
        except Exception:
            pass
        return jsonify({"erro": "Não foi possível ler o arquivo como áudio válido."}), 400

    meta = {
        "id": upload_id,
        "nome": Path(nome_original).stem or "Minha música",
        "mood": "uploaded",
        "duracao": round(duracao, 2),
        "arquivo": dest.name,
        "path": str(dest),
        "created_at": datetime.utcnow().isoformat(),
    }

    # Atualiza index.json (under lock)
    with _uploads_lock:
        idx = _load_uploads_index()
        idx[upload_id] = meta
        try:
            with open(UPLOADS_INDEX, "w", encoding="utf-8") as f_out:
                json.dump(idx, f_out, ensure_ascii=False, indent=2)
        except Exception as e:
            print(f"[uploads] erro salvando index: {e}")

    return jsonify({
        "id": upload_id,
        "nome": meta["nome"],
        "mood": "uploaded",
        "duracao": meta["duracao"],
        "preview_url": f"/api/musicas/{upload_id}/preview",
    })


# ---------------------------------------------------------------------------
# Startup
# ---------------------------------------------------------------------------
if __name__ == "__main__":
    print("=" * 60)
    print("Video Editor Backend - Porta 5054")
    print(f"Temp dir:   {TEMP_DIR}")
    print(f"Output dir: {OUTPUT_DIR}")
    print(f"Music dir:  {MUSIC_DIR}")
    manifest = _load_manifest()
    print(f"Music tracks no manifest: {len(manifest.get('tracks', []))}")
    print("=" * 60)
    app.run(host="0.0.0.0", port=5054, debug=False, threaded=True)
