"""High-level project-oriented CLI commands for qpip."""

from __future__ import annotations

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

from .core import (
    MIRRORS,
    format_command,
    format_mirrors,
    get_default_mirror_settings,
    normalize_mirror_name,
)
from .project_config import (
    ProjectConfigError,
    load_project_config,
    load_project_config_safe,
    update_project_config,
)
from .project_run import load_project_scripts
from .project_venv import (
    build_env_execution_env,
    find_project_root,
    get_active_virtualenv_path,
    get_env_pip_path,
    get_env_python_path,
    get_project_venv_path,
    is_project_venv_active,
    project_env_exists,
)


class ProjectCLIUsageError(ValueError):
    """Raised when a project-oriented qpip command is used incorrectly."""


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 supports_color(output: TextIO, env: Optional[Mapping[str, str]] = None) -> bool:
    """Return whether terminal styling should be enabled."""
    env_map = os.environ if env is None else env
    if env_map.get("NO_COLOR"):
        return False
    isatty = getattr(output, "isatty", None)
    return bool(isatty and isatty())


def style(text: str, code: str, enabled: bool) -> str:
    """Apply ANSI styling when supported."""
    if not enabled:
        return text
    return f"\033[{code}m{text}\033[0m"


def format_status_label(status: str, enabled: bool) -> str:
    """Render a doctor/info status label."""
    mapping = {
        "ok": ("OK", "32"),
        "warn": ("WARN", "33"),
        "info": ("INFO", "36"),
        "fail": ("FAIL", "31"),
    }
    text, code = mapping[status]
    return style(text, code, enabled)


def format_heading(title: str, enabled: bool) -> str:
    """Render a section heading."""
    return style(title, "1", enabled)


def format_bullet(symbol: str, enabled: bool, color: str) -> str:
    """Render a bullet or marker."""
    return style(symbol, color, enabled)


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 require_project_root(cwd: Path) -> Path:
    """Resolve the current project root."""
    project_root = find_project_root(cwd)
    if project_root is None:
        raise ProjectCLIUsageError("当前目录及其上级目录未找到 pyproject.toml 或 .git")
    return project_root


def resolve_project_root(cwd: Path) -> Path:
    """Resolve a usable project root, falling back to the current directory."""
    return find_project_root(cwd) or cwd.resolve()


def run_exec_command(
    args: Sequence[str],
    *,
    cwd: Path,
    output: TextIO = sys.stdout,
    env: Optional[Mapping[str, str]] = None,
    os_name: Optional[str] = None,
    dry_run: bool = False,
) -> int:
    """Run an arbitrary command inside the project environment."""
    if not args or args[0] != "exec":
        raise ProjectCLIUsageError("exec 命令格式无效")
    if len(args) < 2:
        raise ProjectCLIUsageError("用法: qpip exec <command...>")

    project_root = resolve_project_root(cwd)
    if not project_env_exists(project_root):
        raise ProjectCLIUsageError(
            f"当前项目尚未创建虚拟环境: {get_project_venv_path(project_root)}；"
            "请先运行 `qpip env create`，或改用 `qpip install ...` 仅执行 pip 操作"
        )

    command = list(args[1:])
    print(f"[qpip] {format_command(command)}", file=output)

    if dry_run:
        return 0

    execution_env = build_env_execution_env(project_root, env=env, os_name=os_name)
    result = subprocess.run(command, cwd=str(project_root), check=False, env=execution_env)
    return result.returncode


