"""Helpers for qpip virtual environment commands."""

from __future__ import annotations

import json
import os
import platform
import shlex
import subprocess
import sys
from difflib import get_close_matches
from pathlib import Path
from typing import Mapping, Optional, Sequence, TextIO

from .core import format_command
from .project_config import load_project_config, load_project_config_safe

DEFAULT_VENV_DIRNAME = ".venv"
ACTIVE_VENV_ENV_VARS = ("VIRTUAL_ENV", "CONDA_PREFIX")

SHELL_LINUX = "linux"
SHELL_MACOS = "macos"
SHELL_WINDOWS_POWERSHELL = "windows-powershell"
SHELL_WINDOWS_BASH = "windows-bash"
SHELL_WINDOWS_CMD = "windows-cmd"
DEFAULT_GITIGNORE_FILENAME = ".gitignore"
DEFAULT_PROJECT_MARKERS = ("pyproject.toml", ".git")

SHELL_OVERRIDE_ALIASES = {
    "linux": SHELL_LINUX,
    "mac": SHELL_MACOS,
    "macos": SHELL_MACOS,
    "darwin": SHELL_MACOS,
    "powershell": SHELL_WINDOWS_POWERSHELL,
    "pwsh": SHELL_WINDOWS_POWERSHELL,
    "windows-powershell": SHELL_WINDOWS_POWERSHELL,
    "bash": SHELL_WINDOWS_BASH,
    "git-bash": SHELL_WINDOWS_BASH,
    "windows-bash": SHELL_WINDOWS_BASH,
    "cmd": SHELL_WINDOWS_CMD,
    "windows-cmd": SHELL_WINDOWS_CMD,
}


class VenvUsageError(ValueError):
    """Raised when qpip virtual-environment usage is invalid."""


def print_json(data: object, output: TextIO) -> None:
    """Render structured command output as JSON."""
    print(json.dumps(data, ensure_ascii=False, indent=2), file=output)


def suggest_subcommand(value: str, choices: Sequence[str]) -> str:
    """Return a friendly close-match hint for mistyped subcommands."""
    matches = get_close_matches(value, list(choices), n=3, cutoff=0.45)
    if not matches:
        return ""
    return f"；你是不是想用: {', '.join(matches)}"


def build_venv_gitignore_entry(
    venv_dirname: str = DEFAULT_VENV_DIRNAME,
) -> str:
    """Build the .gitignore entry for the project virtual environment."""
    return f"{venv_dirname}/"


def build_venv_command(
    python_executable: Optional[str] = None,
    venv_dirname: str = DEFAULT_VENV_DIRNAME,
) -> list[str]:
    """Build the command used to create the default virtual environment."""
    return [python_executable or sys.executable, "-m", "venv", venv_dirname]


def gitignore_has_venv_entry(
    content: str,
    venv_dirname: str = DEFAULT_VENV_DIRNAME,
) -> bool:
    """Return whether the .gitignore content already covers the venv directory."""
    variants = {
        venv_dirname,
        build_venv_gitignore_entry(venv_dirname),
        f"/{venv_dirname}",
        f"/{venv_dirname}/",
    }

    for raw_line in content.splitlines():
        candidate = raw_line.strip()
        if not candidate or candidate.startswith(("#", "!")):
            continue
        if candidate.replace("\\", "/") in variants:
            return True
    return False


def ensure_gitignore_contains_venv(
    cwd: Path,
    venv_dirname: str = DEFAULT_VENV_DIRNAME,
) -> bool:
    """Ensure the project .gitignore includes the default virtualenv directory."""
    gitignore_path = cwd / DEFAULT_GITIGNORE_FILENAME
    entry = build_venv_gitignore_entry(venv_dirname)

    if gitignore_path.exists():
        content = gitignore_path.read_text(encoding="utf-8")
        if gitignore_has_venv_entry(content, venv_dirname=venv_dirname):
            return False

        separator = "" if not content or content.endswith("\n") else "\n"
        gitignore_path.write_text(f"{content}{separator}{entry}\n", encoding="utf-8")
        return True

    gitignore_path.write_text(f"{entry}\n", encoding="utf-8")
    return True


def get_project_venv_path(
    cwd: Path,
    venv_dirname: Optional[str] = None,
) -> Path:
    """Return the expected project virtual-environment path."""
    config_result = load_project_config_safe(cwd)
    resolved_dirname = venv_dirname or config_result.config.venv_dirname or DEFAULT_VENV_DIRNAME
    return cwd / resolved_dirname


