"""MakePython: Automated Python project building and management tool.

This module provides a command-line interface for common Python development tasks
including building, testing, publishing, and version management.

Features:
    - Automatic build tool detection (uv, poetry, hatch)
    - Project configuration management
    - Command execution with proper error handling
    - PyPI token management
    - Clean build artifacts
    - Test execution with various configurations

Example:
    >>> # Build project
    >>> makepython build
    >>>
    >>> # Run tests
    >>> makepython test
    >>>
    >>> # Publish to PyPI
    >>> makepython publish
"""

from __future__ import annotations

__version__: Final[str] = "0.2.0"
__author__: Final[str] = "pysfi Developers"
__email__: Final[str] = "developers@pysfi.org"
import argparse
import atexit
import json
import logging
import os
import shutil
import subprocess
import sys
import time
from dataclasses import dataclass
from enum import Enum
from functools import cached_property
from pathlib import Path
from typing import Any, Callable, Final, Protocol

if sys.version_info >= (3, 11):
    import tomllib
else:
    import tomli as tomllib  # type: ignore


# Platform detection
IS_WINDOWS: Final[bool] = sys.platform == "win32"

# File and directory constants
CONFIG_DIR_NAME: Final[str] = ".pysfi"
CONFIG_FILE_NAME: Final[str] = "makepython.json"
PYPROJECT_FILE_NAME: Final[str] = "pyproject.toml"

# Default constants
DEFAULT_TIMEOUT_SECONDS: Final[int] = 300
DEFAULT_MAX_RETRIES: Final[int] = 3
DEFAULT_ENCODING: Final[str] = "utf-8"
DEFAULT_SHELL_CONFIG: Final[str] = ".bashrc"
DEFAULT_CACHE_SIZE: Final[int] = 100
DEFAULT_CACHE_EXPIRY: Final[int] = 300  # 5 minutes

# Clean command template
CLEAN_COMMANDS: Final[list[str]] = ["rm", "-rf"]

# Logging setup
LOG_FORMAT: Final[str] = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
LOG_DATE_FORMAT: Final[str] = "%Y-%m-%d %H:%M:%S"

logging.basicConfig(level=logging.INFO, format=LOG_FORMAT, datefmt=LOG_DATE_FORMAT)
logger = logging.getLogger(__name__)
CURRENT_WORKING_DIR: Final[Path] = Path.cwd()

CONFIG_FILE: Final[Path] = Path.home() / CONFIG_DIR_NAME / CONFIG_FILE_NAME


class MakePythonError(Exception):
    """Base exception class for MakePython tool errors."""

    def __init__(self, message: str, error_code: int = 1):
        super().__init__(message)
        self.message = message
        self.error_code = error_code


class ConfigurationError(MakePythonError):
    """Raised when configuration loading or saving fails."""

    pass


class BuildToolNotFoundError(MakePythonError):
    """Raised when no suitable build tool is found."""

    pass


class TokenConfigurationError(MakePythonError):
    """Raised when PyPI token configuration fails."""

    pass


class CommandExecutionError(MakePythonError):
    """Raised when command execution fails."""

    def __init__(
        self, message: str, return_code: int = 1, stdout: str = "", stderr: str = ""
    ):
        super().__init__(message, return_code)
        self.stdout = stdout
        self.stderr = stderr


class BuildTool(Enum):
    """Enumeration of supported build tools.

    Attributes:
        UV: The uv build tool
        POETRY: The poetry build tool
        HATCH: The hatch build tool
    """

    UV = "uv"
    POETRY = "poetry"
    HATCH = "hatch"


class CommandType(Enum):
    """Enumeration of command types.

    Attributes:
        BUILD: Build project command
        CLEAN: Clean build artifacts command
        TEST: Run tests command
        PUBLISH: Publish package command
        BUMP_VERSION: Bump version command
        TOKEN: Manage PyPI token command
    """

    BUILD = "build"
    CLEAN = "clean"
    TEST = "test"
    PUBLISH = "publish"
    BUMP_VERSION = "bumpversion"
    TOKEN = "token"


