"""Cross-platform process termination utility."""

from __future__ import annotations

import argparse
import concurrent.futures
import fnmatch
import logging
import platform
import subprocess
import sys
import time
from dataclasses import dataclass, field
from functools import cached_property
from typing import Final

logging.basicConfig(level=logging.INFO, format="%(message)s", force=True)
logger = logging.getLogger(__name__)

IS_WINDOWS = platform.system() == "Windows"

# Constants
DEFAULT_MATCH_THRESHOLD: Final[int] = 10
FORCE_MATCH_THRESHOLD: Final[int] = 1000
PROCESS_LIST_TIMEOUT: Final[int] = 15
ENCODING_ATTEMPTS: Final[tuple[str, ...]] = ("gbk", "utf8")

# Benchmark test constants
BENCHMARK_LARGE_PROCESS_COUNT: Final[int] = 1000
BENCHMARK_MEDIUM_PROCESS_COUNT: Final[int] = 200
TEST_EXPECTED_MATCHES_TWO: Final[int] = 2
TEST_EXPECTED_CALL_COUNT_TWO: Final[int] = 2
CACHE_TIMEOUT: Final[int] = 5


@dataclass
class ProcessInfo:
    """Represents information about a system process.

    Attributes
    ----------
    name : str
        The name of the process executable
    pid : str
        The process identifier as string
    """

    name: str
    pid: str

    def __hash__(self) -> int:
        """Generate a hash for the ProcessInfo object.

        Returns
        -------
        int
            Hash value based on the process name and PID
        """
        return hash((self.name, self.pid))