def find_project_root(
    cwd: Path,
    markers: Sequence[str] = DEFAULT_PROJECT_MARKERS,
) -> Optional[Path]:
    """Find the nearest parent directory that looks like a project root."""
    current = cwd.resolve()

    for candidate in (current, *current.parents):
        if any((candidate / marker).exists() for marker in markers):
            return candidate
    return None


def require_project_root(cwd: Path) -> Path:
    """Return the current project root or raise a usage error."""
    project_root = find_project_root(cwd)
    if project_root is None:
        raise VenvUsageError("当前目录及其上级目录未找到 pyproject.toml 或 .git")
    return project_root


def get_env_bin_dir(
    cwd: Path,
    venv_dirname: Optional[str] = None,
    os_name: Optional[str] = None,
) -> Path:
    """Return the scripts/bin directory for the project environment."""
    current_os = os.name if os_name is None else os_name
    venv_path = get_project_venv_path(cwd, venv_dirname=venv_dirname)
    if current_os == "nt":
        return venv_path / "Scripts"
    return venv_path / "bin"


def get_env_python_path(
    cwd: Path,
    venv_dirname: Optional[str] = None,
    os_name: Optional[str] = None,
) -> Path:
    """Return the Python executable path for the project environment."""
    bin_dir = get_env_bin_dir(cwd, venv_dirname=venv_dirname, os_name=os_name)
    current_os = os.name if os_name is None else os_name
    return bin_dir / ("python.exe" if current_os == "nt" else "python")


def get_env_pip_path(
    cwd: Path,
    venv_dirname: Optional[str] = None,
    os_name: Optional[str] = None,
) -> Path:
    """Return the pip executable path for the project environment."""
    bin_dir = get_env_bin_dir(cwd, venv_dirname=venv_dirname, os_name=os_name)
    current_os = os.name if os_name is None else os_name
    return bin_dir / ("pip.exe" if current_os == "nt" else "pip")


def project_env_exists(
    cwd: Path,
    venv_dirname: Optional[str] = None,
) -> bool:
    """Return whether the project environment directory exists."""
    return get_project_venv_path(cwd, venv_dirname=venv_dirname).exists()


def build_env_execution_env(
    cwd: Path,
    env: Optional[Mapping[str, str]] = None,
    venv_dirname: Optional[str] = None,
    os_name: Optional[str] = None,
) -> dict[str, str]:
    """Build environment variables that prioritize the project virtualenv."""
    env_map = dict(os.environ if env is None else env)
    venv_path = get_project_venv_path(cwd, venv_dirname=venv_dirname)
    bin_dir = get_env_bin_dir(cwd, venv_dirname=venv_dirname, os_name=os_name)
    current_path = env_map.get("PATH", "")
    env_map["VIRTUAL_ENV"] = str(venv_path)
    env_map["PATH"] = str(bin_dir) if not current_path else os.pathsep.join([str(bin_dir), current_path])
    return env_map


def get_active_virtualenv_path(
    env: Optional[Mapping[str, str]] = None,
) -> Optional[Path]:
    """Return the currently activated virtual-environment path if available."""
    env_map = os.environ if env is None else env

    for env_name in ACTIVE_VENV_ENV_VARS:
        raw_value = env_map.get(env_name, "").strip()
        if raw_value:
            return Path(raw_value).expanduser()
    return None


def normalize_filesystem_path(path: Path) -> str:
    """Normalize a path for reliable cross-platform comparisons."""
    return os.path.normcase(os.path.normpath(str(path.expanduser().resolve(strict=False))))


def is_project_venv_active(
    cwd: Path,
    env: Optional[Mapping[str, str]] = None,
    venv_dirname: Optional[str] = None,
) -> bool:
    """Return whether the current shell session has this project's venv active."""
    active_path = get_active_virtualenv_path(env=env)
    if active_path is None:
        return False

    project_venv_path = get_project_venv_path(cwd, venv_dirname=venv_dirname)
    return normalize_filesystem_path(active_path) == normalize_filesystem_path(
        project_venv_path
    )


def detect_parent_process_name(os_name: Optional[str] = None) -> str:
    """Best-effort parent process detection for shell selection."""
    current_os = os.name if os_name is None else os_name
    parent_pid = os.getppid()

    if current_os == "nt":
        result = subprocess.run(
            ["tasklist", "/fi", f"PID eq {parent_pid}", "/fo", "csv", "/nh"],
            capture_output=True,
            check=False,
            text=True,
        )
        output = result.stdout.strip().strip('"')
        if not output or output.startswith("INFO:"):
            return ""
        return output.split('","', 1)[0]

    result = subprocess.run(
        ["ps", "-p", str(parent_pid), "-o", "comm="],
        capture_output=True,
        check=False,
        text=True,
    )
    return result.stdout.strip()


