import os
import sys
import time
import json
import logging
import threading
import subprocess
import datetime
from pathlib import Path
from io import BytesIO
from concurrent.futures import ThreadPoolExecutor
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext, simpledialog

import yt_dlp
import requests
from PIL import Image, ImageTk
import pyperclip
from plyer import notification

_session = requests.Session()
_adapter = requests.adapters.HTTPAdapter(pool_connections=10, pool_maxsize=10, max_retries=3)
_session.mount('http://', _adapter)
_session.mount('https://', _adapter)

_thumbnail_cache = {}
_thumbnail_cache_lock = threading.Lock()
_executor = ThreadPoolExecutor(max_workers=4)

logging.basicConfig(filename='ytdl_error.log', level=logging.ERROR, format='%(asctime)s - %(levelname)s - %(message)s')

APP_NAME = "YouTube Downloader Pro by RAKIB"
CREATOR = "RAKIB"
DEFAULT_DIR = os.path.join(Path.home(), "Downloads", "YouTube")
HISTORY_FILE = "download_history.json"
SETTINGS_FILE = "settings.json"

SUPPORTED_SITES = {
    'youtube.com': 'YouTube',
    'youtu.be': 'YouTube',
    'facebook.com': 'Facebook',
    'fb.watch': 'Facebook',
    'instagram.com': 'Instagram',
    'twitter.com': 'Twitter/X',
    'x.com': 'Twitter/X',
    'tiktok.com': 'TikTok',
    'reddit.com': 'Reddit',
    'vimeo.com': 'Vimeo',
    'dailymotion.com': 'Dailymotion',
    'soundcloud.com': 'SoundCloud',
}

LIGHT_THEME = {
    'bg': '#ffffff', 'fg': '#000000', 'frame_bg': '#f0f0f0', 'accent': '#0078d4',
    'button': '#e1e1e1', 'button_hover': '#d0d0d0', 'entry_bg': '#ffffff',
    'progress': '#0078d4', 'success': '#28a745', 'error': '#dc3545', 'warning': '#ffc107'
}

DARK_THEME = {
    'bg': '#1e1e1e', 'fg': '#ffffff', 'frame_bg': '#2d2d2d', 'accent': '#3794ff',
    'button': '#3c3c3c', 'button_hover': '#505050', 'entry_bg': '#3c3c3c',
    'progress': '#3794ff', 'success': '#4ec969', 'error': '#f14c4c', 'warning': '#cca700'
}

