"""
WhisperVoice Bootstrapper
=========================

Lightweight launcher that downloads Python + dependencies on first run.
This keeps the initial .exe small (~15-20MB) while downloading the full
runtime (~2-3GB) on first launch.
"""

import os
import sys
import subprocess
import zipfile
import shutil
import threading
import urllib.request
from pathlib import Path

# Use tkinter for the progress UI (built into Python, no extra deps)
import tkinter as tk
from tkinter import ttk

# Configuration
PYTHON_URL = "https://www.python.org/ftp/python/3.11.9/python-3.11.9-embed-amd64.zip"
PYTHON_VERSION = "3.11.9"
GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py"


def get_base_path():
    """Get the directory where the .exe is located."""
    if getattr(sys, 'frozen', False):
        return Path(sys.executable).parent
    return Path(__file__).parent

def log(msg):
    """Safe logging that won't crash if stdout/stderr is None."""
    try:
        if sys.stdout:
            print(msg)
            sys.stdout.flush()
    except:
        pass

class BootstrapperUI:
    """Simple Tkinter UI for download progress."""
    
    def __init__(self):
        self.root = tk.Tk()
        self.root.title("WhisperVoice Setup")
        self.root.geometry("500x220")
        self.root.resizable(False, False)
        self.root.configure(bg="#1a1a2e")
        
        # Center on screen
        self.root.update_idletasks()
        x = (self.root.winfo_screenwidth() - 500) // 2
        y = (self.root.winfo_screenheight() - 220) // 2
        self.root.geometry(f"+{x}+{y}")
        
        # Set Window Icon
        try:
            if getattr(sys, 'frozen', False):
                icon_path = os.path.join(sys._MEIPASS, "app_source", "assets", "icon.ico")
            else:
                icon_path = os.path.join(os.path.dirname(__file__), "assets", "icon.ico")
            
            if os.path.exists(icon_path):
                self.root.iconbitmap(icon_path)
        except:
            pass
            
        # Title
        self.title_label = tk.Label(
            self.root, 
            text="🎤 WhisperVoice Setup",
            font=("Segoe UI", 18, "bold"),
            fg="#00f2ff",
            bg="#1a1a2e"
        )
        self.title_label.pack(pady=(20, 10))
        
        # Status
        self.status_label = tk.Label(
            self.root,
            text="Initializing...",
            font=("Segoe UI", 11),
            fg="#ffffff",
            bg="#1a1a2e"
        )
        self.status_label.pack(pady=5)
        
        # Progress bar
        style = ttk.Style()
        style.theme_use('clam')
        style.configure(
            "Custom.Horizontal.TProgressbar",
            background="#00f2ff",
            troughcolor="#0d0d10",
            borderwidth=0,
            lightcolor="#00f2ff",
            darkcolor="#00f2ff"
        )
        
        self.progress = ttk.Progressbar(
            self.root,
            style="Custom.Horizontal.TProgressbar",
            length=400,
            mode='determinate'
        )
        self.progress.pack(pady=15)
        
        # Detail label
        self.detail_label = tk.Label(
            self.root,
            text="",
            font=("Segoe UI", 9),
            fg="#888888",
            bg="#1a1a2e"
        )
        self.detail_label.pack(pady=5)
        
        self.root.protocol("WM_DELETE_WINDOW", self.on_close)
        self.cancelled = False
        
    def on_close(self):
        self.cancelled = True
        self.root.destroy()
        sys.exit(0)
        
    def set_status(self, text):
        log(f"[STATUS] {text}")
        self.status_label.config(text=text)
        self.root.update()
        
    def set_detail(self, text):
        self.detail_label.config(text=text)
        self.root.update()
        
    def set_progress(self, value):
        self.progress['value'] = value
        self.root.update()
        
    def run_in_thread(self, func):
        """Run a function in a background thread."""
        thread = threading.Thread(target=func, daemon=True)
        thread.start()
        return thread