def detect_shell(
    env: Optional[Mapping[str, str]] = None,
    platform_system: Optional[str] = None,
    os_name: Optional[str] = None,
    parent_process_name: Optional[str] = None,
) -> str:
    """Detect the current session shell for activation snippets."""
    env_map = os.environ if env is None else env
    platform_name = platform.system() if platform_system is None else platform_system
    current_os = os.name if os_name is None else os_name

    override = env_map.get("QPIP_SHELL", "").strip().lower()
    if override:
        try:
            return SHELL_OVERRIDE_ALIASES[override]
        except KeyError as exc:
            choices = ", ".join(sorted(SHELL_OVERRIDE_ALIASES))
            raise VenvUsageError(
                f"QPIP_SHELL={override!r} 无效，可用值: {choices}"
            ) from exc

    if current_os != "nt":
        if platform_name == "Darwin":
            return SHELL_MACOS
        return SHELL_LINUX

    resolved_parent_name = parent_process_name if parent_process_name is not None else detect_parent_process_name(os_name=current_os)
    parent_name = resolved_parent_name.lower()
    if parent_name.endswith(".exe"):
        parent_name = parent_name[:-4]

    if parent_name in {"bash", "sh", "zsh"}:
        return SHELL_WINDOWS_BASH
    if parent_name in {"powershell", "pwsh"}:
        return SHELL_WINDOWS_POWERSHELL
    if parent_name == "cmd":
        return SHELL_WINDOWS_CMD

    shell_value = env_map.get("SHELL", "").lower()
    if env_map.get("MSYSTEM") or "bash" in shell_value or "zsh" in shell_value:
        return SHELL_WINDOWS_BASH
    if env_map.get("PSModulePath") or env_map.get("POWERSHELL_DISTRIBUTION_CHANNEL"):
        return SHELL_WINDOWS_POWERSHELL
    return SHELL_WINDOWS_CMD


def build_activation_command(
    shell_kind: str,
    venv_dirname: str = DEFAULT_VENV_DIRNAME,
) -> str:
    """Build the shell snippet used to activate the virtual environment."""
    if shell_kind in {SHELL_LINUX, SHELL_MACOS}:
        return f"source {shlex.quote(f'{venv_dirname}/bin/activate')}"
    if shell_kind == SHELL_WINDOWS_BASH:
        return f"source {shlex.quote(f'{venv_dirname}/Scripts/activate')}"
    if shell_kind == SHELL_WINDOWS_POWERSHELL:
        return f"& .\\{venv_dirname}\\Scripts\\Activate.ps1"
    if shell_kind == SHELL_WINDOWS_CMD:
        return f"call .\\{venv_dirname}\\Scripts\\activate.bat"
    raise VenvUsageError(f"不支持的 shell 类型: {shell_kind}")


def build_deactivate_command(shell_kind: str) -> str:
    """Build the shell snippet used to deactivate the current virtual environment."""
    if shell_kind in {SHELL_LINUX, SHELL_MACOS, SHELL_WINDOWS_BASH}:
        return "if command -v deactivate >/dev/null 2>&1; then deactivate; fi"
    if shell_kind == SHELL_WINDOWS_POWERSHELL:
        return "if (Get-Command deactivate -ErrorAction SilentlyContinue) { deactivate }"
    if shell_kind == SHELL_WINDOWS_CMD:
        return "if defined VIRTUAL_ENV call deactivate"
    raise VenvUsageError(f"不支持的 shell 类型: {shell_kind}")


def get_activation_script_path(
    cwd: Path,
    shell_kind: str,
    venv_dirname: Optional[str] = None,
) -> Path:
    """Resolve the activation script path for the selected shell."""
    config_result = load_project_config_safe(cwd)
    resolved_dirname = venv_dirname or config_result.config.venv_dirname or DEFAULT_VENV_DIRNAME
    if shell_kind in {SHELL_LINUX, SHELL_MACOS}:
        return cwd / resolved_dirname / "bin" / "activate"
    if shell_kind == SHELL_WINDOWS_BASH:
        return cwd / resolved_dirname / "Scripts" / "activate"
    if shell_kind == SHELL_WINDOWS_POWERSHELL:
        return cwd / resolved_dirname / "Scripts" / "Activate.ps1"
    if shell_kind == SHELL_WINDOWS_CMD:
        return cwd / resolved_dirname / "Scripts" / "activate.bat"
    raise VenvUsageError(f"不支持的 shell 类型: {shell_kind}")


