"""Cross-platform installer utilities for remote-viewer (virt-viewer package)."""

from __future__ import annotations

import hashlib
import json
import os
import platform
import subprocess
import tempfile
import urllib.request
from pathlib import Path
from typing import Dict, Any, Optional
from urllib.parse import urlparse

from rich.console import Console
from rich.progress import Progress

from isard.errors import IsardError

console = Console()
err_console = Console(stderr=True)


class InstallerError(IsardError):
    """Error during installation process."""


class UnsupportedPlatformError(InstallerError):
    """Platform not supported for installation."""


def _get_platform() -> str:
    """Get the current platform (Windows, Linux, Darwin)."""
    return platform.system()


def _is_windows() -> bool:
    """Check if running on Windows."""
    return _get_platform() == "Windows"


def _is_linux() -> bool:
    """Check if running on Linux."""
    return _get_platform() == "Linux"


def _get_linux_distro() -> Optional[str]:
    """Get the Linux distribution name (fedora, ubuntu, etc.)."""
    if not _is_linux():
        return None

    try:
        # Try to read /etc/os-release
        with open("/etc/os-release", "r") as f:
            content = f.read().lower()
            if "fedora" in content:
                return "fedora"
            elif "ubuntu" in content:
                return "ubuntu"
            elif "debian" in content:
                return "ubuntu"  # Treat Debian like Ubuntu for apt
    except FileNotFoundError:
        pass

    # Fallback: check for specific files
    if Path("/etc/fedora-release").exists():
        return "fedora"
    elif Path("/etc/debian_version").exists():
        return "ubuntu"

    return None


def _get_architecture() -> str:
    """Get the current architecture."""
    machine = platform.machine().lower()
    if machine in ("amd64", "x86_64"):
        if _is_windows():
            return "x64"
        else:
            return "x86_64"
    elif machine in ("x86", "i386", "i686"):
        if _is_windows():
            return "x86"
        else:
            return "i386"
    elif machine in ("aarch64", "arm64"):
        return "arm64"
    else:
        raise UnsupportedPlatformError(f"Unsupported architecture: {machine}")


def _run_command(
    cmd: list[str], capture_output: bool = True
) -> subprocess.CompletedProcess:
    """Run a command and return the result."""
    try:
        return subprocess.run(cmd, check=True, capture_output=capture_output, text=True)
    except subprocess.CalledProcessError as e:
        raise InstallerError(
            f"Command failed: {' '.join(cmd)}\n{e.stderr if e.stderr else str(e)}"
        )
    except FileNotFoundError:
        raise InstallerError(f"Command not found: {cmd[0]}")


def _check_sudo_available() -> bool:
    """Check if sudo is available and can be used."""
    try:
        result = subprocess.run(["sudo", "-n", "true"], capture_output=True, timeout=5)
        return result.returncode == 0
    except subprocess.TimeoutExpired, FileNotFoundError:
        return False


def _install_fedora() -> None:
    """Install virt-viewer on Fedora using dnf."""
    console.print("Installing virt-viewer on Fedora...")

    # Check if we need sudo (os.geteuid only exists on Unix)
    needs_sudo = True
    try:
        needs_sudo = os.geteuid() != 0
    except AttributeError:
        # Windows doesn't have geteuid, but we're not on Windows here
        pass
    
    if needs_sudo:
        if not _check_sudo_available():
            raise InstallerError(
                "This installation requires sudo privileges. Please ensure you can run sudo commands."
            )
        cmd = ["sudo", "dnf", "install", "-y", "virt-viewer"]
    else:
        cmd = ["dnf", "install", "-y", "virt-viewer"]

    console.print(f"Running: {' '.join(cmd)}")
    _run_command(cmd, capture_output=False)


def _install_ubuntu() -> None:
    """Install virt-viewer on Ubuntu/Debian using apt."""
    console.print("Installing virt-viewer on Ubuntu/Debian...")

    # Check if we need sudo (os.geteuid only exists on Unix)
    needs_sudo = True
    try:
        needs_sudo = os.geteuid() != 0
    except AttributeError:
        # Windows doesn't have geteuid, but we're not on Windows here
        pass
    
    if needs_sudo:
        if not _check_sudo_available():
            raise InstallerError(
                "This installation requires sudo privileges. Please ensure you can run sudo commands."
            )
        update_cmd = ["sudo", "apt", "update"]
        install_cmd = ["sudo", "apt", "install", "-y", "virt-viewer"]
    else:
        update_cmd = ["apt", "update"]
        install_cmd = ["apt", "install", "-y", "virt-viewer"]

    console.print(f"Running: {' '.join(update_cmd)}")
    _run_command(update_cmd, capture_output=False)

    console.print(f"Running: {' '.join(install_cmd)}")
    _run_command(install_cmd, capture_output=False)