def run_info_command(
    args: Sequence[str],
    *,
    cwd: Path,
    output: TextIO = sys.stdout,
    env: Optional[Mapping[str, str]] = None,
    os_name: Optional[str] = None,
    json_output: bool = False,
) -> int:
    """Print a concise summary of the current project context."""
    if tuple(args) != ("info",):
        raise ProjectCLIUsageError("info 暂不支持额外参数")

    project_root = resolve_project_root(cwd)
    pyproject_path = project_root / "pyproject.toml"
    config_result = load_project_config_safe(project_root)
    project_config = config_result.config
    venv_path = get_project_venv_path(project_root)
    active_path = get_active_virtualenv_path(env=env)
    mirror, warning, mirror_source = get_default_mirror_settings(env=env, project_config=project_config)
    scripts = {}
    scripts_warning = None
    if pyproject_path.exists():
        try:
            scripts = load_project_scripts(pyproject_path)
        except ValueError as exc:
            scripts_warning = str(exc)

    info = {
        "project": str(project_root),
        "pyproject": str(pyproject_path) if pyproject_path.exists() else None,
        "env": str(venv_path),
        "env_exists": project_env_exists(project_root),
        "env_status": "active" if is_project_venv_active(project_root, env=env) else "inactive",
        "active_env": str(active_path) if active_path is not None else None,
        "python": str(get_env_python_path(project_root, os_name=os_name)),
        "pip": str(get_env_pip_path(project_root, os_name=os_name)),
        "mirror": {
            "name": mirror,
            "url": MIRRORS[mirror],
            "warning": warning,
            "source": mirror_source,
        },
        "config": {
            "venv_dirname": project_config.venv_dirname,
            "source_path": str(project_config.source_path) if project_config.source_path else None,
            "warning": config_result.warning,
        },
        "scripts": sorted(scripts),
        "scripts_warning": scripts_warning,
    }
    if json_output:
        print_json(info, output)
        return 0

    color_enabled = supports_color(output, env)
    heading_project = format_heading("Project", color_enabled)
    heading_env = format_heading("Environment", color_enabled)
    heading_mirror = format_heading("Mirror", color_enabled)
    heading_scripts = format_heading("Scripts", color_enabled)
    active_marker = format_bullet("●", color_enabled, "32") if info["env_status"] == "active" else format_bullet("○", color_enabled, "33")

    print(f"{heading_project}:", file=output)
    print(f"  root: {project_root}", file=output)
    print(f"  pyproject: {pyproject_path if pyproject_path.exists() else 'missing'}", file=output)
    print(f"{heading_env}:", file=output)
    print(f"  env: {venv_path}", file=output)
    print(f"  exists: {'yes' if project_env_exists(project_root) else 'no'}", file=output)
    print(f"  status: {active_marker} {info['env_status']}", file=output)
    print(f"  active_env: {active_path if active_path is not None else 'none'}", file=output)
    print(f"  python: {get_env_python_path(project_root, os_name=os_name)}", file=output)
    print(f"  pip: {get_env_pip_path(project_root, os_name=os_name)}", file=output)
    print(f"{heading_mirror}:", file=output)
    print(f"  current: {mirror} ({MIRRORS[mirror]})", file=output)
    print(f"  source: {mirror_source}", file=output)
    if warning:
        print(f"  warning: {warning}", file=output)
    print(f"{heading_scripts}:", file=output)
    print(f"  available: {', '.join(sorted(scripts)) if scripts else 'none'}", file=output)
    if scripts_warning:
        print(f"  warning: {scripts_warning}", file=output)
    if project_config.venv_dirname:
        print(f"  config: venv-dir={project_config.venv_dirname}", file=output)
    if config_result.warning:
        print(f"  config-warning: {config_result.warning}", file=output)
    return 0


