#!/usr/bin/env python3
import os
import sys
import zipfile
import subprocess
import threading
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
from pathlib import Path
from queue import Queue
from concurrent.futures import ThreadPoolExecutor, as_completed

try:
    import ttkbootstrap as tb
    from ttkbootstrap.constants import *
    TTKBOOTSTRAP_AVAILABLE = True
except ImportError:
    TTKBOOTSTRAP_AVAILABLE = False

# ===== CONFIG =====
REMOTE_USER = "sourav"
REMOTE_HOST = "100.124.92.67"
REMOTE_DIR = "shared_files"
MAX_SIZE_GB = 2.5
TAILSCALE_NODE = "darling.tailf96096"  # 🔥 Full Tailscale Funnel subdomain (e.g., "darling.tailf96096")
# ==================

if sys.platform == "win32":
    DEFAULT_FONT = ("Segoe UI", 10)
    HEADER_FONT = ("Segoe UI Semibold", 11)
    TITLE_FONT = ("Segoe UI", 14, "bold")
else:
    DEFAULT_FONT = ("SF Pro Text", 10)
    HEADER_FONT = ("SF Pro Text", 11, "bold")
    TITLE_FONT = ("SF Pro Display", 14, "bold")

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

def is_scp_available():
    try:
        subprocess.run(["scp", "-V"], capture_output=True, timeout=5)
        return True
    except:
        return False

def get_file_icon(filename):
    ext = Path(filename).suffix.lower()
    icons = {
        ('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'): "🖼️",
        ('.zip', '.tar', '.gz', '.rar', '.7z'): "📦",
        ('.mp4', '.mov', '.avi', '.mkv'): "🎥",
        ('.mp3', '.wav', '.flac'): "🎵",
        ('.pdf',): "📕",
        ('.doc', '.docx', '.txt', '.md'): "📝",
        ('.xls', '.xlsx', '.csv'): "📊",
        ('.py', '.js', '.html', '.css'): "💻"
    }
    for exts, icon in icons.items():
        if ext in exts:
            return icon
    return "📄"