def _fetch_releases_info() -> Dict[str, Any]:
    """Fetch the latest release information from GitLab API."""
    try:
        url = "https://gitlab.com/api/v4/projects/virt-viewer%2Fvirt-viewer/releases"
        with urllib.request.urlopen(url) as response:
            data = json.loads(response.read().decode())
            if not data or not isinstance(data, list):
                raise InstallerError("Invalid releases data from GitLab API")
            return data[0]  # Latest release
    except Exception as e:
        raise InstallerError(f"Failed to fetch release information: {e}")


def _find_msi_asset(release_info: Dict[str, Any], arch: str) -> tuple[str, str]:
    """Find the MSI download URL for the specified architecture.

    Args:
        release_info: Release data from GitLab API
        arch: Architecture (x86 or x64)

    Returns:
        Tuple of (download_url, filename)
    """
    # Look for assets/links with MSI files
    assets = release_info.get("assets", {}).get("links", [])

    # Pattern for MSI files: virt-viewer-{arch}-{version}*.msi
    version = release_info.get("tag_name", "").lstrip("v")
    expected_patterns = [
        f"virt-viewer-{arch}-{version}-1.0.msi",  # Most recent format
        f"virt-viewer-{arch}-{version}.msi",  # Older format
    ]

    # First, try to find exact matches
    for pattern in expected_patterns:
        for asset in assets:
            if asset.get("name") == pattern:
                return asset["url"], pattern

    # If no exact match, look for any MSI with the right architecture
    for asset in assets:
        name = asset.get("name", "")
        if name.startswith(f"virt-viewer-{arch}-") and name.endswith(".msi"):
            return asset["url"], name

    raise InstallerError(
        f"No MSI file found for architecture {arch} in release {version}"
    )


def _download_with_progress(url: str, dest: Path) -> None:
    """Download a file with progress bar."""
    with Progress() as progress:
        task = progress.add_task("Downloading virt-viewer...", total=100)

        def update_progress(block_num: int, block_size: int, total_size: int) -> None:
            if total_size > 0:
                percent = min(100, (block_num * block_size / total_size) * 100)
                progress.update(task, completed=percent)

        try:
            urllib.request.urlretrieve(url, dest, update_progress)
        except Exception as e:
            raise InstallerError(f"Failed to download {url}: {e}")


def _get_install_dir() -> Path:
    """Get the installation directory for virt-viewer (user-local, no admin needed)."""
    # Use LOCALAPPDATA for user-local installation
    local_app_data = os.getenv("LOCALAPPDATA")
    if not local_app_data:
        # Fallback to user's home directory
        local_app_data = str(Path.home() / "AppData" / "Local")
    
    install_dir = Path(local_app_data) / "Programs" / "virt-viewer"
    return install_dir


def _extract_msi(msi_path: Path, extract_dir: Path) -> None:
    """Extract MSI files using administrative install (no admin privileges required).
    
    Uses msiexec /a which extracts files without installing to system.
    This is the same approach used by Scoop package manager.
    """
    # Create extraction directory
    extract_dir.mkdir(parents=True, exist_ok=True)
    
    # Use msiexec /a for administrative install (extracts files)
    # /qn = quiet, no UI
    cmd = [
        "msiexec",
        "/a",
        str(msi_path),
        "/qn",
        f"TARGETDIR={extract_dir}"
    ]
    
    console.print(f"Extracting files from MSI...")
    try:
        result = subprocess.run(cmd, check=True, capture_output=True, text=True)
    except subprocess.CalledProcessError as e:
        raise InstallerError(
            f"Failed to extract MSI: {e.stderr if e.stderr else str(e)}"
        )


def _flatten_virt_viewer_dir(extract_dir: Path, install_dir: Path) -> None:
    """Flatten the extracted directory structure.
    
    MSI extraction creates nested folders like VirtViewer v11.0/bin/*.exe
    We need to move these to a simpler structure.
    """
    console.print("Organizing files...")
    
    # Find the VirtViewer directory (pattern: VirtViewer*)
    virt_dirs = list(extract_dir.glob("VirtViewer*"))
    
    if not virt_dirs:
        # Sometimes it extracts to PFiles/VirtViewer or similar
        virt_dirs = list(extract_dir.glob("**/VirtViewer*"))
    
    if not virt_dirs:
        raise InstallerError(
            f"Could not find VirtViewer directory in extracted files at {extract_dir}"
        )
    
    source_dir = virt_dirs[0]
    console.print(f"Found virt-viewer files in: {source_dir.name}")
    
    # Move all contents to install_dir
    import shutil
    
    # Clear install_dir if it exists
    if install_dir.exists():
        shutil.rmtree(install_dir)
    
    install_dir.mkdir(parents=True, exist_ok=True)
    
    # Move contents
    for item in source_dir.iterdir():
        dest = install_dir / item.name
        shutil.move(str(item), str(dest))
    
    console.print(f"Files installed to: {install_dir}")