@dataclass
class ProjectInfo:
    """Information about the current Python project.

    This dataclass holds metadata about a Python project including its name,
    version, build tool, and testing configuration.

    Attributes:
        name: Project name
        version: Project version string
        build_tool: Detected build tool (optional)
        has_tests: Whether tests directory exists
        has_benchmarks: Whether benchmarks directory exists
    """

    name: str
    version: str
    build_tool: BuildTool | None = None
    has_tests: bool = False
    has_benchmarks: bool = False

    @classmethod
    def from_pyproject(cls, directory: Path) -> ProjectInfo | None:
        """Create ProjectInfo from pyproject.toml file.

        Args:
            directory: Directory containing pyproject.toml file

        Returns:
            ProjectInfo instance if pyproject.toml exists and is valid, None otherwise

        Note:
            This method will log warnings for parsing errors but won't raise exceptions.
            Uses caching to improve performance for repeated calls.
        """
        # Check cache first
        cached_info = _project_info_cache.get(directory)
        if cached_info is not None:
            logger.debug(f"Using cached ProjectInfo for {directory}")
            return cached_info

        pyproject_path = directory / PYPROJECT_FILE_NAME
        if not pyproject_path.exists():
            return None

        try:
            with pyproject_path.open("rb") as f:
                data = tomllib.load(f)

            project_data = data.get("project", {})
            name = project_data.get("name", directory.name)
            version = project_data.get("version", "0.1.0")

            # Detect build tool
            build_tool = None
            if "build-system" in data:
                backend = data["build-system"].get("build-backend", "")
                if backend.startswith("poetry."):
                    build_tool = BuildTool.POETRY
                elif backend.startswith("hatchling."):
                    build_tool = BuildTool.HATCH
                else:
                    build_tool = BuildTool.UV

            # Check for tests and benchmarks
            tests_dir = directory / "tests"
            has_tests = tests_dir.exists() and tests_dir.is_dir()

            benchmarks_dir = directory / "tests" / "benchmarks"
            has_benchmarks = benchmarks_dir.exists() and benchmarks_dir.is_dir()

            project_info = cls(
                name=name,
                version=version,
                build_tool=build_tool,
                has_tests=has_tests,
                has_benchmarks=has_benchmarks,
            )

            # Cache the result
            _project_info_cache.put(directory, project_info)

            return project_info
        except Exception as e:
            logger.warning(f"Failed to parse pyproject.toml: {e}")
            return None


@dataclass
class MakePythonConfig:
    """Configuration for MakePython tool.

    Manages the configuration settings for the MakePython tool including
    default build tools, verbosity settings, and retry policies.

    Attributes:
        default_build_tool: Default build tool to use
        auto_detect_tool: Whether to automatically detect build tools
        verbose_output: Enable verbose output
        max_retries: Maximum number of retries for failed operations
        timeout_seconds: Timeout for command execution in seconds
    """

    default_build_tool: BuildTool = BuildTool.UV
    auto_detect_tool: bool = True
    verbose_output: bool = False
    max_retries: int = DEFAULT_MAX_RETRIES
    timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS

    def __post_init__(self) -> None:
        """Initialize configuration after creation."""
        self._load_from_file()
        atexit.register(self.save)

    def _load_from_file(self) -> None:
        """Load configuration from file if it exists.

        Silently continues if configuration file doesn't exist or is invalid.
        Invalid configurations will use default values.
        """
        if not CONFIG_FILE.exists():
            return

        try:
            config_data = json.loads(CONFIG_FILE.read_text(encoding=DEFAULT_ENCODING))
            for key, value in config_data.items():
                if hasattr(self, key):
                    if key == "default_build_tool":
                        try:
                            setattr(self, key, BuildTool(value))
                        except ValueError:
                            logger.warning(
                                f"Invalid build tool value '{value}', using default"
                            )
                            setattr(self, key, self.default_build_tool)
                    else:
                        setattr(self, key, value)
        except (json.JSONDecodeError, TypeError, AttributeError) as e:
            logger.warning(f"Could not load config from {CONFIG_FILE}: {e}")
        except Exception as e:
            logger.error(f"Unexpected error loading configuration: {e}")

    def save(self) -> None:
        """Save current configuration to file.

        Creates parent directories if they don't exist.

        Raises:
            ConfigurationError: If saving configuration fails
        """
        try:
            CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
            config_dict = {
                "default_build_tool": self.default_build_tool.value,
                "auto_detect_tool": self.auto_detect_tool,
                "verbose_output": self.verbose_output,
                "max_retries": self.max_retries,
                "timeout_seconds": self.timeout_seconds,
            }
            CONFIG_FILE.write_text(
                json.dumps(config_dict, indent=4), encoding=DEFAULT_ENCODING
            )
            logger.debug(f"Configuration saved to {CONFIG_FILE}")
        except OSError as e:
            logger.error(f"Failed to save configuration: {e}")
            raise ConfigurationError(f"Failed to save configuration: {e}") from e
        except Exception as e:
            logger.error(f"Unexpected error saving configuration: {e}")
            raise ConfigurationError(
                f"Unexpected error saving configuration: {e}"
            ) from e

    @cached_property
    def supported_tools(self) -> list[BuildTool]:
        """Get list of available build tools.

        Uses caching to avoid repeated system calls for tool detection.

        Returns:
            List of available BuildTool enums
        """
        return [
            tool
            for tool in BuildTool
            if shutil.which(tool.value)
        ]