@dataclass
class TaskkillCommand:
    """Represents a command for terminating system processes.

    Attributes
    ----------
    processes : list[ProcessInfo]
        List of ProcessInfo objects representing current system processes
    force : bool
        Whether to bypass safety thresholds for process termination
    """

    processes: list[ProcessInfo] = field(default_factory=list)
    force: bool = False

    # 添加进程列表缓存
    _process_cache = None
    _cache_timestamp = 0

    def kill(self, process_name: str) -> bool:
        """Terminate processes matching the given name or pattern.

        Parameters
        ----------
        process_name : str
            Name or pattern to match against process names

        Returns
        -------
        bool
            True if all matching processes were successfully terminated,
            False if some or all terminations failed

        Notes
        -----
        Performs case-insensitive wildcard matching. By default uses fuzzy
        matching with automatic * prefix and suffix if no explicit wildcards
        are provided.
        """
        logger.info("Killing processes matching '%s'", process_name)
        matched_processes = self.match(process_name)

        if not matched_processes:
            logger.warning("No processes found matching '%s'", process_name)
            return False

        match_count = len(matched_processes)
        if match_count > self.match_threshold:
            logger.warning(
                "Found %d processes matching '%s' (exceeds threshold of %d)",
                match_count,
                process_name,
                self.match_threshold,
            )
            logger.warning("Process names:")
            for i, p in enumerate(matched_processes, 1):
                logger.warning("  %d. %s (PID: %s)", i, p.name, p.pid)
            logger.warning("Use more specific process name to avoid accidental termination")
            return False

        logger.info(
            "Found %d process(es) matching '%s': %s",
            match_count,
            process_name,
            [m.name for m in matched_processes],
        )

        success_count = 0
        failure_count = 0

        for process in matched_processes:
            if self.kill_by_pid(process.pid):
                logger.info(
                    "Successfully terminated process %s (PID: %s)",
                    process.name,
                    process.pid,
                )
                success_count += 1
            else:
                logger.info(
                    "Failed to terminate process %s (PID: %s)",
                    process.name,
                    process.pid,
                )
                failure_count += 1

        logger.info(
            "Successfully terminated %d process(es) matching '%s'",
            success_count,
            process_name,
        )
        if failure_count > 0:
            logger.warning("Failed to terminate %d process(es)", failure_count)
            return False
        return True

    def kill_by_pid(self, pid: str) -> bool:
        """Terminate the process with the given PID.

        Args:
            pid: Process ID of the process to terminate

        Returns
        -------
            bool: True if process was successfully terminated, False otherwise
        """
        if IS_WINDOWS:
            return self._kill_by_pid_windows(pid)
        return self._kill_by_pid_unix(pid)

    def match(self, process_name: str) -> list[ProcessInfo]:
        """Match processes against the given pattern.

        Performs case-insensitive wildcard matching against process names.
        By default, performs fuzzy matching by adding * prefix and suffix if no
        explicit wildcards (*, ?, [, ]) are present in the input.

        Parameters
        ----------
        process_name : str
            Pattern to match against process names

        Returns
        -------
        list[ProcessInfo]
            List of ProcessInfo objects that match the pattern
        """
        if not any(char in process_name for char in ["*", "?", "[", "]"]):
            pattern = f"*{process_name}*".lower()
        else:
            pattern = process_name.lower()
        return [p for p in self.processes if fnmatch.fnmatch(p.name.lower(), pattern)]

    @cached_property
    def match_threshold(self) -> int:
        """Get the match threshold for process matching.

        Returns
        -------
        int
            The match threshold, which is higher if the force flag is set.
        """
        return FORCE_MATCH_THRESHOLD if self.force else DEFAULT_MATCH_THRESHOLD

    def __post_init__(self) -> None:
        """Initialize the process list based on the operating system."""
        current_time = time.time()

        # 检查缓存是否有效
        if self._process_cache is not None and current_time - self._cache_timestamp < CACHE_TIMEOUT:
            self.processes.extend(self._process_cache)
            return

        # 重新获取进程列表
        if IS_WINDOWS:
            self._get_process_list_windows()
        else:
            self._get_process_list_unix()

        # 更新缓存
        self._process_cache = self.processes.copy()
        self._cache_timestamp = current_time

    def _get_process_list_windows(self, encoding: str = "gbk") -> None:
        try:
            # 使用更高效的参数减少输出量
            result = subprocess.run(
                ["tasklist", "/fo", "csv", "/nh"],
                capture_output=True,
                text=True,
                encoding=encoding,
                check=True,
                timeout=PROCESS_LIST_TIMEOUT,
            )
            logger.debug("Decoded using %s", encoding)
            for line in result.stdout.strip().split("\n"):
                if line:
                    parts = line.split('","')
                    if len(parts) >= 2:  # noqa: PLR2004
                        name = parts[0].strip('"')
                        pid = parts[1].strip('"')
                        self.processes.append(ProcessInfo(name, pid))
        except UnicodeDecodeError:
            logger.warning(
                "Failed to decode using %s encoding, trying other encoding",
                encoding,
            )
            if encoding == ENCODING_ATTEMPTS[-1]:
                logger.exception("All encoding attempts failed, unable to get process list")
                return

            # Try next encoding in sequence
            next_encoding_idx = ENCODING_ATTEMPTS.index(encoding) + 1
            if next_encoding_idx < len(ENCODING_ATTEMPTS):
                next_encoding = ENCODING_ATTEMPTS[next_encoding_idx]
                try:
                    self._get_process_list_windows(encoding=next_encoding)
                    logger.debug("Decoded using %s", next_encoding)
                except (
                    subprocess.SubprocessError,
                    OSError,
                    ValueError,
                    UnicodeDecodeError,
                ):
                    logger.exception("Failed to get process list after encoding fallback")
                    return
        except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
            logger.exception("Failed to execute tasklist command: %s", e.__class__.__name__)
            return
        except Exception as e:
            logger.exception(
                "Unknown error occurred while getting process list: %s",
                e.__class__.__name__,
            )
            return

    def _get_process_list_unix(self) -> None:
        try:
            result = subprocess.run(
                ["ps", "-eo", "pid,comm", "--no-headers"],
                capture_output=True,
                text=True,
                check=True,
                timeout=PROCESS_LIST_TIMEOUT,
            )
            for line in result.stdout.strip().split("\n"):
                if line:
                    parts = line.strip().split(maxsplit=1)
                    if len(parts) >= 2:  # noqa: PLR2004
                        pid = parts[0]
                        name = parts[1]
                        self.processes.append(ProcessInfo(name, pid))
        except (
            subprocess.SubprocessError,
            OSError,
            ValueError,
            subprocess.TimeoutExpired,
        ) as e:
            logger.exception("Failed to get process list: %s", e.__class__.__name__)
            return

    @staticmethod
    def _kill_by_pid_windows(pid: str) -> None:
        """Terminate process by PID on Windows using taskkill command.

        Args:
            pid: Process identifier as string

        Returns
        -------
            True if process was successfully terminated, False otherwise
        """
        try:
            result = subprocess.run(
                ["taskkill", "/F", "/PID", pid],
                capture_output=True,
                text=True,
                check=True,
                timeout=PROCESS_LIST_TIMEOUT,
            )
        except subprocess.CalledProcessError as e:
            logger.exception("Failed to terminate process PID %s", pid)
            if e.stdout:
                logger.debug("taskkill stdout: %s", e.stdout.strip())
            if e.stderr:
                logger.debug("taskkill stderr: %s", e.stderr.strip())
            return False
        except subprocess.TimeoutExpired:
            logger.exception("Timeout while terminating process PID %s", pid)
            return False
        else:
            logger.debug("taskkill output: %s", result.stdout.strip())
            return True

    @staticmethod
    def _kill_by_pid_unix(pid: str) -> None:
        """Terminate process by PID on Unix/Linux using kill command.

        Args:
            pid: Process identifier as string

        Returns
        -------
            True if process was successfully terminated, False otherwise
        """
        try:
            result = subprocess.run(
                ["kill", "-9", pid],
                capture_output=True,
                text=True,
                check=True,
                timeout=PROCESS_LIST_TIMEOUT,
            )
        except subprocess.CalledProcessError as e:
            logger.exception("Failed to terminate process PID %s", pid)
            if e.stdout:
                logger.debug("kill stdout: %s", e.stdout.strip())
            if e.stderr:
                logger.debug("kill stderr: %s", e.stderr.strip())
            return False
        except subprocess.TimeoutExpired:
            logger.exception("Timeout while terminating process PID %s", pid)
            return False
        else:
            logger.debug("kill output: %s", result.stdout.strip())
            return True


