"""OpenClaw installer - installs OpenClaw and starts the gateway."""

import os
import sys
import subprocess
import shutil
from pathlib import Path
from typing import Optional, Dict
from rich.console import Console

console = Console()

MIN_NODE_VERSION = "22.16.0"  # Minimum required Node.js version
DOWNLOAD_NODE_VERSION = "22.16.0"  # Version to download if needed


def get_platform():
    """Get platform identifier."""
    if sys.platform == "win32":
        return "win"
    elif sys.platform == "darwin":
        return "darwin"
    else:
        return "linux"


def get_machine_arch() -> str:
    """Get machine architecture for Node.js download."""
    import platform as pf
    machine = pf.machine().lower()
    
    # Map machine types to Node.js arch names
    if machine in ('amd64', 'x86_64', 'x64'):
        return 'x64'
    elif machine in ('arm64', 'aarch64'):
        return 'arm64'
    elif machine in ('armv7l', 'arm'):
        return 'armv7l'
    else:
        # Default to x64 for unknown architectures
        return 'x64'


def get_node_download_url(version: str) -> str:
    """Get download URL for Node.js."""
    platform = get_platform()
    arch = get_machine_arch()
    
    if platform == "win":
        return f"https://nodejs.org/dist/v{version}/node-v{version}-win-{arch}.zip"
    elif platform == "darwin":
        # macOS uses different naming for arm64
        if arch == "arm64":
            return f"https://nodejs.org/dist/v{version}/node-v{version}-darwin-arm64.tar.gz"
        return f"https://nodejs.org/dist/v{version}/node-v{version}-darwin-x64.tar.gz"
    else:
        return f"https://nodejs.org/dist/v{version}/node-v{version}-linux-{arch}.tar.xz"


def parse_version(version_str: str) -> tuple:
    """Parse version string into tuple for comparison."""
    version_str = version_str.lstrip('v').strip()
    parts = version_str.split('.')
    return tuple(int(p) for p in parts[:3] if p.isdigit())


def check_node_version(node_path: Path) -> bool:
    """Check if Node.js version meets minimum requirement (v22.16+)."""
    try:
        result = subprocess.run(
            [str(node_path), "--version"],
            capture_output=True,
            text=True,
            timeout=10,
        )
        if result.returncode != 0:
            return False
        
        version_str = result.stdout.strip()
        installed_version = parse_version(version_str)
        min_version = parse_version(MIN_NODE_VERSION)
        
        return installed_version >= min_version
    except Exception:
        return False


def get_install_base() -> Path:
    """Get base installation directory (fully isolated)."""
    if sys.platform == "win32":
        base = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
        return base / "openclaw-installer"
    else:
        xdg_data = os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share")
        return Path(xdg_data) / "openclaw-installer"


def get_bin_dir() -> Path:
    """Get binary directory for wrapper scripts."""
    return get_install_base() / "bin"