class YouTubeDownloaderApp:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title(APP_NAME)
        self.root.geometry("950x650")
        self.root.minsize(800, 550)
        
        self.theme = LIGHT_THEME
        self.is_dark = False
        self.is_mini = False
        self.normal_geometry = "950x650"
        
        self.current_metadata = None
        self.available_formats = []
        self.playlist_entries = []
        self.playlist_vars = {}
        self.playlist_search_var = tk.StringVar()
        
        self.tasks = {}
        self.task_widgets = {}
        self.active_tasks = set()
        self.task_counter = 0
        
        self.clipboard_monitor_active = True
        self.last_clipboard = ""
        
        self.download_history = []
        self.schedule_timer = None
        self.scheduled_time = None
        
        self.load_settings()
        self.load_history()
        self.setup_ui()
        self.apply_theme()
        self.check_ffmpeg()
        self.start_clipboard_monitor()
        self.process_manager_queue()
        self.setup_drag_drop()
        
        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
        self.root.mainloop()

    def load_settings(self):
        try:
            if os.path.exists(SETTINGS_FILE):
                with open(SETTINGS_FILE, 'r') as f:
                    settings = json.load(f)
                    self.var_path = tk.StringVar(value=settings.get('path', DEFAULT_DIR))
                    self.var_concurrent = tk.StringVar(value=settings.get('concurrent', '3'))
                    self.var_speed = tk.StringVar(value=settings.get('speed', 'Unlimited'))
                    self.var_retry = tk.IntVar(value=settings.get('retry', 3))
                    self.var_auto_retry = tk.BooleanVar(value=settings.get('auto_retry', True))
                    self.var_theme = tk.StringVar(value=settings.get('theme', 'light'))
                    self.var_filename_template = tk.StringVar(value=settings.get('filename_template', '%(title)s.%(ext)s'))
                    self.var_auto_categorize = tk.BooleanVar(value=settings.get('auto_categorize', True))
                    self.var_proxy = tk.StringVar(value=settings.get('proxy', ''))
                    self.var_notifications = tk.BooleanVar(value=settings.get('notifications', True))
                    self.is_dark = self.var_theme.get() == 'dark'
                    return
        except Exception:
            pass
        self.var_path = tk.StringVar(value=DEFAULT_DIR)
        self.var_concurrent = tk.StringVar(value="3")
        self.var_speed = tk.StringVar(value="Unlimited")
        self.var_retry = tk.IntVar(value=3)
        self.var_auto_retry = tk.BooleanVar(value=True)
        self.var_theme = tk.StringVar(value="light")
        self.var_filename_template = tk.StringVar(value='%(title)s.%(ext)s')
        self.var_auto_categorize = tk.BooleanVar(value=True)
        self.var_proxy = tk.StringVar(value="")
        self.var_notifications = tk.BooleanVar(value=True)

    def save_settings(self):
        settings = {
            'path': self.var_path.get(),
            'concurrent': self.var_concurrent.get(),
            'speed': self.var_speed.get(),
            'retry': self.var_retry.get(),
            'auto_retry': self.var_auto_retry.get(),
            'theme': 'dark' if self.is_dark else 'light',
            'filename_template': self.var_filename_template.get(),
            'auto_categorize': self.var_auto_categorize.get(),
            'proxy': self.var_proxy.get(),
            'notifications': self.var_notifications.get()
        }
        with open(SETTINGS_FILE, 'w') as f:
            json.dump(settings, f)

    def load_history(self):
        try:
            if os.path.exists(HISTORY_FILE):
                with open(HISTORY_FILE, 'r') as f:
                    self.download_history = json.load(f)
        except Exception:
            self.download_history = []

    def save_history(self):
        with open(HISTORY_FILE, 'w') as f:
            json.dump(self.download_history[-500:], f)

    def add_to_history(self, title, url, filepath, duration, file_size):
        entry = {
            'title': title,
            'url': url,
            'filepath': filepath,
            'duration': duration,
            'file_size': file_size,
            'timestamp': datetime.datetime.now().isoformat(),
            'status': 'Completed'
        }
        self.download_history.append(entry)
        self.save_history()
        self.refresh_history_view()

    def setup_ui(self):
        self.notebook = ttk.Notebook(self.root)
        self.notebook.pack(fill='both', expand=True, padx=10, pady=10)
        
        self.tab_download = ttk.Frame(self.notebook)
        self.tab_queue = ttk.Frame(self.notebook)
        self.tab_history = ttk.Frame(self.notebook)
        self.tab_batch = ttk.Frame(self.notebook)
        self.tab_settings = ttk.Frame(self.notebook)
        
        self.notebook.add(self.tab_download, text='Downloader')
        self.notebook.add(self.tab_queue, text='Queue')
        self.notebook.add(self.tab_history, text='History')
        self.notebook.add(self.tab_batch, text='Batch Import')
        self.notebook.add(self.tab_settings, text='Settings')
        
        self.build_downloader_tab()
        self.build_queue_tab()
        self.build_history_tab()
        self.build_batch_tab()
        self.build_settings_tab()

    def build_downloader_tab(self):
        top_frame = ttk.Frame(self.tab_download)
        top_frame.pack(fill='x', padx=10, pady=5)
        
        ttk.Label(top_frame, text="URL:").pack(side='left')
        self.url_entry = ttk.Entry(top_frame, width=60)
        self.url_entry.pack(side='left', fill='x', expand=True, padx=5)
        
        ttk.Button(top_frame, text="Paste", command=self.paste_from_clipboard).pack(side='left', padx=2)
        ttk.Button(top_frame, text="Analyze", command=self.start_analysis).pack(side='left', padx=2)
        
        mid_frame = ttk.Frame(self.tab_download)
        mid_frame.pack(fill='both', expand=True, padx=10, pady=5)
        
        left_frame = ttk.LabelFrame(mid_frame, text="Video Info")
        left_frame.pack(side='left', fill='y', padx=(0, 5))
        
        self.lbl_thumbnail = ttk.Label(left_frame, text="No thumbnail", background='#f0f0f0', width=30, anchor='center')
        self.lbl_thumbnail.pack(pady=10, padx=10)
        
        self.lbl_title = ttk.Label(left_frame, text="Title: -", wraplength=250)
        self.lbl_title.pack(anchor='w', padx=10)
        
        self.lbl_channel = ttk.Label(left_frame, text="Channel: -")
        self.lbl_channel.pack(anchor='w', padx=10)
        
        self.lbl_duration = ttk.Label(left_frame, text="Duration: -")
        self.lbl_duration.pack(anchor='w', padx=10)
        
        self.lbl_size = ttk.Label(left_frame, text="Size: -")
        self.lbl_size.pack(anchor='w', padx=10)
        
        self.lbl_status = ttk.Label(left_frame, text="", foreground='blue')
        self.lbl_status.pack(anchor='w', padx=10, pady=5)
        
        self.lbl_platform = ttk.Label(left_frame, text="Platform: -", foreground='purple', font=('Arial', 9, 'bold'))
        self.lbl_platform.pack(anchor='w', padx=10, pady=2)
        
        right_frame = ttk.LabelFrame(mid_frame, text="Options")
        right_frame.pack(side='right', fill='both', expand=True)
        
        opt_frame = ttk.Frame(right_frame)
        opt_frame.pack(fill='x', padx=10, pady=10)
        
        ttk.Label(opt_frame, text="Type:").grid(row=0, column=0, sticky='w', pady=3)
        self.var_type = tk.StringVar(value="Video")
        self.menu_type = ttk.Combobox(opt_frame, values=["Video", "Audio Only"], textvariable=self.var_type, state='readonly', width=12)
        self.menu_type.grid(row=0, column=1, padx=5, pady=3)
        self.menu_type.bind('<<ComboboxSelected>>', lambda e: self.toggle_options())
        
        ttk.Label(opt_frame, text="Quality:").grid(row=0, column=2, sticky='w', pady=3, padx=(10,0))
        self.var_quality = tk.StringVar(value="1080p")
        self.menu_quality = ttk.Combobox(opt_frame, values=["4K", "1440p", "1080p", "720p", "480p", "360p"], textvariable=self.var_quality, state='readonly', width=8)
        self.menu_quality.grid(row=0, column=3, padx=5, pady=3)
        
        self.var_audio_fmt = tk.StringVar(value="mp3")
        self.menu_audio = ttk.Combobox(opt_frame, values=["mp3", "m4a", "wav", "flac", "aac"], textvariable=self.var_audio_fmt, state='readonly', width=8)
        self.menu_audio.grid(row=0, column=3, padx=5, pady=3)
        self.menu_audio.grid_forget()
        
        ttk.Label(opt_frame, text="Format:").grid(row=1, column=0, sticky='w', pady=3)
        self.var_format = tk.StringVar(value="MP4")
        self.menu_format = ttk.Combobox(opt_frame, values=["MP4", "MKV", "WEBM", "MOV"], textvariable=self.var_format, state='readonly', width=8)
        self.menu_format.grid(row=1, column=1, padx=5, pady=3)
        
        self.btn_formats = ttk.Button(opt_frame, text="Show All Formats", command=self.show_all_formats)
        self.btn_formats.grid(row=1, column=2, padx=5, pady=3, columnspan=2)
        
        self.format_popup = None
        
        ttk.Label(opt_frame, text="Subtitle:").grid(row=2, column=0, sticky='w', pady=3)
        self.var_sub_lang = tk.StringVar(value="en")
        self.menu_sub_lang = ttk.Combobox(opt_frame, values=["en", "es", "fr", "de", "it", "pt", "ru", "ja", "ko", "zh"], textvariable=self.var_sub_lang, state='readonly', width=8)
        self.menu_sub_lang.grid(row=2, column=1, padx=5, pady=3)
        
        self.var_subs = tk.BooleanVar(value=False)
        ttk.Checkbutton(opt_frame, text="Subtitles", variable=self.var_subs).grid(row=2, column=2, columnspan=2, sticky='w', pady=3, padx=(10,0))
        
        self.var_thumb = tk.BooleanVar(value=True)
        ttk.Checkbutton(opt_frame, text="Thumbnail", variable=self.var_thumb).grid(row=3, column=0, columnspan=2, sticky='w', pady=3)
        
        self.var_meta = tk.BooleanVar(value=True)
        ttk.Checkbutton(opt_frame, text="Metadata", variable=self.var_meta).grid(row=3, column=2, columnspan=2, sticky='w', pady=3, padx=(10,0))
        
        playlist_frame = ttk.LabelFrame(right_frame, text="Playlist")
        playlist_frame.pack(fill='both', expand=True, padx=10, pady=5)
        
        search_frame = ttk.Frame(playlist_frame)
        search_frame.pack(fill='x', padx=5, pady=3)
        ttk.Label(search_frame, text="Search:").pack(side='left')
        self.playlist_search_var = tk.StringVar()
        self.playlist_search_var.trace('w', self.filter_playlist)
        ttk.Entry(search_frame, textvariable=self.playlist_search_var, width=20).pack(side='left', padx=5, fill='x', expand=True)
        
        range_frame = ttk.Frame(playlist_frame)
        range_frame.pack(fill='x', padx=5, pady=2)
        ttk.Label(range_frame, text="Range:").pack(side='left')
        self.var_range_start = tk.StringVar(value="1")
        self.var_range_end = tk.StringVar(value="-1")
        ttk.Entry(range_frame, textvariable=self.var_range_start, width=5).pack(side='left', padx=2)
        ttk.Label(range_frame, text="to").pack(side='left')
        ttk.Entry(range_frame, textvariable=self.var_range_end, width=5).pack(side='left', padx=2)
        
        self.playlist_canvas = tk.Canvas(playlist_frame, height=150)
        self.playlist_scroll = ttk.Scrollbar(playlist_frame, orient='vertical', command=self.playlist_canvas.yview)
        self.playlist_list = ttk.Frame(self.playlist_canvas)
        
        self.playlist_list.bind('<Configure>', lambda e: self.playlist_canvas.configure(scrollregion=self.playlist_canvas.bbox('all')))
        self.playlist_canvas.create_window((0, 0), window=self.playlist_list, anchor='nw')
        self.playlist_canvas.configure(yscrollcommand=self.playlist_scroll.set)
        
        self.playlist_canvas.pack(side='left', fill='both', expand=True)
        self.playlist_scroll.pack(side='right', fill='y')
        
        btn_frame = ttk.Frame(right_frame)
        btn_frame.pack(fill='x', padx=10, pady=5)
        ttk.Button(btn_frame, text="Select All", command=lambda: self.toggle_playlist(True)).pack(side='left', padx=2)
        ttk.Button(btn_frame, text="Deselect All", command=lambda: self.toggle_playlist(False)).pack(side='left', padx=2)
        
        bottom_frame = ttk.Frame(self.tab_download)
        bottom_frame.pack(fill='x', padx=10, pady=5)
        self.btn_download = ttk.Button(bottom_frame, text="Add to Download Queue", command=self.add_to_manager)
        self.btn_download.pack(fill='x')

    def build_queue_tab(self):
        header_frame = ttk.Frame(self.tab_queue)
        header_frame.pack(fill='x', padx=10, pady=5)
        
        ttk.Label(header_frame, text="Download Queue", font=('Arial', 12, 'bold')).pack(side='left')
        
        ttk.Button(header_frame, text="Open Folder", command=self.open_folder).pack(side='right', padx=2)
        ttk.Button(header_frame, text="Pause All", command=self.pause_all).pack(side='right', padx=2)
        ttk.Button(header_frame, text="Resume All", command=self.resume_all).pack(side='right', padx=2)
        ttk.Button(header_frame, text="Clear Done", command=self.clear_completed).pack(side='right', padx=2)
        
        self.overall_progress_frame = ttk.Frame(self.tab_queue)
        self.overall_progress_frame.pack(fill='x', padx=10, pady=2)
        ttk.Label(self.overall_progress_frame, text="Overall Progress:").pack(side='left')
        self.overall_progress = ttk.Progressbar(self.overall_progress_frame, length=400, mode='determinate')
        self.overall_progress.pack(side='left', padx=10, fill='x', expand=True)
        self.lbl_overall = ttk.Label(self.overall_progress_frame, text="0/0")
        self.lbl_overall.pack(side='left', padx=5)
        
        self.queue_frame = ttk.Frame(self.tab_queue)
        self.queue_frame.pack(fill='both', expand=True, padx=10, pady=5)
        
        self.queue_canvas = tk.Canvas(self.queue_frame)
        self.queue_scroll = ttk.Scrollbar(self.queue_frame, orient='vertical', command=self.queue_canvas.yview)
        self.queue_list = ttk.Frame(self.queue_canvas)
        
        self.queue_list.bind('<Configure>', lambda e: self.queue_canvas.configure(scrollregion=self.queue_canvas.bbox('all')))
        self.queue_canvas.create_window((0, 0), window=self.queue_list, anchor='nw')
        self.queue_canvas.configure(yscrollcommand=self.queue_scroll.set)
        
        self.queue_canvas.pack(side='left', fill='both', expand=True)
        self.queue_scroll.pack(side='right', fill='y')

    def build_history_tab(self):
        search_frame = ttk.Frame(self.tab_history)
        search_frame.pack(fill='x', padx=10, pady=5)
        
        ttk.Label(search_frame, text="Search:").pack(side='left')
        self.history_search = ttk.Entry(search_frame, width=30)
        self.history_search.pack(side='left', padx=5)
        self.history_search.bind('<KeyRelease>', lambda e: self.filter_history())
        
        ttk.Button(search_frame, text="Clear History", command=self.clear_history).pack(side='right', padx=2)
        ttk.Button(search_frame, text="Open File", command=self.open_history_file).pack(side='right', padx=2)
        
        self.history_canvas = tk.Canvas(self.tab_history)
        self.history_scroll = ttk.Scrollbar(self.tab_history, orient='vertical', command=self.history_canvas.yview)
        self.history_list = ttk.Frame(self.history_canvas)
        
        self.history_list.bind('<Configure>', lambda e: self.history_canvas.configure(scrollregion=self.history_canvas.bbox('all')))
        self.history_canvas.create_window((0, 0), window=self.history_list, anchor='nw')
        self.history_canvas.configure(yscrollcommand=self.history_scroll.set)
        
        self.history_canvas.pack(side='left', fill='both', expand=True, padx=(10,0), pady=5)
        self.history_scroll.pack(side='right', fill='y', pady=5, padx=(0,10))
        
        self.refresh_history_view()

    def refresh_history_view(self):
        for widget in self.history_list.winfo_children():
            widget.destroy()
        
        search = self.history_search.get().lower() if hasattr(self, 'history_search') else ""
        
        for entry in reversed(self.download_history[-100:]):
            if search and search not in entry.get('title', '').lower():
                continue
            
            row = ttk.Frame(self.history_list)
            row.pack(fill='x', pady=2, padx=5)
            
            title = entry.get('title', 'Unknown')[:50]
            date = entry.get('timestamp', '')[:19].replace('T', ' ')
            size = self.format_size(entry.get('file_size', 0))
            
            lbl = ttk.Label(row, text=f"{title} | {size} | {date}", font=('Arial', 8))
            lbl.pack(side='left')
            
            btn = ttk.Button(row, text="Open", command=lambda p=entry.get('filepath', ''): self.open_file_location(p), width=8)
            btn.pack(side='right')

    def filter_history(self, *args):
        self.refresh_history_view()

    def clear_history(self):
        if messagebox.askyesno("Clear History", "Are you sure you want to clear all download history?"):
            self.download_history = []
            self.save_history()
            self.refresh_history_view()

    def open_history_file(self):
        filepath = filedialog.askopenfilename(title="Select History File", filetypes=[("JSON", "*.json")])
        if filepath:
            try:
                with open(filepath, 'r') as f:
                    imported = json.load(f)
                    self.download_history.extend(imported)
                    self.save_history()
                    self.refresh_history_view()
                messagebox.showinfo("Success", f"Imported {len(imported)} entries")
            except Exception as e:
                messagebox.showerror("Error", str(e))

    def build_batch_tab(self):
        ttk.Label(self.tab_batch, text="Batch URL Import - by RAKIB", font=('Arial', 14, 'bold')).pack(pady=10)
        
        info_frame = ttk.Frame(self.tab_batch)
        info_frame.pack(fill='both', expand=True, padx=20, pady=10)
        
        supported = "Supported: YouTube, Facebook, Instagram, Twitter/X, TikTok, Reddit, Vimeo, Dailymotion, SoundCloud"
        ttk.Label(info_frame, text=supported, font=('Arial', 8), foreground='gray').pack(anchor='w', pady=(0,5))
        
        ttk.Label(info_frame, text="Enter URLs (one per line):").pack(anchor='w')
        
        self.batch_text = scrolledtext.ScrolledText(info_frame, height=15, width=70)
        self.batch_text.pack(fill='both', expand=True, pady=5)
        
        btn_frame = ttk.Frame(info_frame)
        btn_frame.pack(fill='x', pady=5)
        
        ttk.Button(btn_frame, text="Load from File", command=self.load_urls_from_file).pack(side='left', padx=2)
        ttk.Button(btn_frame, text="Clear", command=lambda: self.batch_text.delete('1.0', 'end')).pack(side='left', padx=2)
        ttk.Button(btn_frame, text="Add All to Queue", command=self.add_batch_to_queue).pack(side='left', padx=2)
        
        opt_frame = ttk.LabelFrame(info_frame, text="Batch Options")
        opt_frame.pack(fill='x', pady=10)
        
        ttk.Label(opt_frame, text="Type:").grid(row=0, column=0, padx=5, pady=5, sticky='w')
        self.var_batch_type = tk.StringVar(value="Video")
        ttk.Combobox(opt_frame, values=["Video", "Audio Only"], textvariable=self.var_batch_type, state='readonly', width=12).grid(row=0, column=1, padx=5, pady=5)
        
        ttk.Label(opt_frame, text="Quality:").grid(row=0, column=2, padx=5, pady=5, sticky='w')
        self.var_batch_quality = tk.StringVar(value="1080p")
        ttk.Combobox(opt_frame, values=["4K", "1440p", "1080p", "720p", "480p"], textvariable=self.var_batch_quality, state='readonly', width=8).grid(row=0, column=3, padx=5, pady=5)

    def load_urls_from_file(self):
        filepath = filedialog.askopenfilename(title="Select URL File", filetypes=[("Text", "*.txt"), ("All", "*.*")])
        if filepath:
            try:
                with open(filepath, 'r') as f:
                    urls = f.read()
                    self.batch_text.delete('1.0', 'end')
                    self.batch_text.insert('1.0', urls)
            except Exception as e:
                messagebox.showerror("Error", str(e))

    def add_batch_to_queue(self):
        urls = self.batch_text.get('1.0', 'end').strip().split('\n')
        urls = [u.strip() for u in urls if u.strip()]
        
        if not urls:
            return
        
        added = 0
        for url in urls:
            platform = self.detect_platform(url)
            if platform != "Unknown":
                self._create_task(f"Batch: {url[:30]}", url, platform, type_=self.var_batch_type.get(), quality=self.var_batch_quality.get())
                added += 1
        
        messagebox.showinfo("Batch Import", f"Added {added} URLs to queue")
        self.notebook.select(1)
        self.process_manager_queue()

    def build_settings_tab(self):
        ttk.Label(self.tab_settings, text="Settings", font=('Arial', 14, 'bold')).pack(pady=10)
        
        notebook_settings = ttk.Notebook(self.tab_settings)
        notebook_settings.pack(fill='both', expand=True, padx=10, pady=10)
        
        tab_general = ttk.Frame(notebook_settings)
        tab_network = ttk.Frame(notebook_settings)
        tab_advanced = ttk.Frame(notebook_settings)
        tab_appearance = ttk.Frame(notebook_settings)
        
        notebook_settings.add(tab_general, text="General")
        notebook_settings.add(tab_network, text="Network")
        notebook_settings.add(tab_advanced, text="Advanced")
        notebook_settings.add(tab_appearance, text="Appearance")
        
        path_frame = ttk.LabelFrame(tab_general, text="Save Location")
        path_frame.pack(fill='x', padx=10, pady=10)
        
        ttk.Label(path_frame, text="Directory:").pack(side='left', padx=5)
        ttk.Entry(path_frame, textvariable=self.var_path, width=50).pack(side='left', fill='x', expand=True, padx=5)
        ttk.Button(path_frame, text="Browse", command=self.browse_folder).pack(side='left', padx=5)
        
        schedule_frame = ttk.LabelFrame(tab_general, text="Schedule Download")
        schedule_frame.pack(fill='x', padx=10, pady=10)
        
        ttk.Label(schedule_frame, text="Start at:").pack(side='left', padx=5)
        self.var_schedule_hour = tk.StringVar(value="00")
        self.var_schedule_min = tk.StringVar(value="00")
        ttk.Entry(schedule_frame, textvariable=self.var_schedule_hour, width=3).pack(side='left')
        ttk.Label(schedule_frame, text=":").pack(side='left')
        ttk.Entry(schedule_frame, textvariable=self.var_schedule_min, width=3).pack(side='left')
        ttk.Button(schedule_frame, text="Schedule", command=self.schedule_download).pack(side='left', padx=10)
        ttk.Button(schedule_frame, text="Cancel", command=self.cancel_schedule).pack(side='left', padx=2)
        self.lbl_schedule = ttk.Label(schedule_frame, text="", foreground='blue')
        self.lbl_schedule.pack(side='left', padx=5)
        
        net_frame = ttk.LabelFrame(tab_network, text="Network Settings")
        net_frame.pack(fill='x', padx=10, pady=10)
        
        ttk.Label(net_frame, text="Max Concurrent:").grid(row=0, column=0, padx=5, pady=5, sticky='w')
        ttk.Combobox(net_frame, values=["1", "2", "3", "5", "10"], textvariable=self.var_concurrent, state='readonly', width=10).grid(row=0, column=1, padx=5, pady=5)
        
        ttk.Label(net_frame, text="Speed Limit:").grid(row=1, column=0, padx=5, pady=5, sticky='w')
        ttk.Combobox(net_frame, values=["Unlimited", "500K", "1M", "5M", "10M"], textvariable=self.var_speed, state='readonly', width=10).grid(row=1, column=1, padx=5, pady=5)
        
        ttk.Label(net_frame, text="Proxy:").grid(row=2, column=0, padx=5, pady=5, sticky='w')
        ttk.Entry(net_frame, textvariable=self.var_proxy, width=40).grid(row=2, column=1, columnspan=2, padx=5, pady=5, sticky='w')
        
        adv_frame = ttk.LabelFrame(tab_advanced, text="Download Settings")
        adv_frame.pack(fill='x', padx=10, pady=10)
        
        ttk.Label(adv_frame, text="Retry Attempts:").grid(row=0, column=0, padx=5, pady=5, sticky='w')
        ttk.Spinbox(adv_frame, from_=1, to=10, textvariable=self.var_retry, width=10).grid(row=0, column=1, padx=5, pady=5, sticky='w')
        
        ttk.Checkbutton(adv_frame, text="Auto-retry failed downloads", variable=self.var_auto_retry).grid(row=1, column=0, columnspan=2, padx=5, pady=5, sticky='w')
        
        ttk.Label(adv_frame, text="Filename Template:").grid(row=2, column=0, padx=5, pady=5, sticky='w')
        ttk.Entry(adv_frame, textvariable=self.var_filename_template, width=35).grid(row=2, column=1, columnspan=2, padx=5, pady=5, sticky='w')
        
        help_text = ttk.Label(adv_frame, text="Vars: %(title)s, %(channel)s, %(uploader)s, %(playlist)s, %(index)s, %(date)s", font=('Arial', 8), foreground='gray')
        help_text.grid(row=3, column=0, columnspan=3, padx=5, sticky='w')
        
        ttk.Checkbutton(adv_frame, text="Auto-categorize by channel/playlist", variable=self.var_auto_categorize).grid(row=4, column=0, columnspan=2, padx=5, pady=5, sticky='w')
        
        ttk.Checkbutton(adv_frame, text="Desktop Notifications", variable=self.var_notifications).grid(row=5, column=0, columnspan=2, padx=5, pady=5, sticky='w')
        
        theme_frame = ttk.LabelFrame(tab_appearance, text="Theme")
        theme_frame.pack(fill='x', padx=10, pady=10)
        
        ttk.Label(theme_frame, text="Theme:").pack(side='left', padx=5)
        self.var_theme = tk.StringVar(value="light")
        ttk.Radiobutton(theme_frame, text="Light", variable=self.var_theme, value="light", command=self.toggle_theme).pack(side='left', padx=5)
        ttk.Radiobutton(theme_frame, text="Dark", variable=self.var_theme, value="dark", command=self.toggle_theme).pack(side='left', padx=5)
        
        mode_frame = ttk.LabelFrame(tab_appearance, text="Window Mode")
        mode_frame.pack(fill='x', padx=10, pady=10)
        
        ttk.Button(mode_frame, text="Toggle Mini Mode", command=self.toggle_mini_mode).pack(pady=10)

    def toggle_theme(self):
        self.is_dark = self.var_theme.get() == 'dark'
        self.theme = DARK_THEME if self.is_dark else LIGHT_THEME
        self.apply_theme()
        self.save_settings()

    def apply_theme(self):
        style = ttk.Style()
        
        if self.is_dark:
            style.theme_use('clam')
            self.root.configure(bg=DARK_THEME['bg'])
            style.configure('.', background=DARK_THEME['bg'], foreground=DARK_THEME['fg'])
            style.configure('TFrame', background=DARK_THEME['bg'])
            style.configure('TLabel', background=DARK_THEME['bg'], foreground=DARK_THEME['fg'])
            style.configure('TLabelframe', background=DARK_THEME['bg'], foreground=DARK_THEME['fg'])
            style.configure('TLabelframe.Label', background=DARK_THEME['bg'], foreground=DARK_THEME['fg'])
            style.configure('TEntry', fieldbackground=DARK_THEME['entry_bg'], foreground=DARK_THEME['fg'])
            style.configure('TButton', background=DARK_THEME['button'], foreground=DARK_THEME['fg'])
            style.configure('TCheckbutton', background=DARK_THEME['bg'], foreground=DARK_THEME['fg'])
            style.map('TButton', background=[('active', DARK_THEME['button_hover'])])
            self.lbl_thumbnail.configure(background='#404040')
        else:
            style.theme_use('clam')
            self.root.configure(bg=LIGHT_THEME['bg'])
            style.configure('.', background=LIGHT_THEME['bg'], foreground=LIGHT_THEME['fg'])
            style.configure('TFrame', background=LIGHT_THEME['bg'])
            style.configure('TLabel', background=LIGHT_THEME['bg'], foreground=LIGHT_THEME['fg'])
            style.configure('TLabelframe', background=LIGHT_THEME['bg'], foreground=LIGHT_THEME['fg'])
            style.configure('TLabelframe.Label', background=LIGHT_THEME['bg'], foreground=LIGHT_THEME['fg'])
            style.configure('TEntry', fieldbackground=LIGHT_THEME['entry_bg'], foreground=LIGHT_THEME['fg'])
            style.configure('TButton', background=LIGHT_THEME['button'], foreground=LIGHT_THEME['fg'])
            style.configure('TCheckbutton', background=LIGHT_THEME['bg'], foreground=LIGHT_THEME['fg'])
            style.map('TButton', background=[('active', LIGHT_THEME['button_hover'])])
            self.lbl_thumbnail.configure(background='#f0f0f0')

    def toggle_mini_mode(self):
        if self.is_mini:
            self.root.geometry(self.normal_geometry)
            self.is_mini = False
        else:
            self.normal_geometry = self.root.geometry()
            self.root.geometry("400x300")
            self.is_mini = True

    def toggle_options(self):
        if self.var_type.get() == "Audio Only":
            self.menu_quality.grid_forget()
            self.menu_audio.grid(row=0, column=3, padx=5, pady=3)
        else:
            self.menu_audio.grid_forget()
            self.menu_quality.grid(row=0, column=3, padx=5, pady=3)

    def toggle_playlist(self, state):
        for var in self.playlist_vars.values():
            var.set(state)

    def filter_playlist(self, *args):
        search = self.playlist_search_var.get().lower()
        for widget in self.playlist_list.winfo_children():
            text = widget.cget('text').lower() if hasattr(widget, 'cget') else ''
            if search and search in text:
                widget.pack()
            elif search:
                widget.pack_forget()
            else:
                widget.pack()

    def paste_from_clipboard(self):
        self.url_entry.delete(0, 'end')
        self.url_entry.insert(0, pyperclip.paste())

    def browse_folder(self):
        folder = filedialog.askdirectory()
        if folder:
            self.var_path.set(folder)

    def show_all_formats(self):
        if not self.current_metadata:
            self.lbl_status.configure(text="Please analyze a URL first!", foreground='red')
            return
        
        url = self.url_entry.get().strip()
        if not url:
            return
        
        self.lbl_status.configure(text="Fetching formats...", foreground='orange')
        threading.Thread(target=self._fetch_formats, args=(url,), daemon=True).start()

    def _fetch_formats(self, url):
        ydl_opts = {'quiet': True, 'no_warnings': True, 'http_chunk_size': 10485760}
        proxy = self.var_proxy.get().strip()
        if proxy:
            ydl_opts['proxy'] = proxy
            
        try:
            with yt_dlp.YoutubeDL(ydl_opts) as ydl:
                info = ydl.extract_info(url, download=False)
                formats = []
                
                for f in info.get('formats', []):
                    if f.get('ext') in ['mp4', 'm4a', 'webm', 'mkv']:
                        res = f.get('resolution', 'audio only')
                        size = self.format_size(f.get('filesize') or f.get('filesize_estimate', 0))
                        fmt = f"{f.get('ext').upper()} | {res} | {size}"
                        formats.append((fmt, f.get('format_id')))
                
                self.root.after(0, self._show_format_popup, formats[:15])
        except Exception as e:
            self.root.after(0, lambda: self.lbl_status.configure(text=f"Error: {str(e)}", foreground='red'))

    def _show_format_popup(self, formats):
        if self.format_popup:
            self.format_popup.destroy()
        
        self.format_popup = tk.Toplevel(self.root)
        self.format_popup.title("Select Format")
        self.format_popup.geometry("400x400")
        
        ttk.Label(self.format_popup, text="Select Format:", font=('Arial', 11, 'bold')).pack(pady=10)
        
        canvas = tk.Canvas(self.format_popup)
        scroll = ttk.Scrollbar(self.format_popup, orient='vertical', command=canvas.yview)
        frame = ttk.Frame(canvas)
        
        frame.bind('<Configure>', lambda e: canvas.configure(scrollregion=canvas.bbox('all')))
        canvas.create_window((0, 0), window=frame, anchor='nw')
        canvas.configure(yscrollcommand=scroll.set)
        
        self.selected_format = tk.StringVar()
        
        for fmt, fmt_id in formats:
            ttk.Radiobutton(frame, text=fmt, variable=self.selected_format, value=fmt_id).pack(anchor='w', padx=20, pady=2)
        
        canvas.pack(side='left', fill='both', expand=True, padx=(10,0), pady=10)
        scroll.pack(side='right', fill='y', pady=10, padx=(0,10))
        
        btn = ttk.Button(self.format_popup, text="Select", command=self.format_popup.destroy)
        btn.pack(pady=10)

    def start_analysis(self):
        url = self.url_entry.get().strip()
        if not url:
            return
        
        self.lbl_status.configure(text="Analyzing...", foreground='orange')
        
        for widget in self.playlist_list.winfo_children():
            widget.destroy()
        self.playlist_vars.clear()
        self.playlist_entries.clear()
        self.available_formats = []
        
        threading.Thread(target=self._analyze_worker, args=(url,), daemon=True).start()

    def _analyze_worker(self, url):
        platform = self.detect_platform(url)
        
        ydl_opts = {
            'quiet': True, 
            'no_warnings': True,
            'extract_flat': False,
            'http_chunk_size': 10485760,
        }
        proxy = self.var_proxy.get().strip()
        if proxy:
            ydl_opts['proxy'] = proxy
            
        try:
            with yt_dlp.YoutubeDL(ydl_opts) as ydl:
                info = ydl.extract_info(url, download=False)
            self.root.after(0, self._analysis_complete, info, platform)
        except Exception as e:
            self.root.after(0, self._analysis_error, str(e))

    def detect_platform(self, url):
        for site, name in SUPPORTED_SITES.items():
            if site in url.lower():
                return name
        return "Unknown"

    def _analysis_error(self, err_msg):
        self.lbl_status.configure(text="Error: " + err_msg, foreground='red')
        self.lbl_title.configure(text="Title: Error")

    def load_thumbnail(self, url, widget):
        def _load():
            try:
                if url in _thumbnail_cache:
                    img_data = _thumbnail_cache[url]
                else:
                    resp = _session.get(url, timeout=10)
                    img_data = resp.content
                    with _thumbnail_cache_lock:
                        _thumbnail_cache[url] = img_data
                
                img = Image.open(BytesIO(img_data))
                img.thumbnail((250, 200))
                photo = ImageTk.PhotoImage(img)
                
                def _update():
                    widget.configure(image=photo, text="")
                    widget.image = photo
                self.root.after(0, _update)
            except Exception:
                pass
        _executor.submit(_load)

    def _analysis_complete(self, info, platform="Unknown"):
        self.current_metadata = info
        self.lbl_status.configure(text=f"Analysis Complete!", foreground='green')
        self.lbl_platform.configure(text=f"Platform: {platform}")
        
        title = info.get('title', 'Unknown Title')
        self.lbl_title.configure(text=f"Title: {title[:50]}...")
        
        channel = info.get('uploader') or info.get('channel') or info.get('author') or 'Unknown'
        self.lbl_channel.configure(text=f"Channel: {channel}")
        
        dur = info.get('duration', 0)
        self.lbl_duration.configure(text=f"Duration: {dur//3600:02d}:{(dur%3600)//60:02d}:{dur%60:02d}" if dur else "Duration: --:--")
        
        size = info.get('filesize') or info.get('filesize_estimate', 0)
        self.lbl_size.configure(text=f"Size: {self.format_size(size)}")
        
        thumb_url = info.get('thumbnail')
        if thumb_url:
            self.load_thumbnail(thumb_url, self.lbl_thumbnail)
        
        if 'entries' in info:
            playlist_thumb = info.get('thumbnails', [{}])[0].get('url') if info.get('thumbnails') else None
            if playlist_thumb and 'entries' in info:
                self.load_thumbnail(playlist_thumb, self.lbl_thumbnail)
            
            self.lbl_status.configure(text=f"Playlist: {len(info['entries'])} videos ({platform})")
            self.playlist_entries = info['entries']
            
            for widget in self.playlist_list.winfo_children():
                widget.destroy()
            self.playlist_vars.clear()
            
            for i, entry in enumerate(self.playlist_entries[:50]):
                if not entry:
                    continue
                v_title = entry.get('title', f"Video {i+1}")[:40]
                v_id = entry.get('id')
                var = tk.BooleanVar(value=True)
                self.playlist_vars[v_id] = var
                
                row = ttk.Frame(self.playlist_list)
                row.pack(anchor='w', padx=5, pady=1)
                
                try:
                    entry_thumb = entry.get('thumbnail')
                    if entry_thumb:
                        lbl = ttk.Label(row, width=3, text="")
                        lbl.pack(side='left')
                        def load_entry_thumb(url=entry_thumb, w=lbl):
                            try:
                                resp = _session.get(url, timeout=5)
                                img = Image.open(BytesIO(resp.content))
                                img.thumbnail((30, 30))
                                photo = ImageTk.PhotoImage(img)
                                w.configure(image=photo, text="")
                                w.image = photo
                            except:
                                pass
                        _executor.submit(load_entry_thumb)
                except:
                    pass
                
                ttk.Checkbutton(row, text=v_title, variable=var).pack(side='left', padx=2)
        else:
            self.playlist_entries = [info]
            self.playlist_vars[info.get('id')] = tk.BooleanVar(value=True)

    def format_size(self, size):
        for unit in ['B', 'KB', 'MB', 'GB']:
            if size < 1024:
                return f"{size:.1f} {unit}"
            size /= 1024
        return f"{size:.1f} TB"

    def add_to_manager(self):
        if not self.current_metadata:
            self.lbl_status.configure(text="Please analyze a URL first!", foreground='red')
            return
        
        playlist_name = "Single Videos"
        if 'entries' in self.current_metadata:
            playlist_name = "".join(x for x in self.current_metadata.get('title', 'Playlist') if x.isalnum() or x in " -_")
        
        channel_name = self.current_metadata.get('uploader', 'Unknown')
        
        url = self.url_entry.get().strip()
        platform = self.detect_platform(url)
        
        try:
            range_start = int(self.var_range_start.get()) - 1
            range_end = int(self.var_range_end.get())
            if range_end == -1:
                range_end = len(self.playlist_entries)
        except ValueError:
            range_start = 0
            range_end = len(self.playlist_entries)
        
        added_count = 0
        for i, entry in enumerate(self.playlist_entries):
            if i < range_start:
                continue
            if i >= range_end and range_end != -1:
                break
            if not entry:
                continue
            vid_id = entry.get('id')
            if self.playlist_vars.get(vid_id) and self.playlist_vars[vid_id].get():
                if platform == 'YouTube':
                    url = f"https://www.youtube.com/watch?v={vid_id}"
                folder = playlist_name
                if self.var_auto_categorize.get():
                    folder = os.path.join(channel_name, playlist_name)
                self._create_task(entry.get('title', 'Unknown'), url, folder)
                added_count += 1
        
        if added_count == 0:
            self.lbl_status.configure(text="No videos selected!", foreground='red')
            return
        
        self.lbl_status.configure(text=f"Added {added_count} items to queue.", foreground='blue')
        self.notebook.select(1)
        self.process_manager_queue()

    def _create_task(self, title, url, folder_name, type_=None, quality=None):
        self.task_counter += 1
        task_id = f"task_{self.task_counter}"
        
        platform = self.detect_platform(url)
        
        task_data = {
            'id': task_id,
            'title': title[:50],
            'url': url,
            'folder': folder_name,
            'status': 'Queued',
            'type': type_ or self.var_type.get(),
            'quality': quality or self.var_quality.get(),
            'audio_fmt': self.var_audio_fmt.get(),
            'format': self.var_format.get(),
            'subs': self.var_subs.get(),
            'sub_lang': self.var_sub_lang.get(),
            'thumb': self.var_thumb.get(),
            'meta': self.var_meta.get(),
            'pause_flag': False,
            'cancel_flag': False,
            'retry_count': 0,
            'filename_template': self.var_filename_template.get(),
            'platform': platform,
        }
        
        row = ttk.Frame(self.queue_list)
        row.pack(fill='x', pady=2, padx=5)
        
        lbl_title = ttk.Label(row, text=f"[{platform}] {title[:40]}", font=('Arial', 9, 'bold'))
        lbl_title.pack(anchor='w')
        
        progress = ttk.Progressbar(row, length=300, mode='determinate')
        progress.pack(fill='x', pady=2)
        
        lbl_stats = ttk.Label(row, text="Queued | 0% | -- MB/s | ETA: --", font=('Arial', 8))
        lbl_stats.pack(anchor='w')
        
        btn_frame = ttk.Frame(row)
        btn_frame.pack(anchor='e')
        
        btn_cancel = ttk.Button(btn_frame, text="Cancel", command=lambda: self.cancel_task(task_id))
        btn_cancel.pack(side='left', padx=2)
        
        self.tasks[task_id] = task_data
        self.task_widgets[task_id] = {
            'row': row,
            'progress': progress,
            'lbl_stats': lbl_stats,
            'btn_cancel': btn_cancel,
            'lbl_title': lbl_title,
        }
        
        self.update_overall_progress()

    def cancel_task(self, task_id):
        if task_id in self.tasks:
            self.tasks[task_id]['cancel_flag'] = True
            self.task_widgets[task_id]['lbl_stats'].configure(text="Cancelling...", foreground='red')

    def pause_all(self):
        for task in self.tasks.values():
            if task['status'] == 'Downloading':
                task['pause_flag'] = True

    def resume_all(self):
        for task in self.tasks.values():
            if task['status'] == 'Paused':
                task['status'] = 'Queued'
                task['pause_flag'] = False

    def clear_completed(self):
        to_remove = []
        for task_id, task in self.tasks.items():
            if task['status'] in ['Completed', 'Cancelled', 'Error']:
                to_remove.append(task_id)
        
        for task_id in to_remove:
            if task_id in self.task_widgets:
                self.task_widgets[task_id]['row'].destroy()
                del self.task_widgets[task_id]
            del self.tasks[task_id]
        
        self.update_overall_progress()

    def update_overall_progress(self):
        total = len(self.tasks)
        completed = sum(1 for t in self.tasks.values() if t['status'] == 'Completed')
        
        if total > 0:
            pct = (completed / total) * 100
            self.overall_progress['value'] = pct
            self.lbl_overall.configure(text=f"{completed}/{total}")
        else:
            self.overall_progress['value'] = 0
            self.lbl_overall.configure(text="0/0")

    def process_manager_queue(self):
        active_count = len(self.active_tasks)
        max_concurrent = int(self.var_concurrent.get())
        
        for task_id, task in self.tasks.items():
            if active_count >= max_concurrent:
                break
            
            if task['status'] == 'Queued':
                task['status'] = 'Downloading'
                self.active_tasks.add(task_id)
                self.task_widgets[task_id]['lbl_stats'].configure(text="Starting...", foreground='cyan')
                threading.Thread(target=self._download_worker, args=(task_id,), daemon=True).start()
                active_count += 1

    def _download_worker(self, task_id):
        task = self.tasks[task_id]
        widget = self.task_widgets[task_id]
        
        base_path = os.path.join(self.var_path.get(), task['folder'])
        os.makedirs(base_path, exist_ok=True)
        
        template = task.get('filename_template', '%(title)s.%(ext)s')
        
        ydl_opts = {
            'outtmpl': os.path.join(base_path, template),
            'noplaylist': True,
            'progress_hooks': [lambda d: self._progress_hook(d, task, widget)],
            'postprocessors': [],
            'quiet': True,
            'no_warnings': True,
            'http_chunk_size': 10485760,
            'retries': self.var_retry.get(),
            'fragment_retries': 3,
            'concurrent_fragment_downloads': 4,
        }
        
        proxy = self.var_proxy.get().strip()
        if proxy:
            ydl_opts['proxy'] = proxy
        
        platform = task.get('platform', 'Unknown')
        
        speed_limit = self.var_speed.get()
        if speed_limit != "Unlimited":
            rate_map = {"500K": 500*1024, "1M": 1024*1024, "5M": 5*1024*1024, "10M": 10*1024*1024}
            ydl_opts['ratelimit'] = rate_map.get(speed_limit)
        
        if task['type'] == "Audio Only":
            ydl_opts['format'] = 'bestaudio/best'
            ydl_opts['postprocessors'].append({
                'key': 'FFmpegExtractAudio',
                'preferredcodec': task['audio_fmt'],
                'preferredquality': '320' if task['audio_fmt'] == 'mp3' else '192',
            })
        else:
            res_map = {'4K': '2160', '1440p': '1440', '1080p': '1080', '720p': '720', '480p': '480', '360p': '360'}
            res = res_map.get(task['quality'], '1080')
            ext_map = {'MP4': 'mp4', 'MKV': 'mkv', 'WEBM': 'webm', 'MOV': 'mov'}
            ext = ext_map.get(task['format'], 'mp4')
            
            if platform in ['Facebook', 'Instagram', 'TikTok']:
                ydl_opts['format'] = 'best'
                ydl_opts['merge_output_format'] = 'mp4'
            else:
                ydl_opts['format'] = f'bestvideo[height<={res}][ext={ext}]+bestaudio[ext=m4a]/best[height<={res}][ext={ext}]/best'
                ydl_opts['merge_output_format'] = ext
        
        if task['meta']:
            ydl_opts['postprocessors'].append({'key': 'FFmpegMetadata'})
        
        if task['thumb']:
            ydl_opts['writethumbnail'] = True
            if task['meta'] and task['type'] == 'Audio Only':
                ydl_opts['postprocessors'].append({'key': 'EmbedThumbnail'})
        
        if task['subs']:
            ydl_opts['writesubtitles'] = True
            ydl_opts['subtitleslangs'] = [task.get('sub_lang', 'en')]
        
        filepath = None
        try:
            with yt_dlp.YoutubeDL(ydl_opts) as ydl:
                ydl.download([task['url']])
            
            info = ydl.extract_info(task['url'], download=False)
            filepath = ydl.prepare_filename(info)
            if task['type'] == "Audio Only":
                base, ext = os.path.splitext(filepath)
                filepath = base + '.' + task['audio_fmt']
            
            file_size = os.path.getsize(filepath) if filepath and os.path.exists(filepath) else 0
            
            if self.var_notifications.get():
                notification.notify(title="Download Complete", message=task['title'], timeout=5)
            
            self.add_to_history(task['title'], task['url'], filepath, info.get('duration', 0), file_size)
            self.root.after(0, lambda: self._task_finished(task_id, "Completed"))
        except Exception as e:
            error_msg = str(e)
            if "CANCELLED" in error_msg:
                self.root.after(0, lambda: self._task_finished(task_id, "Cancelled"))
            else:
                logging.error(f"Task Error {task_id}: {error_msg}")
                if self.var_auto_retry.get() and task['retry_count'] < self.var_retry.get():
                    task['retry_count'] += 1
                    task['status'] = 'Queued'
                    self.root.after(0, lambda: self.task_widgets[task_id]['lbl_stats'].configure(text=f"Retrying ({task['retry_count']}/{self.var_retry.get()})...", foreground='orange'))
                    self.root.after(2000, self.process_manager_queue)
                else:
                    self.root.after(0, lambda: self._task_finished(task_id, "Error"))

    def _progress_hook(self, d, task, widget):
        if task.get('cancel_flag'):
            raise Exception("CANCELLED")
        
        if d['status'] == 'downloading':
            total = d.get('total_bytes') or d.get('total_bytes_estimate', 1)
            down = d.get('downloaded_bytes', 0)
            if total == 0:
                total = 1
            
            percent = down / total
            speed_mb = (d.get('speed', 0) or 0) / 1024 / 1024
            eta = d.get('eta', 0) or 0
            eta_str = f"{eta//60:02d}:{eta%60:02d}"
            
            self.root.after(0, widget['progress'].configure, {'value': percent * 100})
            self.root.after(0, widget['lbl_stats'].configure, {
                'text': f"Downloading | {percent*100:.1f}% | {speed_mb:.2f} MB/s | ETA: {eta_str}",
                'foreground': 'blue'
            })

    def _task_finished(self, task_id, status_code):
        if task_id in self.active_tasks:
            self.active_tasks.remove(task_id)
        
        if task_id not in self.task_widgets:
            return
        
        widget = self.task_widgets[task_id]
        self.tasks[task_id]['status'] = status_code
        
        if status_code == "Completed":
            widget['lbl_stats'].configure(text="Completed", foreground='green')
            widget['progress'].configure(value=100)
        elif status_code == "Cancelled":
            widget['lbl_stats'].configure(text="Cancelled", foreground='red')
        else:
            widget['lbl_stats'].configure(text="Error", foreground='red')
        
        widget['btn_cancel'].configure(state='disabled')
        self.update_overall_progress()
        self.root.after(500, self.process_manager_queue)

    def schedule_download(self):
        try:
            hour = int(self.var_schedule_hour.get())
            minute = int(self.var_schedule_min.get())
            
            now = datetime.datetime.now()
            scheduled = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
            
            if scheduled <= now:
                scheduled = scheduled + datetime.timedelta(days=1)
            
            delay = (scheduled - now).total_seconds()
            self.scheduled_time = scheduled
            
            self.lbl_schedule.configure(text=f"Scheduled for {scheduled.strftime('%H:%M')}")
            
            if self.schedule_timer:
                self.schedule_timer.cancel()
            
            self.schedule_timer = threading.Timer(delay, self.start_scheduled_downloads)
            self.schedule_timer.start()
            
        except ValueError:
            messagebox.showerror("Error", "Invalid time format. Use HH:MM")

    def cancel_schedule(self):
        if self.schedule_timer:
            self.schedule_timer.cancel()
            self.schedule_timer = None
        self.lbl_schedule.configure(text="")

    def start_scheduled_downloads(self):
        self.root.after(0, lambda: self.lbl_schedule.configure(text="Starting scheduled downloads...", foreground='green'))
        self.root.after(0, self.process_manager_queue)

    def start_clipboard_monitor(self):
        _executor.submit(self._clipboard_monitor_worker)
    
    def _clipboard_monitor_worker(self):
        while self.clipboard_monitor_active:
            try:
                text = pyperclip.paste().strip()
                if text and text != self.last_clipboard:
                    platform = self.detect_platform(text)
                    if platform != "Unknown":
                        self.last_clipboard = text
                        self.root.after(0, lambda: self.url_entry.delete(0, 'end'))
                        self.root.after(0, lambda: self.url_entry.insert(0, text))
                        self.root.after(0, lambda: self.lbl_status.configure(text=f"New {platform} link detected!", foreground='cyan'))
            except Exception:
                pass
            time.sleep(1.0)

    def setup_drag_drop(self):
        pass

    def handle_drop(self, event):
        pass

    def open_file_location(self, filepath):
        if filepath and os.path.exists(filepath):
            folder = os.path.dirname(filepath)
            os.startfile(folder)
        else:
            messagebox.showwarning("File Not Found", "The file may have been moved or deleted.")

    def check_ffmpeg(self):
        try:
            subprocess.run(["ffmpeg", "-version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        except FileNotFoundError:
            messagebox.showwarning("FFmpeg Missing", "FFmpeg is required for audio extraction. Please install it.")

    def open_folder(self):
        os.makedirs(self.var_path.get(), exist_ok=True)
        os.startfile(self.var_path.get())

    def on_closing(self):
        self.clipboard_monitor_active = False
        self.save_settings()
        if self.schedule_timer:
            self.schedule_timer.cancel()
        self.root.destroy()

def main():
    app = YouTubeDownloaderApp()

if __name__ == "__main__":
    main()