class CommandExecutor(Protocol):
    """Protocol for command execution.

    Defines the interface for executing commands in a consistent manner.
    Implementations should handle subprocess execution with proper error handling.
    """

    def execute(
        self, command: list[str], cwd: Path
    ) -> subprocess.CompletedProcess[str]:
        """Execute a command in the specified directory.

        Args:
            command: Command to execute as list of arguments
            cwd: Working directory for command execution

        Returns:
            CompletedProcess object with command results

        Raises:
            subprocess.CalledProcessError: If command execution fails
            subprocess.TimeoutExpired: If command times out
        """
        ...


class SubprocessExecutor:
    """Execute commands using subprocess.

    Provides robust subprocess execution with configurable timeouts,
    proper error handling, and encoding support.

    Attributes:
        config: MakePython configuration object
    """

    def __init__(self, config: MakePythonConfig):
        """Initialize the executor with configuration.

        Args:
            config: MakePython configuration object
        """
        self.config = config

    def execute(
        self, command: list[str], cwd: Path
    ) -> subprocess.CompletedProcess[str]:
        """Execute command with proper error handling.

        Args:
            command: Command to execute as list of arguments
            cwd: Working directory for command execution

        Returns:
            CompletedProcess object with command results

        Raises:
            CommandExecutionError: For command execution failures
            subprocess.TimeoutExpired: If command times out
        """
        logger.debug(f"Executing command: {' '.join(command)} in {cwd}")

        try:
            return _execute_command(
                command,
                cwd,
                check=True,
                capture=True,
                timeout=self.config.timeout_seconds,
                fallback_encoding=True,
            )
        except subprocess.CalledProcessError as e:
            raise CommandExecutionError(
                f"Command failed: {' '.join(command)}",
                return_code=e.returncode,
                stdout=e.stdout or "",
                stderr=e.stderr or "",
            ) from e
        except Exception as e:
            raise CommandExecutionError(
                f"Unexpected error executing command: {e}"
            ) from e


class ProjectInfoCache:
    """Simple cache for ProjectInfo objects to improve performance.

    Caches parsed pyproject.toml data to avoid repeated file I/O operations.
    """

    def __init__(self, max_size: int = DEFAULT_CACHE_SIZE):
        self._cache: dict[Path, tuple[ProjectInfo, float]] = {}
        self._max_size = max_size
        self._access_order: list[Path] = []

    def get(self, directory: Path) -> ProjectInfo | None:
        """Get cached ProjectInfo for directory.

        Args:
            directory: Directory to get cached info for

        Returns:
            Cached ProjectInfo or None if not found/expired
        """
        if directory not in self._cache:
            return None

        info, timestamp = self._cache[directory]
        # Cache expires after 5 minutes
        if time.time() - timestamp > DEFAULT_CACHE_EXPIRY:
            del self._cache[directory]
            if directory in self._access_order:
                self._access_order.remove(directory)
            return None

        # Move to end for LRU
        if directory in self._access_order:
            self._access_order.remove(directory)
        self._access_order.append(directory)
        return info

    def put(self, directory: Path, info: ProjectInfo) -> None:
        """Store ProjectInfo in cache.

        Args:
            directory: Directory key
            info: ProjectInfo to cache
        """
        # Remove oldest if cache is full
        if len(self._cache) >= self._max_size:
            oldest = self._access_order.pop(0)
            del self._cache[oldest]

        self._cache[directory] = (info, time.time())
        self._access_order.append(directory)


# Global cache instance
_project_info_cache = ProjectInfoCache()


# Removed _get_build_command_from_toml as it duplicates _get_build_command functionality


def _get_build_command(directory: Path, config: MakePythonConfig) -> str | None:
    """Get build command from directory using configuration.

    Args:
        directory: Directory to search for project configuration
        config: MakePython configuration object

    Returns:
        Build command string or None if not found

    Raises:
        BuildToolNotFoundError: If no build tools are available
    """
    project_info = ProjectInfo.from_pyproject(directory)
    if project_info and project_info.build_tool:
        logger.debug(f"Detected build tool: {project_info.build_tool.value}")
        return project_info.build_tool.value

    # Fallback to available tools
    if config.supported_tools:
        selected_tool = config.supported_tools[0]
        logger.debug(f"Using available build tool: {selected_tool.value}")
        return selected_tool.value

    logger.error(f"No build command found in {directory}")
    logger.error("Please install uv, poetry, or hatch")
    raise BuildToolNotFoundError(
        f"No build tools found. Please install uv, poetry, or hatch in {directory}"
    )