def run_venv_command(
    args: Sequence[str],
    cwd: Path,
    output: TextIO = sys.stdout,
    dry_run: bool = False,
    python_executable: Optional[str] = None,
    venv_dirname: Optional[str] = None,
) -> int:
    """Create the default .venv in the current project directory."""
    if tuple(args) not in {("venv",), ("env", "create")}:
        raise VenvUsageError("env create 暂不支持额外参数")

    config_result = load_project_config_safe(cwd)
    resolved_dirname = venv_dirname or config_result.config.venv_dirname or DEFAULT_VENV_DIRNAME
    command = build_venv_command(python_executable=python_executable, venv_dirname=resolved_dirname)
    print(f"[qpip] {format_command(command)}", file=output)

    if dry_run:
        return 0

    try:
        ensure_gitignore_contains_venv(cwd, venv_dirname=resolved_dirname)
    except OSError as exc:
        raise VenvUsageError(f"更新 .gitignore 失败: {exc}") from exc

    result = subprocess.run(command, cwd=str(cwd), check=False)
    return result.returncode


def run_env_command(
    args: Sequence[str],
    cwd: Path,
    output: TextIO = sys.stdout,
    env: Optional[Mapping[str, str]] = None,
    dry_run: bool = False,
    python_executable: Optional[str] = None,
    platform_system: Optional[str] = None,
    os_name: Optional[str] = None,
    parent_process_name: Optional[str] = None,
    json_output: bool = False,
) -> int:
    """Handle `qpip env ...` subcommands."""
    if not args or args[0] != "env":
        raise VenvUsageError("env 命令格式无效")

    if len(args) < 2:
        raise VenvUsageError("用法: qpip env <create|use|current|activated|path|python|pip|list|deactivate>")

    project_root = find_project_root(cwd) or cwd.resolve()
    configured_venv_dirname = load_project_config_safe(project_root).config.venv_dirname
    subcommand = args[1]
    sub_args = tuple(args[:2])

    if subcommand == "create":
        if len(args) != 2:
            raise VenvUsageError("env create 暂不支持额外参数")
        return run_venv_command(
            sub_args,
            cwd=project_root,
            output=output,
            dry_run=dry_run,
            python_executable=python_executable,
            venv_dirname=configured_venv_dirname,
        )

    if subcommand == "use":
        if len(args) != 2:
            raise VenvUsageError("env use 暂不支持额外参数")
        return run_activate_command(
            ("activate",),
            cwd=project_root,
            output=output,
            env=env,
            platform_system=platform_system,
            os_name=os_name,
            parent_process_name=parent_process_name,
        )

    if subcommand == "deactivate":
        if len(args) != 2:
            raise VenvUsageError("env deactivate 暂不支持额外参数")
        return run_deactivate_command(
            ("deactivate",),
            cwd=project_root,
            output=output,
            env=env,
            platform_system=platform_system,
            os_name=os_name,
            parent_process_name=parent_process_name,
        )

    if subcommand == "activated":
        if len(args) != 2:
            raise VenvUsageError("env activated 暂不支持额外参数")
        if json_output:
            active_path = get_active_virtualenv_path(env=env)
            is_active = is_project_venv_active(project_root, env=env)
            print_json(
                {
                    "project": str(project_root),
                    "env": str(get_project_venv_path(project_root)),
                    "active_env": str(active_path) if active_path is not None else None,
                    "activated": is_active,
                },
                output,
            )
            return 0 if is_active else 1
        return run_active_command(("activated",), cwd=project_root, output=output, env=env)

    if subcommand == "path":
        if len(args) != 2:
            raise VenvUsageError("env path 暂不支持额外参数")
        env_path = get_project_venv_path(project_root, venv_dirname=configured_venv_dirname)
        if json_output:
            print_json({"path": str(env_path)}, output)
            return 0
        print(env_path, file=output)
        return 0

    if subcommand == "python":
        if len(args) != 2:
            raise VenvUsageError("env python 暂不支持额外参数")
        python_path = get_env_python_path(project_root, venv_dirname=configured_venv_dirname, os_name=os_name)
        if json_output:
            print_json({"python": str(python_path)}, output)
            return 0
        print(python_path, file=output)
        return 0

    if subcommand == "pip":
        if len(args) != 2:
            raise VenvUsageError("env pip 暂不支持额外参数")
        pip_path = get_env_pip_path(project_root, venv_dirname=configured_venv_dirname, os_name=os_name)
        if json_output:
            print_json({"pip": str(pip_path)}, output)
            return 0
        print(pip_path, file=output)
        return 0

    if subcommand == "list":
        if len(args) != 2:
            raise VenvUsageError("env list 暂不支持额外参数")
        venv_path = get_project_venv_path(project_root, venv_dirname=configured_venv_dirname)
        if project_env_exists(project_root, venv_dirname=configured_venv_dirname):
            marker = "*" if is_project_venv_active(project_root, env=env) else " "
            if json_output:
                print_json(
                    [
                        {
                            "name": venv_path.name,
                            "path": str(venv_path),
                            "active": marker == "*",
                        }
                    ],
                    output,
                )
                return 0
            print(f"{marker} {venv_path.name:<12} {venv_path}", file=output)
            return 0
        if json_output:
            print_json([], output)
            return 1
        print("当前项目尚未创建虚拟环境", file=output)
        return 1

    if subcommand == "current":
        if len(args) != 2:
            raise VenvUsageError("env current 暂不支持额外参数")
        venv_path = get_project_venv_path(project_root, venv_dirname=configured_venv_dirname)
        python_path = get_env_python_path(project_root, venv_dirname=configured_venv_dirname, os_name=os_name)
        status = "active" if is_project_venv_active(project_root, env=env) else "inactive"
        if json_output:
            print_json(
                {
                    "project": str(project_root),
                    "env": str(venv_path),
                    "python": str(python_path),
                    "status": status,
                },
                output,
            )
            return 0
        print(f"project: {project_root}", file=output)
        print(f"env: {venv_path}", file=output)
        print(f"python: {python_path}", file=output)
        print(f"status: {status}", file=output)
        return 0

    raise VenvUsageError(
        "不支持的 env 子命令: "
        f"{subcommand}{suggest_subcommand(subcommand, ('create', 'use', 'current', 'activated', 'path', 'python', 'pip', 'list', 'deactivate'))}"
    )


