"""Command-line interface for piptool."""

from __future__ import annotations

import argparse
import json
import logging
import pathlib
import subprocess
import sys
from dataclasses import dataclass
from functools import cached_property

__version__ = "0.1.0"
__build_date__ = "2026-02-07"


# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)


CONFIG_FILE = pathlib.Path.home() / ".pytola" / "piptool.json"


@dataclass
class PipToolsConfig:
    """Configuration for PipTools."""

    NAME: str = "pip_tools"
    TRUSTED_PIP_URL: list[str] = None
    DEFAULT_PACKAGES_DIR: str = "packages"

    def __post_init__(self) -> None:
        """Initialize default values after dataclass creation."""
        if self.TRUSTED_PIP_URL is None:
            self.TRUSTED_PIP_URL = [
                "--trusted-host",
                "mirrors.aliyun.com",
                "-i",
                "http://mirrors.aliyun.com/pypi/simple/",
            ]

    def __init__(
        self,
        name: str = "pip_tools",
        trusted_pip_url: list[str] | None = None,
        default_packages_dir: str = "packages",
    ) -> None:
        """Initialize configuration with defaults and load from file if exists."""
        self.NAME = name
        self.TRUSTED_PIP_URL = trusted_pip_url
        self.DEFAULT_PACKAGES_DIR = default_packages_dir

        # Load existing configuration from file
        if CONFIG_FILE.exists():
            try:
                config_data = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
                # Update configuration items, keeping defaults as fallback
                for key, value in config_data.items():
                    if hasattr(self, key):
                        setattr(self, key, value)
            except (json.JSONDecodeError, TypeError, AttributeError):
                logger.warning(f"Could not load config from {CONFIG_FILE}")

        # Set defaults for lists
        if self.TRUSTED_PIP_URL is None:
            self.TRUSTED_PIP_URL = [
                "--trusted-host",
                "mirrors.aliyun.com",
                "-i",
                "http://mirrors.aliyun.com/pypi/simple/",
            ]

    def save(self) -> None:
        """Save current configuration to file."""
        CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
        config_dict = {
            "NAME": self.NAME,
            "TRUSTED_PIP_URL": self.TRUSTED_PIP_URL,
            "DEFAULT_PACKAGES_DIR": self.DEFAULT_PACKAGES_DIR,
        }
        CONFIG_FILE.write_text(json.dumps(config_dict, indent=4), encoding="utf-8")

    @cached_property
    def packages_path(self) -> pathlib.Path:
        """Get packages directory path."""
        return pathlib.Path(self.DEFAULT_PACKAGES_DIR)


conf = PipToolsConfig()


def _exec_command(
    cmd: list[str],
    cwd: str | None = None,
    check: bool = True,
    capture_output: bool = False,
    timeout: int | None = None,
) -> subprocess.CompletedProcess:
    """Execute external command with proper error handling.

    Args:
        cmd: Command and arguments list
        cwd: Working directory
        check: Whether to check return code
        capture_output: Whether to capture output
        timeout: Timeout in seconds

    Returns
    -------
        subprocess.CompletedProcess: Command execution result

    Raises
    ------
        subprocess.CalledProcessError: Command execution failed
        subprocess.TimeoutExpired: Command timeout
        FileNotFoundError: Command not found
    """
    cmd_str = [str(arg) for arg in cmd]
    logger.debug(f"Executing command: {' '.join(cmd_str)}")

    try:
        result = subprocess.run(
            cmd_str,
            cwd=cwd,
            capture_output=capture_output,
            text=True,
            check=check,
            timeout=timeout,
        )

        if capture_output and result.stdout:
            logger.debug(f"Command output: {result.stdout.strip()}")
        if capture_output and result.stderr:
            logger.warning(f"Command stderr: {result.stderr.strip()}")

        return result

    except subprocess.TimeoutExpired:
        logger.exception(f"Command timeout: {' '.join(cmd_str)}")
        raise
    except subprocess.CalledProcessError as e:
        logger.exception(f"Command failed with exit code {e.returncode}: {e.cmd}")
        if e.stderr:
            logger.exception(f"Error output: {e.stderr.strip()}")
        raise
    except FileNotFoundError:
        logger.exception(f"Command not found: {cmd_str[0]}")
        raise