@dataclass
class Command:
    """Represents a command that can be executed."""

    name: str
    alias: str
    command_type: CommandType
    cmds: list[str] | Callable[..., Any] | None = None
    description: str = ""


def _validate_token_format(token: str) -> tuple[bool, str]:
    """Validate PyPI token format.

    Args:
        token: Token string to validate

    Returns:
        Tuple of (is_valid, message)
    """
    if not token:
        return False, "Token cannot be empty"

    if len(token) < 10:
        return False, "Token appears too short to be valid"

    # Basic format validation
    if not (token.startswith("pypi-") or token.startswith("PYPI-")):
        return True, "Warning: Token doesn't start with 'pypi-' (may still be valid)"

    return True, "Token format appears valid"


def _show_detailed_help(commands: list[Command]) -> None:
    """Show detailed command descriptions and usage information.

    Args:
        commands: List of available commands
    """
    print("\nMakePython - Detailed Command Help")
    print("=" * 40)
    print(f"Version: {__version__}")
    print(f"Author: {__author__}")
    print("\nAvailable Commands:")
    print("-" * 20)

    # Group commands by type for better organization
    command_groups = {}
    for command in commands:
        group = command.command_type.value
        if group not in command_groups:
            command_groups[group] = []
        command_groups[group].append(command)

    for group_name, group_commands in command_groups.items():
        print(f"\n{group_name.upper()} COMMANDS:")
        for command in group_commands:
            aliases = f" ({command.alias})" if command.alias != command.name else ""
            print(f"  {command.name}{aliases:<8} - {command.description}")

    print("\nUSAGE EXAMPLES:")
    print("  makepython build          # Build the project")
    print("  makepython test           # Run tests")
    print("  makepython publish        # Publish to PyPI")
    print("  makepython token          # Configure PyPI token")
    print("  makepython clean          # Clean build artifacts")

    print("\nOPTIONS:")
    print("  --debug, -d              Enable debug output")
    print("  --verbose, -v            Enable verbose output")
    print("  --quiet, -q              Suppress most messages")
    print("  --version                Show version information")
    print("  --help-commands          Show this detailed help")

    print("\nFor more information, visit: https://github.com/pysfi/makepython")


def _clean(root_dir: Path = CURRENT_WORKING_DIR) -> None:
    """Clean build artifacts and temporary files.

    Args:
        root_dir: Root directory to clean
    """
    logger.info(f"Cleaning build artifacts in {root_dir}")

    # Clean common build directories
    targets = ["dist", "build", "*.egg-info"]
    cleaned_count = 0
    failed_count = 0

    for target in targets:
        try:
            _execute_command([*CLEAN_COMMANDS, target], root_dir, check=False)
            cleaned_count += 1
        except Exception as e:
            logger.warning(f"Failed to clean {target}: {e}")
            failed_count += 1
            continue

    logger.info(f"Clean operation completed: {cleaned_count} succeeded, {failed_count} failed")


def _dispatch_command(command: Command, *, realtime: bool = False) -> None:
    """Execute a command based on its type.

    Args:
        command: Command object to execute
        realtime: Whether to stream output in real-time
    """
    if callable(command.cmds):
        command.cmds()
    elif isinstance(command.cmds, list):
        # Use realtime output for test commands or if explicitly requested
        use_realtime = realtime or command.command_type == CommandType.TEST
        _run_command(command.cmds, CURRENT_WORKING_DIR, realtime=use_realtime)
    else:
        logger.debug("No preset commands found for this command type")


def _handle_publish_command(build_command: str) -> None:
    """Handle publish command with token validation.

    Args:
        build_command: Build command string
    """
    if not _check_pypi_token(build_command):
        logger.warning(
            "PyPI token not configured. Starting configuration process..."
        )
        _set_token(build_command)
    _run_command([build_command, "publish"], CURRENT_WORKING_DIR)