def _add_to_user_path(directory: Path) -> bool:
    """Add directory to user's PATH environment variable (Windows only).
    
    Returns True if successful, False otherwise.
    """
    try:
        import winreg
        
        # Open user environment variables key
        key = winreg.OpenKey(
            winreg.HKEY_CURRENT_USER,
            r"Environment",
            0,
            winreg.KEY_READ | winreg.KEY_WRITE
        )
        
        try:
            # Read current PATH
            current_path, _ = winreg.QueryValueEx(key, "Path")
        except FileNotFoundError:
            current_path = ""
        
        # Check if already in PATH
        path_dirs = [p.strip() for p in current_path.split(";") if p.strip()]
        dir_str = str(directory / "bin")
        
        if dir_str in path_dirs:
            console.print(f"Already in PATH: {dir_str}")
            winreg.CloseKey(key)
            return True
        
        # Add to PATH
        new_path = current_path
        if new_path and not new_path.endswith(";"):
            new_path += ";"
        new_path += dir_str
        
        winreg.SetValueEx(key, "Path", 0, winreg.REG_EXPAND_SZ, new_path)
        winreg.CloseKey(key)
        
        # Notify system of environment change
        import ctypes
        HWND_BROADCAST = 0xFFFF
        WM_SETTINGCHANGE = 0x1A
        ctypes.windll.user32.SendMessageW(
            HWND_BROADCAST, WM_SETTINGCHANGE, 0, "Environment"
        )
        
        console.print(f"Added to PATH: {dir_str}")
        console.print("Note: You may need to restart your terminal for PATH changes to take effect")
        return True
        
    except Exception as e:
        console.print(f"Warning: Could not add to PATH: {e}")
        return False


def _install_windows(force: bool = False, silent: bool = True) -> None:
    """Install virt-viewer on Windows by extracting from MSI (no admin required).
    
    Uses the Scoop approach: extract MSI files to user-local directory and add to PATH.
    This avoids the need for administrator privileges.
    """
    arch = _get_architecture()
    console.print(f"Installing virt-viewer for {arch} architecture...")

    # Fetch latest release information
    console.print("Fetching latest release information...")
    release_info = _fetch_releases_info()
    version = release_info.get("tag_name", "unknown")
    console.print(f"Latest version: {version}")

    # Find the MSI asset for this architecture
    download_url, filename = _find_msi_asset(release_info, arch)
    console.print(f"Found MSI: {filename}")

    # Get install directory (user-local, no admin needed)
    install_dir = _get_install_dir()
    
    # Create temporary directory for download and extraction
    with tempfile.TemporaryDirectory() as temp_dir:
        temp_path = Path(temp_dir)
        msi_path = temp_path / filename
        extract_dir = temp_path / "extracted"

        # Download the MSI
        console.print(f"Downloading from: {download_url}")
        _download_with_progress(download_url, msi_path)

        # Extract the MSI (no installation, just file extraction)
        _extract_msi(msi_path, extract_dir)
        
        # Flatten directory structure and move to final location
        _flatten_virt_viewer_dir(extract_dir, install_dir)
    
    # Add to PATH
    _add_to_user_path(install_dir)
    
    # Verify installation
    bin_dir = install_dir / "bin"
    remote_viewer_exe = bin_dir / "remote-viewer.exe"
    
    if remote_viewer_exe.exists():
        console.print(f"remote-viewer.exe installed at: {remote_viewer_exe}")
    else:
        console.print(f"Warning: remote-viewer.exe not found at expected location: {remote_viewer_exe}")


def _is_virt_viewer_installed() -> bool:
    """Check if remote-viewer is already installed."""
    try:
        # Try to run remote-viewer --version
        result = subprocess.run(
            ["remote-viewer", "--version"], check=True, capture_output=True, text=True
        )
        return result.returncode == 0
    except (subprocess.CalledProcessError, FileNotFoundError):
        # Check if it's installed in our user-local directory (but not yet in PATH)
        if _is_windows():
            install_dir = _get_install_dir()
            remote_viewer_exe = install_dir / "bin" / "remote-viewer.exe"
            return remote_viewer_exe.exists()
        return False