def _libnames_default() -> list[str]:
    """Default library names list."""
    return []


def check_uv_callable() -> bool:
    """Check if uv is callable.

    Returns
    -------
        bool: True if uv is callable, False otherwise
    """
    try:
        result = subprocess.run(
            ["uv", "--version"],
            capture_output=True,
            text=True,
            timeout=5,
            check=False,
        )
        return result.returncode == 0
    except (subprocess.TimeoutExpired, FileNotFoundError):
        return False


def pip_download(libnames: list[str] | None = None) -> None:
    """Download dependency packages to local directory.

    Args:
        libnames: List of package names to download
    """
    if libnames is None:
        libnames = _libnames_default()

    if not libnames:
        logger.error("Please specify package names to download")
        return

    packages_dir = pathlib.Path(conf.DEFAULT_PACKAGES_DIR)
    packages_dir.mkdir(exist_ok=True)

    cmds = [
        "pip",
        "download",
        *libnames,
        *conf.TRUSTED_PIP_URL,
        "-d",
        str(packages_dir),
    ]

    try:
        _exec_command(cmds)
        logger.info(f"Successfully downloaded packages to {packages_dir}")
    except subprocess.CalledProcessError:
        logger.exception("Download failed")


def pip_download_requirements(requirements_file: str = "requirements.txt") -> None:
    """从requirements文件下载依赖包.

    Args:
        requirements_file: requirements文件路径
    """
    req_path = pathlib.Path(requirements_file)
    if not req_path.exists():
        logger.error(f"requirements文件不存在: {requirements_file}")
        return

    packages_dir = pathlib.Path(conf.DEFAULT_PACKAGES_DIR)
    packages_dir.mkdir(exist_ok=True)

    cmds = [
        "pip",
        "download",
        "-r",
        requirements_file,
        "-d",
        str(packages_dir),
        *conf.TRUSTED_PIP_URL,
    ]

    try:
        _exec_command(cmds)
        logger.info(f"成功从 {requirements_file} 下载包到 {packages_dir}")
    except subprocess.CalledProcessError:
        logger.exception("下载失败")


def pip_freeze(
    output_file: str = "requirements.txt",
    exclude_editable: bool = True,
) -> None:
    """生成当前环境的依赖清单.

    Args:
        output_file: 输出文件名
        exclude_editable: 是否排除可编辑安装的包
    """
    logger.info(f"piptool {__version__}, 构建日期: {__build_date__}")

    # 优先使用 uv 如果可用
    if check_uv_callable():
        logger.info("使用 uv 生成依赖清单...")
        cmd = ["uv", "pip", "freeze"]
    else:
        logger.info("使用 pip 生成依赖清单...")
        cmd = ["pip", "freeze"]

    try:
        result = _exec_command(cmd, capture_output=True, timeout=30)

        # 处理输出
        lines = result.stdout.splitlines()
        if exclude_editable:
            lines = [line for line in lines if not line.startswith("-e")]

        output_path = pathlib.Path(output_file)
        output_path.write_text("\n".join(lines) + "\n", encoding="utf-8")

        logger.info(f"依赖清单已生成: {output_file} ({len(lines)} 个包)")

    except (subprocess.CalledProcessError, OSError) as e:
        logger.exception(f"生成依赖清单失败: {e}")


def pip_install(libnames: list[str] | None = None, upgrade: bool = False) -> None:
    """Install Python packages.

    Args:
        libnames: List of package names to install
        upgrade: Whether to upgrade existing packages
    """
    if libnames is None:
        libnames = _libnames_default()

    if not libnames:
        logger.error("Please specify package names to install")
        return

    cmds = ["pip", "install", *libnames, *conf.TRUSTED_PIP_URL]
    if upgrade:
        cmds.append("--upgrade")

    try:
        _exec_command(cmds)
        logger.info(f"Successfully installed packages: {', '.join(libnames)}")
    except subprocess.CalledProcessError:
        logger.exception("Installation failed")