def main() -> None:
    """Main entry point for MakePython tool.

    Parses command line arguments, loads configuration, detects build tools,
    and executes the requested command.

    Raises:
        SystemExit: If command execution fails or unknown command is provided
    """
    try:
        # Initialize configuration
        config = MakePythonConfig()

        # Get build command
        try:
            build_command = _get_build_command(CURRENT_WORKING_DIR, config) or ""
        except BuildToolNotFoundError as e:
            logger.error(str(e))
            sys.exit(1)

        # Create commands with proper structure
        commands = [
            Command(
                name="build",
                alias="b",
                command_type=CommandType.BUILD,
                cmds=[build_command, "build"],
                description="Build the project package",
            ),
            Command(
                name="bumpversion",
                alias="bump",
                command_type=CommandType.BUMP_VERSION,
                cmds=["bumpversion", "patch", "--tag"],
                description="Bump project version (patch/minor/major)",
            ),
            Command(
                name="clean",
                alias="c",
                command_type=CommandType.CLEAN,
                cmds=_clean,
                description="Clean build artifacts and temporary files",
            ),
            Command(
                name="publish",
                alias="p",
                command_type=CommandType.PUBLISH,
                description="Publish package to PyPI (requires token)",
            ),
            Command(
                name="test",
                alias="t",
                command_type=CommandType.TEST,
                cmds=["pytest", "-n", "8"],
                description="Run tests with parallel execution",
            ),
            Command(
                name="test-benchmark",
                alias="tb",
                command_type=CommandType.TEST,
                cmds=["pytest", "-m", "benchmark", "-n", "8"],
                description="Run benchmark tests specifically",
            ),
            Command(
                name="test-coverage",
                alias="tc",
                command_type=CommandType.TEST,
                cmds=["pytest", "--cov=sfi", "-n", "8"],
                description="Run tests with coverage reporting",
            ),
            Command(
                name="token",
                alias="tk",
                command_type=CommandType.TOKEN,
                cmds=lambda: _set_token(build_command),
                description="Configure PyPI token for publishing",
            ),
        ]
        command_dict = {command.name: command for command in commands}
        command_dict.update({command.alias: command for command in commands})
        choices = [command.alias for command in commands]
        choices.extend([command.name for command in commands])

        # Parse arguments
        parser = argparse.ArgumentParser(
            prog="makepython",
            description="Automated Python project building and management tool",
            epilog="""Examples:
  makepython build           Build the project
  makepython test            Run tests with real-time output (default)
  makepython test -r         Run tests with real-time output (explicit)
  makepython publish          Publish to PyPI
  makepython clean            Clean build artifacts

Note: Test commands (test, test-benchmark, test-coverage) automatically use real-time output.
Use --realtime/-r for other commands if you want to see output as it happens.""",
            formatter_class=argparse.RawDescriptionHelpFormatter,
        )

        # Add argument groups for better organization
        info_group = parser.add_argument_group("Information Options")
        info_group.add_argument(
            "--version",
            action="version",
            version=f"MakePython v{__version__}",
            help="Show version information and exit",
        )
        info_group.add_argument(
            "--help-commands",
            action="store_true",
            help="Show detailed command descriptions and exit",
        )

        config_group = parser.add_argument_group("Configuration Options")
        config_group.add_argument(
            "--config-path", type=Path, help="Specify custom configuration file path"
        )
        config_group.add_argument(
            "--build-tool",
            choices=[tool.value for tool in BuildTool],
            help="Force specific build tool (uv, poetry, hatch)",
        )

        logging_group = parser.add_argument_group("Logging Options")
        logging_group.add_argument(
            "--debug",
            "-d",
            action="store_true",
            help="Enable debug mode with verbose output",
        )
        logging_group.add_argument(
            "--verbose", "-v", action="store_true", help="Enable verbose output"
        )
        logging_group.add_argument(
            "--quiet", "-q", action="store_true", help="Suppress most output messages"
        )

        output_group = parser.add_argument_group("Output Options")
        output_group.add_argument(
            "--realtime",
            "-r",
            action="store_true",
            help="Stream command output in real-time (useful for tests)",
        )

        parser.add_argument(
            "command",
            nargs="?",
            type=str,
            choices=choices,
            help=f"Command to run. Available: {', '.join(choices)}",
        )

        args = parser.parse_args()

        # Handle help commands option
        if hasattr(args, "help_commands") and args.help_commands:
            _show_detailed_help(commands)
            return

        # Validate that a command was provided
        if not args.command:
            parser.print_help()
            logger.error("No command specified. Use --help for usage information.")
            sys.exit(1)

        # Set logging level
        if args.quiet:
            logger.setLevel(logging.ERROR)
            config.verbose_output = False
        elif args.debug:
            logger.setLevel(logging.DEBUG)
            config.verbose_output = True
        elif args.verbose:
            logger.setLevel(logging.INFO)
            config.verbose_output = True

        logger.debug(f"Using build command: {build_command}")
        logger.debug(f"Working directory: {CURRENT_WORKING_DIR}")

        # Execute command
        command = command_dict.get(args.command)
        if command:
            logger.info(f"Executing command: {command.name} ({command.description})")
            _dispatch_command(command, realtime=args.realtime)
        else:
            logger.error(f"Unknown command: {args.command}")
            sys.exit(1)

        # Special handling for publish command
        if args.command in {"publish", "p"}:
            _handle_publish_command(build_command)

    except KeyboardInterrupt:
        logger.info("Operation cancelled by user")
        sys.exit(130)  # Standard exit code for Ctrl+C
    except MakePythonError as e:
        logger.error(f"MakePython error: {e.message}")
        sys.exit(e.error_code)
    except Exception as e:
        logger.error(f"Unexpected error: {e}")
        sys.exit(1)


