"""
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):
        """Refresh app source files. Skips if already exists to save time."""
        # Optimization: If app/main.py exists, skip update to improve startup speed.
        # The user can delete the 'runtime' folder to force an update.
        if (self.app_path / "main.py").exists():
            log("App already exists. Skipping update.")
            return True

        if self.ui: self.ui.set_status("Updating app files...")
        
        try:
            # Preserve settings.json if it exists
            settings_path = self.app_path / "settings.json"
            temp_settings = None
            if settings_path.exists():
                try:
                    temp_settings = settings_path.read_bytes()
                except:
                    log("Failed to backup settings.json, it involves risk of data loss.")

            if self.app_path.exists():
                shutil.rmtree(self.app_path, ignore_errors=True)
                
            shutil.copytree(
                self.source_path,
                self.app_path,
                ignore=shutil.ignore_patterns(
                    '__pycache__', '*.pyc', '.git', 'venv', 
                    'build', 'dist', '*.egg-info', 'runtime'
                )
            )

            # Restore settings.json
            if temp_settings:
                try:
                    settings_path.write_bytes(temp_settings)
                    log("Restored settings.json")
                except:
                    log("Failed to restore settings.json")
                    
            return True
        except Exception as e:
            log(f"Error refreshing app source: {e}")
            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}")