def parse_args() -> argparse.Namespace:
    """Parse command-line arguments for the taskkill utility.

    Returns
    -------
    argparse.Namespace
        Parsed command-line arguments
    """
    parser = argparse.ArgumentParser(
        description="Cross-platform process termination utility",
        epilog="Examples:\n  taskk notepad.exe\n  taskk python*\n  taskk chrome.exe --debug\n  taskk temp* --force",
    )
    parser.add_argument(
        "processes",
        type=str,
        nargs="+",
        help="Process name or pattern to terminate (supports wildcards)",
    )
    parser.add_argument(
        "--debug",
        "-d",
        action="store_true",
        help="Enable debug logging for detailed output",
    )
    parser.add_argument(
        "--force",
        "-f",
        action="store_true",
        help="Force termination even if match count exceeds safety threshold",
    )
    return parser.parse_args()


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

    Returns
    -------
        int: Exit code (0 for success, non-zero for failure)
    """
    args = parse_args()

    if args.debug:
        logger.setLevel(logging.DEBUG)
        logger.debug("Debug mode enabled")

    if args.force:
        cmd = TaskkillCommand(force=True)
        logger.debug("Force mode enabled, threshold increased to %d", cmd.match_threshold)
    else:
        cmd = TaskkillCommand()

    t0 = time.perf_counter()
    max_workers = min(10, len(args.processes))
    with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = [executor.submit(cmd.kill, process) for process in args.processes]

    # Collect results to ensure all operations complete
    results = []
    for future in concurrent.futures.as_completed(futures):
        result = future.result()
        results.append(result)

    # Flush stdout to ensure all output is displayed
    sys.stdout.flush()

    # Return overall success status
    success = all(results) if results else False
    logger.info("Operation completed in %.4f seconds", time.perf_counter() - t0)
    sys.stdout.flush()
    return 0 if success else 1