class OpenClawInstaller:
    """OpenClaw installer - installs and starts gateway."""

    def __init__(self, dry_run: bool = False, use_system_node: bool = False):
        self.dry_run = dry_run
        self.use_system_node = use_system_node
        self.node_bins: Dict[str, Path] = {}
        self.bin_dir = get_bin_dir()

    def install(self) -> bool:
        """Run installation."""
        console.print("")
        console.print("[bold blue]OpenClaw Installer[/]")
        console.print("[dim]Installs OpenClaw and starts the gateway[/]")
        console.print("")

        if self.dry_run:
            console.print("[yellow]DRY RUN MODE - No changes will be made[/]")
            console.print("")

        steps = [
            ("Setting up Node.js", self._setup_node),
            ("Installing OpenClaw", self._install_openclaw),
            ("Starting gateway", self._start_gateway),
        ]

        for name, step_func in steps:
            if not step_func():
                console.print(f"[red]Installation failed at: {name}[/]")
                return False

        # Auto-login to dashboard after successful install
        self._auto_login()
        
        self._print_success()
        return True

    def _setup_node(self) -> bool:
        """Ensure Node.js v22.16+ is available."""
        console.print("[dim]Setting up Node.js...[/]", end="")

        if self.dry_run:
            console.print(" [yellow](skipped)[/]")
            return True

        install_base = get_install_base()
        node_dir = install_base / "node"

        # Check for already-downloaded Node.js
        if sys.platform == "win32":
            node_exe = node_dir / "node.exe"
            npm_exe = node_dir / "npm.cmd"
        else:
            node_exe = node_dir / "bin" / "node"
            npm_exe = node_dir / "bin" / "npm"

        if not self.use_system_node and node_exe.exists() and check_node_version(node_exe):
            console.print(f" [green]OK[/] Using bundled Node.js v{DOWNLOAD_NODE_VERSION}")
            self.node_bins = {"node": node_exe, "npm": npm_exe, "system": False}
            return True
        
        # Check for system Node.js v22.16+
        system_node = shutil.which("node")
        if system_node:
            system_node_path = Path(system_node)
            if check_node_version(system_node_path):
                system_npm = shutil.which("npm")
                if system_npm:
                    if self.use_system_node:
                        console.print(f" [green]OK[/] Using system Node.js (as requested)")
                    else:
                        console.print(f" [green]OK[/] Using system Node.js")
                    self.node_bins = {"node": system_node_path, "npm": Path(system_npm), "system": True}
                    return True
            else:
                console.print(f" [yellow]system Node.js too old, need v{MIN_NODE_VERSION}+[/]", end=" ")

        # If --system-node was requested but system node is not suitable, fail
        if self.use_system_node:
            console.print(f" [red]FAIL[/]")
            console.print(f"[red]System Node.js is not v{MIN_NODE_VERSION}+. Please upgrade or run without --system-node flag.[/]")
            return False

        # Download Node.js
        console.print(f" [dim]downloading v{DOWNLOAD_NODE_VERSION}...[/]", end="")
        try:
            import urllib.request
            import tempfile

            url = get_node_download_url(DOWNLOAD_NODE_VERSION)
            ext = ".zip" if sys.platform == "win32" else ".tar.gz" if sys.platform == "darwin" else ".tar.xz"

            with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as tmp:
                urllib.request.urlretrieve(url, tmp.name)
                tmp_path = Path(tmp.name)

            # Extract
            # Remove existing directories first (Windows can't rename over existing dir)
            # Use onerror to handle permission issues on Windows
            def _onerror(func, path, exc_info):
                import stat
                # Try to make the file writable and retry
                if os.path.exists(path):
                    os.chmod(path, stat.S_IWUSR | stat.S_IREAD)
                    func(path)
            
            arch = get_machine_arch()
            if node_dir.exists():
                try:
                    shutil.rmtree(node_dir, onerror=_onerror)
                except Exception:
                    # If we can't delete, try renaming it out of the way
                    import time
                    backup_name = node_dir.parent / f"node-old-{int(time.time())}"
                    try:
                        node_dir.rename(backup_name)
                    except Exception:
                        pass  # Will fail later if we can't proceed
            
            extracted_dir_win = node_dir.parent / f"node-v{DOWNLOAD_NODE_VERSION}-win-{arch}"
            if extracted_dir_win.exists():
                try:
                    shutil.rmtree(extracted_dir_win, onerror=_onerror)
                except Exception:
                    pass  # Can still proceed if this fails
            
            if sys.platform == "win32":
                import zipfile
                with zipfile.ZipFile(tmp_path, 'r') as z:
                    z.extractall(node_dir.parent)
                extracted = node_dir.parent / f"node-v{DOWNLOAD_NODE_VERSION}-win-{arch}"
                if extracted.exists():
                    extracted.rename(node_dir)
            else:
                # Clean up potential leftover extracted directories on Unix too
                platform = get_platform()
                arch = get_machine_arch()
                if platform == "darwin" and arch == "arm64":
                    extracted_dir_unix = node_dir.parent / f"node-v{DOWNLOAD_NODE_VERSION}-darwin-arm64"
                else:
                    extracted_dir_unix = node_dir.parent / f"node-v{DOWNLOAD_NODE_VERSION}-{platform}-{arch}"
                if extracted_dir_unix.exists():
                    try:
                        shutil.rmtree(extracted_dir_unix, onerror=_onerror)
                    except Exception:
                        pass
                
                import tarfile
                with tarfile.open(tmp_path, 'r:*') as t:
                    t.extractall(node_dir.parent)
                # macOS arm64 has different naming
                if platform == "darwin" and arch == "arm64":
                    extracted = node_dir.parent / f"node-v{DOWNLOAD_NODE_VERSION}-darwin-arm64"
                else:
                    extracted = node_dir.parent / f"node-v{DOWNLOAD_NODE_VERSION}-{platform}-{arch}"
                if extracted.exists():
                    extracted.rename(node_dir)

            tmp_path.unlink()

            if node_exe.exists():
                console.print(" [green]OK[/]")
                self.node_bins = {"node": node_exe, "npm": npm_exe, "system": False}
                return True
            else:
                console.print(" [red]FAIL (extraction failed)[/]")
                return False

        except Exception as e:
            console.print(f" [red]FAIL {e}[/]")
            return False

    def _get_env_with_node(self) -> dict:
        """Get environment with bundled Node.js in PATH (cross-platform)."""
        env = os.environ.copy()
        node_bin = str(self.node_bins["node"].parent)
        npm_global_bin = self._get_npm_global_bin_dir()
        
        # Build new PATH: our Node bin + npm global bin + original PATH
        # This ensures our Node is found first, and npm global packages are available
        new_path_parts = [node_bin]
        if npm_global_bin:
            new_path_parts.append(str(npm_global_bin))
        new_path_parts.append(env.get("PATH", ""))
        
        env["PATH"] = os.pathsep.join(new_path_parts)
        return env

    def _get_npm_global_bin_dir(self) -> Optional[Path]:
        """Get npm global bin directory where 'openclaw' command is installed."""
        # Use the same prefix we set during install with --prefix
        npm_prefix = self._get_npm_prefix()
        
        if sys.platform == "win32":
            # On Windows with --prefix, binaries are in {prefix}
            return npm_prefix
        else:
            # On Unix, binaries are in {prefix}/bin
            return npm_prefix / "bin"

    def _get_openclaw_exe(self) -> Path:
        """Get the openclaw executable path (cross-platform)."""
        # First check npm global bin
        npm_bin = self._get_npm_global_bin_dir()
        if npm_bin:
            if sys.platform == "win32":
                openclaw_cmd = npm_bin / "openclaw.cmd"
                openclaw_ps1 = npm_bin / "openclaw.ps1"
                if openclaw_cmd.exists():
                    return openclaw_cmd
                if openclaw_ps1.exists():
                    return openclaw_ps1
            else:
                openclaw_sh = npm_bin / "openclaw"
                if openclaw_sh.exists():
                    return openclaw_sh
        
        # Fallback to PATH lookup
        which_openclaw = shutil.which("openclaw")
        if which_openclaw:
            return Path(which_openclaw)
        
        # Final fallback - try common locations
        if sys.platform == "win32":
            appdata = os.environ.get("APPDATA", str(Path.home() / "AppData" / "Roaming"))
            return Path(appdata) / "npm" / "openclaw.cmd"
        else:
            return get_install_base() / "node" / "bin" / "openclaw"

    def _get_npm_prefix(self) -> Path:
        """Get the npm global prefix directory."""
        if sys.platform == "win32":
            appdata = os.environ.get("APPDATA", str(Path.home() / "AppData" / "Roaming"))
            return Path(appdata) / "npm"
        else:
            # Use the bundled node directory as npm prefix
            return get_install_base() / "node"

    def _install_openclaw(self) -> bool:
        """Install OpenClaw npm package with proper Node.js PATH (cross-platform)."""
        console.print("[dim]Installing OpenClaw...[/]", end="")

        if self.dry_run:
            console.print(" [yellow](skipped)[/]")
            return True

        try:
            # Use npm from our bundled Node.js with PATH set correctly
            # This ensures child processes (post-install scripts) use our Node
            env = self._get_env_with_node()
            npm_exe = str(self.node_bins["npm"])
            
            # Set npm prefix to ensure global install goes to the right place
            npm_prefix = self._get_npm_prefix()
            npm_prefix.mkdir(parents=True, exist_ok=True)
            
            # Use --prefix flag which is more reliable than env var on Windows
            result = subprocess.run(
                [npm_exe, "install", "-g", "--prefix", str(npm_prefix), "openclaw"],
                capture_output=True,
                text=True,
                timeout=300,
                env=env,
            )

            if result.returncode != 0:
                console.print(f" [red]FAIL[/]")
                if result.stderr:
                    console.print(f"[dim]{result.stderr[:500]}[/]")
                return False

            console.print(" [green]OK[/]")
            return True

        except Exception as e:
            console.print(f" [red]FAIL {e}[/]")
            return False

    def _start_gateway(self) -> bool:
        """Start the OpenClaw gateway using the official CLI commands (cross-platform)."""
        console.print("[dim]Configuring gateway...[/]", end="")

        if self.dry_run:
            console.print(" [yellow](skipped)[/]")
            return True

        try:
            env = self._get_env_with_node()
            openclaw_exe = self._get_openclaw_exe()
            
            if not openclaw_exe.exists():
                console.print(f" [red]FAIL (openclaw not found at {openclaw_exe})[/]")
                return False

            # Step 1: Configure gateway.mode=local (required before install)
            # Use a longer timeout and don't fail if it times out (config may already be set)
            try:
                result = subprocess.run(
                    [str(openclaw_exe), "config", "set", "gateway.mode", "local"],
                    capture_output=True,
                    text=True,
                    timeout=60,
                    env=env,
                )
            except subprocess.TimeoutExpired:
                # Config might already be set, check if we can proceed
                console.print(" [yellow]timeout[/] (config may already be set)")
            
            console.print(" [green]OK[/]")
            
            # Step 2: Install the gateway on port 18789
            console.print("[dim]Installing gateway...[/]", end="")
            install_result = subprocess.run(
                [str(openclaw_exe), "gateway", "install", "--force", "--port", "18789"],
                capture_output=True,
                text=True,
                timeout=120,  # Longer timeout for slow devices (e.g., ARM boards)
                env=env,
            )
            
            if install_result.returncode != 0:
                console.print(" [red]FAIL (install)[/]")
                if install_result.stderr:
                    console.print(f"[dim]{install_result.stderr[:500]}[/]")
                return False
            
            console.print(" [green]OK[/]")
            
            # Step 3: Start the gateway
            console.print("[dim]Starting gateway...[/]", end="")
            
            if sys.platform == "win32":
                subprocess.Popen(
                    [str(openclaw_exe), "gateway", "start"],
                    creationflags=subprocess.CREATE_NEW_PROCESS_GROUP,
                    stdout=subprocess.DEVNULL,
                    stderr=subprocess.DEVNULL,
                    env=env,
                )
            else:
                subprocess.Popen(
                    [str(openclaw_exe), "gateway", "start"],
                    start_new_session=True,
                    stdout=subprocess.DEVNULL,
                    stderr=subprocess.DEVNULL,
                    env=env,
                )

            # Wait for gateway to start (longer on slower devices like ARM boards)
            import time
            time.sleep(5)

            console.print(" [green]OK[/]")
            return True

        except Exception as e:
            console.print(f" [red]FAIL {e}[/]")
            return False

    def _print_success(self) -> None:
        """Print success message."""
        console.print("")
        console.print("[bold green]Installation Complete![/]")
        console.print("")
        console.print("OpenClaw is installed and the gateway is running.")
        console.print("")

    def _auto_login(self) -> bool:
        """Automatically login to dashboard after installation."""
        console.print("")
        console.print("[dim]Opening dashboard...[/]", end="")
        
        try:
            # Import automation module
            from .automation.auto_login import auto_login
            
            success = auto_login()
            if success:
                console.print(" [green]OK[/]")
            else:
                console.print(" [yellow]skipped[/]")
            return success
        except Exception as e:
            console.print(f" [dim](skipped: {e})[/]")
            return True  # Don't fail installation if auto-login fails

    def uninstall(self) -> bool:
        """Uninstall OpenClaw."""
        console.print("")
        console.print("[bold blue]OpenClaw Uninstaller[/]")
        console.print("")

        if self.dry_run:
            console.print("[yellow]DRY RUN MODE - No changes will be made[/]")
            console.print("")

        steps = [
            ("Removing OpenClaw", self._uninstall_openclaw),
            ("Cleaning up Node.js", self._cleanup_node),
        ]

        for name, step_func in steps:
            if not step_func():
                console.print(f"[red]Uninstall failed at: {name}[/]")
                return False

        console.print("")
        console.print("[bold green]Uninstall Complete![/]")
        console.print("")
        return True

    def _uninstall_openclaw(self) -> bool:
        """Uninstall OpenClaw npm package."""
        console.print("[dim]Removing OpenClaw...[/]", end="")

        if self.dry_run:
            console.print(" [yellow](skipped)[/]")
            return True

        try:
            env = self._get_env_with_node()
            npm_exe = str(self.node_bins.get("npm", "npm"))
            
            result = subprocess.run(
                [npm_exe, "uninstall", "-g", "openclaw"],
                capture_output=True,
                text=True,
                timeout=60,
                env=env,
            )

            # Remove wrapper if exists
            if sys.platform == "win32":
                wrapper = self.bin_dir / "openclaw.cmd"
            else:
                wrapper = self.bin_dir / "openclaw"

            if wrapper.exists():
                wrapper.unlink()

            console.print(" [green]OK[/]")
            return True
        except Exception as e:
            console.print(f" [red]FAIL {e}[/]")
            return False

    def _cleanup_node(self) -> bool:
        """Clean up Node.js installation."""
        console.print("[dim]Cleaning up Node.js...[/]", end="")

        if self.dry_run:
            console.print(" [yellow](skipped)[/]")
            return True

        try:
            install_base = get_install_base()
            node_dir = install_base / "node"
            if node_dir.exists():
                shutil.rmtree(node_dir)

            console.print(" [green]OK[/]")
            return True
        except Exception as e:
            console.print(f" [red]FAIL {e}[/]")
            return False
