"""Platform Detection Module for AiCippy Installer.

Detects operating system, shell type, and system configuration
to enable cross-platform installation support.
"""

from __future__ import annotations

import ctypes
import os
import platform
import re
import shutil
import subprocess
import sys
from dataclasses import dataclass, field
from enum import Enum, auto
from pathlib import Path
from typing import Final


class OperatingSystem(Enum):
    """Supported operating systems."""

    WINDOWS = auto()
    MACOS = auto()
    LINUX = auto()
    UNKNOWN = auto()


class ShellType(Enum):
    """Shell types for command execution."""

    POWERSHELL = auto()
    CMD = auto()
    BASH = auto()
    ZSH = auto()
    FISH = auto()
    SH = auto()
    UNKNOWN = auto()


class Architecture(Enum):
    """CPU architecture types."""

    X86_64 = auto()
    ARM64 = auto()
    X86 = auto()
    ARM = auto()
    UNKNOWN = auto()


# Default installation paths per OS
DEFAULT_INSTALL_PATHS: Final[dict[OperatingSystem, Path]] = {
    OperatingSystem.WINDOWS: Path(os.environ.get("ProgramFiles", "C:\\Program Files")) / "AiCippy",  # noqa: SIM112 -- Windows env var is case-sensitive
    OperatingSystem.MACOS: Path("/usr/local/opt/aicippy"),
    OperatingSystem.LINUX: Path("/opt/aicippy"),
}

# User-level installation paths
USER_INSTALL_PATHS: Final[dict[OperatingSystem, Path]] = {
    OperatingSystem.WINDOWS: Path(os.environ.get("LOCALAPPDATA", "~")) / "Programs" / "AiCippy",
    OperatingSystem.MACOS: Path("~/Library/Application Support/AiCippy"),
    OperatingSystem.LINUX: Path("~/.local/share/aicippy"),
}

# Cache/temp directory paths
CACHE_PATHS: Final[dict[OperatingSystem, Path]] = {
    OperatingSystem.WINDOWS: Path(os.environ.get("TEMP", "C:\\Temp")) / "aicippy",
    OperatingSystem.MACOS: Path("~/Library/Caches/aicippy"),
    OperatingSystem.LINUX: Path("~/.cache/aicippy"),
}

# Config directory paths
CONFIG_PATHS: Final[dict[OperatingSystem, Path]] = {
    OperatingSystem.WINDOWS: Path(os.environ.get("APPDATA", "~")) / "AiCippy",
    OperatingSystem.MACOS: Path("~/.aicippy"),
    OperatingSystem.LINUX: Path("~/.aicippy"),
}

# Minimum Python version
MIN_PYTHON_VERSION: Final[tuple[int, int]] = (3, 11)


@dataclass
class PlatformInfo:
    """Complete platform information."""

    os_type: OperatingSystem
    os_name: str
    os_version: str
    architecture: Architecture
    shell_type: ShellType
    python_version: tuple[int, int, int]
    python_path: Path
    pip_path: Path | None
    is_admin: bool
    home_dir: Path
    install_path: Path
    user_install_path: Path
    cache_path: Path
    config_path: Path
    path_separator: str
    env_path_key: str
    disk_space_gb: float = 0.0
    disk_space_sufficient: bool = False
    has_sudo: bool = False
    errors: list[str] = field(default_factory=list)

    @property
    def python_meets_minimum(self) -> bool:
        """Check if Python version meets minimum requirement."""
        return self.python_version[:2] >= MIN_PYTHON_VERSION

    @property
    def is_windows(self) -> bool:
        """Check if running on Windows."""
        return self.os_type == OperatingSystem.WINDOWS

    @property
    def is_macos(self) -> bool:
        """Check if running on macOS."""
        return self.os_type == OperatingSystem.MACOS

    @property
    def is_linux(self) -> bool:
        """Check if running on Linux."""
        return self.os_type == OperatingSystem.LINUX

    @property
    def is_unix(self) -> bool:
        """Check if running on Unix-like system."""
        return self.os_type in (OperatingSystem.MACOS, OperatingSystem.LINUX)


def detect_os() -> OperatingSystem:
    """Detect the operating system."""
    system = platform.system().lower()
    return {
        "windows": OperatingSystem.WINDOWS,
        "darwin": OperatingSystem.MACOS,
        "linux": OperatingSystem.LINUX,
    }.get(system, OperatingSystem.UNKNOWN)