def _set_token(build_command: str, show_header: bool = True) -> None:
    """Set PyPI token for the specified build command.

    Args:
        build_command: The build tool to configure (uv, poetry, hatch)
        show_header: Whether to show the header message

    Raises:
        TokenConfigurationError: If unknown build command is provided or token setting fails
    """
    if show_header:
        logger.info(f"Setting PyPI token for {build_command}...")
        logger.info(
            "Note: Your token will be stored securely in the appropriate configuration files."
        )

    if build_command.lower() not in [tool.value for tool in BuildTool]:
        error_msg = f"Unknown build command: {build_command}. Please use one of: {[tool.value for tool in BuildTool]}"
        logger.error(error_msg)
        raise TokenConfigurationError(error_msg)

    token = input("Enter your PyPI token (leave empty to cancel): ").strip()
    if not token:
        logger.info("Operation cancelled - no token provided.")
        return

    # Validate token format
    is_valid, message = _validate_token_format(token)
    if not is_valid:
        logger.error(f"Invalid token: {message}")
        raise TokenConfigurationError(message)
    elif "Warning" in message:
        logger.warning(message)

    try:
        build_tool = BuildTool(build_command)
        _set_build_tool_token(token, build_tool)
        logger.info("PyPI token configured successfully!")
        logger.info("You can now use 'makepython publish' to publish your package.")
    except Exception as e:
        error_msg = f"Failed to set token: {e}"
        logger.error(error_msg)
        raise TokenConfigurationError(error_msg) from e


def _save_token_to_file(filepath: Path, content: str, description: str) -> None:
    """Save token content to file with proper error handling.

    Args:
        filepath: Path to save the file
        content: Content to write
        description: Description for logging

    Raises:
        OSError: If file operation fails
    """
    try:
        filepath.parent.mkdir(parents=True, exist_ok=True)
        filepath.write_text(content, encoding=DEFAULT_ENCODING)
        logger.info(f"Token saved to {filepath} ({description})")
    except OSError as e:
        logger.error(f"Failed to save token to {filepath}: {e}")
        raise


def _set_build_tool_token(token: str, build_tool: BuildTool) -> None:
    """Set PyPI token for the specified build tool.

    Args:
        token: PyPI token to set
        build_tool: Build tool to configure

    Raises:
        TokenConfigurationError: If token setting fails
    """
    try:
        if build_tool == BuildTool.UV:
            # Set UV environment variable and config file
            _write_to_env_file("UV_PUBLISH_TOKEN", token)
            config_content = f"""[publish]
token = "{token}"
"""
            config_path = Path.home() / ".config" / "uv" / "uv.toml"
            _save_token_to_file(config_path, config_content, "UV config")

        elif build_tool == BuildTool.POETRY:
            # Set Poetry environment variable and config
            _write_to_env_file("POETRY_PYPI_TOKEN_PYPI", token)
            _run_command(
                ["poetry", "config", "pypi-token.pypi", token], CURRENT_WORKING_DIR
            )
            logger.debug("Token saved to Poetry configuration.")

        elif build_tool == BuildTool.HATCH:
            # Write to .pypirc
            pypirc_content = f"""[pypi]
repository = https://upload.pypi.org/legacy/
username = __token__
password = {token}
"""
            pypirc_path = Path.home() / ".pypirc"
            _save_token_to_file(pypirc_path, pypirc_content, "PyPI RC")
    except Exception as e:
        raise TokenConfigurationError(f"Failed to set token for {build_tool.value}: {e}") from e


