import os
import time
import json
import re
import threading
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin, unquote

# --- KIVY IMPORTS ---
from kivy.app import App
from kivy.lang import Builder
from kivy.clock import Clock, mainthread
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.popup import Popup
from kivy.uix.label import Label
from kivy.core.clipboard import Clipboard
from kivy.properties import StringProperty, BooleanProperty, NumericProperty
from kivy.uix.screenmanager import ScreenManager, Screen, NoTransition

# --- CONFIGURATION ---
CONFIG_FILE = "dm_v4_settings.json"
MAX_CONCURRENT_DOWNLOADS = 3
MAX_RETRIES = 3

def get_default_download_folder():
    if hasattr(os, 'uname') and 'android' in os.uname().release.lower():
        path = "/storage/emulated/0/Download"
        if not os.access(path, os.W_OK): path = os.getcwd()
        return path
    return os.path.join(os.path.expanduser("~"), "Downloads")

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

def parse_raw_size(size_str):
    if not size_str or size_str in ["-", "?"]: return 0
    size_str = size_str.upper().strip().replace(',', '')
    match = re.search(r'([\d\.]+)\s*([KMGTP]?)', size_str)
    if not match: return 0
    val = float(match.group(1))
    unit = match.group(2)
    
    if unit == 'K': val *= 1024
    elif unit == 'M': val *= 1024**2
    elif unit == 'G': val *= 1024**3
    elif unit == 'T': val *= 1024**4
    return int(val)

def format_time(seconds):
    if seconds < 0 or seconds == float('inf'): return "--:--"
    m, s = divmod(int(seconds), 60)
    h, m = divmod(m, 60)
    if h > 0: return f"{h}h {m}m"
    return f"{m}m {s}s"

def get_file_type(filename, is_dir):
    if is_dir: return "[DIR]"
    ext = os.path.splitext(filename)[1].lower()
    if ext in ['.mp4', '.mkv', '.avi', '.webm', '.flv']: return "[VID]"
    if ext in ['.jpg', '.png', '.jpeg', '.gif']: return "[IMG]"
    if ext in ['.zip', '.rar', '.apk', '.7z', '.iso']: return "[ZIP]"
    if ext in ['.mp3', '.wav', '.flac', '.m4a']: return "[AUD]"
    if ext in ['.pdf', '.txt', '.doc', '.docx']: return "[DOC]"
    return "[FILE]"

