"""
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=42):
    """
    Calculate font size proportional to video height.
    Reduced further 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(16, min(scaled_size, 42))

def calculate_scaled_shadow(video_height, reference_height=1080, reference_shadow=2):
    """
    Calculate shadow/outline proportional to video height.
    """
    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_advanced(text, words_per_line=10, max_lines=2):
    """
    Wrap text with control over words per line and maximum lines.
    
    Args:
        text: The text to wrap
        words_per_line: Number of words to allow per line (0 = auto)
        max_lines: Maximum number of lines (1, 2, or 3)
    
    Returns:
        Wrapped text with \n separators
    """
    text = text.strip()
    if not text:
        return text
    
    words = text.split()
    
    # If words_per_line is 0 or auto, calculate based on text length
    if words_per_line <= 0:
        # Auto-calculate: aim for balanced lines
        total_words = len(words)
        if max_lines == 1:
            words_per_line = total_words
        elif max_lines == 2:
            words_per_line = (total_words + 1) // 2
        else:  # 3 lines
            words_per_line = (total_words + 2) // 3
        words_per_line = max(1, min(words_per_line, total_words))
    
    # Split into chunks
    chunks = []
    for i in range(0, len(words), words_per_line):
        chunks.append(' '.join(words[i:i+words_per_line]))
    
    # If we have more chunks than max_lines, redistribute
    if len(chunks) > max_lines:
        # Flatten and redistribute more evenly
        all_words = words
        if max_lines == 1:
            chunks = [' '.join(all_words)]
        elif max_lines == 2:
            # Split roughly in half
            mid = len(all_words) // 2
            chunks = [' '.join(all_words[:mid]), ' '.join(all_words[mid:])]
        else:  # 3 lines
            total = len(all_words)
            line1_size = total // 3
            line2_size = total // 3
            line3_size = total - line1_size - line2_size
            chunks = [
                ' '.join(all_words[:line1_size]),
                ' '.join(all_words[line1_size:line1_size+line2_size]),
                ' '.join(all_words[line1_size+line2_size:])
            ]
    
    # Ensure we don't exceed max_lines
    if len(chunks) > max_lines:
        # Merge the last few lines if needed
        excess = len(chunks) - max_lines
        if excess > 0:
            # Merge the last (excess+1) lines
            merged = ' '.join(chunks[max_lines-1:])
            chunks = chunks[:max_lines-1] + [merged]
    
    # Clean up empty lines
    chunks = [chunk.strip() for chunk in chunks if chunk.strip()]
    
    return '\n'.join(chunks)

