"""
SubtitleBurner — Auto-transcribe, translate to English, burn subtitles into video.
Uses faster-whisper for GPU/CPU transcription and ffmpeg for subtitle burning.
"""

import os
import sys
import time
import json
import shutil
import subprocess
import threading
import tempfile
import platform
import re
import textwrap
from pathlib import Path

import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext

# ──────────────────────────────────────────────
# ASS Style Configuration with Dynamic Scaling
# ──────────────────────────────────────────────

def get_video_dimensions(video_path):
    """Get video width and height using ffprobe."""
    try:
        cmd = [
            "ffprobe", "-v", "error",
            "-select_streams", "v:0",
            "-show_entries", "stream=width,height",
            "-of", "csv=p=0",
            str(video_path)
        ]
        result = subprocess.run(cmd, capture_output=True, text=True, check=True)
        width, height = map(int, result.stdout.strip().split(','))
        return width, height
    except Exception as e:
        print(f"Error getting video dimensions: {e}")
        return 1920, 1080  # Default fallback

def calculate_scaled_font_size(video_height, reference_height=1080, reference_font_size=48):
    """
    Calculate font size proportional to video height.
    Reduced default from 76 to 48 for better fit.
    """
    scale_factor = video_height / reference_height
    scaled_size = int(reference_font_size * scale_factor)
    # Ensure minimum readable size but not too large
    return max(18, min(scaled_size, 48))

def calculate_scaled_shadow(video_height, reference_height=1080, reference_shadow=3):
    """
    Calculate shadow/outline proportional to video height.
    Reduced from 15 to 3 for subtle shadow.
    """
    scale_factor = video_height / reference_height
    scaled_shadow = max(1, int(reference_shadow * scale_factor))
    return scaled_shadow

def calculate_scaled_outline(video_height, reference_height=1080, reference_outline=1):
    """Calculate outline proportional to video height."""
    scale_factor = video_height / reference_height
    scaled_outline = max(0, int(reference_outline * scale_factor))
    return scaled_outline

def calculate_scaled_margins(video_height, reference_height=1080, reference_margin=10):
    """Calculate margins proportional to video height."""
    scale_factor = video_height / reference_height
    scaled_margin = max(5, int(reference_margin * scale_factor))
    return scaled_margin

def hex_to_ass_color(hex_color):
    """
    Convert hex color to ASS format (BGR with alpha).
    #FEBD00 -> RGB(254, 189, 0) -> ASS: &H00 00 BD FE (BGR)
    """
    hex_color = hex_color.lstrip('#')
    r, g, b = int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16)
    # ASS uses &HAABBGGRR (Alpha, Blue, Green, Red)
    return f"&H00{b:02x}{g:02x}{r:02x}".upper()

def wrap_text_for_width(text, max_chars=35):
    """
    Wrap text to fit within video width.
    max_chars is approximate - will be adjusted based on video resolution.
    """
    # Clean the text
    text = text.strip()
    if not text:
        return text
    
    # Try to split intelligently at punctuation or spaces
    words = text.split()
    lines = []
    current_line = []
    current_length = 0
    
    for word in words:
        # Add word length + 1 for space
        word_len = len(word)
        if current_length + word_len + (1 if current_line else 0) <= max_chars:
            current_line.append(word)
            current_length += word_len + (1 if current_line else 0)
        else:
            if current_line:
                lines.append(' '.join(current_line))
            current_line = [word]
            current_length = word_len
    
    if current_line:
        lines.append(' '.join(current_line))
    
    # If we have more than 2 lines, try to rebalance
    if len(lines) > 2:
        # Join back and try a different approach
        full_text = ' '.join(words)
        # Force to max 2 lines by splitting roughly in half
        midpoint = len(full_text) // 2
        # Find nearest space to midpoint
        split_pos = full_text.rfind(' ', 0, midpoint)
        if split_pos == -1:
            split_pos = full_text.find(' ', midpoint)
        if split_pos != -1:
            lines = [full_text[:split_pos], full_text[split_pos+1:]]
        else:
            lines = [full_text]
    
    return '\n'.join(lines)

def wrap_segment_text(segments, video_width, reference_width=1920):
    """
    Adjust text wrapping based on video width.
    """
    # Calculate character limit based on video width
    # At 1920px, ~50 chars per line. At 860px, ~22 chars per line
    max_chars = max(20, int(50 * (video_width / reference_width)))
    
    wrapped_segments = []
    for start, end, text in segments:
        wrapped_text = wrap_text_for_width(text, max_chars)
        wrapped_segments.append((start, end, wrapped_text))
    
    return wrapped_segments