def pip_install_offline(
    libnames: list[str] | None = None,
    find_links: str = ".",
    packages_dir: str | None = None,
) -> None:
    """Install Python packages offline.

    Args:
        libnames: List of package names to install
        find_links: Directory to find packages
        packages_dir: Packages directory (if different from find_links)
    """
    if libnames is None:
        libnames = _libnames_default()

    if not libnames:
        logger.error("Please specify package names to install")
        return

    if packages_dir is None:
        packages_dir = find_links

    packages_path = pathlib.Path(packages_dir)
    if not packages_path.exists():
        logger.error(f"Packages directory does not exist: {packages_dir}")
        return

    cmds = [
        "pip",
        "install",
        *libnames,
        *conf.TRUSTED_PIP_URL,
        "--no-index",
        "--find-links",
        find_links,
    ]

    try:
        _exec_command(cmds)
        logger.info(f"Successfully installed packages offline: {', '.join(libnames)}")
    except subprocess.CalledProcessError:
        logger.exception("Offline installation failed")


def pip_install_requirements(
    requirements_file: str = "requirements.txt",
    upgrade: bool = False,
) -> None:
    """Install dependencies from requirements file.

    Args:
        requirements_file: Path to requirements file
        upgrade: Whether to upgrade existing packages
    """
    req_path = pathlib.Path(requirements_file)
    if not req_path.exists():
        logger.error(f"Requirements file does not exist: {requirements_file}")
        return

    cmds = ["pip", "install", *conf.TRUSTED_PIP_URL, "-r", requirements_file]
    if upgrade:
        cmds.append("--upgrade")

    try:
        _exec_command(cmds)
        logger.info(f"Successfully installed dependencies from {requirements_file}")
    except subprocess.CalledProcessError:
        logger.exception("Installation failed")


def pip_reinstall(libnames: list[str] | None = None) -> None:
    """Reinstall Python packages.

    Args:
        libnames: List of package names to reinstall
    """
    if libnames is None:
        libnames = _libnames_default()

    if not libnames:
        logger.error("Please specify package names to reinstall")
        return

    # First uninstall
    uninstall_cmd = ["pip", "uninstall", "-y", *libnames]
    try:
        _exec_command(uninstall_cmd)
        logger.info(f"Uninstalled packages: {', '.join(libnames)}")
    except subprocess.CalledProcessError:
        logger.warning("Error during uninstallation, continuing with installation")

    # Then install
    install_cmd = ["pip", "install", *libnames, *conf.TRUSTED_PIP_URL]
    try:
        _exec_command(install_cmd)
        logger.info(f"Successfully reinstalled packages: {', '.join(libnames)}")
    except subprocess.CalledProcessError:
        logger.exception("Reinstallation failed")


def pip_uninstall(libnames: list[str] | None = None) -> None:
    """Uninstall Python packages.

    Args:
        libnames: List of package names to uninstall
    """
    if libnames is None:
        libnames = _libnames_default()

    if not libnames:
        logger.error("Please specify package names to uninstall")
        return

    cmds = ["pip", "uninstall", "-y", *libnames]

    try:
        _exec_command(cmds)
        logger.info(f"Successfully uninstalled packages: {', '.join(libnames)}")
    except subprocess.CalledProcessError:
        logger.exception("Uninstallation failed")


def pip_uninstall_requirements(requirements_file: str = "requirements.txt") -> None:
    """Uninstall dependencies from requirements file.

    Args:
        requirements_file: Path to requirements file
    """
    req_path = pathlib.Path(requirements_file)
    if not req_path.exists():
        logger.error(f"Requirements file does not exist: {requirements_file}")
        return

    cmds = ["pip", "uninstall", "-y", "-r", requirements_file]

    try:
        _exec_command(cmds)
        logger.info(f"Successfully uninstalled dependencies from {requirements_file}")
    except subprocess.CalledProcessError:
        logger.exception("Uninstallation failed")