class FileManagerApp:
    def __init__(self, root):
        self.root = root
        root.title("Tailscale File Manager Pro")
        root.geometry("1000x750")
        root.minsize(900, 700)
        root.option_add("*Font", DEFAULT_FONT)
        
        self.colors = {
            'primary': '#0066FF', 'success': '#34C759',
            'danger': '#FF3B30', 'secondary': '#8E8E93',
            'bg_light': '#F8F9FA', 'bg_card': '#FFFFFF',
            'border': '#E0E0E0', 'text_secondary': '#6C757D'
        }
        
        if not TTKBOOTSTRAP_AVAILABLE:
            style = ttk.Style()
            style.theme_use("vista" if sys.platform == "win32" else "clam")
            style.configure("Treeview", rowheight=32, fieldbackground="#FFFFFF")
            style.configure("Treeview.Heading", font=HEADER_FONT, padding=8)
            style.map("Treeview", background=[("selected", "#E3F2FD")])
        
        self.progress_win = None
        self.current_path = REMOTE_DIR
        self.task_queue = Queue()
        self.task_lock = threading.Lock()
        self.task_counter = 0
        self.progress_items = {}
        self.checked_items = set()
        
        threading.Thread(target=self._queue_worker, daemon=True).start()
        
        # UI Setup
        main = ttk.Frame(root, padding=25)
        main.pack(fill="both", expand=True)
        
        # Header with better styling
        header = ttk.Frame(main)
        header.pack(fill="x", pady=(0, 20))
        
        title_frame = ttk.Frame(header)
        title_frame.pack(side="left", fill="x", expand=True)
        ttk.Label(title_frame, text="📁 File Manager Pro", font=TITLE_FONT,
                 foreground=self.colors['primary']).pack(side="left")
        ttk.Label(title_frame, text="  •  ", foreground=self.colors['text_secondary']).pack(side="left")
        self.path_label = ttk.Label(title_frame, text=f"📍 {REMOTE_DIR}", 
                                   foreground=self.colors['text_secondary'], font=DEFAULT_FONT)
        self.path_label.pack(side="left")
        
        # Actions with improved card design
        action = ttk.Frame(main)
        action.pack(fill="x", pady=(0, 20))
        
        upload_card = ttk.Frame(action, relief="solid", borderwidth=1, padding=15)
        upload_card.pack(side="left", fill="x", expand=True, padx=(0, 8))
        
        ButtonClass = tb.Button if TTKBOOTSTRAP_AVAILABLE else ttk.Button
        
        upload_header = ttk.Frame(upload_card)
        upload_header.pack(fill="x", pady=(0, 10))
        ttk.Label(upload_header, text="📤 Upload Files", font=HEADER_FONT).pack(side="left")
        
        btn_frame = ttk.Frame(upload_card)
        btn_frame.pack(fill="x")
        
        btn = ButtonClass(btn_frame, text="📤 Select Files", command=self.browse_files, width=14)
        if TTKBOOTSTRAP_AVAILABLE: btn.config(bootstyle="primary")
        btn.pack(side="left", padx=(0, 6))
        
        btn = ButtonClass(btn_frame, text="📁 Select Folder", command=self.browse_folders, width=14)
        if TTKBOOTSTRAP_AVAILABLE: btn.config(bootstyle="primary-outline")
        btn.pack(side="left", padx=(0, 10))
        
        self.make_public = tk.BooleanVar()
        check = (tb.Checkbutton if TTKBOOTSTRAP_AVAILABLE else ttk.Checkbutton)(
            btn_frame, text="🌐 Make Public", variable=self.make_public)
        check.pack(side="left", padx=(0, 0))
        
        control = ttk.Frame(action, relief="solid", borderwidth=1, padding=15)
        control.pack(side="right")
        
        ttk.Label(control, text="Quick Actions", font=HEADER_FONT).pack(pady=(0, 8))
        btn = ButtonClass(control, text="🔄 Refresh",
                         command=lambda: threading.Thread(target=self._refresh_list_async, daemon=True).start(),
                         width=14)
        if TTKBOOTSTRAP_AVAILABLE: btn.config(bootstyle="info-outline")
        btn.pack(pady=(0, 6))
        
        btn = ButtonClass(control, text="🔒 Kill Funnel",
                         command=self.kill_funnel_session,
                         width=14)
        if TTKBOOTSTRAP_AVAILABLE: btn.config(bootstyle="danger-outline")
        btn.pack()
        
        # File list with improved design
        list_container = ttk.Frame(main, relief="solid", borderwidth=1, padding=0)
        list_container.pack(fill="both", expand=True, pady=(0, 15))
        
        toolbar = ttk.Frame(list_container, padding=12)
        toolbar.pack(fill="x")
        ttk.Label(toolbar, text="📂 Files & Folders", font=HEADER_FONT).pack(side="left")
        self.selection_label = ttk.Label(toolbar, text="0 selected", 
                                         foreground=self.colors['text_secondary'])
        self.selection_label.pack(side="right", padx=10)
        
        # Main content area
        content_wrapper = ttk.Frame(list_container)
        content_wrapper.pack(fill="both", expand=True, padx=12, pady=(0, 12))
        
        content = ttk.Frame(content_wrapper)
        content.pack(fill="both", expand=True, side="left")
        
        tree_frame = ttk.Frame(content)
        tree_frame.pack(fill="both", expand=True)
        
        scroll = ttk.Scrollbar(tree_frame)
        scroll.pack(side="right", fill="y")
        
        self.file_tree = ttk.Treeview(tree_frame, columns=("Select", "Type", "Size", "Public"),
                                      show="tree headings", height=20, yscrollcommand=scroll.set)
        scroll.config(command=self.file_tree.yview)
        
        self.file_tree.heading("#0", text="Name", anchor="w")
        self.file_tree.heading("Select", text="☐", command=self.toggle_select_all, anchor="center")
        self.file_tree.heading("Type", text="Type", anchor="w")
        self.file_tree.heading("Size", text="Size", anchor="e")
        self.file_tree.heading("Public", text="Public", anchor="center")
        
        self.file_tree.column("#0", width=400, anchor="w", minwidth=200)
        self.file_tree.column("Select", width=70, anchor="center", minwidth=50)
        self.file_tree.column("Type", width=130, anchor="w", minwidth=100)
        self.file_tree.column("Size", width=110, anchor="e", minwidth=80)
        self.file_tree.column("Public", width=100, anchor="center", minwidth=80)
        
        self.file_tree.pack(fill="both", expand=True)
        self.file_tree.bind("<Double-Button-1>", self.on_double_click)
        self.file_tree.bind("<Button-1>", self.on_tree_click)
        self.file_tree.bind("<<TreeviewSelect>>", self.on_selection_change)
        
        # Action buttons panel with better styling
        btn_frame = ttk.Frame(content_wrapper, padding=12)
        btn_frame.pack(side="right", fill="y", padx=(12, 0))
        
        ttk.Label(btn_frame, text="⚡ Actions", font=HEADER_FONT).pack(pady=(0, 12))
        
        btn = ButtonClass(btn_frame, text="⬇️ Download", command=self.download_selected, width=16)
        if TTKBOOTSTRAP_AVAILABLE: btn.config(bootstyle="success")
        btn.pack(pady=5, fill="x")
        
        btn = ButtonClass(btn_frame, text="🗑️ Delete", command=self.delete_selected, width=16)
        if TTKBOOTSTRAP_AVAILABLE: btn.config(bootstyle="danger")
        btn.pack(pady=5, fill="x")
        
        btn = ButtonClass(btn_frame, text="🔗 Copy Link", command=self.copy_public_link, width=16)
        if TTKBOOTSTRAP_AVAILABLE: btn.config(bootstyle="info")
        btn.pack(pady=5, fill="x")
        
        ttk.Separator(btn_frame, orient="horizontal").pack(pady=12, fill="x")
        
        ttk.Label(btn_frame, text="Selection", font=("Segoe UI", 9), 
                 foreground=self.colors['text_secondary']).pack(pady=(0, 6))
        
        btn = ButtonClass(btn_frame, text="☑️ Select All", command=self.select_all_items, width=16)
        if TTKBOOTSTRAP_AVAILABLE: btn.config(bootstyle="secondary-outline")
        btn.pack(pady=3, fill="x")
        
        btn = ButtonClass(btn_frame, text="☐ Clear All", command=self.deselect_all_items, width=16)
        if TTKBOOTSTRAP_AVAILABLE: btn.config(bootstyle="secondary-outline")
        btn.pack(pady=3, fill="x")
        
        # Status bar with improved design
        status_frame = ttk.Frame(main, relief="solid", borderwidth=1, padding=10)
        status_frame.pack(fill="x")
        
        status_left = ttk.Frame(status_frame)
        status_left.pack(side="left", fill="x", expand=True)
        self.status = ttk.Label(status_left, text="✓ Ready", anchor="w", font=DEFAULT_FONT)
        self.status.pack(side="left")
        
        status_right = ttk.Frame(status_frame)
        status_right.pack(side="right")
        self.status_detail = ttk.Label(status_right, text="", anchor="e", 
                                       foreground=self.colors['text_secondary'], font=DEFAULT_FONT)
        self.status_detail.pack(side="right")
        
        if not is_scp_available():
            messagebox.showerror("Error", "scp not found!")
            root.destroy()
        else:
            self.status.config(text="⏳ Connecting to server...")
            threading.Thread(target=self._refresh_list_async, daemon=True).start()
    
    def on_selection_change(self, event=None):
        count = len(self.checked_items)
        if count == 0:
            self.selection_label.config(text="No selection", foreground=self.colors['text_secondary'])
        elif count == 1:
            self.selection_label.config(text=f"1 item selected", foreground=self.colors['primary'])
        else:
            self.selection_label.config(text=f"{count} items selected", foreground=self.colors['primary'])
    
    def on_tree_click(self, event):
        region = self.file_tree.identify("region", event.x, event.y)
        if region == "cell":
            column = self.file_tree.identify_column(event.x)
            item = self.file_tree.identify_row(event.y)
            if column == "#1" and item:  # Select column
                current = self.file_tree.item(item, "values")
                is_checked = current[0] == "☑️"
                new_check = "☐" if is_checked else "☑️"
                new_values = (new_check,) + current[1:]
                self.file_tree.item(item, values=new_values)
                
                name = self.file_tree.item(item, "text").split(" ", 1)[1] if " " in self.file_tree.item(item, "text") else self.file_tree.item(item, "text")
                if is_checked:
                    self.checked_items.discard(name)
                else:
                    self.checked_items.add(name)
                self.on_selection_change()
    
    def toggle_select_all(self):
        all_items = list(self.checked_items)
        total = len(self.file_tree.get_children())
        if len(all_items) == total and total > 0:
            self.deselect_all_items()
        else:
            self.select_all_items()
    
    def select_all_items(self):
        self.checked_items.clear()
        for item in self.file_tree.get_children():
            text = self.file_tree.item(item, "text")
            name = text.split(" ", 1)[1] if " " in text else text
            self.checked_items.add(name)
            current = self.file_tree.item(item, "values")
            self.file_tree.item(item, values=("☑️",) + current[1:])
        self.on_selection_change()
    
    def deselect_all_items(self):
        self.checked_items.clear()
        for item in self.file_tree.get_children():
            current = self.file_tree.item(item, "values")
            self.file_tree.item(item, values=("☐",) + current[1:])
        self.on_selection_change()
    
    def on_double_click(self, event):
        sel = self.file_tree.selection()
        if not sel:
            return

        item = sel[0]
        text = self.file_tree.item(item, "text")
        name = text.split(" ", 1)[1] if " " in text else text
        values = self.file_tree.item(item, "values")

        if name == "..":
            if self.current_path != REMOTE_DIR:
                parts = self.current_path.split('/')
                self.current_path = '/'.join(parts[:-1]) or REMOTE_DIR
                self.status.config(text="⏳ Loading directory...")
                threading.Thread(target=self._refresh_list_async, daemon=True).start()
        elif len(values) > 1 and values[1] == "Directory":
            new_path = f"{self.current_path}/{name}"
            self.current_path = new_path
            self.status.config(text="⏳ Loading directory...")
            threading.Thread(target=self._refresh_list_async, daemon=True).start()
        # Optional: auto-download on double-click file
        # else:
        #     self.download_selected()

    def _ensure_progress_window(self):
        if self.progress_win is None or not self.progress_win.winfo_exists():
            ToplevelClass = tb.Toplevel if TTKBOOTSTRAP_AVAILABLE else tk.Toplevel
            self.progress_win = ToplevelClass(self.root)
            self.progress_win.title("Active Transfers")
            self.progress_win.geometry("650x550")
            self.progress_win.transient(self.root)
            self.progress_win.resizable(True, True)
            
            # Header with better styling
            header = ttk.Frame(self.progress_win, padding=20)
            header.pack(fill="x")
            ttk.Label(header, text="🔄 Active Transfers", font=TITLE_FONT,
                     foreground=self.colors['primary']).pack(anchor="w")
            ttk.Label(header, text="Monitor your file transfers in real-time", 
                     foreground=self.colors['text_secondary'], font=DEFAULT_FONT).pack(anchor="w", pady=(5, 0))
            
            # Scrollable content area
            canvas_frame = ttk.Frame(self.progress_win)
            canvas_frame.pack(fill="both", expand=True, padx=20, pady=(0, 20))
            
            self.progress_canvas = tk.Canvas(canvas_frame, highlightthickness=0, bg="#FFFFFF")
            scroll = ttk.Scrollbar(canvas_frame, orient="vertical", command=self.progress_canvas.yview)
            self.progress_frame = ttk.Frame(self.progress_canvas)
            
            self.progress_frame.bind("<Configure>", 
                                    lambda e: self.progress_canvas.configure(scrollregion=self.progress_canvas.bbox("all")))
            
            self.progress_canvas.create_window((0, 0), window=self.progress_frame, anchor="nw")
            self.progress_canvas.configure(yscrollcommand=scroll.set)
            
            self.progress_canvas.pack(side="left", fill="both", expand=True)
            scroll.pack(side="right", fill="y")
        return self.progress_win

    def _add_progress_item(self, task_id, filename):
        def _add():
            self._ensure_progress_window()
            frame = ttk.Frame(self.progress_frame, relief="solid", borderwidth=1, padding=15)
            frame.pack(fill="x", padx=5, pady=8)
            
            LabelClass = ttk.Label if not TTKBOOTSTRAP_AVAILABLE else tb.Label
            ProgressClass = ttk.Progressbar if not TTKBOOTSTRAP_AVAILABLE else tb.Progressbar
            
            # Header with icon and filename
            header = ttk.Frame(frame)
            header.pack(fill="x", pady=(0, 10))
            
            icon = get_file_icon(filename)
            display_name = filename if len(filename) <= 55 else filename[:52] + "..."
            label = LabelClass(header, text=f"{icon} {display_name}", font=HEADER_FONT)
            label.pack(side="left")
            
            # Progress bar with better styling
            progress = ProgressClass(frame, mode='determinate', length=600)
            if TTKBOOTSTRAP_AVAILABLE: 
                progress.config(bootstyle="primary-striped")
            else:
                style = ttk.Style()
                style.configure("TProgressbar", thickness=8)
            progress.pack(fill="x", pady=(0, 8))
            
            # Status and percentage
            status_frame = ttk.Frame(frame)
            status_frame.pack(fill="x")
            
            status = LabelClass(status_frame, text="⏳ Queued...", anchor="w", 
                              foreground=self.colors['text_secondary'])
            status.pack(side="left")
            
            percent = LabelClass(status_frame, text="0%", anchor="e", 
                               font=("Segoe UI", 10, "bold"), foreground=self.colors['primary'])
            percent.pack(side="right")
            
            self.progress_items[task_id] = {
                'frame': frame, 'progress': progress,
                'status': status, 'percent': percent
            }
            
            if hasattr(self, 'progress_canvas'):
                self.progress_canvas.update_idletasks()
                self.progress_canvas.yview_moveto(1.0)
        
        if threading.current_thread() == threading.main_thread():
            _add()
        else:
            self.root.after(0, _add)
    
    def _update_progress_item(self, task_id, percentage=None, status=None):
        def _update():
            if task_id in self.progress_items:
                item = self.progress_items[task_id]
                if percentage is not None:
                    item['progress']['value'] = percentage
                    item['percent'].config(text=f"{int(percentage)}%")
                if status:
                    # Color code status messages
                    if "✅" in status or "Done" in status:
                        item['status'].config(text=status, foreground=self.colors['success'])
                    elif "❌" in status or "Error" in status or "Failed" in status:
                        item['status'].config(text=status, foreground=self.colors['danger'])
                    elif "Uploading" in status or "Downloading" in status:
                        item['status'].config(text=status, foreground=self.colors['primary'])
                    else:
                        item['status'].config(text=status, foreground=self.colors['text_secondary'])
        
        if threading.current_thread() == threading.main_thread():
            _update()
        else:
            self.root.after(0, _update)
    
    def _remove_progress_item(self, task_id):
        def _remove():
            if task_id in self.progress_items:
                self.progress_items[task_id]['frame'].destroy()
                del self.progress_items[task_id]
                if not self.progress_items and self.progress_win:
                    self.progress_win.destroy()
                    self.progress_win = None
        
        if threading.current_thread() == threading.main_thread():
            _remove()
        else:
            self.root.after(0, _remove)
    
    def _queue_worker(self):
        while True:
            task = self.task_queue.get()
            if task is None: break
            try:
                if task['type'] == 'upload':
                    self._process_upload_task(task)
                elif task['type'] == 'download':
                    self._process_download_task(task)
            except Exception as e:
                self._update_progress_item(task['id'], status=f"❌ {str(e)[:40]}")
            finally:
                self.task_queue.task_done()
    
    def _process_upload_task(self, task):
        task_id = task['id']
        file_path = task['file_path']
        make_public = task.get('make_public', False)
        
        try:
            src = Path(file_path)
            temp_file = src
            item_name = src.name
            
            self._update_progress_item(task_id, percentage=0, status="Preparing...")
            
            if src.is_dir():
                self._update_progress_item(task_id, percentage=5, status="📦 Compressing...")
                zip_path = src.parent / (src.name + ".zip")
                with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED, compresslevel=1) as zf:
                    files = list(src.rglob('*'))
                    total = len([f for f in files if f.is_file()])
                    for idx, fp in enumerate(files):
                        if fp.is_file():
                            zf.write(fp, arcname=fp.relative_to(src))
                            prog = 5 + int((idx / total) * 25) if total else 5
                            self._update_progress_item(task_id, percentage=prog, 
                                                      status=f"📦 {idx+1}/{total}")
                temp_file = zip_path
                item_name = zip_path.name
            elif src.is_file() and src.stat().st_size / (1024**3) > MAX_SIZE_GB:
                self._update_progress_item(task_id, percentage=5, status="📦 Compressing...")
                zip_path = src.parent / (src.stem + ".zip")
                with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED, compresslevel=1) as zf:
                    zf.write(src, arcname=src.name)
                temp_file = zip_path
                item_name = zip_path.name
            
            remote = f"{REMOTE_USER}@{REMOTE_HOST}:{self.current_path}/{item_name}"
            self._update_progress_item(task_id, percentage=30, status="📤 Uploading...")
            
            cmd = ["scp", str(temp_file), remote]
            proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            
            def sim():
                import time
                p = 30
                while proc.poll() is None and p < 90:
                    time.sleep(0.3)
                    p = min(p + 3, 90)
                    self._update_progress_item(task_id, percentage=p, status="📤 Uploading...")
            
            threading.Thread(target=sim, daemon=True).start()
            
            stdout, stderr = proc.communicate(timeout=600)
            if proc.returncode != 0:
                raise Exception(stderr.decode() if stderr else "Upload failed")
            
            self._update_progress_item(task_id, percentage=95, status="✓ Finalizing...")
            
            if make_public:
                subprocess.run([
                    "ssh", f"{REMOTE_USER}@{REMOTE_HOST}",
                    f"touch {self.current_path}/.public.{item_name}"
                ], check=True, timeout=10)
            
            if temp_file != src:
                temp_file.unlink()
            
            self._update_progress_item(task_id, percentage=100, status="✅ Done!")
            threading.Timer(2.5, lambda: self._remove_progress_item(task_id)).start()
            
        except Exception as e:
            self._update_progress_item(task_id, status=f"❌ {str(e)[:40]}")
            raise
    
    def _process_download_task(self, task):
        task_id = task['id']
        filename = task['filename']
        save_path = task['save_path']
        
        try:
            self._update_progress_item(task_id, percentage=0, status="Starting...")
            
            remote = f"{REMOTE_USER}@{REMOTE_HOST}:{self.current_path}/{filename}"
            self._update_progress_item(task_id, percentage=10, status="⬇️ Connecting...")
            
            cmd = ["scp", remote, save_path]
            proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            
            def sim():
                import time
                p = 10
                while proc.poll() is None and p < 90:
                    time.sleep(0.3)
                    p = min(p + 4, 90)
                    self._update_progress_item(task_id, percentage=p, status="⬇️ Downloading...")
            
            threading.Thread(target=sim, daemon=True).start()
            
            stdout, stderr = proc.communicate(timeout=600)
            if proc.returncode != 0:
                raise Exception(stderr.decode() if stderr else "Download failed")
            
            self._update_progress_item(task_id, percentage=100, status="✅ Done!")
            threading.Timer(2.5, lambda: self._remove_progress_item(task_id)).start()
            
        except Exception as e:
            self._update_progress_item(task_id, status=f"❌ {str(e)[:40]}")
            raise
    
    def browse_files(self):
        paths = filedialog.askopenfilenames(title="Select files")
        if paths:
            self.queue_uploads(paths, parallel=True)
    
    def browse_folders(self):
        path = filedialog.askdirectory(title="Select folder")
        if path:
            self.queue_uploads([path], parallel=False)
    
    def queue_uploads(self, paths, parallel=False):
        make_public = self.make_public.get()
        
        if parallel and len(paths) > 1:
            self._upload_parallel(paths, make_public)
        else:
            for fp in paths:
                with self.task_lock:
                    self.task_counter += 1
                    tid = self.task_counter
                
                task = {'type': 'upload', 'id': tid, 'file_path': fp, 'make_public': make_public}
                self._add_progress_item(tid, Path(fp).name)
                self.task_queue.put(task)
                
                if fp == paths[-1]:
                    threading.Thread(target=self._wait_and_refresh, args=(1,), daemon=True).start()
    
    def _upload_parallel(self, paths, make_public):
        def _do():
            with ThreadPoolExecutor(max_workers=min(5, len(paths))) as ex:
                futures = {}
                for fp in paths:
                    with self.task_lock:
                        self.task_counter += 1
                        tid = self.task_counter
                    
                    fn = Path(fp).name
                    self.root.after(0, lambda t=tid, f=fn: self._add_progress_item(t, f))
                    
                    task = {'type': 'upload', 'id': tid, 'file_path': fp, 'make_public': make_public}
                    future = ex.submit(self._process_upload_task, task)
                    futures[future] = tid
                
                for future in as_completed(futures):
                    try:
                        future.result()
                    except Exception as e:
                        self._update_progress_item(futures[future], status=f"❌ {str(e)[:40]}")
                
                threading.Thread(target=self._wait_and_refresh, args=(1,), daemon=True).start()
        
        threading.Thread(target=_do, daemon=True).start()
    
    def _wait_and_refresh(self, delay=1):
        import time
        time.sleep(delay)
        self.root.after(0, lambda: threading.Thread(target=self._refresh_list_async, daemon=True).start())
    
    def _refresh_list_async(self):
        """FIXED: Proper size fetching"""
        try:
            items_data = []
            
            if self.current_path != REMOTE_DIR:
                items_data.append(("..", True, "☐", "Directory", "", "", ("dir",)))
            
            safe_path = self.current_path.replace("'", "'\\''")
            
            # List directory contents
            result = subprocess.run(
                ["ssh", f"{REMOTE_USER}@{REMOTE_HOST}", f"cd '{safe_path}' && ls -1Ap 2>/dev/null"],
                capture_output=True, text=True, timeout=10
            )
            
            if result.returncode != 0:
                raise Exception("Failed to list directory")
            
            items = []
            for line in result.stdout.strip().split('\n'):
                if not line or line.startswith('.public.'):
                    continue
                name = line.rstrip('/').strip()
                is_dir = line.endswith('/')
                items.append((name, is_dir))
            
            items.sort(key=lambda x: (not x[1], x[0].lower()))
            
            # Get file info - FIXED VERSION
            file_info = {}
            file_names = [n for n, is_d in items if not is_d]
            
            if file_names:
                script_lines = []
                for name in file_names:
                    safe_name = name.replace("'", r"'\''")
                    script_lines.append(f"""
if [ -f '{safe_path}/{safe_name}' ]; then
    size=$(stat -f%z '{safe_path}/{safe_name}' 2>/dev/null || stat -c%s '{safe_path}/{safe_name}' 2>/dev/null || echo 0)
    pub=0
    [ -f '{safe_path}/.public.{safe_name}' ] && pub=1
    echo "{safe_name}|$size|$pub"
fi
""")
                
                script = "\n".join(script_lines)
                
                try:
                    res = subprocess.run(
                        ["ssh", f"{REMOTE_USER}@{REMOTE_HOST}", script],
                        capture_output=True, text=True, timeout=20
                    )
                    
                    if res.returncode == 0:
                        for line in res.stdout.strip().split('\n'):
                            if '|' in line:
                                parts = line.split('|', 2)
                                if len(parts) == 3:
                                    name, size_str, pub_str = parts
                                    try:
                                        size_bytes = int(size_str)
                                        file_info[name.strip()] = {
                                            'size': format_size(size_bytes),
                                            'public': pub_str.strip() == '1'
                                        }
                                    except ValueError:
                                        file_info[name.strip()] = {'size': '', 'public': False}
                except subprocess.TimeoutExpired:
                    pass
            
            # Build display data
            for name, is_dir in items:
                if is_dir:
                    display_name = "📁 " + name
                    items_data.append((name, is_dir, "☐", "Directory", "", "", ("dir",)))
                else:
                    ext = Path(name).suffix.lower()
                    item_type = ext[1:].upper() + " File" if ext else "File"
                    info = file_info.get(name, {'size': '', 'public': False})
                    size = info['size']
                    is_pub = info['public']
                    public_text = "Yes" if is_pub else "No"
                    tags = ("public",) if is_pub else ("private",)
                    display_name = get_file_icon(name) + " " + name
                    items_data.append((name, is_dir, "☐", item_type, size, public_text, tags))
            
            # Update UI
            def update_ui():
                for item in self.file_tree.get_children():
                    self.file_tree.delete(item)
                self.checked_items.clear()
                
                for name, is_dir, check, itype, size, pub, tags in items_data:
                    display = ("📁 " + name) if is_dir else (get_file_icon(name) + " " + name)
                    self.file_tree.insert("", "end", values=(check, itype, size, pub), text=display, tags=tags)
                
                # Configure tree tags with better colors
                if TTKBOOTSTRAP_AVAILABLE:
                    self.file_tree.tag_configure("public", foreground="#28a745")
                    self.file_tree.tag_configure("dir", foreground="#007bff")
                else:
                    style = ttk.Style()
                    style.map("Treeview", foreground=[("selected", "#000000")])
                    # Try to set tag colors (may not work on all systems)
                    try:
                        self.file_tree.tag_configure("public", foreground="#28a745")
                        self.file_tree.tag_configure("dir", foreground="#007bff")
                    except:
                        pass
                
                item_count = len([x for x in items_data if x[0] != '..'])
                path_display = self.current_path.split('/')[-1] if self.current_path != REMOTE_DIR else REMOTE_DIR
                self.status.config(text=f"✓ Ready • {item_count} item{'s' if item_count != 1 else ''} loaded")
                self.status_detail.config(text=f"📂 {path_display}")
                self.path_label.config(text=f"📍 {path_display}")
            
            self.root.after(0, update_ui)
            
        except Exception as e:
            def show_error():
                for item in self.file_tree.get_children():
                    self.file_tree.delete(item)
                self.file_tree.insert("", "end", values=("☐", "Error", "", ""), text=f"⚠️ {e}")
                self.status.config(text="❌ Refresh failed")
            self.root.after(0, show_error)

    def get_selected_filenames(self):
        selected = []
        for name in self.checked_items:
            selected.append(name)
        return selected

    def download_selected(self):
        filenames = self.get_selected_filenames()
        if not filenames:
            messagebox.showwarning("No Selection", "Please select file(s) to download.")
            return

        if len(filenames) == 1:
            save_path = filedialog.asksaveasfilename(initialfile=filenames[0])
            if not save_path:
                return
            paths = [save_path]
        else:
            folder = filedialog.askdirectory(title="Save downloaded files to...")
            if not folder:
                return
            paths = [os.path.join(folder, fn) for fn in filenames]

        for i, filename in enumerate(filenames):
            with self.task_lock:
                self.task_counter += 1
                tid = self.task_counter
            task = {'type': 'download', 'id': tid, 'filename': filename, 'save_path': paths[i]}
            self._add_progress_item(tid, filename)
            self.task_queue.put(task)

    def delete_selected(self):
        filenames = self.get_selected_filenames()
        if not filenames:
            messagebox.showwarning("No Selection", "Please select file(s) to delete.")
            return

        if not messagebox.askyesno("Confirm Delete", f"Delete {len(filenames)} item(s) from server?\nThis cannot be undone."):
            return

        try:
            for name in filenames:
                # Delete file and public marker
                file_path = f"{self.current_path}/{name}"
                marker_path = f"{self.current_path}/.public.{name}"
                # Use proper quoting for special chars
                subprocess.run([
                    "ssh", f"{REMOTE_USER}@{REMOTE_HOST}",
                    f"rm -f {{'{file_path}','{marker_path}'}}"
                ], shell=True, check=True)
            messagebox.showinfo("Success", f"Deleted {len(filenames)} item(s).")
            self.deselect_all_items()
            threading.Thread(target=self._refresh_list_async, daemon=True).start()
        except Exception as e:
            messagebox.showerror("Delete Failed", str(e))

    def copy_public_link(self):
        filenames = self.get_selected_filenames()
        if not filenames:
            messagebox.showwarning("No Selection", "Please select a file.")
            return
        if len(filenames) > 1:
            messagebox.showwarning("Too Many", "Select only one file for sharing.")
            return

        filename = filenames[0]
        # No need to check .public.* marker anymore — funnel is public by design
        
        # Construct the path relative to REMOTE_DIR (server document root)
        # Server at 8080 serves from REMOTE_DIR, so use relative paths
        if self.current_path == REMOTE_DIR:
            file_path = filename
        else:
            # Get relative path from REMOTE_DIR
            if self.current_path.startswith(REMOTE_DIR + "/"):
                rel_path = self.current_path[len(REMOTE_DIR) + 1:]
            else:
                rel_path = self.current_path
            file_path = f"{rel_path}/{filename}" if rel_path else filename
        
        # Generate Tailscale Funnel link
        link = f"https://{TAILSCALE_NODE}.ts.net/{file_path}"
        
        self.root.clipboard_clear()
        self.root.clipboard_append(link)
        messagebox.showinfo("Public Link Ready", 
                           f"🔗 Copied public download link:\n{link}\n\n"
                           f"✅ Works anywhere on the internet!\n"
                           f"🌐 Powered by Tailscale Funnel")
    
    def kill_funnel_session(self):
        """Kill switch to terminate any ongoing Tailscale Funnel sessions"""
        if not messagebox.askyesno("Confirm Kill Switch", 
                                   "⚠️ This will terminate all active Tailscale Funnel sessions.\n\n"
                                   "Are you sure you want to proceed?"):
            return
        
        try:
            # Run tailscale funnel --reset on the remote server
            result = subprocess.run(
                ["ssh", f"{REMOTE_USER}@{REMOTE_HOST}", "tailscale funnel --reset"],
                capture_output=True,
                text=True,
                timeout=10
            )
            
            if result.returncode == 0:
                messagebox.showinfo("Success", 
                                   "✅ All Tailscale Funnel sessions have been terminated.\n\n"
                                   "🔒 Public links are no longer accessible.")
                self.status.config(text="✓ Funnel sessions terminated")
            else:
                error_msg = result.stderr.strip() if result.stderr else "Unknown error"
                messagebox.showerror("Kill Switch Failed", 
                                    f"Failed to terminate funnel sessions:\n{error_msg}")
        except subprocess.TimeoutExpired:
            messagebox.showerror("Timeout", "Operation timed out. Please try again.")
        except FileNotFoundError:
            messagebox.showerror("Error", "SSH not found. Please ensure SSH is installed and accessible.")
        except Exception as e:
            messagebox.showerror("Error", f"An error occurred:\n{str(e)}")

# === MAIN ===
if __name__ == "__main__":
    if TTKBOOTSTRAP_AVAILABLE:
        root = tb.Window(themename="cosmo")
    else:
        root = tk.Tk()
    app = FileManagerApp(root)
    root.mainloop()