def wrap_segment_text_advanced(segments, words_per_line, max_lines, video_width, reference_width=1920):
    """
    Adjust text wrapping based on user settings and video width.
    """
    wrapped_segments = []
    for start, end, text in segments:
        wrapped_text = wrap_text_advanced(text, words_per_line, max_lines)
        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, words_per_line=10, max_lines=2):
    """
    Convert segments list to ASS format string with angle support and proper positioning.
    """
    # Wrap text based on user settings
    wrapped_segments = wrap_segment_text_advanced(segments, words_per_line, max_lines, 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=1,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(750, 580)

        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))

        # Row 1: Basic options
        opts_row1 = ttk.Frame(opt_frame)
        opts_row1.pack(fill="x", pady=(0, 8))
        
        self.translate_var = tk.BooleanVar(value=True)
        ttk.Checkbutton(opts_row1, text="Translate to English", variable=self.translate_var).pack(side="left")

        ttk.Label(opts_row1, text="   Format:").pack(side="left")
        self.sub_format_var = tk.StringVar(value="ass")
        format_combo = ttk.Combobox(opts_row1, textvariable=self.sub_format_var, width=8, state="readonly",
                                   values=["ass", "srt"])
        format_combo.pack(side="left", padx=6)
        
        ttk.Label(opts_row1, text="   Angle (°):").pack(side="left")
        self.angle_var = tk.StringVar(value="135")
        angle_entry = ttk.Entry(opts_row1, textvariable=self.angle_var, width=6)
        angle_entry.pack(side="left", padx=6)
        
        ttk.Label(opts_row1, text="   Font adjust:").pack(side="left")
        self.font_adjust_var = tk.StringVar(value="0")
        font_adjust = ttk.Combobox(opts_row1, 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_row1, text="   Model:").pack(side="left")
        self.model_var = tk.StringVar(value="auto")
        model_combo = ttk.Combobox(opts_row1, textvariable=self.model_var, width=10, state="readonly",
                                   values=["auto", "tiny", "base", "small", "medium", "large-v3"])
        model_combo.pack(side="left", padx=6)

        # Row 2: Line control options
        opts_row2 = ttk.Frame(opt_frame)
        opts_row2.pack(fill="x")
        
        ttk.Label(opts_row2, text="Line control:", font=("Segoe UI", 10, "bold")).pack(side="left")
        
        ttk.Label(opts_row2, text="   Words per line:").pack(side="left", padx=(10, 0))
        self.words_per_line_var = tk.IntVar(value=10)
        words_spin = ttk.Spinbox(opts_row2, from_=3, to=20, width=5, textvariable=self.words_per_line_var)
        words_spin.pack(side="left", padx=6)
        
        ttk.Label(opts_row2, text="   Max lines:").pack(side="left", padx=(10, 0))
        self.max_lines_var = tk.IntVar(value=2)
        lines_combo = ttk.Combobox(opts_row2, textvariable=self.max_lines_var, width=5, state="readonly",
                                   values=[1, 2, 3])
        lines_combo.pack(side="left", padx=6)
        
        ttk.Label(opts_row2, text="   Preview:").pack(side="left", padx=(10, 0))
        self.preview_btn = ttk.Button(opts_row2, text="Test wrapping", command=self._test_wrapping, width=12)
        self.preview_btn.pack(side="left", padx=6)
        
        ttk.Label(opts_row2, text="   Save .ass/.srt:").pack(side="left", padx=(10, 0))
        self.save_sub_var = tk.BooleanVar(value=True)
        ttk.Checkbutton(opts_row2, 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.")

    # ── Test wrapping function ─────────────────
    
    def _test_wrapping(self):
        """Test the text wrapping with current settings."""
        # Create a sample text for testing
        sample_text = "This is a sample subtitle text to test how the wrapping works with your current settings"
        
        words_per_line = self.words_per_line_var.get()
        max_lines = self.max_lines_var.get()
        
        wrapped = wrap_text_advanced(sample_text, words_per_line, max_lines)
        
        # Show preview in a dialog
        preview_msg = f"""Current settings:
• Words per line: {words_per_line}
• Max lines: {max_lines}

Original text:
{sample_text}

Wrapped text ({len(wrapped.split(chr(10)))} lines):
{wrapped}

Tip: Adjust words per line and max lines until the text fits well within your video width."""
        
        messagebox.showinfo("Text Wrapping Preview", preview_msg)
        self._log(f"Preview: Words/line={words_per_line}, Max lines={max_lines} → {wrapped.replace(chr(10), ' | ')}")
    
    # ── 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)
            
            # Calculate recommended words per line based on video width
            recommended_words = max(5, int(15 * (width / 1920)))
            
            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
• Color: Gold (#FEBD00)

Recommended Line Settings:
• Words per line: {recommended_words} (current: {self.words_per_line_var.get()})
• Max lines: 2-3 lines recommended

Current Settings:
• Words per line: {self.words_per_line_var.get()}
• Max lines: {self.max_lines_var.get()}
• Format: {self.sub_format_var.get().upper()}
• Angle: {self.angle_var.get()}°"""
            
            messagebox.showinfo("Video Information", info_msg)
            self._log(f"Video info: {width}x{height} → Font: {font_size}, Shadow: {shadow}, Rec words/line: {recommended_words}")
        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}")
                # Suggest optimal words per line
                suggested = max(5, int(15 * (width / 1920)))
                self._log(f"Suggested words per line for this video: {suggested}")
            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)
            words_per_line = self.words_per_line_var.get()
            max_lines = self.max_lines_var.get()
            
            # 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")
                self._log(f"Line settings: {words_per_line} words/line, max {max_lines} lines")
            except Exception as e:
                video_width, video_height = 1920, 1080
                final_font_size = 36
                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"Format: {subtitle_format.upper()}  Angle: {angle}°  Font: {final_font_size}px")
            self._log(f"Line wrapping: {words_per_line} words/line, max {max_lines} lines")
            self._log(f"Video size: {video_width}x{video_height}")
            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 with advanced wrapping
            if subtitle_format == "ass":
                # Wrap text based on user settings
                wrapped_segments = wrap_segment_text_advanced(segments, words_per_line, max_lines, 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 with simple wrapping
                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):
                    # Apply wrapping to SRT too
                    wrapped_text = wrap_text_advanced(text, words_per_line, max_lines)
                    srt_lines.append(str(i))
                    srt_lines.append(f"{fmt_time(start)} --> {fmt_time(end)}")
                    srt_lines.append(wrapped_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")
                self._log(f"  • Shadow: {shadow}px")
                self._log(f"  • Color: Gold (#FEBD00)")
                self._log(f"  • Line wrapping: {words_per_line} words/line, max {max_lines} lines")

            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()