def _check_uv_token() -> bool:
    """Check if UV PyPI token is configured."""
    # Check environment variables
    token_env_vars = ["UV_PUBLISH_TOKEN", "PYPI_API_TOKEN"]
    for var in token_env_vars:
        if os.getenv(var):
            logger.debug(f"Found PyPI token in environment variable: {var}")
            return True

    # Check config file
    config_path = Path.home() / ".config" / "uv" / "uv.toml"
    if config_path.exists():
        logger.debug(f"Found uv config file: {config_path}")
        return True

    return False


def _check_poetry_token() -> bool:
    """Check if Poetry PyPI token is configured."""
    # Check environment variable
    if os.getenv("POETRY_PYPI_TOKEN_PYPI"):
        logger.debug("Found PyPI token in POETRY_PYPI_TOKEN_PYPI environment variable")
        return True

    # Check Poetry config
    try:
        result = _execute_command(
            ["poetry", "config", "pypi-token.pypi"],
            CURRENT_WORKING_DIR,
            check=False,
            capture=True,
        )
        if result.stdout.strip() and result.stdout.strip() != "None":
            logger.debug("Found PyPI token in Poetry configuration")
            return True
    except Exception as e:
        logger.debug(f"Error checking Poetry token: {e}")
    return False


def _check_hatch_token() -> bool:
    """Check if Hatch PyPI token is configured."""
    pypirc_path = Path.home() / ".pypirc"
    if pypirc_path.exists():
        logger.debug(f"Found .pypirc file: {pypirc_path}")
        return True
    return False


def _check_build_tool_token(build_tool: BuildTool) -> bool:
    """Check if PyPI token is configured for the specified build tool.

    Args:
        build_tool: Build tool to check

    Returns:
        True if token is found, False otherwise
    """
    check_functions = {
        BuildTool.UV: _check_uv_token,
        BuildTool.POETRY: _check_poetry_token,
        BuildTool.HATCH: _check_hatch_token,
    }

    checker = check_functions.get(build_tool)
    if checker:
        return checker()

    logger.error(f"Unknown build tool for token checking: {build_tool}")
    return False


def _check_pypi_token(build_command: str) -> bool:
    """Check if PyPI token is configured before publishing.

    Args:
        build_command: Build tool name (uv, poetry, hatch)

    Returns:
        True if token is configured, False otherwise
    """
    logger.info("Checking PyPI token configuration...")

    try:
        build_tool = BuildTool(build_command.lower())
        return _check_build_tool_token(build_tool)
    except ValueError:
        logger.error(f"Unknown build command for token checking: {build_command}")
        return False


def _execute_command(
    cmd: list[str],
    directory: Path,
    *,
    check: bool = True,
    capture: bool = True,
    timeout: int | None = None,
    fallback_encoding: bool = True,
    realtime: bool = False,
) -> subprocess.CompletedProcess[str]:
    """Execute command with comprehensive error handling.

    Args:
        cmd: Command to execute as list of arguments
        directory: Working directory
        check: Whether to raise CalledProcessError on non-zero exit
        capture: Whether to capture stdout/stderr (must be False for realtime)
        timeout: Timeout in seconds (None for no timeout)
        fallback_encoding: Whether to try fallback encoding on Unicode errors
        realtime: Whether to stream output in real-time (overrides capture)

    Returns:
        CompletedProcess object with command results

    Raises:
        SystemExit: If command fails to execute and check=True
        subprocess.CalledProcessError: If command fails and check=True
        subprocess.TimeoutExpired: If command times out
    """
    logger.debug(f"Running command: {' '.join(cmd)} in {directory}")

    # If realtime output is requested, disable capture
    if realtime:
        capture = False

    def _run_with_encoding(encoding: str) -> subprocess.CompletedProcess[str]:
        """Run command with specified encoding."""
        return subprocess.run(
            cmd,
            cwd=directory,
            capture_output=capture,
            text=capture,
            encoding=encoding if capture else None,
            errors="replace" if capture else None,
            check=check,
            timeout=timeout,
        )

    try:
        result = _run_with_encoding(DEFAULT_ENCODING)
        if result.stdout and capture:
            print(result.stdout)
        if result.stderr and capture:
            print(result.stderr, file=sys.stderr)
        return result

    except subprocess.CalledProcessError as e:
        logger.error(f"Command failed with exit code {e.returncode}: {' '.join(cmd)}")
        if e.stdout:
            logger.error(f"STDOUT: {e.stdout}")
        if e.stderr:
            logger.error(f"STDERR: {e.stderr}")
        raise

    except subprocess.TimeoutExpired:
        logger.error(f"Command timed out: {' '.join(cmd)}")
        raise

    except UnicodeDecodeError as e:
        if not fallback_encoding or realtime:
            logger.error(f"Encoding error while processing command output: {e}")
            raise

        logger.warning(f"Encoding error, trying fallback encoding: {e}")
        try:
            result = _run_with_encoding("gbk" if IS_WINDOWS else "utf-8")
            if result.stdout and capture:
                print(result.stdout)
            if result.stderr and capture:
                print(result.stderr, file=sys.stderr)
            return result
        except Exception as fallback_error:
            logger.error(f"Fallback encoding also failed: {fallback_error}")
            raise

    except Exception as e:
        logger.error(f"Unexpected error executing command {' '.join(cmd)}: {e}")
        raise