def run_doctor_command(
    args: Sequence[str],
    *,
    cwd: Path,
    output: TextIO = sys.stdout,
    env: Optional[Mapping[str, str]] = None,
    os_name: Optional[str] = None,
    json_output: bool = False,
) -> int:
    """Diagnose the current project setup."""
    if tuple(args) != ("doctor",):
        raise ProjectCLIUsageError("doctor 暂不支持额外参数")

    project_root = find_project_root(cwd)
    config_result = load_project_config_safe(cwd)
    project_config = config_result.config
    mirror, warning, mirror_source = get_default_mirror_settings(env=env, project_config=project_config)
    issues = 0
    checks: list[dict[str, object]] = []

    if project_root is None:
        checks.append(
            {
                "name": "project_root",
                "status": "fail",
                "message": "当前目录及其上级目录未找到 pyproject.toml 或 .git",
            }
        )
        checks.append(
            {
                "name": "mirror",
                "status": "ok",
                "message": f"{mirror} ({MIRRORS[mirror]}) [source={mirror_source}]",
            }
        )
        if warning:
            checks.append({"name": "mirror_warning", "status": "warn", "message": warning})
            issues += 1
        if json_output:
            print_json({"ok": False, "checks": checks}, output)
            return 1
        color_enabled = supports_color(output, env)
        print(f"{format_status_label('fail', color_enabled)} project_root 当前目录及其上级目录未找到 pyproject.toml 或 .git", file=output)
        print(f"{format_status_label('ok', color_enabled)} mirror {mirror} ({MIRRORS[mirror]}) [source={mirror_source}]", file=output)
        if warning:
            print(f"{format_status_label('warn', color_enabled)} mirror {warning}", file=output)
        print(style("Summary:", "1", color_enabled), "1 fail", file=output)
        return 1

    color_enabled = supports_color(output, env)

    pyproject_path = project_root / "pyproject.toml"
    if pyproject_path.exists():
        checks.append({"name": "pyproject", "status": "ok", "message": str(pyproject_path)})
        if not json_output:
            print(f"{format_status_label('ok', color_enabled)} pyproject {pyproject_path}", file=output)
    else:
        checks.append({"name": "pyproject", "status": "warn", "message": f"{pyproject_path} 不存在"})
        if not json_output:
            print(f"{format_status_label('warn', color_enabled)} pyproject {pyproject_path} 不存在", file=output)
        issues += 1

    venv_path = get_project_venv_path(project_root)
    if project_env_exists(project_root):
        checks.append({"name": "env", "status": "ok", "message": str(venv_path)})
        if not json_output:
            print(f"{format_status_label('ok', color_enabled)} env {venv_path}", file=output)
    else:
        checks.append({"name": "env", "status": "warn", "message": f"{venv_path} 不存在，建议运行 `qpip env create`"})
        if not json_output:
            print(f"{format_status_label('warn', color_enabled)} env {venv_path} 不存在，建议运行 `qpip env create`", file=output)
        issues += 1

    env_python = get_env_python_path(project_root, os_name=os_name)
    if env_python.exists():
        checks.append({"name": "python", "status": "ok", "message": str(env_python)})
        if not json_output:
            print(f"{format_status_label('ok', color_enabled)} python {env_python}", file=output)
    else:
        checks.append({"name": "python", "status": "warn", "message": f"{env_python} 不存在"})
        if not json_output:
            print(f"{format_status_label('warn', color_enabled)} python {env_python} 不存在", file=output)
        issues += 1

    is_active = is_project_venv_active(project_root, env=env)
    checks.append(
        {
            "name": "activated",
            "status": "ok" if is_active else "info",
            "message": "当前项目虚拟环境已激活" if is_active else "当前项目虚拟环境未激活；`qpip run` 和 `qpip exec` 仍会优先使用项目环境",
        }
    )
    if not json_output:
        print(
            f"{format_status_label('ok' if is_active else 'info', color_enabled)} activated "
            f"{'当前项目虚拟环境已激活' if is_active else '当前项目虚拟环境未激活；`qpip run` 和 `qpip exec` 仍会优先使用项目环境'}",
            file=output,
        )

    checks.append({"name": "mirror", "status": "ok", "message": f"{mirror} ({MIRRORS[mirror]}) [source={mirror_source}]"})
    if not json_output:
        print(f"{format_status_label('ok', color_enabled)} mirror {mirror} ({MIRRORS[mirror]}) [source={mirror_source}]", file=output)
    if warning:
        checks.append({"name": "mirror_warning", "status": "warn", "message": warning})
        if not json_output:
            print(f"{format_status_label('warn', color_enabled)} mirror {warning}", file=output)
        issues += 1
    if config_result.warning:
        checks.append({"name": "config", "status": "warn", "message": config_result.warning})
        if not json_output:
            print(f"{format_status_label('warn', color_enabled)} config {config_result.warning}", file=output)
        issues += 1

    if pyproject_path.exists():
        try:
            scripts = load_project_scripts(pyproject_path)
        except ValueError as exc:
            checks.append({"name": "scripts", "status": "warn", "message": str(exc)})
            if not json_output:
                print(f"{format_status_label('warn', color_enabled)} scripts {exc}", file=output)
            issues += 1
        else:
            if scripts:
                checks.append({"name": "scripts", "status": "ok", "message": ", ".join(sorted(scripts))})
                if not json_output:
                    print(f"{format_status_label('ok', color_enabled)} scripts {', '.join(sorted(scripts))}", file=output)
            else:
                checks.append({"name": "scripts", "status": "warn", "message": "未找到可运行脚本"})
                if not json_output:
                    print(f"{format_status_label('warn', color_enabled)} scripts 未找到可运行脚本", file=output)
                issues += 1

    if json_output:
        print_json({"ok": issues == 0, "checks": checks}, output)
        return 0 if issues == 0 else 1

    ok_count = sum(1 for check in checks if check["status"] == "ok")
    warn_count = sum(1 for check in checks if check["status"] == "warn")
    info_count = sum(1 for check in checks if check["status"] == "info")
    fail_count = sum(1 for check in checks if check["status"] == "fail")
    summary_parts = [f"{ok_count} ok"]
    if warn_count:
        summary_parts.append(f"{warn_count} warn")
    if info_count:
        summary_parts.append(f"{info_count} info")
    if fail_count:
        summary_parts.append(f"{fail_count} fail")
    readiness = "ready" if issues == 0 else "action needed"
    print(f"{style('Summary:', '1', color_enabled)} {', '.join(summary_parts)} [{readiness}]", file=output)
    return 0 if issues == 0 else 1