def generate_ass_header(video_width, video_height, angle=135):
    """
    Generate ASS header with dynamically scaled styles based on video dimensions.
    """
    # Calculate scaled values
    font_size = calculate_scaled_font_size(video_height)
    shadow = calculate_scaled_shadow(video_height)
    outline = calculate_scaled_outline(video_height)
    margin = calculate_scaled_margins(video_height)
    
    # Gold color #FEBD00
    gold_color = hex_to_ass_color("#FEBD00")
    
    # For small videos, reduce values further
    if video_height < 500:
        shadow = max(1, shadow)
        outline = 0
        margin = max(5, margin)
    
    return f"""[Script Info]
Title: SubtitleBurner Generated Subtitles
ScriptType: v4.00+
Collisions: Normal
PlayDepth: 0
Timer: 100.0000
VideoWidth: {video_width}
VideoHeight: {video_height}
WrapStyle: 2

[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,Arial,{font_size},{gold_color},&H00FFFFFF,&H00000000,&H00000000,-1,0,0,0,100,100,0,0,1,{outline},{shadow},2,{margin},{margin},{margin},1

[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
"""

def seconds_to_ass_time(seconds):
    """Convert seconds to ASS time format (H:MM:SS.cc)"""
    hours = int(seconds // 3600)
    minutes = int((seconds % 3600) // 60)
    secs = seconds % 60
    centiseconds = int((secs - int(secs)) * 100)
    return f"{hours}:{minutes:02d}:{int(secs):02d}.{centiseconds:02d}"

def segments_to_ass(segments, video_width, video_height, angle=135):
    """
    Convert segments list to ASS format string with angle support and proper positioning.
    """
    # Wrap text based on video width
    wrapped_segments = wrap_segment_text(segments, video_width)
    
    header = generate_ass_header(video_width, video_height, angle)
    lines = [header]
    
    # Calculate bottom margin (3% from bottom for better positioning)
    bottom_margin = max(5, int(video_height * 0.03))
    
    for start, end, text in wrapped_segments:
        start_time = seconds_to_ass_time(start)
        end_time = seconds_to_ass_time(end)
        
        # Split into lines for ASS display
        text_lines = text.split('\n')
        
        # \an2 = Center alignment at bottom
        # \ang{angle} rotates text
        # Use \N for line breaks in ASS
        ass_text = '\\N'.join(text_lines)
        styled_text = f"{{\\an2\\ang{angle}}}{ass_text}"
        
        lines.append(f"Dialogue: 0,{start_time},{end_time},Default,,0,0,{bottom_margin},,{styled_text}")
    
    return "\n".join(lines)


# ──────────────────────────────────────────────
# Resource detection
# ──────────────────────────────────────────────

def detect_resources():
    """Detect GPU, CUDA, RAM and pick the best whisper device/compute_type."""
    info = {
        "platform": platform.system(),
        "cpu_cores": os.cpu_count(),
        "ram_gb": None,
        "gpu": None,
        "cuda": False,
        "device": "cpu",
        "compute_type": "int8",
        "model_size": "medium",
    }

    # RAM
    try:
        import psutil
        info["ram_gb"] = round(psutil.virtual_memory().total / 1e9, 1)
    except ImportError:
        pass

    # CUDA / GPU
    try:
        import torch
        if torch.cuda.is_available():
            info["cuda"] = True
            info["gpu"] = torch.cuda.get_device_name(0)
            vram = torch.cuda.get_device_properties(0).total_memory / 1e9
            info["vram_gb"] = round(vram, 1)
            if vram >= 8:
                info["device"] = "cuda"
                info["compute_type"] = "float16"
                info["model_size"] = "large-v3"
            elif vram >= 4:
                info["device"] = "cuda"
                info["compute_type"] = "float16"
                info["model_size"] = "medium"
            else:
                info["device"] = "cuda"
                info["compute_type"] = "int8"
                info["model_size"] = "small"
        elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
            info["gpu"] = "Apple Silicon MPS"
            info["device"] = "cpu"  # faster-whisper uses CPU on MPS
            info["compute_type"] = "int8"
            info["model_size"] = "medium"
    except ImportError:
        pass

    # CPU fallback model sizing
    if info["device"] == "cpu":
        ram = info.get("ram_gb") or 4
        if ram >= 16:
            info["model_size"] = "medium"
        elif ram >= 8:
            info["model_size"] = "small"
        else:
            info["model_size"] = "base"

    return info


def check_ffmpeg():
    """Return True if ffmpeg is available."""
    return shutil.which("ffmpeg") is not None

def check_ffprobe():
    """Return True if ffprobe is available."""
    return shutil.which("ffprobe") is not None


# ──────────────────────────────────────────────
# Core processing
# ──────────────────────────────────────────────

def transcribe_video(video_path, device, compute_type, model_size, translate, progress_cb, log_cb):
    """
    Run faster-whisper on the video, return list of (start, end, text) segments.
    If translate=True, translate to English.
    """
    from faster_whisper import WhisperModel

    log_cb(f"Loading Whisper model '{model_size}' on {device} ({compute_type})…")
    model = WhisperModel(model_size, device=device, compute_type=compute_type)

    task = "translate" if translate else "transcribe"
    log_cb(f"Starting {task}… (this may take a while for long videos)")

    segments_iter, info = model.transcribe(
        video_path,
        task=task,
        beam_size=5,
        vad_filter=True,
        word_timestamps=False,
    )

    log_cb(f"Detected language: {info.language} (confidence {info.language_probability:.0%})")

    segments = []
    for seg in segments_iter:
        segments.append((seg.start, seg.end, seg.text.strip()))
        progress_cb(min(95, int(seg.end / info.duration * 80) + 5) if info.duration else 50)
        log_cb(f"  [{seg.start:.1f}s → {seg.end:.1f}s] {seg.text.strip()}")

    return segments, info.language


def burn_subtitles_ass(video_path, ass_path, output_path, progress_cb, log_cb):
    """Use ffmpeg to burn ASS/SSA subtitles into video."""
    log_cb("Burning ASS subtitles into video with ffmpeg…")

    # Escape path for ffmpeg subtitles filter
    ass_escaped = str(ass_path).replace("\\", "/").replace(":", "\\:")

    cmd = [
        "ffmpeg", "-y",
        "-i", str(video_path),
        "-vf", f"ass='{ass_escaped}'",
        "-c:a", "copy",
        "-preset", "fast",
        str(output_path),
    ]

    log_cb(f"Running ffmpeg with ASS subtitle filter…")

    process = subprocess.Popen(
        cmd,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        universal_newlines=True,
    )

    duration_sec = None
    for line in process.stdout:
        line = line.strip()
        if "Duration:" in line and duration_sec is None:
            try:
                dur_str = line.split("Duration:")[1].split(",")[0].strip()
                h, m, s = dur_str.split(":")
                duration_sec = int(h) * 3600 + int(m) * 60 + float(s)
            except Exception:
                pass
        if "time=" in line and duration_sec:
            try:
                time_str = line.split("time=")[1].split(" ")[0]
                h, m, s = time_str.split(":")
                current = int(h) * 3600 + int(m) * 60 + float(s)
                pct = int(95 + (current / duration_sec) * 4)
                progress_cb(min(99, pct))
            except Exception:
                pass

    process.wait()
    if process.returncode != 0:
        raise RuntimeError("ffmpeg failed — check the log for details.")
    log_cb("✓ Subtitle burning complete.")


def burn_subtitles_srt(video_path, srt_path, output_path, final_font_size, progress_cb, log_cb):
    """Use ffmpeg to burn SRT subtitles into video."""
    log_cb("Burning SRT subtitles into video with ffmpeg…")
    
    # Escape path for ffmpeg - replace backslashes and handle Windows paths
    escaped_path = str(srt_path).replace('\\', '/')
    # For Windows, we need to escape colons
    if platform.system() == "Windows":
        escaped_path = escaped_path.replace(':', '\\:')
    
    style = f"FontName=Arial,FontSize={final_font_size},PrimaryColour=&H0000BDFE,OutlineColour=&H00000000,Bold=-1,Shadow=2,Alignment=2"
    
    cmd = [
        "ffmpeg", "-y",
        "-i", str(video_path),
        "-vf", f"subtitles='{escaped_path}':force_style='{style}'",
        "-c:a", "copy",
        "-preset", "fast",
        str(output_path),
    ]

    log_cb(f"Running ffmpeg with SRT subtitle filter…")

    process = subprocess.Popen(
        cmd,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        universal_newlines=True,
    )

    for line in process.stdout:
        line = line.strip()
        # Just log progress
        if "time=" in line:
            log_cb(f"  Processing: {line[:80]}...")
    
    process.wait()
    if process.returncode != 0:
        raise RuntimeError("ffmpeg failed — check the log for details.")
    log_cb("✓ Subtitle burning complete.")


# ──────────────────────────────────────────────
# GUI
# ──────────────────────────────────────────────

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("SubtitleBurner")
        self.resizable(True, True)
        self.minsize(680, 520)

        self.resources = None
        self.processing = False
        self.video_dimensions = None

        self._build_ui()
        self._detect_resources_async()

    # ── UI construction ──────────────────────

    def _build_ui(self):
        self.configure(bg="#FAFAF8")
        style = ttk.Style(self)
        style.theme_use("clam")
        style.configure("TFrame", background="#FAFAF8")
        style.configure("TLabel", background="#FAFAF8", font=("Segoe UI", 10))
        style.configure("TButton", font=("Segoe UI", 10), padding=6)
        style.configure("Accent.TButton", font=("Segoe UI", 10, "bold"), padding=6)
        style.configure("TCheckbutton", background="#FAFAF8", font=("Segoe UI", 10))
        style.configure("TLabelframe", background="#FAFAF8", font=("Segoe UI", 10, "bold"))
        style.configure("TLabelframe.Label", background="#FAFAF8", font=("Segoe UI", 10, "bold"))
        style.configure("TProgressbar", thickness=8)

        root = ttk.Frame(self, padding="16 16 16 16")
        root.pack(fill="both", expand=True)

        # ── Header
        hdr = ttk.Frame(root)
        hdr.pack(fill="x", pady=(0, 12))
        ttk.Label(hdr, text="SubtitleBurner", font=("Segoe UI", 18, "bold")).pack(side="left")
        ttk.Label(hdr, text="Auto-transcribe · Translate · Burn", foreground="#888", font=("Segoe UI", 10)).pack(side="left", padx=12, pady=4)

        # ── Resources panel
        res_frame = ttk.LabelFrame(root, text="Detected resources", padding=8)
        res_frame.pack(fill="x", pady=(0, 10))
        self.res_label = ttk.Label(res_frame, text="Detecting…", foreground="#666")
        self.res_label.pack(anchor="w")

        # ── Input file
        inp_frame = ttk.LabelFrame(root, text="Input video", padding=8)
        inp_frame.pack(fill="x", pady=(0, 10))

        row = ttk.Frame(inp_frame)
        row.pack(fill="x")
        self.input_var = tk.StringVar()
        ttk.Entry(row, textvariable=self.input_var, font=("Segoe UI", 10)).pack(side="left", fill="x", expand=True)
        ttk.Button(row, text="Browse…", command=self._browse_input).pack(side="left", padx=(6, 0))

        # ── Output file
        out_frame = ttk.LabelFrame(root, text="Output video", padding=8)
        out_frame.pack(fill="x", pady=(0, 10))

        row2 = ttk.Frame(out_frame)
        row2.pack(fill="x")
        self.output_var = tk.StringVar()
        ttk.Entry(row2, textvariable=self.output_var, font=("Segoe UI", 10)).pack(side="left", fill="x", expand=True)
        ttk.Button(row2, text="Browse…", command=self._browse_output).pack(side="left", padx=(6, 0))

        # ── Options
        opt_frame = ttk.LabelFrame(root, text="Options", padding=8)
        opt_frame.pack(fill="x", pady=(0, 10))

        opts_row = ttk.Frame(opt_frame)
        opts_row.pack(fill="x")

        self.translate_var = tk.BooleanVar(value=True)
        ttk.Checkbutton(opts_row, text="Translate to English", variable=self.translate_var).pack(side="left")

        ttk.Label(opts_row, text="   Subtitle format:").pack(side="left")
        self.sub_format_var = tk.StringVar(value="ass")
        format_combo = ttk.Combobox(opts_row, textvariable=self.sub_format_var, width=8, state="readonly",
                                   values=["ass", "srt"])
        format_combo.pack(side="left", padx=6)
        
        ttk.Label(opts_row, text="   Angle (°):").pack(side="left")
        self.angle_var = tk.StringVar(value="135")
        angle_entry = ttk.Entry(opts_row, textvariable=self.angle_var, width=6)
        angle_entry.pack(side="left", padx=6)
        
        ttk.Label(opts_row, text="   Font size adjust:").pack(side="left")
        self.font_adjust_var = tk.StringVar(value="0")
        font_adjust = ttk.Combobox(opts_row, textvariable=self.font_adjust_var, width=6, state="readonly",
                                   values=["-8", "-4", "0", "+4", "+8"])
        font_adjust.pack(side="left", padx=6)

        ttk.Label(opts_row, text="  Model:").pack(side="left")
        self.model_var = tk.StringVar(value="auto")
        model_combo = ttk.Combobox(opts_row, textvariable=self.model_var, width=10, state="readonly",
                                   values=["auto", "tiny", "base", "small", "medium", "large-v3"])
        model_combo.pack(side="left", padx=6)

        ttk.Label(opts_row, text="  Save .ass/.srt:").pack(side="left")
        self.save_sub_var = tk.BooleanVar(value=True)
        ttk.Checkbutton(opts_row, variable=self.save_sub_var).pack(side="left")

        # ── Progress
        prog_frame = ttk.LabelFrame(root, text="Progress", padding=8)
        prog_frame.pack(fill="x", pady=(0, 10))

        self.progress_var = tk.IntVar(value=0)
        self.progress_bar = ttk.Progressbar(prog_frame, variable=self.progress_var, maximum=100, length=400)
        self.progress_bar.pack(fill="x")
        self.status_label = ttk.Label(prog_frame, text="Ready.", foreground="#555")
        self.status_label.pack(anchor="w", pady=(4, 0))

        # ── Log
        log_frame = ttk.LabelFrame(root, text="Log", padding=8)
        log_frame.pack(fill="both", expand=True, pady=(0, 10))
        self.log_box = scrolledtext.ScrolledText(log_frame, height=8, font=("Courier New", 9),
                                                 bg="#F4F3EF", relief="flat", wrap="word")
        self.log_box.pack(fill="both", expand=True)

        # ── Buttons
        btn_row = ttk.Frame(root)
        btn_row.pack(fill="x")
        self.run_btn = ttk.Button(btn_row, text="▶  Start Processing", command=self._start,
                                  style="Accent.TButton")
        self.run_btn.pack(side="left")
        ttk.Button(btn_row, text="Clear log", command=self._clear_log).pack(side="left", padx=8)
        ttk.Button(btn_row, text="Check video info", command=self._check_video_info).pack(side="left", padx=8)

        # FFmpeg warning
        if not check_ffmpeg():
            self._log("⚠  ffmpeg not found in PATH — please install it (see README).")
        if not check_ffprobe():
            self._log("⚠  ffprobe not found — auto-scaling may use defaults.")

    # ── Video info ───────────────────────────
    
    def _check_video_info(self):
        """Get and display video dimensions."""
        video_in = self.input_var.get().strip()
        if not video_in or not Path(video_in).exists():
            messagebox.showwarning("Warning", "Please select a valid video file first.")
            return
        
        try:
            width, height = get_video_dimensions(video_in)
            base_font = calculate_scaled_font_size(height)
            font_size = base_font + int(self.font_adjust_var.get() or 0)
            shadow = calculate_scaled_shadow(height)
            
            info_msg = f"""Video Dimensions: {width} x {height}
Reference: 1920 x 1080
Scale Factor: {height/1080:.2f}

Calculated Styling:
• Font Size: {font_size}px (base: {base_font}px + {self.font_adjust_var.get()})
• Shadow: {shadow}px (reference: 3px)
• Color: Gold (#FEBD00)

Max line length: {max(20, int(50 * (width / 1920)))} chars
Suggested Format: ASS (better scaling support)"""
            
            messagebox.showinfo("Video Information", info_msg)
            self._log(f"Video info: {width}x{height} → Font: {font_size}, Shadow: {shadow}")
        except Exception as e:
            messagebox.showerror("Error", f"Could not read video info: {e}")
    
    # ── Resource detection ───────────────────

    def _detect_resources_async(self):
        def _run():
            try:
                r = detect_resources()
                self.resources = r
                parts = [f"CPU: {r['cpu_cores']} cores"]
                if r.get("ram_gb"):
                    parts.append(f"RAM: {r['ram_gb']} GB")
                if r.get("gpu"):
                    parts.append(f"GPU: {r['gpu']}")
                    if r.get("vram_gb"):
                        parts.append(f"VRAM: {r['vram_gb']} GB")
                parts.append(f"→ Will use: {r['device'].upper()} / {r['compute_type']} / model={r['model_size']}")
                self.after(0, lambda: self.res_label.config(text="   ".join(parts), foreground="#333"))
                self.after(0, lambda: self._log("Resource detection complete: " + " | ".join(parts)))
            except Exception as e:
                self.after(0, lambda: self.res_label.config(text=f"Detection error: {e}", foreground="red"))
        threading.Thread(target=_run, daemon=True).start()

    # ── File browsing ────────────────────────

    def _browse_input(self):
        path = filedialog.askopenfilename(
            title="Select video file",
            filetypes=[("Video files", "*.mp4 *.mkv *.mov *.avi *.webm *.m4v *.flv"), ("All files", "*.*")]
        )
        if path:
            self.input_var.set(path)
            if not self.output_var.get():
                p = Path(path)
                self.output_var.set(str(p.parent / (p.stem + "_subtitled" + p.suffix)))
            # Auto-detect dimensions
            try:
                width, height = get_video_dimensions(path)
                self.video_dimensions = (width, height)
                self._log(f"Detected video: {width}x{height}")
            except Exception:
                pass

    def _browse_output(self):
        path = filedialog.asksaveasfilename(
            title="Save output video",
            defaultextension=".mp4",
            filetypes=[("MP4 video", "*.mp4"), ("MKV video", "*.mkv"), ("All files", "*.*")]
        )
        if path:
            self.output_var.set(path)

    # ── Logging ──────────────────────────────

    def _log(self, msg):
        def _do():
            self.log_box.insert("end", msg + "\n")
            self.log_box.see("end")
        self.after(0, _do)

    def _clear_log(self):
        self.log_box.delete("1.0", "end")

    def _set_status(self, msg):
        self.after(0, lambda: self.status_label.config(text=msg))

    def _set_progress(self, pct):
        self.after(0, lambda: self.progress_var.set(pct))

    # ── Main processing ──────────────────────

    def _start(self):
        if self.processing:
            return

        video_in = self.input_var.get().strip()
        video_out = self.output_var.get().strip()

        if not video_in:
            messagebox.showerror("Error", "Please select an input video.")
            return
        if not Path(video_in).exists():
            messagebox.showerror("Error", f"Input file not found:\n{video_in}")
            return
        if not video_out:
            messagebox.showerror("Error", "Please specify an output path.")
            return
        if not check_ffmpeg():
            messagebox.showerror("Error", "ffmpeg not found in PATH.\nPlease install ffmpeg and ensure it is on your PATH.")
            return

        self.processing = True
        self.run_btn.config(state="disabled", text="Processing…")
        self._set_progress(0)
        threading.Thread(target=self._process_thread, args=(video_in, video_out), daemon=True).start()

    def _process_thread(self, video_in, video_out):
        try:
            r = self.resources or detect_resources()
            model_size = self.model_var.get()
            if model_size == "auto":
                model_size = r["model_size"]
            device = r["device"]
            compute_type = r["compute_type"]
            translate = self.translate_var.get()
            subtitle_format = self.sub_format_var.get()
            font_adjust = int(self.font_adjust_var.get() or 0)
            
            # Get angle value
            try:
                angle = int(self.angle_var.get())
            except ValueError:
                angle = 135
                self._log(f"⚠ Invalid angle, using default 135°")
            
            # Get video dimensions for scaling
            try:
                video_width, video_height = get_video_dimensions(video_in)
                self._log(f"Video dimensions: {video_width}x{video_height}")
                base_font_size = calculate_scaled_font_size(video_height)
                final_font_size = base_font_size + font_adjust
                shadow = calculate_scaled_shadow(video_height)
                self._log(f"Scaling: Base font {base_font_size}px + adjust {font_adjust} = {final_font_size}px, Shadow={shadow}px")
            except Exception as e:
                video_width, video_height = 1920, 1080
                final_font_size = 40
                self._log(f"⚠ Could not detect video dimensions, using defaults: {video_width}x{video_height}")

            self._log(f"\n{'='*60}")
            self._log(f"Input:  {video_in}")
            self._log(f"Output: {video_out}")
            self._log(f"Model: {model_size}  Device: {device}  Compute: {compute_type}  Translate: {translate}")
            self._log(f"Subtitle format: {subtitle_format.upper()}  Angle: {angle}°  Font: {final_font_size}px")
            self._log(f"Video size: {video_width}x{video_height} (auto-scaled styling)")
            self._log(f"{'='*60}")
            self._set_status("Transcribing audio…")
            self._set_progress(2)

            # Check faster-whisper import
            try:
                import faster_whisper  # noqa
            except ImportError:
                self._log("❌ faster-whisper not installed. Run: pip install faster-whisper")
                raise RuntimeError("faster-whisper missing — see log.")

            segments, lang = transcribe_video(
                video_in, device, compute_type, model_size, translate,
                progress_cb=self._set_progress,
                log_cb=self._log,
            )

            if not segments:
                raise RuntimeError("No speech detected in the video.")

            self._log(f"\nDetected {len(segments)} subtitle segments.")
            self._set_status("Generating subtitles…")
            self._set_progress(85)

            # For ASS format
            if subtitle_format == "ass":
                # Wrap text based on video width
                wrapped_segments = wrap_segment_text(segments, video_width)
                
                # Build ASS content with proper header
                header = generate_ass_header(video_width, video_height, angle)
                
                # Adjust font size if needed
                if font_adjust != 0:
                    new_font_size = final_font_size
                    header = re.sub(r'Default,Arial,\d+,', f'Default,Arial,{new_font_size},', header)
                
                lines = [header]
                bottom_margin = max(5, int(video_height * 0.03))
                
                for start, end, text in wrapped_segments:
                    start_time = seconds_to_ass_time(start)
                    end_time = seconds_to_ass_time(end)
                    text_lines = text.split('\n')
                    ass_text = '\\N'.join(text_lines)
                    styled_text = f"{{\\an2\\ang{angle}}}{ass_text}"
                    lines.append(f"Dialogue: 0,{start_time},{end_time},Default,,0,0,{bottom_margin},,{styled_text}")
                
                subtitle_content = "\n".join(lines)
                subtitle_ext = ".ass"
            else:
                # SRT format
                def fmt_time(t):
                    h = int(t // 3600)
                    m = int((t % 3600) // 60)
                    s = int(t % 60)
                    ms = int((t - int(t)) * 1000)
                    return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"
                
                srt_lines = []
                for i, (start, end, text) in enumerate(segments, 1):
                    srt_lines.append(str(i))
                    srt_lines.append(f"{fmt_time(start)} --> {fmt_time(end)}")
                    srt_lines.append(text)
                    srt_lines.append("")
                subtitle_content = "\n".join(srt_lines)
                subtitle_ext = ".srt"

            # Write subtitle file
            subtitle_path = Path(video_out).with_suffix(subtitle_ext)
            subtitle_path.write_text(subtitle_content, encoding="utf-8")
            self._log(f"Subtitle file saved: {subtitle_path}")
            
            if subtitle_format == "ass":
                self._log(f"  • Font size: {final_font_size}px (scaled from base {base_font_size}px)")
                self._log(f"  • Shadow: {shadow}px")
                self._log(f"  • Color: Gold (#FEBD00)")

            self._set_status(f"Burning subtitles ({subtitle_format.upper()})…")
            self._set_progress(88)

            # Burn subtitles
            if subtitle_format == "ass":
                burn_subtitles_ass(video_in, subtitle_path, video_out,
                                   progress_cb=self._set_progress,
                                   log_cb=self._log)
            else:
                burn_subtitles_srt(video_in, subtitle_path, video_out, final_font_size,
                                   progress_cb=self._set_progress,
                                   log_cb=self._log)

            # Optionally delete subtitle file
            if not self.save_sub_var.get():
                subtitle_path.unlink(missing_ok=True)
                self._log(f"Subtitle file deleted (as per settings)")

            self._set_progress(100)
            self._set_status(f"✅ Done! Saved to {video_out}")
            self._log(f"\n✅ All done → {video_out}")
            self.after(0, lambda: messagebox.showinfo("Done", f"Video saved:\n{video_out}"))

        except Exception as e:
            self._log(f"\n❌ Error: {e}")
            self._set_status(f"Error: {e}")
            import traceback
            self._log(traceback.format_exc())
            self.after(0, lambda: messagebox.showerror("Processing failed", str(e)))

        finally:
            self.processing = False
            self.after(0, lambda: self.run_btn.config(state="normal", text="▶  Start Processing"))


# ──────────────────────────────────────────────
# Entry point
# ──────────────────────────────────────────────

if __name__ == "__main__":
    app = App()
    app.mainloop()