import json
import os
import pathlib
import re
import subprocess
import tempfile
import unicodedata
import zipfile
from datetime import datetime

import pandas as pd
from tqdm import tqdm


def slugify(text: str) -> str:
    text = unicodedata.normalize("NFKD", text)
    text = text.encode("ascii", "ignore").decode("ascii")
    text = re.sub(r"\W+", "_", text.lower())
    return re.sub(r"_+", "_", text[:40].strip("_")) or "clip"


def hhmmss_to_sec(t: str) -> float:
    dt = datetime.strptime(
        t, "%H:%M:%S.%f") if "." in t else datetime.strptime(t, "%H:%M:%S")
    return dt.hour * 3600 + dt.minute * 60 + dt.second + dt.microsecond / 1e6


def sec_to_hhmmss(seconds: float) -> str:
    s = max(0, seconds)
    h = int(s // 3600)
    m = int((s % 3600) // 60)
    s = s % 60
    return f"{h:02}:{m:02}:{s:06.3f}"


def clasifica(segundos):
    if segundos <= 30:
        return "muy_corto"
    elif segundos <= 90:
        return "ideal"
    elif segundos <= 179:
        return "largo"
    else:
        return "muy_largo"


def procesar_clips(
    archivo_json,
    video_h=None,
    video_v=None,
    offset=0,
    offset_h=0,
    categoria=None,
    duracion=None,
    dry_run=False,
    export_zip=False,
    out_dir="clips"
):
    ruta = pathlib.Path(out_dir)
    out_h = ruta / "horizontal"
    out_v = ruta / "vertical"
    out_h.mkdir(parents=True, exist_ok=True)
    out_v.mkdir(parents=True, exist_ok=True)

    with open(archivo_json, encoding="utf-8") as f:
        raw_clips = json.load(f)

    clips = [c for c in raw_clips if not categoria or c.get(
        "category") == categoria]

    if duracion:
        clips = [c for c in clips if clasifica(hhmmss_to_sec(
            c["end"]) - hhmmss_to_sec(c["start"])) == duracion]

    excel_data = []
    for i, c in enumerate(clips):
        dur = round(hhmmss_to_sec(c["end"]) - hhmmss_to_sec(c["start"]), 2)
        excel_data.append({
            "slug": c.get("slug"),
            "titulo": c.get("titulo"),
            "descripcion": c.get("descripcion", ""),
            "feeling": c.get("feeling", ""),
            "category": c.get("category", ""),
            "start": c.get("start"),
            "end": c.get("end"),
            "duracion_segundos": dur,
            "duracion_mmss": f"{int(dur // 60)}:{int(dur % 60):02}",
            "clasificacion_duracion": clasifica(dur),
            "score": c.get("score", ""),
            "tags": c.get("tags", ""),
            "quote": c.get("quote", ""),
            "kewords_detected": c.get("kewords_detected", ""),
            "personas_mencionadas": c.get("personas_mencionadas", ""),
            "actionable_tip": c.get("actionable_tip", ""),
            "thumbnail_hint": c.get("thumbnail_hint", ""),
            "platform_fit": c.get("platform_fit", ""),
            "transcript_excerpt": c.get("transcript_excerpt", ""),
            "ia_notes": c.get("ia_notes", ""),
            "fecha_publicacion": "",
            "estado": "por_publicar",
            "used_in_publication": c.get("used_in_publication", ""),
        })
    excel_path = ruta / "estado_clips.xlsx"
    pd.DataFrame(excel_data).to_excel(excel_path, index=False)

    if video_h:
        print("🎬 Procesando video horizontal...")
        for i, c in enumerate(tqdm(clips, desc="Horizontal")):
            start = hhmmss_to_sec(c["start"]) - offset_h
            end = hhmmss_to_sec(c["end"]) - offset_h
            slug = slugify(c.get("slug", f"clip_{i:02d}"))
            out = str(out_h / f"{i:02d}_{slug}.mp4").replace("\\", "/")
            cmd = f'ffmpeg -y -hide_banner -loglevel error -ss {sec_to_hhmmss(start)} -to {sec_to_hhmmss(end)} -i "{video_h}" -c copy "{out}"'
            if dry_run:
                print("[DRY RUN]", cmd)
            else:
                subprocess.run(cmd, shell=True, check=True)

    if video_v:
        print("🎬 Procesando video vertical...")
        for i, c in enumerate(tqdm(clips, desc="Vertical")):
            start = hhmmss_to_sec(c["start"]) - offset
            end = hhmmss_to_sec(c["end"]) - offset
            slug = slugify(c.get("slug", f"clip_{i:02d}"))
            out = str(out_v / f"{i:02d}_{slug}.mp4").replace("\\", "/")
            cmd = f'ffmpeg -y -hide_banner -loglevel error -ss {sec_to_hhmmss(start)} -to {sec_to_hhmmss(end)} -i "{video_v}" -c copy "{out}"'
            if dry_run:
                print("[DRY RUN]", cmd)
            else:
                subprocess.run(cmd, shell=True, check=True)

    if export_zip:
        zip_path = ruta / "clips_exportados.zip"
        with zipfile.ZipFile(zip_path, "w") as zipf:
            for folder in [out_h, out_v]:
                for file in folder.glob("*.mp4"):
                    zipf.write(file, arcname=file.relative_to(ruta))
            zipf.write(ruta / "estado_clips.xlsx", arcname="estado_clips.xlsx")
        print("🗜️ ZIP generado:", zip_path)

    print("\n✅ ¡Listo! Clips generados.")
    print("🗂️  Excel:", ruta / "estado_clips.xlsx")
    if export_zip:
        print("📦 ZIP:", zip_path)

    return {
        "clips": clips,
        "excel": str(excel_path),
        "zip": str(zip_path) if export_zip else None
    }


def obtener_duracion_video(video_path: str) -> float:
    """Obtiene la duración total del video en segundos usando ffprobe."""
    cmd = [
        "ffprobe", "-v", "error",
        "-show_entries", "format=duration",
        "-of", "default=noprint_wrappers=1:nokey=1",
        video_path
    ]
    result = subprocess.run(
        cmd, capture_output=True, text=True, check=True
    )
    return float(result.stdout.strip())


def invertir_rangos(cortes: list, duracion_total: float) -> list:
    """
    Dado una lista de cortes (partes a REMOVER), retorna los rangos a CONSERVAR.

    Args:
        cortes: Lista de dicts con 'inicio' y 'final' en formato HH:MM:SS
        duracion_total: Duración total del video en segundos

    Returns:
        Lista de tuplas (inicio_seg, fin_seg) con los rangos a conservar
    """
    # Convertir cortes a segundos y ordenar
    rangos_corte = sorted(
        [(hhmmss_to_sec(c["inicio"]), hhmmss_to_sec(c["final"]))
         for c in cortes],
        key=lambda x: x[0]
    )

    # Fusionar rangos solapados
    fusionados = []
    for inicio, fin in rangos_corte:
        if fusionados and inicio <= fusionados[-1][1]:
            fusionados[-1] = (fusionados[-1][0], max(fusionados[-1][1], fin))
        else:
            fusionados.append((inicio, fin))

    # Invertir: calcular los rangos a conservar
    conservar = []
    cursor = 0.0
    for inicio, fin in fusionados:
        if cursor < inicio:
            conservar.append((cursor, inicio))
        cursor = fin
    if cursor < duracion_total:
        conservar.append((cursor, duracion_total))

    return conservar


def limpiar_video(
    archivo_json: str,
    video: str,
    out_file: str = None,
    margin: float = 0.0,
    fade: float = 0.0,
    dry_run: bool = False,
    preview: bool = False,
):
    """
    Limpia un video removiendo los segmentos indicados en el JSON.

    Args:
        archivo_json: Ruta al JSON con cortes_sugeridos
        video: Ruta al video fuente
        out_file: Ruta del video de salida (default: video_limpio.mp4)
        margin: Segundos de margen extra en cada corte (amplía los cortes)
        fade: Segundos de crossfade entre segmentos (0 = sin fade)
        dry_run: Si True, solo muestra los comandos sin ejecutar
        preview: Si True, solo genera el reporte sin procesar video

    Returns:
        Dict con info del resultado
    """
    with open(archivo_json, encoding="utf-8") as f:
        data = json.load(f)

    cortes = data.get("cortes_sugeridos", [])

    if not cortes:
        print("⚠️  No se encontraron cortes sugeridos en el JSON.")
        return {"segmentos_conservados": [], "video": None}

    # Aplicar margen a los cortes (ampliar cada corte)
    if margin > 0:
        cortes_ajustados = []
        for c in cortes:
            inicio_sec = max(0, hhmmss_to_sec(c["inicio"]) - margin)
            final_sec = hhmmss_to_sec(c["final"]) + margin
            cortes_ajustados.append({
                "inicio": sec_to_hhmmss(inicio_sec),
                "final": sec_to_hhmmss(final_sec),
                "razon": c.get("razon", "")
            })
        cortes = cortes_ajustados

    # Obtener duración del video
    if not preview:
        duracion_total = obtener_duracion_video(video)
    else:
        # En preview, intentar obtener duración; si no hay video, usar el final del último corte + 1h
        try:
            duracion_total = obtener_duracion_video(video)
        except Exception:
            ultimo_final = max(hhmmss_to_sec(c["final"]) for c in cortes)
            duracion_total = ultimo_final + 3600
            print(
                f"⚠️  No se pudo obtener la duración del video. Usando estimación: {sec_to_hhmmss(duracion_total)}")

    segmentos = invertir_rangos(cortes, duracion_total)

    # --- Reporte ---
    duracion_removida = sum(
        hhmmss_to_sec(c["final"]) - hhmmss_to_sec(c["inicio"]) for c in cortes
    )
    duracion_conservada = sum(fin - ini for ini, fin in segmentos)

    print("\n🧹 LIMPIEZA DE VIDEO — Reporte")
    print("=" * 60)
    print(f"📹 Video fuente:       {video}")
    print(f"⏱️  Duración total:     {sec_to_hhmmss(duracion_total)}")
    print(f"✂️  Cortes a remover:   {len(cortes)}")
    print(f"🗑️  Duración removida:  {sec_to_hhmmss(duracion_removida)}")
    print(f"✅ Duración final:     {sec_to_hhmmss(duracion_conservada)}")
    if duracion_total > 0:
        pct = (duracion_conservada / duracion_total) * 100
        print(f"📊 Contenido conservado: {pct:.1f}%")
    print()

    print("🗑️  Segmentos REMOVIDOS:")
    for i, c in enumerate(cortes):
        dur = hhmmss_to_sec(c["final"]) - hhmmss_to_sec(c["inicio"])
        print(
            f"   {i + 1}. [{c['inicio']} → {c['final']}] ({sec_to_hhmmss(dur)}) — {c.get('razon', '')}")
    print()

    print("✅ Segmentos CONSERVADOS:")
    for i, (ini, fin) in enumerate(segmentos):
        dur = fin - ini
        print(
            f"   {i + 1}. [{sec_to_hhmmss(ini)} → {sec_to_hhmmss(fin)}] ({sec_to_hhmmss(dur)})")
    print()

    if preview:
        print("👁️  Modo preview — no se procesó el video.")
        return {
            "segmentos_conservados": segmentos,
            "segmentos_removidos": cortes,
            "duracion_total": duracion_total,
            "duracion_removida": duracion_removida,
            "duracion_conservada": duracion_conservada,
            "video": None,
        }

    # --- Procesamiento con ffmpeg ---
    if not out_file:
        base = pathlib.Path(video)
        out_file = str(base.parent / f"{base.stem}_limpio{base.suffix}")

    tmpdir = tempfile.mkdtemp(prefix="clipkly_clean_")

    try:
        # Extraer cada segmento conservado
        seg_files = []
        print("🎬 Extrayendo segmentos conservados...")
        for i, (ini, fin) in enumerate(tqdm(segmentos, desc="Segmentos")):
            seg_path = os.path.join(tmpdir, f"seg_{i:04d}.ts")
            seg_files.append(seg_path)

            if fade > 0 and i > 0:
                # Con re-encode para fade
                cmd = (
                    f'ffmpeg -y -hide_banner -loglevel error '
                    f'-ss {sec_to_hhmmss(ini)} -to {sec_to_hhmmss(fin)} '
                    f'-i "{video}" '
                    f'-c:v libx264 -preset fast -crf 18 '
                    f'-c:a aac -b:a 192k '
                    f'-af "afade=t=in:st=0:d={fade}" '
                    f'-vf "fade=t=in:st=0:d={fade}" '
                    f'-bsf:v h264_mp4toannexb '
                    f'-f mpegts "{seg_path}"'
                )
            else:
                # Sin fade: stream copy a mpegts para concat
                cmd = (
                    f'ffmpeg -y -hide_banner -loglevel error '
                    f'-ss {sec_to_hhmmss(ini)} -to {sec_to_hhmmss(fin)} '
                    f'-i "{video}" '
                    f'-c copy -bsf:v h264_mp4toannexb '
                    f'-f mpegts "{seg_path}"'
                )

            if dry_run:
                print(f"[DRY RUN] {cmd}")
            else:
                subprocess.run(cmd, shell=True, check=True)

        # Concatenar todos los segmentos
        if not dry_run:
            concat_input = "|".join(seg_files)
            concat_cmd = (
                f'ffmpeg -y -hide_banner -loglevel error '
                f'-i "concat:{concat_input}" '
                f'-c copy -movflags +faststart '
                f'"{out_file}"'
            )

            if dry_run:
                print(f"[DRY RUN] {concat_cmd}")
            else:
                print("🔗 Concatenando segmentos...")
                subprocess.run(concat_cmd, shell=True, check=True)

        print(f"\n✅ ¡Video limpio generado!")
        print(f"📁 Salida: {out_file}")

    finally:
        # Limpiar archivos temporales
        if not dry_run:
            for f in seg_files:
                try:
                    os.remove(f)
                except OSError:
                    pass
            try:
                os.rmdir(tmpdir)
            except OSError:
                pass

    return {
        "segmentos_conservados": segmentos,
        "segmentos_removidos": cortes,
        "duracion_total": duracion_total,
        "duracion_removida": duracion_removida,
        "duracion_conservada": duracion_conservada,
        "video": out_file,
    }