def run_mirror_command(
    args: Sequence[str],
    *,
    cwd: Path,
    output: TextIO = sys.stdout,
    env: Optional[Mapping[str, str]] = None,
    json_output: bool = False,
) -> int:
    """Handle simple mirror queries."""
    if not args or args[0] != "mirror":
        raise ProjectCLIUsageError("mirror 命令格式无效")
    if len(args) < 2:
        raise ProjectCLIUsageError("用法: qpip mirror <list|current|use>")

    if args[1] == "list":
        if len(args) != 2:
            raise ProjectCLIUsageError("mirror list 暂不支持额外参数")
        if json_output:
            print_json(
                [{"name": name, "url": url} for name, url in MIRRORS.items()],
                output,
            )
            return 0
        print(format_mirrors(), file=output)
        return 0

    if args[1] == "current":
        if len(args) != 2:
            raise ProjectCLIUsageError("mirror current 暂不支持额外参数")
        config_result = load_project_config_safe(cwd)
        project_config = config_result.config
        mirror, warning, source = get_default_mirror_settings(env=env, project_config=project_config)
        if json_output:
            print_json(
                {
                    "name": mirror,
                    "url": MIRRORS[mirror],
                    "warning": warning,
                    "source": source,
                    "config_warning": config_result.warning,
                },
                output,
            )
            return 0
        print(f"{mirror} {MIRRORS[mirror]} [source={source}]", file=output)
        if warning:
            print(f"warning: {warning}", file=output)
        if config_result.warning:
            print(f"config-warning: {config_result.warning}", file=output)
        return 0

    if args[1] == "use":
        if len(args) != 3:
            raise ProjectCLIUsageError("用法: qpip mirror use <name>")
        try:
            normalized_mirror = normalize_mirror_name(args[2])
            next_config = update_project_config(cwd, mirror=normalized_mirror)
            mirror, warning, source = get_default_mirror_settings(
                env={},
                project_config=next_config,
            )
        except (ProjectConfigError, ValueError) as exc:
            raise ProjectCLIUsageError(str(exc)) from exc
        if json_output:
            print_json(
                {
                    "name": mirror,
                    "url": MIRRORS[mirror],
                    "warning": warning,
                    "source": source,
                    "updated": True,
                },
                output,
            )
            return 0
        print(f"已将项目默认镜像设置为 {mirror} ({MIRRORS[mirror]})", file=output)
        return 0

    raise ProjectCLIUsageError(
        f"不支持的 mirror 子命令: {args[1]}{suggest_subcommand(args[1], ('list', 'current', 'use'))}"
    )


def run_config_command(
    args: Sequence[str],
    *,
    cwd: Path,
    output: TextIO = sys.stdout,
    json_output: bool = False,
) -> int:
    """Handle project-level qpip config inspection."""
    if not args or args[0] != "config":
        raise ProjectCLIUsageError("config 命令格式无效")
    if len(args) < 2:
        raise ProjectCLIUsageError("用法: qpip config <get|list>")

    config_result = load_project_config_safe(cwd)
    if config_result.warning:
        if json_output:
            print_json({"error": config_result.warning}, output)
            return 1
        print(f"配置解析失败: {config_result.warning}", file=output)
        return 1
    config = config_result.config
    payload = {
        "mirror": config.mirror,
        "venv-dir": config.venv_dirname,
        "source_path": str(config.source_path) if config.source_path else None,
    }

    if args[1] == "list":
        if len(args) != 2:
            raise ProjectCLIUsageError("config list 暂不支持额外参数")
        if json_output:
            print_json(payload, output)
            return 0
        print(f"mirror = {payload['mirror'] or 'unset'}", file=output)
        print(f"venv-dir = {payload['venv-dir'] or 'unset'}", file=output)
        print(f"source = {payload['source_path'] or 'none'}", file=output)
        return 0

    if args[1] == "get":
        if len(args) != 3:
            raise ProjectCLIUsageError("用法: qpip config get <mirror|venv-dir>")
        key = args[2]
        alias_map = {
            "mirror": "mirror",
            "venv-dir": "venv-dir",
            "venv_dir": "venv-dir",
        }
        try:
            resolved_key = alias_map[key]
        except KeyError as exc:
            raise ProjectCLIUsageError("config get 仅支持 mirror 或 venv-dir") from exc
        value = payload[resolved_key]
        if json_output:
            print_json({"key": resolved_key, "value": value, "source_path": payload["source_path"]}, output)
            return 0
        print("unset" if value is None else value, file=output)
        return 0

    raise ProjectCLIUsageError(
        f"不支持的 config 子命令: {args[1]}{suggest_subcommand(args[1], ('get', 'list'))}"
    )