def _run_command(cmd: list[str], directory: Path, *, realtime: bool = False) -> None:
    """Run a command in specified directory with proper error handling.

    Args:
        cmd: Command to execute as list of arguments
        directory: Directory to run the command in
        realtime: Whether to stream output in real-time

    Raises:
        SystemExit: If command fails to execute
        ValueError: If inputs are invalid
    """
    # Validate inputs
    if not directory.exists() or not directory.is_dir():
        raise ValueError(f"Invalid directory: {directory}")

    if not cmd or not all(isinstance(c, str) for c in cmd):
        raise ValueError("Invalid command list")

    try:
        _execute_command(cmd, directory, check=True, realtime=realtime)
    except subprocess.CalledProcessError as e:
        sys.exit(e.returncode)
    except subprocess.TimeoutExpired:
        sys.exit(124)  # Standard exit code for timeout
    except Exception as e:
        logger.error(f"Command execution failed: {e}")
        sys.exit(1)


def _write_to_env_file(key: str, value: str) -> None:
    """Write key-value pair to environment file.

    Sets environment variables either through system utilities (Windows)
    or shell configuration files (Unix-like systems).

    Args:
        key: Environment variable name
        value: Environment variable value
    """
    if IS_WINDOWS:
        try:
            _execute_command(
                ["setx", key, value],
                CURRENT_WORKING_DIR,
                check=True,
                capture=False,
            )
            logger.info(f"Environment variable {key} set successfully")
        except Exception as e:
            logger.warning(f"Failed to set environment variable {key}: {e}")
            logger.info("You may need to restart your shell for changes to take effect")
    else:
        _write_to_shell_config(f"export {key}='{value}'")


def _get_shell_config_path() -> Path:
    """Get the appropriate shell config file based on the current shell.

    Detects the user's shell and returns the corresponding configuration file path.
    Defaults to .bashrc for bash/zsh shells.

    Returns:
        Path to the appropriate shell configuration file
    """
    # Try to detect the shell
    shell = os.getenv("SHELL", "")
    if "zsh" in shell:
        return Path.home() / ".zshrc"
    else:
        # Default to .bashrc
        return Path.home() / DEFAULT_SHELL_CONFIG


def _write_to_shell_config(content: str) -> None:
    """Write content to shell configuration file, replacing existing entries.

    Updates shell configuration files by removing existing export statements
    for the same variable and adding the new content.

    Args:
        content: Content to write (should be export statements)

    Raises:
        ValueError: If export statement format is invalid
        OSError: If file operation fails
    """
    config_path = _get_shell_config_path()
    if not config_path.exists():
        logger.warning(f"{config_path} does not exist, creating it...")
        config_path.touch()

    # Extract the variable name from the export statement
    # Expected format: export VARIABLE_NAME=value
    var_name = None
    for line in content.strip().split("\n"):
        if line.startswith("export ") and "=" in line:
            var_name = line.split("=")[0].removeprefix("export ").strip()
            break

    if not var_name:
        raise ValueError(
            "Invalid export statement format. Expected: export VARIABLE_NAME=value"
        )

    # Read existing content
    existing_lines = config_path.read_text(encoding=DEFAULT_ENCODING).split("\n")

    # Find and remove existing export statements for this variable
    new_lines = []
    found_existing = False
    for line in existing_lines:
        # Check if this line exports the same variable
        if line.strip().startswith(f"export {var_name}=") or line.strip().startswith(
            f"export {var_name} ="
        ):
            found_existing = True
            continue
        new_lines.append(line)

    if found_existing:
        logger.debug(f"Found existing export statement for {var_name}, replacing it...")

    # Add new content
    new_lines.append(content.strip())

    # Write back to file
    config_path.write_text("\n".join(new_lines), encoding=DEFAULT_ENCODING)

    logger.info(f"Content written to {config_path}")
    logger.info(f"Run `source {config_path}` to apply the changes")
