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

from __future__ import annotations

import argparse
import fnmatch
import logging
import subprocess
import sys
from dataclasses import dataclass
from typing import Final

logging.basicConfig(level=logging.INFO, format="%(message)s")

logger = logging.getLogger(__name__)


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

# Global state (consider refactoring to class-based approach in future)
_cached_process_list: list[ProcessInfo] = []
_processes_cached: bool = False
_max_match_threshold: int = DEFAULT_MATCH_THRESHOLD


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

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

    name: str
    pid: str


def get_matched_process(process_name: str) -> list[ProcessInfo]:
    """Get processes matching 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.


    Args:
        process_name: Pattern to match against process names

    Returns
    -------
        List of ProcessInfo objects that match the pattern

    Examples
    --------
        get_matched_process("python")     # Matches *python* (fuzzy)
        get_matched_process("python*")    # Matches python* (prefix)
        get_matched_process("*python*")   # Matches *python* (explicit)
        get_matched_process("*thon.exe")  # Matches *thon.exe (suffix)
    """
    # Auto-add wildcards for fuzzy matching if no explicit wildcards present
    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 _cached_process_list if fnmatch.fnmatch(p.name.lower(), pattern)]


def get_process_list(*, force_refresh: bool = False) -> None:
    """Retrieve the current list of system processes.

    Caches the process list to avoid repeated system calls. Use force_refresh=True
    to bypass the cache when fresh data is required.

    Args:
        force_refresh: If True, forces refreshing the process list even if cached.
            Defaults to False.
    """
    global _processes_cached, _cached_process_list

    if _processes_cached and not force_refresh:
        logger.debug("Using cached process list")
        return

    _cached_process_list.clear()
    if sys.platform == "win32":
        _get_process_list_windows()
    else:
        _get_process_list_unix()
    _processes_cached = True
    logger.debug(f"Retrieved {len(_cached_process_list)} processes")


def _get_process_list_windows(encoding: str = "gbk") -> None:
    """Retrieve process list on Windows using tasklist command.

    Attempts to decode output with specified encoding, falling back to alternatives
    if decoding fails.

    Args:
        encoding: Text encoding to try first. Defaults to "gbk".
    """
    try:
        result = subprocess.run(
            ["tasklist", "/fo", "csv", "/nh"],
            capture_output=True,
            text=True,
            encoding=encoding,
            check=True,
            timeout=PROCESS_LIST_TIMEOUT,
        )
        logger.debug(f"Decoded using {encoding}")
        for line in result.stdout.strip().split("\n"):
            if line:
                parts = line.split('","')
                if len(parts) >= 2:
                    name = parts[0].strip('"')
                    pid = parts[1].strip('"')
                    _cached_process_list.append(ProcessInfo(name, pid))
    except UnicodeDecodeError:
        logger.warning(
            f"Failed to decode using {encoding} encoding, trying other 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:
                _get_process_list_windows(encoding=next_encoding)
                logger.debug(f"Decoded using {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(f"Failed to execute tasklist command: {e.__class__.__name__}")
        return
    except Exception as e:
        logger.exception(
            f"Unknown error occurred while getting process list: {e.__class__.__name__}",
        )
        return


def _get_process_list_unix() -> None:
    """Retrieve process list on Unix-like systems using ps command."""
    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:
                    pid = parts[0]
                    name = parts[1]
                    _cached_process_list.append(ProcessInfo(name, pid))
    except (
        subprocess.SubprocessError,
        OSError,
        ValueError,
        subprocess.TimeoutExpired,
    ) as e:
        logger.exception(f"Failed to get process list: {e.__class__.__name__}")
        return


def kill_process(process_name: str, *, force_refresh: bool = True) -> None:
    """Terminate processes matching the given name or pattern.

    Implements safety mechanisms including match threshold limits to prevent
    accidental mass termination of processes.

    Args:
        process_name: Process name or pattern to match (supports wildcards)
        force_refresh: Whether to refresh the process list before matching.
            Defaults to True.
    """
    get_process_list(force_refresh=force_refresh)
    matched_processes = get_matched_process(process_name)

    if not matched_processes:
        logger.warning(f"Process `{process_name}` not found")
        return

    match_count = len(matched_processes)

    if match_count > _max_match_threshold:
        logger.warning(
            f"Found {match_count} processes matching  '{process_name}' (exceeds threshold of {_max_match_threshold})",
        )
        logger.warning("Process names:")
        for i, p in enumerate(matched_processes, 1):
            logger.warning(f"  {i}. {p.name} (PID: {p.pid})")
        logger.warning("Use more specific process name to avoid accidental termination")
        return

    logger.info(
        f"Found {match_count} process(es) matching '{process_name}': {[m.name for m in matched_processes]}",
    )

    success_count = 0
    failure_count = 0

    for process in matched_processes:
        if _kill_process_by_pid(process.pid):
            logger.info(
                f"Successfully terminated process {process.name} (PID: {process.pid})",
            )
            success_count += 1
        else:
            logger.info(
                f"Failed to terminate process {process.name} (PID: {process.pid})",
            )
            failure_count += 1

    logger.info(
        f"Successfully terminated {success_count} process(es) matching '{process_name}'",
    )
    if failure_count > 0:
        logger.warning(f"Failed to terminate {failure_count} process(es)")


def _kill_process_by_pid(pid: str) -> bool:
    if sys.platform == "win32":
        return _kill_process_by_pid_windows(pid)
    return _kill_process_by_pid_unix(pid)


def _kill_process_by_pid_windows(pid: str) -> bool:
    """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(f"Failed to terminate process PID {pid}")
        if e.stdout:
            logger.debug(f"taskkill stdout: {e.stdout.strip()}")
        if e.stderr:
            logger.debug(f"taskkill stderr: {e.stderr.strip()}")
        return False
    except subprocess.TimeoutExpired:
        logger.exception(f"Timeout while terminating process PID {pid}")
        return False
    else:
        logger.debug(f"taskkill output: {result.stdout.strip()}")
        return True


def _kill_process_by_pid_unix(pid: str) -> bool:
    """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(f"Failed to terminate process PID {pid}")
        if e.stdout:
            logger.debug(f"kill stdout: {e.stdout.strip()}")
        if e.stderr:
            logger.debug(f"kill stderr: {e.stderr.strip()}")
        return False
    except subprocess.TimeoutExpired:
        logger.exception(f"Timeout while terminating process PID {pid}")
        return False
    else:
        logger.debug(f"kill output: {result.stdout.strip()}")
        return True


def main() -> None:
    """Run entry point for the command-line interface."""
    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",
    )

    args = parser.parse_args()

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

    # Temporarily increase threshold if force flag is set
    global _max_match_threshold
    original_threshold = _max_match_threshold
    if args.force:
        _max_match_threshold = FORCE_MATCH_THRESHOLD
        logger.debug(
            f"Force mode enabled, threshold increased to {_max_match_threshold}",
        )

    # Process each target
    for i, process_name in enumerate(args.processes):
        # Refresh process list only for the first process to avoid redundant calls
        force_refresh = i == 0
        logger.info(f"Terminating processes matching: {process_name}")
        kill_process(process_name, force_refresh=force_refresh)

    # Restore original threshold
    _max_match_threshold = original_threshold
    if args.force:
        logger.debug("Restored original match threshold")