def detect_architecture() -> Architecture:
    """Detect CPU architecture."""
    machine = platform.machine().lower()
    if machine in ("x86_64", "amd64"):
        return Architecture.X86_64
    if machine in ("arm64", "aarch64"):
        return Architecture.ARM64
    if machine in ("i386", "i686", "x86"):
        return Architecture.X86
    if machine.startswith("arm"):
        return Architecture.ARM
    return Architecture.UNKNOWN


def detect_shell() -> ShellType:
    """Detect the current shell type."""
    os_type = detect_os()

    if os_type == OperatingSystem.WINDOWS:
        # Check for PowerShell
        if os.environ.get("PSModulePath"):  # noqa: SIM112 -- Windows env var is case-sensitive
            return ShellType.POWERSHELL
        return ShellType.CMD

    # Unix-like systems - extract the shell basename for precise matching
    shell_path = os.environ.get("SHELL", "").lower()
    shell_name = Path(shell_path).name if shell_path else ""
    if shell_name == "zsh":
        return ShellType.ZSH
    if shell_name == "bash":
        return ShellType.BASH
    if shell_name == "fish":
        return ShellType.FISH
    if shell_name in ("sh", "dash"):
        return ShellType.SH
    return ShellType.UNKNOWN


def detect_pip_path() -> Path | None:
    """Find pip executable path."""
    # Try pip3 first, then pip
    for pip_name in ("pip3", "pip"):
        pip_path = shutil.which(pip_name)
        if pip_path:
            return Path(pip_path)
    return None


def check_admin_privileges() -> bool:
    """Check if running with admin/root privileges."""
    os_type = detect_os()

    if os_type == OperatingSystem.WINDOWS:
        try:
            return ctypes.windll.shell32.IsUserAnAdmin() != 0  # type: ignore[attr-defined]  # windll is Windows-only; no stubs
        except Exception:
            return False
    else:
        return os.geteuid() == 0


def check_sudo_available() -> bool:
    """Check if sudo is available on Unix systems."""
    os_type = detect_os()
    if os_type == OperatingSystem.WINDOWS:
        return False
    return shutil.which("sudo") is not None


def get_python_version() -> tuple[int, int, int]:
    """Get current Python version as tuple."""
    return (sys.version_info.major, sys.version_info.minor, sys.version_info.micro)


# Minimum disk space required in GB
MIN_DISK_SPACE_GB: Final[float] = 0.5


def check_disk_space(install_path: Path | None = None) -> tuple[float, bool]:
    """Check available disk space on the installation path.

    Uses shutil.disk_usage() for cross-platform disk space detection.

    Args:
        install_path: Path to check disk space for. If None or non-existent,
                      falls back to the user's home directory.

    Returns:
        Tuple of (available_gb, sufficient) where sufficient means
        >= MIN_DISK_SPACE_GB (0.5 GB) free.

    """
    try:
        # Use the install path if it exists, otherwise fall back to home
        check_path = install_path if install_path and install_path.exists() else Path.home()
        usage = shutil.disk_usage(check_path)
        available_gb = usage.free / (1024**3)
        sufficient = available_gb >= MIN_DISK_SPACE_GB
        return available_gb, sufficient
    except OSError:
        # If disk_usage fails, return 0 and insufficient
        return 0.0, False


def detect_platform() -> PlatformInfo:
    """Detect complete platform information.

    Returns:
        PlatformInfo with all detected system information.

    """
    os_type = detect_os()
    errors: list[str] = []

    # Get paths with expansion
    home_dir = Path.home()

    install_path = DEFAULT_INSTALL_PATHS.get(os_type, Path("/opt/aicippy"))
    user_install_path = USER_INSTALL_PATHS.get(
        os_type,
        home_dir / ".local" / "share" / "aicippy",
    ).expanduser()
    cache_path = CACHE_PATHS.get(os_type, home_dir / ".cache" / "aicippy").expanduser()
    config_path = CONFIG_PATHS.get(os_type, home_dir / ".aicippy").expanduser()

    # Detect pip
    pip_path = detect_pip_path()
    if pip_path is None:
        errors.append("pip not found in PATH")

    # Python version check
    py_version = get_python_version()
    if py_version[:2] < MIN_PYTHON_VERSION:
        errors.append(
            f"Python {MIN_PYTHON_VERSION[0]}.{MIN_PYTHON_VERSION[1]}+ required, found {py_version[0]}.{py_version[1]}",
        )

    # Disk space check
    disk_space_gb, disk_space_sufficient = check_disk_space(user_install_path)

    return PlatformInfo(
        os_type=os_type,
        os_name=platform.system(),
        os_version=platform.release(),
        architecture=detect_architecture(),
        shell_type=detect_shell(),
        python_version=py_version,
        python_path=Path(sys.executable),
        pip_path=pip_path,
        is_admin=check_admin_privileges(),
        home_dir=home_dir,
        install_path=install_path,
        user_install_path=user_install_path,
        cache_path=cache_path,
        config_path=config_path,
        path_separator=";" if os_type == OperatingSystem.WINDOWS else ":",
        env_path_key="Path" if os_type == OperatingSystem.WINDOWS else "PATH",
        disk_space_gb=disk_space_gb,
        disk_space_sufficient=disk_space_sufficient,
        has_sudo=check_sudo_available(),
        errors=errors,
    )