# --- KIVY UI LAYOUT ---
KV = '''
<BrowserRow>:
    orientation: 'horizontal'
    size_hint_y: None
    height: '55dp'
    padding: '2dp'
    spacing: '2dp'
    canvas.before:
        Color:
            rgba: (0, 0.35, 0.7, 0.4) if self.selected else (0.12, 0.12, 0.12, 1)
        Rectangle:
            pos: self.pos
            size: self.size
    
    Label:
        text: root.icon
        size_hint_x: 0.1
        font_size: '9sp'
        color: (1, 0.75, 0, 1) if root.icon == '[DIR]' else (0.7, 0.7, 0.7, 1)
    
    Label:
        text: root.name_text
        size_hint_x: 0.48
        font_size: '10sp'
        text_size: self.width, self.height
        halign: 'left'
        valign: 'middle'
        shorten: False 
    
    Label:
        text: root.size_text
        size_hint_x: 0.15
        font_size: '9sp'
        color: (0.6, 0.6, 0.6, 1)
        text_size: self.size
        halign: 'right'
        valign: 'middle'
        
    Button:
        text: '[x]' if root.selected else '[ ]'
        size_hint_x: 0.12
        font_size: '9sp'
        background_color: (0.6, 0.2, 0.2, 1) if root.selected else (0.3, 0.3, 0.3, 1)
        on_release: root.selected = not root.selected
        
    Button:
        text: 'OPEN'
        size_hint_x: 0.15
        font_size: '9sp'
        disabled: not root.is_dir
        opacity: 1 if root.is_dir else 0.2
        background_color: (0, 0.5, 0.2, 1)
        on_release: app.load_url(root.url)

<QueueRow>:
    orientation: 'vertical'
    size_hint_y: None
    height: '60dp'
    padding: '2dp'
    spacing: '2dp'
    canvas.before:
        Color:
            rgba: (0.15, 0.15, 0.15, 1)
        Rectangle:
            pos: self.pos
            size: self.size
            
    BoxLayout:
        size_hint_y: 0.4
        Label:
            text: root.name_text
            font_size: '10sp'
            text_size: self.width, self.height
            halign: 'left'
            valign: 'middle'
            shorten: False 
        Label:
            text: root.size_text
            size_hint_x: 0.3
            font_size: '8sp'
            color: (0.7, 0.7, 0.7, 1)
            text_size: self.size
            halign: 'right'
            valign: 'middle'
            
    ProgressBar:
        size_hint_y: 0.15
        max: 100
        value: root.progress_val
        
    BoxLayout:
        size_hint_y: 0.35
        Label:
            text: root.speed_eta_text
            size_hint_x: 0.5
            font_size: '8sp'
            color: (0.4, 0.8, 1, 1)
            text_size: self.size
            halign: 'left'
            valign: 'middle'
        Label:
            text: root.status_text
            size_hint_x: 0.3
            font_size: '9sp'
            color: (0, 1, 0, 1) if root.status_text == "Done" else (1, 1, 1, 1)
        Button:
            text: root.btn_text
            size_hint_x: 0.2
            font_size: '9sp'
            background_color: (0.8, 0.1, 0.1, 1) if root.btn_text == 'STOP' else (0.2, 0.6, 0.2, 1)
            on_release: app.toggle_task(root.task_id)

BoxLayout:
    orientation: 'vertical'

    # --- SCREEN MANAGER (TABS) ---
    ScreenManager:
        id: sm
        transition: __import__('kivy.uix.screenmanager').uix.screenmanager.NoTransition()

        # ====== TAB 1: BROWSER ======
        Screen:
            name: 'browser_tab'
            BoxLayout:
                orientation: 'vertical'
                padding: '2dp'
                spacing: '2dp'

                # --- TOP SETTINGS ---
                GridLayout:
                    cols: 1
                    size_hint_y: None
                    height: '90dp'
                    spacing: '2dp'

                    BoxLayout: 
                        Label:
                            text: 'URL:'
                            size_hint_x: 0.15
                            font_size: '10sp'
                        TextInput:
                            id: inp_url
                            size_hint_x: 0.85
                            font_size: '10sp'
                            multiline: False
                            
                    BoxLayout: 
                        Label:
                            text: 'Proxy:'
                            size_hint_x: 0.15
                            font_size: '10sp'
                        TextInput:
                            id: inp_proxy
                            size_hint_x: 0.65
                            font_size: '10sp'
                            multiline: False
                        ToggleButton:
                            id: tgl_proxy
                            text: 'ON' if self.state == 'down' else 'OFF'
                            size_hint_x: 0.2
                            font_size: '10sp'
                            background_color: (0, 0.6, 0.2, 1) if self.state == 'down' else (0.4, 0.4, 0.4, 1)
                            
                    BoxLayout: 
                        Label:
                            text: 'Save To:'
                            size_hint_x: 0.15
                            font_size: '10sp'
                        TextInput:
                            id: inp_save_path
                            size_hint_x: 0.85
                            font_size: '10sp'
                            multiline: False

                BoxLayout:
                    size_hint_y: None
                    height: '30dp'
                    spacing: '2dp'
                    Button:
                        text: 'CONNECT'
                        font_size: '10sp'
                        size_hint_x: 0.5
                        background_color: (0.1, 0.5, 0.8, 1)
                        on_release: app.connect()
                    Button:
                        text: 'COPY URL'
                        font_size: '10sp'
                        size_hint_x: 0.5
                        background_color: (0.3, 0.3, 0.3, 1)
                        on_release: app.copy_current_url()

                # --- BROWSER NAVIGATION & TOOLS ---
                BoxLayout:
                    size_hint_y: None
                    height: '30dp'
                    spacing: '2dp'
                    Button:
                        text: '< Back'
                        font_size: '9sp'
                        size_hint_x: 0.15
                        on_release: app.go_back()
                    Button:
                        text: '^ Up'
                        font_size: '9sp'
                        size_hint_x: 0.12
                        on_release: app.go_up()
                    Button:
                        text: 'SORT'
                        font_size: '9sp'
                        size_hint_x: 0.15
                        background_color: (0.4, 0.4, 0.4, 1)
                        on_release: app.cycle_sort()
                    Button:
                        text: 'SELECT ALL'
                        font_size: '9sp'
                        size_hint_x: 0.2
                        background_color: (0.4, 0.4, 0.4, 1)
                        on_release: app.toggle_select_all()

                # --- ENHANCED SEARCH BAR ---
                BoxLayout:
                    size_hint_y: None
                    height: '32dp'
                    spacing: '2dp'
                    TextInput:
                        id: inp_search
                        hint_text: 'Search term...'
                        font_size: '10sp'
                        size_hint_x: 0.4
                        multiline: False
                        on_text: app.filter_local_browser(self.text)
                    Spinner:
                        id: spn_category
                        text: 'All Categories'
                        values: ['All Categories', 'Movies', 'Series/TV', 'Games', 'Software']
                        font_size: '9sp'
                        size_hint_x: 0.3
                    Button:
                        id: btn_deep_search
                        text: 'DEEP CRAWL'
                        font_size: '9sp'
                        bold: True
                        size_hint_x: 0.3
                        background_color: (0.6, 0.2, 0.8, 1)
                        on_release: app.toggle_deep_search()

                # --- BROWSER LIST ---
                ScrollView:
                    canvas.before:
                        Color:
                            rgba: (0.05, 0.05, 0.05, 1)
                        Rectangle:
                            pos: self.pos
                            size: self.size
                    GridLayout:
                        id: browser_list
                        cols: 1
                        size_hint_y: None
                        height: self.minimum_height
                        spacing: '1dp'

                Button:
                    text: 'QUEUE SELECTED FILES/FOLDERS'
                    size_hint_y: None
                    height: '40dp'
                    background_color: (0, 0.6, 0.3, 1)
                    font_size: '11sp'
                    bold: True
                    on_release: app.download_selected()

        # ====== TAB 2: DOWNLOADS ======
        Screen:
            name: 'downloads_tab'
            BoxLayout:
                orientation: 'vertical'
                padding: '2dp'
                spacing: '2dp'

                # --- DOWNLOAD QUEUE CONTROLS ---
                BoxLayout:
                    size_hint_y: None
                    height: '40dp'
                    spacing: '2dp'
                    canvas.before:
                        Color:
                            rgba: (0.1, 0.1, 0.1, 1)
                        Rectangle:
                            pos: self.pos
                            size: self.size
                    Button:
                        text: 'PAUSE ALL'
                        font_size: '9sp'
                        background_color: (0.6, 0.4, 0, 1)
                        on_release: app.pause_all()
                    Button:
                        text: 'RESUME ALL'
                        font_size: '9sp'
                        background_color: (0, 0.5, 0.2, 1)
                        on_release: app.resume_all()
                    Button:
                        text: 'CLEAR DONE'
                        font_size: '9sp'
                        background_color: (0.4, 0.4, 0.4, 1)
                        on_release: app.clear_done_tasks()
                    Button:
                        text: 'CLEAR ALL'
                        font_size: '9sp'
                        background_color: (0.8, 0.2, 0.2, 1)
                        on_release: app.clear_all_tasks()

                # --- DOWNLOAD LIST ---
                ScrollView:
                    canvas.before:
                        Color:
                            rgba: (0.05, 0.05, 0.05, 1)
                        Rectangle:
                            pos: self.pos
                            size: self.size
                    GridLayout:
                        id: queue_list
                        cols: 1
                        size_hint_y: None
                        height: self.minimum_height
                        spacing: '2dp'

    # --- GLOBAL STATUS BAR ---
    Label:
        id: lbl_status
        size_hint_y: None
        height: '25dp'
        text: 'Ready'
        font_size: '10sp'
        color: (0.3, 1, 0.5, 1)
        canvas.before:
            Color:
                rgba: (0.12, 0.12, 0.12, 1)
            Rectangle:
                pos: self.pos
                size: self.size

    # --- BOTTOM NAVIGATION BAR ---
    BoxLayout:
        size_hint_y: None
        height: '50dp'
        spacing: '1dp'
        canvas.before:
            Color:
                rgba: (0.05, 0.05, 0.05, 1)
            Rectangle:
                pos: self.pos
                size: self.size
                
        ToggleButton:
            text: 'BROWSER'
            font_size: '12sp'
            bold: True
            group: 'nav_tabs'
            state: 'down'
            allow_no_selection: False
            background_color: (0.1, 0.4, 0.7, 1) if self.state == 'down' else (0.2, 0.2, 0.2, 1)
            on_release: sm.current = 'browser_tab'
            
        ToggleButton:
            text: 'DOWNLOADS'
            font_size: '12sp'
            bold: True
            group: 'nav_tabs'
            allow_no_selection: False
            background_color: (0.1, 0.4, 0.7, 1) if self.state == 'down' else (0.2, 0.2, 0.2, 1)
            on_release: sm.current = 'downloads_tab'
'''