class Bootstrapper:
    """Main bootstrapper logic."""
    
    def __init__(self, ui=None):
        self.ui = ui
        self.base_path = get_base_path()
        self.runtime_path = self.base_path / "runtime"
        self.python_path = self.runtime_path / "python"
        self.app_path = self.runtime_path / "app"
        
        if getattr(sys, 'frozen', False):
            self.source_path = Path(sys._MEIPASS) / "app_source"
        else:
            self.source_path = self.base_path
            
    def is_python_ready(self):
        """Check if Python runtime exists."""
        return (self.python_path / "python.exe").exists()
        
    def download_file(self, url, dest_path, desc="Downloading"):
        """Download a file with progress."""
        if self.ui: self.ui.set_status(desc)
        
        def progress_hook(count, block_size, total_size):
            if self.ui and total_size > 0:
                percent = min(100, (count * block_size * 100) // total_size)
                self.ui.set_progress(percent)
                mb_done = (count * block_size) / (1024 * 1024)
                mb_total = total_size / (1024 * 1024)
                self.ui.set_detail(f"{mb_done:.1f} / {mb_total:.1f} MB")
                
        urllib.request.urlretrieve(url, dest_path, progress_hook)
        
    def download_python(self):
        """Download and extract Python embeddable."""
        if self.ui: self.ui.set_status("Downloading Python...")
        
        self.runtime_path.mkdir(parents=True, exist_ok=True)
        zip_path = self.runtime_path / "python.zip"
        self.download_file(PYTHON_URL, zip_path, "Downloading Python runtime...")
        
        if self.ui: 
            self.ui.set_status("Extracting Python...")
            self.ui.set_progress(0)
        
        self.python_path.mkdir(parents=True, exist_ok=True)
        with zipfile.ZipFile(zip_path, 'r') as zf:
            total = len(zf.namelist())
            for i, name in enumerate(zf.namelist()):
                zf.extract(name, self.python_path)
                if self.ui: self.ui.set_progress((i * 100) // total)
                
        zip_path.unlink()
        self._fix_pth_file()
            
    def _fix_pth_file(self):
        """CRITICAL: Configure ._pth file to support imports from our app folder."""
        # Find the _pth file in the python folder
        pth_files = list(self.python_path.glob("*._pth"))
        if not pth_files: return
        
        pth_file = pth_files[0]
        
        # Standard names in embeddable: python311.zip, ., import site
        content = [
            "python311.zip", # Hardcoded for now but works for 3.11
            ".",
            "../app",        # Include app folder in search path!
            "Lib/site-packages",
            "import site"    # Enable site-packages
        ]
        pth_file.write_text("\n".join(content) + "\n")
        
        # Ensure site-packages exists
        (self.python_path / "Lib" / "site-packages").mkdir(parents=True, exist_ok=True)

    def install_pip(self):
        """Install pip into embedded Python."""
        if self.ui: self.ui.set_status("Installing pip...")
        get_pip_path = self.runtime_path / "get-pip.py"
        self.download_file(GET_PIP_URL, get_pip_path, "Downloading pip installer...")
        
        subprocess.run(
            [str(self.python_path / "python.exe"), str(get_pip_path), "--no-warn-script-location"],
            cwd=str(self.python_path),
            capture_output=True,
            creationflags=subprocess.CREATE_NO_WINDOW
        )
        get_pip_path.unlink()
        
    def install_packages(self):
        """Install required packages via pip."""
        if self.ui: 
            self.ui.set_status("Installing packages...")
            self.ui.set_progress(10)
            
        req_file = self.source_path / "requirements.txt"
        
        process = subprocess.Popen(
            [str(self.python_path / "python.exe"), "-m", "pip", "install", "-r", str(req_file)],
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            text=True,
            cwd=str(self.python_path),
            creationflags=subprocess.CREATE_NO_WINDOW
        )
        
        for line in process.stdout:
            if self.ui: self.ui.set_detail(line.strip()[:60])
        process.wait()
        
    def refresh_app_source(self):
        """
        Smartly updates app source files by only copying changed files.
        Preserves user settings and reduces disk I/O.
        """
        if self.ui: self.ui.set_status("Checking for updates...")
        
        try:
            # 1. Ensure destination exists
            if not self.app_path.exists():
                self.app_path.mkdir(parents=True, exist_ok=True)
                
            # 2. Walk source and sync
            # source_path is the temporary bundled folder
            # app_path is the persistent runtime folder
            
            changes_made = 0
            
            for src_dir, dirs, files in os.walk(self.source_path):
                # Determine relative path from source root
                rel_path = Path(src_dir).relative_to(self.source_path)
                dst_dir = self.app_path / rel_path
                
                # Ensure directory exists
                if not dst_dir.exists():
                    dst_dir.mkdir(parents=True, exist_ok=True)
                
                for file in files:
                    # Skip ignored files
                    if file in ['__pycache__', '.git', 'settings.json'] or file.endswith('.pyc'):
                        continue
                        
                    src_file = Path(src_dir) / file
                    dst_file = dst_dir / file
                    
                    # Check if update needed
                    should_copy = False
                    if not dst_file.exists():
                        should_copy = True
                    else:
                        # Compare size first (fast)
                        if src_file.stat().st_size != dst_file.stat().st_size:
                            should_copy = True
                        else:
                            # Compare content (slower but accurate)
                            # Only read if size matches to verify diff
                            if src_file.read_bytes() != dst_file.read_bytes():
                                should_copy = True
                    
                    if should_copy:
                        shutil.copy2(src_file, dst_file)
                        changes_made += 1
                        if self.ui: self.ui.set_detail(f"Updated: {file}")

            # 3. Cleanup logic (Optional: remove files in dest that are not in source)
            # For now, we only add/update to prevent deleting generated user files (logs, etc)
            
            if changes_made > 0:
                log(f"Update complete. {changes_made} files changed.")
            else:
                log("App is up to date.")
                
            return True
        except Exception as e:
            log(f"Error refreshing app source: {e}")
            # Fallback to nuclear option if sync fails completely? 
            # No, 'smart_sync' failing might mean permissions, nuclear wouldn't help.
            return False
        
    def run_app(self):
        """Launch the main application and exit."""
        python_exe = self.python_path / "python.exe"
        main_py = self.app_path / "main.py"
        
        env = os.environ.copy()
        env["PYTHONPATH"] = str(self.app_path)
        
        try:
            subprocess.Popen(
                [str(python_exe), str(main_py)],
                cwd=str(self.app_path),
                env=env,
                creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS
            )
            return True
        except Exception as e:
            messagebox.showerror("WhisperVoice Error", f"Failed to launch app: {e}")
            return False

    def setup_and_run(self):
        """Full setup/update and run flow."""
        try:
            if not self.is_python_ready():
                self.download_python()
                self.install_pip()
                self.install_packages()
            
            # Always refresh source to ensure we have the latest bundled code
            self.refresh_app_source()
            
            # Launch
            if self.run_app():
                if self.ui: self.ui.root.quit()
        except Exception as e:
            messagebox.showerror("Setup Error", f"Installation failed: {e}")
            import traceback
            traceback.print_exc()


def main():
    base_path = get_base_path()
    python_exe = base_path / "runtime" / "python" / "python.exe"
    main_py = base_path / "runtime" / "app" / "main.py"
    
    # Fast path: if already set up and not frozen (development), just launch
    if not getattr(sys, 'frozen', False) and python_exe.exists() and main_py.exists():
        boot = Bootstrapper()
        boot.run_app()
        return

    # Normal path: Show UI for possible setup/update
    ui = BootstrapperUI()
    boot = Bootstrapper(ui)
    
    threading.Thread(target=boot.setup_and_run, daemon=True).start()
    ui.root.mainloop()


if __name__ == "__main__":
    try:
        main()
    except Exception as e:
        import tkinter.messagebox as mb
        mb.showerror("Fatal Error", f"Bootstrapper crashed: {e}")