def pip_list(pattern: str | None = None, outdated: bool = False) -> None:
    """List installed packages.

    Args:
        pattern: Package name pattern filter
        outdated: Whether to show only outdated packages
    """
    cmd = ["pip", "list"]
    if outdated:
        cmd.append("--outdated")
    if pattern:
        cmd.extend(["--format", "freeze"])

    try:
        result = _exec_command(cmd, capture_output=True)
        output = result.stdout.strip()
        if pattern:
            lines = [line for line in output.split("\n") if pattern.lower() in line.lower()]
            output = "\n".join(lines)

        if output:
            logger.info(f"Found {len(output.splitlines())} packages")
        else:
            logger.info("No matching packages found")
    except subprocess.CalledProcessError:
        logger.exception("Failed to list packages")


def pip_show(package_name: str) -> None:
    """Show detailed package information.

    Args:
        package_name: Package name
    """
    if not package_name:
        logger.error("Please specify package name")
        return

    cmd = ["pip", "show", package_name]

    try:
        _exec_command(cmd, capture_output=True)
    except subprocess.CalledProcessError:
        logger.exception(f"Cannot show package info: {package_name}")


def pip_check() -> None:
    """Check for dependency conflicts."""
    cmd = ["pip", "check"]

    try:
        _exec_command(cmd)
        logger.info("Dependency check completed, no conflicts found")
    except subprocess.CalledProcessError:
        logger.exception("Dependency conflicts found")


def pip_upgrade_pip() -> None:
    """升级pip本身."""
    cmd = ["python", "-m", "pip", "install", "--upgrade", "pip"]

    try:
        _exec_command(cmd)
        logger.info("pip升级成功")
    except subprocess.CalledProcessError:
        logger.exception("pip升级失败")