def _get_virt_viewer_version() -> Optional[str]:
    """Get the installed remote-viewer version."""
    try:
        result = subprocess.run(
            ["remote-viewer", "--version"], check=True, capture_output=True, text=True
        )
        if result.returncode == 0:
            # Extract version from output (e.g., "remote-viewer 11.0")
            version_output = result.stdout.strip()
            if version_output:
                # Try to extract version number
                import re

                match = re.search(r"(\d+\.\d+)", version_output)
                if match:
                    return match.group(1)
                else:
                    return version_output
    except (subprocess.CalledProcessError, FileNotFoundError):
        # Check if it's installed in our user-local directory (but not yet in PATH)
        if _is_windows():
            install_dir = _get_install_dir()
            remote_viewer_exe = install_dir / "bin" / "remote-viewer.exe"
            if remote_viewer_exe.exists():
                try:
                    result = subprocess.run(
                        [str(remote_viewer_exe), "--version"],
                        check=True,
                        capture_output=True,
                        text=True
                    )
                    version_output = result.stdout.strip()
                    if version_output:
                        import re
                        match = re.search(r"(\d+\.\d+)", version_output)
                        if match:
                            return match.group(1)
                        return version_output
                except:
                    pass
    return None


def _check_package_manager_status() -> dict[str, Any]:
    """Check virt-viewer status via package managers on Linux."""
    status = {}

    if not _is_linux():
        return status

    distro = _get_linux_distro()

    try:
        if distro == "fedora":
            # Check with dnf
            result = subprocess.run(
                ["rpm", "-q", "virt-viewer"], capture_output=True, text=True
            )
            if result.returncode == 0:
                status["package_installed"] = True
                # Extract version from rpm output
                version_match = result.stdout.strip()
                if version_match:
                    status["package_version"] = version_match
            else:
                status["package_installed"] = False

        elif distro == "ubuntu":
            # Check with dpkg
            result = subprocess.run(
                ["dpkg", "-l", "virt-viewer"], capture_output=True, text=True
            )
            if result.returncode == 0 and "ii" in result.stdout:
                status["package_installed"] = True
                # Extract version from dpkg output
                lines = result.stdout.strip().split("\n")
                for line in lines:
                    if line.startswith("ii") and "virt-viewer" in line:
                        parts = line.split()
                        if len(parts) >= 3:
                            status["package_version"] = parts[2]
                        break
            else:
                status["package_installed"] = False

    except subprocess.CalledProcessError, FileNotFoundError:
        status["package_installed"] = False

    return status


def install_virt_viewer(force: bool = False, silent: bool = True) -> None:
    """Install virt-viewer package (includes remote-viewer) on Windows, Fedora, or Ubuntu.

    This installs the virt-viewer package which provides both virt-viewer and remote-viewer
    commands. remote-viewer is specifically designed for SPICE connections.

    Args:
        force: Force installation even if already installed
        silent: Suppress verbose output during installation

    Raises:
        UnsupportedPlatformError: If the platform is not supported
        InstallerError: If installation fails
    """
    platform_name = _get_platform()

    if _is_windows():
        console.print("Installing virt-viewer on Windows...")
    elif _is_linux():
        distro = _get_linux_distro()
        if distro in ("fedora", "ubuntu"):
            console.print(f"Installing virt-viewer on {distro.title()}...")
        else:
            raise UnsupportedPlatformError(
                f"Linux distribution '{distro}' is not supported. "
                "Supported distributions: Fedora, Ubuntu/Debian"
            )
    else:
        raise UnsupportedPlatformError(
            f"Platform '{platform_name}' is not supported. "
            "Supported platforms: Windows, Linux (Fedora, Ubuntu/Debian)"
        )

    # Check if already installed
    if not force and _is_virt_viewer_installed():
        console.print("virt-viewer is already installed. Use --force to reinstall.")
        return

    try:
        if _is_windows():
            _install_windows(force, silent)
        elif _is_linux():
            distro = _get_linux_distro()
            if distro == "fedora":
                _install_fedora()
            elif distro == "ubuntu":
                _install_ubuntu()

        console.print("virt-viewer installation completed!")
        console.print("You can now use 'virt-viewer' and 'remote-viewer' commands.")

    except Exception as e:
        raise InstallerError(f"Installation failed: {e}")