def run_activate_command(
    args: Sequence[str],
    cwd: Path,
    output: TextIO = sys.stdout,
    env: Optional[Mapping[str, str]] = None,
    platform_system: Optional[str] = None,
    os_name: Optional[str] = None,
    parent_process_name: Optional[str] = None,
) -> int:
    """Print an activation snippet for the current shell session."""
    if tuple(args) != ("activate",):
        raise VenvUsageError("activate 暂不支持额外参数")

    shell_kind = detect_shell(
        env=env,
        platform_system=platform_system,
        os_name=os_name,
        parent_process_name=parent_process_name,
    )
    activation_script = get_activation_script_path(cwd, shell_kind)
    if not activation_script.exists():
        raise VenvUsageError(
            f"未找到虚拟环境激活脚本: {activation_script}; 请先运行 `qpip env create`"
        )

    configured_venv_dirname = load_project_config_safe(cwd).config.venv_dirname or DEFAULT_VENV_DIRNAME
    print(build_activation_command(shell_kind, venv_dirname=configured_venv_dirname), file=output)
    return 0


def run_active_command(
    args: Sequence[str],
    cwd: Path,
    output: TextIO = sys.stdout,
    env: Optional[Mapping[str, str]] = None,
) -> int:
    """Report whether the current project virtual environment is active."""
    if tuple(args) != ("activated",):
        raise VenvUsageError("activated 暂不支持额外参数")

    active_path = get_active_virtualenv_path(env=env)
    if active_path is None:
        print("当前未激活任何虚拟环境", file=output)
        return 1

    if is_project_venv_active(cwd, env=env):
        print(f"当前项目虚拟环境已激活: {active_path}", file=output)
        return 0

    print(f"当前激活的是其他虚拟环境: {active_path}", file=output)
    return 1


def run_deactivate_command(
    args: Sequence[str],
    cwd: Path,
    output: TextIO = sys.stdout,
    env: Optional[Mapping[str, str]] = None,
    platform_system: Optional[str] = None,
    os_name: Optional[str] = None,
    parent_process_name: Optional[str] = None,
) -> int:
    """Print a deactivation snippet for the current shell session."""
    del cwd

    if tuple(args) != ("deactivate",):
        raise VenvUsageError("deactivate 暂不支持额外参数")

    shell_kind = detect_shell(
        env=env,
        platform_system=platform_system,
        os_name=os_name,
        parent_process_name=parent_process_name,
    )
    print(build_deactivate_command(shell_kind), file=output)
    return 0