def get_shell_profile_path(platform_info: PlatformInfo) -> Path | None:
    """Get the shell profile path for adding to PATH.

    Args:
        platform_info: Platform information.

    Returns:
        Path to shell profile or None if not applicable.

    """
    if platform_info.is_windows:
        return None  # Windows uses registry

    home = platform_info.home_dir
    shell = platform_info.shell_type

    profiles = {
        ShellType.ZSH: home / ".zshrc",
        ShellType.BASH: home / ".bashrc",
        ShellType.FISH: home / ".config" / "fish" / "config.fish",
        ShellType.SH: home / ".profile",
    }

    # Check for .bash_profile on macOS
    if platform_info.is_macos and shell == ShellType.BASH:
        bash_profile = home / ".bash_profile"
        if bash_profile.exists():
            return bash_profile

    return profiles.get(shell, home / ".profile")


# Dangerous shell patterns that must never appear in shell commands.
# These prevent command injection when shell=True is required (e.g. for &&).
_DANGEROUS_SHELL_PATTERNS: Final[tuple[re.Pattern[str], ...]] = (
    re.compile(r"\$\("),  # Command substitution $(...)
    re.compile(r"`"),  # Backtick command substitution
    re.compile(r"\$\{"),  # Variable expansion ${...}
    re.compile(r"\.\./\.\./"),  # Path traversal
    re.compile(r";\s*rm\s"),  # Chained rm
    re.compile(r"\brm\s+-rf\s+/"),  # Destructive rm
    re.compile(r">\s*/dev/"),  # Writing to device files
    re.compile(r"curl.*\|\s*(ba)?sh", re.IGNORECASE),  # Pipe to shell
    re.compile(r"wget.*\|\s*(ba)?sh", re.IGNORECASE),  # Pipe to shell
)


def run_shell_command(
    command: str | list[str],
    platform_info: PlatformInfo,
    capture_output: bool = True,
    check: bool = False,
    timeout: int | None = 60,
) -> subprocess.CompletedProcess[str]:
    """Run a shell command appropriate for the platform.

    When *command* is a string on Unix, ``shell=True`` is used to support
    compound commands (e.g. ``&&``). The string is validated against known
    dangerous patterns before execution to mitigate shell injection risks.

    Args:
        command: Command to run (string or list).
        platform_info: Platform information.
        capture_output: Whether to capture stdout/stderr.
        check: Whether to raise on non-zero exit.
        timeout: Timeout in seconds.

    Returns:
        CompletedProcess result.

    Raises:
        ValueError: If a string command contains dangerous shell patterns.

    """
    shell = False

    if platform_info.is_windows:
        if platform_info.shell_type == ShellType.POWERSHELL:
            if isinstance(command, str):
                cmd: str | list[str] = ["powershell", "-NoProfile", "-Command", command]
            else:
                cmd = ["powershell", "-NoProfile", "-Command", *command]
        else:
            cmd = ["cmd", "/c", command] if isinstance(command, str) else ["cmd", "/c", *command]
    elif isinstance(command, str):
        # Validate against dangerous patterns before using shell=True
        for pattern in _DANGEROUS_SHELL_PATTERNS:
            if pattern.search(command):
                raise ValueError(
                    f"Shell command rejected: matches dangerous pattern '{pattern.pattern}'",
                )
        cmd = command
        shell = True
    else:
        cmd = command
        shell = False

    return subprocess.run(  # nosec B602
        cmd,
        capture_output=capture_output,
        text=True,
        check=check,
        timeout=timeout,
        shell=shell,  # nosec B602
    )


def format_path_for_shell(path: Path, platform_info: PlatformInfo) -> str:
    """Format a path appropriately for the shell.

    Args:
        path: Path to format.
        platform_info: Platform information.

    Returns:
        Formatted path string.

    """
    if platform_info.is_windows:
        return str(path).replace("/", "\\")
    return str(path)