def check_virt_viewer_status() -> dict[str, Any]:
    """Check virt-viewer installation status and return details.

    Returns:
        Dictionary with installation status information
    """
    status = {
        "installed": False,
        "version": None,
        "platform": _get_platform(),
        "architecture": platform.machine(),
    }

    platform_name = _get_platform()

    if _is_linux():
        distro = _get_linux_distro()
        status["distribution"] = distro
        if distro not in ("fedora", "ubuntu"):
            status["error"] = f"Unsupported Linux distribution: {distro}"
            return status
    elif not _is_windows():
        status["error"] = f"Unsupported platform: {platform_name}"
        return status

    # Check if virt-viewer command is available
    version = _get_virt_viewer_version()
    if version:
        status["installed"] = True
        status["version"] = version

    # Add package manager info for Linux
    if _is_linux():
        package_status = _check_package_manager_status()
        status.update(package_status)

    # Try to find installation path on Windows
    if _is_windows() and status["installed"]:
        # Check our user-local installation first
        user_install_dir = _get_install_dir()
        
        common_paths = [
            user_install_dir,  # Our extraction-based install
            Path("C:/Program Files/VirtViewer v11.0"),
            Path("C:/Program Files (x86)/VirtViewer v11.0"),
            Path("C:/Program Files/VirtViewer"),
            Path("C:/Program Files (x86)/VirtViewer"),
            Path.home() / "scoop/apps/virt-viewer",
        ]

        for path in common_paths:
            if path.exists():
                # Check for executables
                for exe_path in [
                    path / "bin" / "remote-viewer.exe",
                    path / "remote-viewer.exe",
                    path / "bin" / "virt-viewer.exe",
                    path / "virt-viewer.exe",
                ]:
                    if exe_path.exists():
                        status["path"] = str(path)
                        break
                if status.get("path"):
                    break

    return status


def launch_virt_viewer(spice_file: str, desktop_name: str = "", debug: bool = False) -> None:
    """Launch remote-viewer with a SPICE file.

    Args:
        spice_file: The SPICE connection file content
        desktop_name: Optional desktop name for the window title
        debug: Show debug output from remote-viewer (default: False)

    Raises:
        InstallerError: If remote-viewer is not installed or launch fails
    """
    platform_name = _get_platform()

    # Check platform support
    if _is_linux():
        distro = _get_linux_distro()
        if distro not in ("fedora", "ubuntu"):
            raise UnsupportedPlatformError(
                f"Linux distribution '{distro}' is not supported for remote-viewer launch"
            )
    elif not _is_windows():
        raise UnsupportedPlatformError(
            f"Platform '{platform_name}' is not supported for remote-viewer launch"
        )

    # Check if remote-viewer is installed
    if not _is_virt_viewer_installed():
        platform_msg = (
            "Windows/Linux" if _is_windows() or _is_linux() else platform_name
        )
        raise InstallerError(
            f"remote-viewer is not installed. Use the 'view' command with --install to install it automatically."
        )

    # Create temporary SPICE file
    with tempfile.NamedTemporaryFile(mode="w", suffix=".vv", delete=False) as f:
        f.write(spice_file)
        spice_file_path = Path(f.name)

    try:
        # Determine command - check if in PATH or use full path
        remote_viewer_cmd = "remote-viewer"
        
        # On Windows, check if it's in our user-local install but not in PATH yet
        if _is_windows():
            try:
                # Try the PATH version first
                subprocess.run(
                    ["remote-viewer", "--version"],
                    check=True,
                    capture_output=True,
                    timeout=5
                )
            except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
                # Not in PATH, use full path to our installation
                install_dir = _get_install_dir()
                remote_viewer_exe = install_dir / "bin" / "remote-viewer.exe"
                if remote_viewer_exe.exists():
                    remote_viewer_cmd = str(remote_viewer_exe)
        
        # Build command
        cmd = [remote_viewer_cmd, str(spice_file_path)]

        console.print(
            f"Launching remote-viewer for desktop: {desktop_name or 'Unknown'}"
        )

        # Launch remote-viewer (don't wait for it to finish)
        if debug:
            # Show all output for debugging
            subprocess.Popen(cmd)
        else:
            # Suppress debug messages for clean output
            subprocess.Popen(
                cmd,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
                stdin=subprocess.DEVNULL
            )
        console.print("remote-viewer launched successfully")

    except FileNotFoundError:
        raise InstallerError(
            "remote-viewer command not found in PATH. Try reinstalling with the 'view' command."
        )
    except Exception as e:
        raise InstallerError(f"Failed to launch remote-viewer: {e}")
    finally:
        # Clean up the temporary file after a short delay
        # We can't delete immediately as remote-viewer needs to read it
        import threading
        import time

        def cleanup():
            time.sleep(5)  # Give remote-viewer time to read the file
            try:
                spice_file_path.unlink(missing_ok=True)
            except:
                pass

        threading.Thread(target=cleanup, daemon=True).start()