def parse_args():
    """Parse command line arguments."""
    import sys

    # If no arguments provided, show help
    if len(sys.argv) == 1:
        sys.argv.append("--help")

    parser = argparse.ArgumentParser(
        description="Python package management tool",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  piptool install requests numpy
  piptool download pandas scikit-learn
  piptool freeze
  piptool list --outdated
  piptool show django
  piptool check
        """,
    )

    parser.add_argument(
        "--version",
        action="version",
        version=f"%(prog)s {__version__}",
    )

    subparsers = parser.add_subparsers(dest="command", help="可用命令")

    # install 命令
    install_parser = subparsers.add_parser("install", aliases=["i"], help="安装包")
    install_parser.add_argument("packages", nargs="*", help="要安装的包名")
    install_parser.add_argument("--upgrade", "-U", action="store_true", help="升级包")

    # download 命令
    download_parser = subparsers.add_parser("download", aliases=["d"], help="下载包")
    download_parser.add_argument("packages", nargs="*", help="要下载的包名")

    # download-req 命令
    download_req_parser = subparsers.add_parser(
        "download-req",
        aliases=["dr"],
        help="从requirements文件下载包",
    )
    download_req_parser.add_argument(
        "--file",
        "-f",
        default="requirements.txt",
        help="requirements文件路径",
    )

    # freeze 命令
    freeze_parser = subparsers.add_parser("freeze", aliases=["f"], help="生成依赖清单")
    freeze_parser.add_argument(
        "--output",
        "-o",
        default="requirements.txt",
        help="输出文件名",
    )
    freeze_parser.add_argument(
        "--include-editable",
        action="store_true",
        help="包含可编辑安装的包",
    )

    # install-req 命令
    install_req_parser = subparsers.add_parser(
        "install-req",
        aliases=["ir"],
        help="从requirements文件安装包",
    )
    install_req_parser.add_argument(
        "--file",
        "-f",
        default="requirements.txt",
        help="requirements文件路径",
    )
    install_req_parser.add_argument(
        "--upgrade",
        "-U",
        action="store_true",
        help="升级包",
    )

    # install-offline 命令
    install_offline_parser = subparsers.add_parser(
        "install-offline",
        aliases=["io"],
        help="离线安装包",
    )
    install_offline_parser.add_argument("packages", nargs="*", help="要安装的包名")
    install_offline_parser.add_argument(
        "--find-links",
        "-f",
        default=".",
        help="查找链接目录",
    )

    # reinstall 命令
    reinstall_parser = subparsers.add_parser(
        "reinstall",
        aliases=["r"],
        help="重新安装包",
    )
    reinstall_parser.add_argument("packages", nargs="*", help="要重新安装的包名")

    # uninstall 命令
    uninstall_parser = subparsers.add_parser("uninstall", aliases=["u"], help="卸载包")
    uninstall_parser.add_argument("packages", nargs="*", help="要卸载的包名")

    # uninstall-req 命令
    uninstall_req_parser = subparsers.add_parser(
        "uninstall-req",
        aliases=["ur"],
        help="从requirements文件卸载包",
    )
    uninstall_req_parser.add_argument(
        "--file",
        "-f",
        default="requirements.txt",
        help="requirements文件路径",
    )

    # list 命令
    list_parser = subparsers.add_parser("list", aliases=["l"], help="列出已安装的包")
    list_parser.add_argument("--pattern", "-p", help="包名模式过滤")
    list_parser.add_argument(
        "--outdated",
        "-o",
        action="store_true",
        help="只显示过期的包",
    )

    # show 命令
    show_parser = subparsers.add_parser("show", aliases=["s"], help="显示包信息")
    show_parser.add_argument("package", help="包名")

    # check 命令
    subparsers.add_parser("check", aliases=["c"], help="检查依赖冲突")

    # upgrade-pip 命令
    subparsers.add_parser("upgrade-pip", aliases=["up"], help="升级pip")

    return parser.parse_args()


def main() -> None:
    """Run entry point for the command-line interface.

    Cli utility tool

    Examples
    --------
      cli [options] <arguments>
    """
    # Parse arguments (will show help if no args provided)
    args = parse_args()

    # Command mapping
    command_map = {
        "install": pip_install,
        "i": pip_install,
        "download": pip_download,
        "d": pip_download,
        "download-req": pip_download_requirements,
        "dr": pip_download_requirements,
        "freeze": pip_freeze,
        "f": pip_freeze,
        "install-req": pip_install_requirements,
        "ir": pip_install_requirements,
        "install-offline": pip_install_offline,
        "io": pip_install_offline,
        "reinstall": pip_reinstall,
        "r": pip_reinstall,
        "uninstall": pip_uninstall,
        "u": pip_uninstall,
        "uninstall-req": pip_uninstall_requirements,
        "ur": pip_uninstall_requirements,
        "list": pip_list,
        "l": pip_list,
        "show": pip_show,
        "s": pip_show,
        "check": pip_check,
        "c": pip_check,
        "upgrade-pip": pip_upgrade_pip,
        "up": pip_upgrade_pip,
    }

    # Execute corresponding command
    try:
        if args.command in {"install", "i"}:
            command_map[args.command](args.packages or [], upgrade=args.upgrade)
        elif args.command in {"download", "d"}:
            command_map[args.command](args.packages or [])
        elif args.command in {"download-req", "dr"}:
            command_map[args.command](args.file)
        elif args.command in {"freeze", "f"}:
            command_map[args.command](args.output, not args.include_editable)
        elif args.command in {"install-req", "ir"}:
            command_map[args.command](args.file, args.upgrade)
        elif args.command in {"install-offline", "io"}:
            command_map[args.command](args.packages or [], args.find_links)
        elif args.command in {"reinstall", "r"} or args.command in {"uninstall", "u"}:
            command_map[args.command](args.packages or [])
        elif args.command in {"uninstall-req", "ur"}:
            command_map[args.command](args.file)
        elif args.command in {"list", "l"}:
            command_map[args.command](args.pattern, args.outdated)
        elif args.command in {"show", "s"}:
            command_map[args.command](args.package)
        else:
            command_map[args.command]()
    except KeyboardInterrupt:
        logger.info("\nOperation interrupted by user")
        sys.exit(1)
    except Exception as e:
        logger.exception(f"Error executing command: {e}")
        sys.exit(1)


if __name__ == "__main__":
    main()