# --- CUSTOM WIDGETS ---
class BrowserRow(BoxLayout):
    icon = StringProperty('')
    name_text = StringProperty('')
    size_text = StringProperty('')
    url = StringProperty('')
    is_dir = BooleanProperty(False)
    selected = BooleanProperty(False)

class QueueRow(BoxLayout):
    task_id = StringProperty('')
    name_text = StringProperty('')
    size_text = StringProperty('-- / --')
    progress_val = NumericProperty(0)
    speed_eta_text = StringProperty('-- KB/s | ETA: --:--')
    status_text = StringProperty('Queued')
    btn_text = StringProperty('STOP')

# --- MAIN APP LOGIC ---
class DownloadManagerApp(App):
    def build(self):
        self.browser_cache = []
        self.history = []
        self.sort_mode = 0
        
        self.deep_searching = False
        self.deep_search_results = []
        
        self.tasks = {}
        self.queue_widgets = {}
        self.active_downloads = 0
        
        self.root = Builder.load_string(KV)
        self.load_settings()
        
        Clock.schedule_interval(self.process_download_queue, 1.0)
        return self.root

    def load_settings(self):
        default = {
            "url": "http://172.16.50.4", 
            "proxy": "socks5://test:test@103.166.253.92:1088",
            "proxy_on": True,
            "save_path": get_default_download_folder()
        }
        if os.path.exists(CONFIG_FILE):
            try:
                with open(CONFIG_FILE, 'r') as f:
                    settings = {**default, **json.load(f)}
            except: settings = default
        else: settings = default
            
        self.root.ids.inp_url.text = settings["url"]
        self.root.ids.inp_proxy.text = settings["proxy"]
        self.root.ids.tgl_proxy.state = 'down' if settings.get("proxy_on", True) else 'normal'
        self.root.ids.inp_save_path.text = settings["save_path"]

    def save_settings(self):
        try:
            with open(CONFIG_FILE, 'w') as f:
                json.dump({
                    "url": self.root.ids.inp_url.text, 
                    "proxy": self.root.ids.inp_proxy.text,
                    "proxy_on": self.root.ids.tgl_proxy.state == 'down',
                    "save_path": self.root.ids.inp_save_path.text
                }, f)
        except: pass

    def get_proxies(self):
        if self.root.ids.tgl_proxy.state == 'normal': return None
        p = self.root.ids.inp_proxy.text.strip()
        if not p: return None
        return {"http": p, "https": p}

    def show_error(self, message):
        popup = Popup(title='Notice', content=Label(text=message, font_size='11sp', text_size=(250, None)), size_hint=(0.8, 0.3))
        popup.open()

    @mainthread
    def set_status(self, msg):
        self.root.ids.lbl_status.text = msg

    def copy_current_url(self):
        Clipboard.copy(self.root.ids.inp_url.text)
        self.set_status("URL Copied to Clipboard!")

    # --- BROWSER & NAVIGATION ---
    def connect(self):
        self.deep_searching = False
        url = self.root.ids.inp_url.text.strip()
        if not url.startswith("http"): url = "http://" + url
        self.root.ids.inp_url.text = url
        self.save_settings()
        self.load_url(url)

    def load_url(self, url):
        self.deep_searching = False
        self.root.ids.btn_deep_search.text = "DEEP CRAWL"
        threading.Thread(target=self._fetch_thread, args=(url,), daemon=True).start()

    def _fetch_thread(self, url):
        self.set_status("Loading Directory...")
        try:
            r = requests.get(url, proxies=self.get_proxies(), timeout=15)
            r.raise_for_status()
            soup = BeautifulSoup(r.text, "html.parser")
            size_pattern = re.compile(r'(\d+(?:\.\d+)?\s*[KMGTP]?B)', re.IGNORECASE)
            items = []
            
            for a in soup.find_all("a"):
                href = a.get("href")
                txt = a.text.strip()
                if not href or txt in ["Parent Directory", "../", "Name", "Size", "Description"]: continue
                
                full_url = urljoin(url, href)
                is_dir = href.endswith("/")
                raw_size_str = ""
                
                if not is_dir:
                    if a.next_sibling:
                        match = size_pattern.search(str(a.next_sibling))
                        if match: raw_size_str = match.group(1)
                    if not raw_size_str and getattr(a.parent, 'name', '') == 'td':
                        for cell in a.parent.parent.find_all('td'):
                            match = size_pattern.search(cell.text)
                            if match and cell.text != txt:
                                raw_size_str = match.group(1)
                                break
                
                parsed_bytes = parse_raw_size(raw_size_str)
                final_size = format_size(parsed_bytes) if parsed_bytes > 0 else "-"
                                
                items.append({
                    "name": txt, "url": full_url, "is_dir": is_dir, 
                    "icon": get_file_type(txt, is_dir), "size": final_size
                })
            self.update_browser_ui(url, items)
        except Exception as e:
            self.set_status("Connection Failed")
            Clock.schedule_once(lambda dt: self.show_error(str(e)), 0)

    @mainthread
    def update_browser_ui(self, url, items):
        self.browser_cache = items
        self.root.ids.inp_url.text = url
        if not self.history or self.history[-1] != url:
            self.history.append(url)
        self.refresh_ui_list()
        self.set_status(f"Loaded {len(items)} items")

    def cycle_sort(self):
        self.sort_mode = (self.sort_mode + 1) % 3
        modes = ["Folders First", "By Type", "A-Z"]
        self.set_status(f"Sorted: {modes[self.sort_mode]}")
        self.refresh_ui_list()

    def refresh_ui_list(self):
        data = self.deep_search_results if self.deep_searching else self.browser_cache
        if self.sort_mode == 0:
            data.sort(key=lambda x: (not x["is_dir"], x["name"].lower()))
        elif self.sort_mode == 1:
            data.sort(key=lambda x: (x["icon"], x["name"].lower()))
        else:
            data.sort(key=lambda x: x["name"].lower())
        self.filter_local_browser(self.root.ids.inp_search.text, data_source=data)

    def filter_local_browser(self, query, data_source=None):
        if data_source is None:
            data_source = self.deep_search_results if self.deep_searching else self.browser_cache
        query = query.lower()
        container = self.root.ids.browser_list
        container.clear_widgets()
        
        for item in data_source:
            if query in item["name"].lower() or query in item["url"].lower():
                row = BrowserRow(
                    icon=item["icon"], name_text=item["name"], size_text=item["size"],
                    url=item["url"], is_dir=item["is_dir"], selected=False
                )
                container.add_widget(row)

    def toggle_select_all(self):
        container = self.root.ids.browser_list
        if not container.children: return
        all_selected = all(c.selected for c in container.children)
        for child in container.children:
            child.selected = not all_selected

    def go_back(self):
        self.deep_searching = False
        self.root.ids.btn_deep_search.text = "DEEP CRAWL"
        if len(self.history) > 1:
            self.history.pop()
            self.load_url(self.history.pop())

    def go_up(self):
        self.deep_searching = False
        self.root.ids.btn_deep_search.text = "DEEP CRAWL"
        curr = self.root.ids.inp_url.text
        if curr.endswith("/"): curr = curr[:-1]
        self.load_url(os.path.dirname(curr) + "/")

    # --- DEEP SEARCH ENGINE (CRAWLER with Filters) ---
    def toggle_deep_search(self):
        if self.deep_searching:
            self.deep_searching = False
            self.root.ids.btn_deep_search.text = "DEEP CRAWL"
            self.set_status("Deep Search Cancelled.")
            self.refresh_ui_list()
        else:
            query = self.root.ids.inp_search.text.strip().lower()
            category = self.root.ids.spn_category.text
            
            self.deep_searching = True
            self.deep_search_results = []
            self.root.ids.browser_list.clear_widgets()
            self.root.ids.btn_deep_search.text = "STOP CRAWL"
            self.set_status(f"Crawling for '{query}' in {category}...")
            
            threading.Thread(target=self._deep_search_worker, args=(self.root.ids.inp_url.text, query, category), daemon=True).start()

    def _deep_search_worker(self, base_url, query, category):
        visited = set()
        queue = [base_url]
        found = 0
        
        movie_kw = ['movie', 'film', 'cinema', '1080p', '720p', 'bluray']
        series_kw = ['series', 'tv', 'season', 'episode', 's0', 's1', 'e0', 'e1']
        game_kw = ['game', 'pc', 'repack', 'crack']
        soft_kw = ['soft', 'app', 'windows', 'mac', 'crack']

        while queue and self.deep_searching:
            current_url = queue.pop(0)
            if current_url in visited: continue
            visited.add(current_url)
            
            Clock.schedule_once(lambda dt, cu=current_url: self.set_status(f"Crawling: {unquote(os.path.basename(cu.strip('/')))}..."), 0)
            
            try:
                r = requests.get(current_url, proxies=self.get_proxies(), timeout=8)
                soup = BeautifulSoup(r.text, 'html.parser')
                size_pattern = re.compile(r'(\d+(?:\.\d+)?\s*[KMGTP]?B)', re.IGNORECASE)

                for a in soup.find_all('a'):
                    if not self.deep_searching: break
                    
                    href = a.get('href')
                    txt = unquote(a.text.strip())
                    if not href or txt in ["Parent Directory", "../", "Name", "Size", "Description"]: continue
                    
                    full_url = urljoin(current_url, href)
                    is_dir = href.endswith("/")
                    
                    if is_dir:
                        queue.append(full_url)
                    else:
                        url_lower = full_url.lower()
                        if category == 'Movies' and not any(k in url_lower for k in movie_kw): continue
                        if category == 'Series/TV' and not any(k in url_lower for k in series_kw): continue
                        if category == 'Games' and not any(k in url_lower for k in game_kw): continue
                        if category == 'Software' and not any(k in url_lower for k in soft_kw): continue
                        
                        if query in txt.lower() or query in url_lower:
                            found += 1
                            raw_size_str = ""
                            if a.next_sibling:
                                match = size_pattern.search(str(a.next_sibling))
                                if match: raw_size_str = match.group(1)
                            parsed_bytes = parse_raw_size(raw_size_str)
                            final_size = format_size(parsed_bytes) if parsed_bytes > 0 else "?"
                            
                            item = {
                                "name": txt, "url": full_url, "is_dir": False, 
                                "icon": get_file_type(txt, False), "size": final_size
                            }
                            self.deep_search_results.append(item)
                            Clock.schedule_once(lambda dt: self.refresh_ui_list(), 0)
                            
            except Exception: pass
                
        if self.deep_searching:
            self.deep_searching = False
            Clock.schedule_once(lambda dt: self._finish_deep_search(found), 0)

    @mainthread
    def _finish_deep_search(self, found):
        self.root.ids.btn_deep_search.text = "DEEP CRAWL"
        self.set_status(f"Search Complete. Found {found} matching files.")

    # --- DOWNLOAD MANAGER LOGIC ---
    def download_selected(self):
        container = self.root.ids.browser_list
        files_q, folders_q = 0, 0
        
        base_save_dir = self.root.ids.inp_save_path.text.strip()
        if not base_save_dir: base_save_dir = get_default_download_folder()
        self.save_settings() 
        
        for child in container.children:
            if getattr(child, 'selected', False):
                if child.is_dir:
                    threading.Thread(target=self._scrape_folder_thread, args=(child.url, child.name_text, base_save_dir), daemon=True).start()
                    folders_q += 1
                else:
                    rel_path = ""
                    if self.deep_searching:
                        base = self.root.ids.inp_url.text
                        if child.url.startswith(base):
                            rel_dir = os.path.dirname(child.url[len(base):])
                            rel_path = unquote(rel_dir).strip('/')
                            
                    save_dir = os.path.join(base_save_dir, rel_path) if rel_path else base_save_dir
                    self.add_download_task(child.name_text, child.url, save_dir)
                    files_q += 1
                child.selected = False
                
        if files_q > 0 or folders_q > 0:
            self.set_status(f"Queued {files_q} files. Check DOWNLOADS tab.")

    def _scrape_folder_thread(self, root_url, root_folder_name, base_save_dir):
        root_folder_name = root_folder_name.strip('/')
        urls_to_process = [(root_url, root_folder_name)]
        files_found = 0
        
        while urls_to_process:
            current_url, relative_path = urls_to_process.pop(0)
            try:
                r = requests.get(current_url, proxies=self.get_proxies(), timeout=15)
                soup = BeautifulSoup(r.text, 'html.parser')
                
                for a in soup.find_all("a"):
                    href = a.get("href")
                    txt = unquote(a.text.strip())
                    if not href or txt in ["Parent Directory", "../", "Name", "Size", "Description"]: continue
                    
                    full_url = urljoin(current_url, href)
                    if href.endswith("/"):
                        urls_to_process.append((full_url, os.path.join(relative_path, txt.strip('/'))))
                    else:
                        save_directory = os.path.join(base_save_dir, relative_path)
                        Clock.schedule_once(lambda dt, n=txt, u=full_url, d=save_directory: self.add_download_task(n, u, d), 0)
                        files_found += 1
            except Exception: pass
                
        Clock.schedule_once(lambda dt: self.set_status(f"Folder Scan Done: Added {files_found} files"), 0)

    def add_download_task(self, name, url, save_dir):
        task_id = str(time.time()) + str(hash(url))
        row = QueueRow(task_id=task_id, name_text=name)
        self.root.ids.queue_list.add_widget(row)
        self.queue_widgets[task_id] = row
        
        self.tasks[task_id] = {
            "id": task_id, "name": name, "url": url, "save_dir": save_dir,
            "status": "Queued", "stop_flag": False
        }

    def process_download_queue(self, dt):
        if self.active_downloads >= MAX_CONCURRENT_DOWNLOADS: return
        for tid, task in self.tasks.items():
            if task["status"] == "Queued":
                task["status"] = "Downloading"
                self.active_downloads += 1
                threading.Thread(target=self._download_worker, args=(task,), daemon=True).start()
                if self.active_downloads >= MAX_CONCURRENT_DOWNLOADS: break

    # --- GLOBAL QUEUE CONTROLS ---
    def toggle_task(self, task_id):
        if task_id not in self.tasks: return
        task = self.tasks[task_id]
        if task["status"] in ["Downloading", "Queued"]:
            task["stop_flag"] = True
            if task["status"] == "Queued":
                task["status"] = "Stopped"
                self.update_dl_ui(task_id, status="Stopped", btn="RESUME")
        elif task["status"] in ["Stopped", "Error"]:
            task["stop_flag"] = False
            task["status"] = "Queued"
            self.update_dl_ui(task_id, status="Queued", btn="STOP", speed_eta="-- KB/s | ETA: --:--")

    def pause_all(self):
        for task in self.tasks.values():
            if task["status"] in ["Downloading", "Queued"]:
                task["stop_flag"] = True
                if task["status"] == "Queued":
                    task["status"] = "Stopped"
                    self.update_dl_ui(task["id"], status="Stopped", btn="RESUME")

    def resume_all(self):
        for task in self.tasks.values():
            if task["status"] in ["Stopped", "Error"]:
                task["stop_flag"] = False
                task["status"] = "Queued"
                self.update_dl_ui(task["id"], status="Queued", btn="STOP", speed_eta="Queued for download")

    def clear_done_tasks(self):
        self._clear_tasks_by_status(["Done", "Stopped", "Error", "Cancelled"])

    def clear_all_tasks(self):
        self.pause_all()
        self._clear_tasks_by_status(None)

    def _clear_tasks_by_status(self, status_list):
        container = self.root.ids.queue_list
        to_remove = [c for c in container.children if status_list is None or c.status_text in status_list]
        for child in to_remove:
            container.remove_widget(child)
            if child.task_id in self.queue_widgets: del self.queue_widgets[child.task_id]
            if child.task_id in self.tasks: del self.tasks[child.task_id]

    @mainthread
    def update_dl_ui(self, task_id, progress=None, status=None, size_text=None, speed_eta=None, btn=None):
        if task_id in self.queue_widgets:
            row = self.queue_widgets[task_id]
            if progress is not None: row.progress_val = progress
            if status is not None: row.status_text = status
            if size_text is not None: row.size_text = size_text
            if speed_eta is not None: row.speed_eta_text = speed_eta
            if btn is not None: row.btn_text = btn

    # --- DOWNLOAD WORKER (WITH AUTO-RETRY) ---
    def _download_worker(self, task):
        task_id = task["id"]
        os.makedirs(task["save_dir"], exist_ok=True)
        path = os.path.join(task["save_dir"], task["name"])
        
        retries = 0
        while retries < MAX_RETRIES:
            if task["stop_flag"]: break
            
            headers = {}
            existing_size = 0
            if os.path.exists(path):
                existing_size = os.path.getsize(path)
                if existing_size > 0:
                    headers['Range'] = f'bytes={existing_size}-'
                    self.update_dl_ui(task_id, status="Resuming...")
            
            try:
                with requests.get(task["url"], stream=True, proxies=self.get_proxies(), headers=headers, timeout=20) as r:
                    if r.status_code == 416: 
                        self.active_downloads -= 1
                        task["status"] = "Done"
                        self.update_dl_ui(task_id, progress=100, status="Done", btn="DONE", speed_eta="Already complete")
                        return

                    if r.status_code == 206:
                        mode = "ab"
                        total_size = existing_size + int(r.headers.get('content-length', 0))
                        dl_bytes = existing_size
                    else:
                        r.raise_for_status()
                        mode = "wb"
                        total_size = int(r.headers.get('content-length', 0))
                        dl_bytes = 0

                    total_str = format_size(total_size) if total_size else "Unknown"
                    self.update_dl_ui(task_id, status="Downloading", btn="STOP")
                    
                    start_time = time.time()
                    last_update = start_time
                    bytes_since_update = 0
                    
                    with open(path, mode) as f:
                        for chunk in r.iter_content(32768):
                            if task["stop_flag"]:
                                self.active_downloads -= 1
                                task["status"] = "Stopped"
                                self.update_dl_ui(task_id, status="Stopped", btn="RESUME", speed_eta="Paused by user")
                                return
                            
                            if chunk:
                                f.write(chunk)
                                chunk_len = len(chunk)
                                dl_bytes += chunk_len
                                bytes_since_update += chunk_len
                                
                                now = time.time()
                                if now - last_update > 0.5:
                                    speed_bps = bytes_since_update / (now - last_update)
                                    speed_str = format_size(speed_bps) + "/s"
                                    
                                    eta_str = "--:--"
                                    if total_size > 0 and speed_bps > 0:
                                        eta_str = format_time((total_size - dl_bytes) / speed_bps)
                                    
                                    pct = (dl_bytes / total_size * 100) if total_size > 0 else 0
                                    self.update_dl_ui(task_id, progress=pct, size_text=f"{format_size(dl_bytes)} / {total_str}", speed_eta=f"{speed_str} | ETA: {eta_str}")
                                    last_update = now
                                    bytes_since_update = 0

                self.active_downloads -= 1
                task["status"] = "Done"
                self.update_dl_ui(task_id, progress=100, status="Done", btn="DONE", size_text=f"{total_str} / {total_str}", speed_eta="Finished")
                return 
                
            except Exception as e:
                retries += 1
                if retries < MAX_RETRIES and not task["stop_flag"]:
                    self.update_dl_ui(task_id, status=f"Retrying {retries}/{MAX_RETRIES}...", speed_eta="Connection dropped. Waiting 3s...")
                    time.sleep(3)
                else:
                    self.active_downloads -= 1
                    task["status"] = "Error"
                    self.update_dl_ui(task_id, status="Error", btn="RETRY", speed_eta=f"Failed: {str(e)[:25]}")
                    return

if __name__ == "__main__":
    DownloadManagerApp().run()