"""Process scanner for AYN Antivirus.

Inspects running processes for known crypto-miners, anomalous CPU usage,
and hidden / stealth processes.  Uses ``psutil`` for cross-platform process
enumeration and ``/proc`` on Linux for hidden-process detection.
"""

from __future__ import annotations

import logging
import os
import signal
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional

import psutil

from ayn_antivirus.constants import (
    CRYPTO_MINER_PROCESS_NAMES,
    HIGH_CPU_THRESHOLD,
)
from ayn_antivirus.scanners.base import BaseScanner

logger = logging.getLogger(__name__)


class ProcessScanner(BaseScanner):
    """Scan running processes for malware, miners, and anomalies.

    Parameters
    ----------
    cpu_threshold:
        CPU-usage percentage above which a process is flagged.  Defaults to
        :pydata:`constants.HIGH_CPU_THRESHOLD`.
    """

    def __init__(self, cpu_threshold: float = HIGH_CPU_THRESHOLD) -> None:
        self.cpu_threshold = cpu_threshold

    # ------------------------------------------------------------------
    # BaseScanner interface
    # ------------------------------------------------------------------

    @property
    def name(self) -> str:
        return "process_scanner"

    @property
    def description(self) -> str:
        return "Inspects running processes for miners and suspicious activity"

    def scan(self, target: Any = None) -> Dict[str, Any]:
        """Run a full process scan.

        *target* is ignored — all live processes are inspected.

        Returns
        -------
        dict
            ``total``, ``suspicious``, ``high_cpu``, ``hidden``.
        """
        all_procs = self.get_all_processes()
        suspicious = self.find_suspicious_processes()
        high_cpu = self.find_high_cpu_processes()
        hidden = self.find_hidden_processes()

        return {
            "total": len(all_procs),
            "suspicious": suspicious,
            "high_cpu": high_cpu,
            "hidden": hidden,
        }

    # ------------------------------------------------------------------
    # Process enumeration
    # ------------------------------------------------------------------

    @staticmethod
    def get_all_processes() -> List[Dict[str, Any]]:
        """Return a snapshot of every running process.

        Each dict contains: ``pid``, ``name``, ``cmdline``, ``cpu_percent``,
        ``memory_percent``, ``username``, ``create_time``, ``connections``,
        ``open_files``.
        """
        result: List[Dict[str, Any]] = []
        attrs = [
            "pid", "name", "cmdline", "cpu_percent",
            "memory_percent", "username", "create_time",
        ]

        for proc in psutil.process_iter(attrs):
            try:
                info = proc.info
                # Connections and open files are expensive; fetch lazily.
                try:
                    connections = [
                        {
                            "fd": c.fd,
                            "family": str(c.family),
                            "type": str(c.type),
                            "laddr": f"{c.laddr.ip}:{c.laddr.port}" if c.laddr else "",
                            "raddr": f"{c.raddr.ip}:{c.raddr.port}" if c.raddr else "",
                            "status": c.status,
                        }
                        for c in proc.net_connections()
                    ]
                except (psutil.AccessDenied, psutil.NoSuchProcess, OSError):
                    connections = []

                try:
                    open_files = [f.path for f in proc.open_files()]
                except (psutil.AccessDenied, psutil.NoSuchProcess, OSError):
                    open_files = []

                create_time = info.get("create_time")
                result.append({
                    "pid": info["pid"],
                    "name": info.get("name", ""),
                    "cmdline": info.get("cmdline") or [],
                    "cpu_percent": info.get("cpu_percent") or 0.0,
                    "memory_percent": info.get("memory_percent") or 0.0,
                    "username": info.get("username", "?"),
                    "create_time": (
                        datetime.utcfromtimestamp(create_time).isoformat()
                        if create_time
                        else None
                    ),
                    "connections": connections,
                    "open_files": open_files,
                })
            except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
                continue

        return result

    # ------------------------------------------------------------------
    # Suspicious-process detection
    # ------------------------------------------------------------------

    def find_suspicious_processes(self) -> List[Dict[str, Any]]:
        """Return processes whose name or command line matches a known miner.

        Matches are case-insensitive against
        :pydata:`constants.CRYPTO_MINER_PROCESS_NAMES`.
        """
        suspicious: List[Dict[str, Any]] = []

        for proc in psutil.process_iter(["pid", "name", "cmdline", "cpu_percent", "username"]):
            try:
                info = proc.info
                pname = (info.get("name") or "").lower()
                cmdline = " ".join(info.get("cmdline") or []).lower()

                for miner in CRYPTO_MINER_PROCESS_NAMES:
                    if miner in pname or miner in cmdline:
                        suspicious.append({
                            "pid": info["pid"],
                            "name": info.get("name", ""),
                            "cmdline": info.get("cmdline") or [],
                            "cpu_percent": info.get("cpu_percent") or 0.0,
                            "username": info.get("username", "?"),
                            "matched_signature": miner,
                            "reason": f"Known miner process: {miner}",
                            "severity": "CRITICAL",
                        })
                        break  # one match per process
            except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
                continue

        return suspicious

    # ------------------------------------------------------------------
    # High-CPU detection
    # ------------------------------------------------------------------

    def find_high_cpu_processes(
        self,
        threshold: Optional[float] = None,
    ) -> List[Dict[str, Any]]:
        """Return processes whose CPU usage exceeds *threshold* percent.

        Parameters
        ----------
        threshold:
            Override the instance-level ``cpu_threshold``.
        """
        limit = threshold if threshold is not None else self.cpu_threshold
        high: List[Dict[str, Any]] = []

        for proc in psutil.process_iter(["pid", "name", "cmdline", "cpu_percent", "username"]):
            try:
                info = proc.info
                cpu = info.get("cpu_percent") or 0.0
                if cpu > limit:
                    high.append({
                        "pid": info["pid"],
                        "name": info.get("name", ""),
                        "cmdline": info.get("cmdline") or [],
                        "cpu_percent": cpu,
                        "username": info.get("username", "?"),
                        "reason": f"High CPU usage: {cpu:.1f}%",
                        "severity": "HIGH",
                    })
            except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
                continue

        return high

    # ------------------------------------------------------------------
    # Hidden-process detection (Linux only)
    # ------------------------------------------------------------------

    @staticmethod
    def find_hidden_processes() -> List[Dict[str, Any]]:
        """Detect processes visible in ``/proc`` but hidden from ``psutil``.

        On non-Linux systems this returns an empty list.

        A mismatch may indicate a userland rootkit that hooks the process
        listing syscalls.
        """
        proc_dir = Path("/proc")
        if not proc_dir.is_dir():
            return []  # not Linux

        # PIDs visible via /proc filesystem.
        proc_pids: set[int] = set()
        try:
            for entry in proc_dir.iterdir():
                if entry.name.isdigit():
                    proc_pids.add(int(entry.name))
        except PermissionError:
            logger.warning("Cannot enumerate /proc")
            return []

        # PIDs visible via psutil (which ultimately calls getdents / readdir).
        psutil_pids = set(psutil.pids())

        hidden: List[Dict[str, Any]] = []
        for pid in proc_pids - psutil_pids:
            # Read whatever we can from /proc/<pid>.
            name = ""
            cmdline = ""
            try:
                comm = proc_dir / str(pid) / "comm"
                if comm.exists():
                    name = comm.read_text().strip()
            except OSError:
                pass
            try:
                cl = proc_dir / str(pid) / "cmdline"
                if cl.exists():
                    cmdline = cl.read_bytes().replace(b"\x00", b" ").decode(errors="replace").strip()
            except OSError:
                pass

            hidden.append({
                "pid": pid,
                "name": name,
                "cmdline": cmdline,
                "reason": "Process visible in /proc but hidden from psutil (possible rootkit)",
                "severity": "CRITICAL",
            })

        return hidden

    # ------------------------------------------------------------------
    # Single-process detail
    # ------------------------------------------------------------------

    @staticmethod
    def get_process_details(pid: int) -> Dict[str, Any]:
        """Return comprehensive information about a single process.

        Raises
        ------
        psutil.NoSuchProcess
            If the PID does not exist.
        psutil.AccessDenied
            If the caller lacks permission to inspect the process.
        """
        proc = psutil.Process(pid)
        with proc.oneshot():
            info: Dict[str, Any] = {
                "pid": proc.pid,
                "name": proc.name(),
                "exe": "",
                "cmdline": proc.cmdline(),
                "status": proc.status(),
                "username": "",
                "cpu_percent": proc.cpu_percent(interval=0.1),
                "memory_percent": proc.memory_percent(),
                "memory_info": {},
                "create_time": datetime.utcfromtimestamp(proc.create_time()).isoformat(),
                "cwd": "",
                "open_files": [],
                "connections": [],
                "threads": proc.num_threads(),
                "nice": None,
                "environ": {},
            }

            try:
                info["exe"] = proc.exe()
            except (psutil.AccessDenied, OSError):
                pass

            try:
                info["username"] = proc.username()
            except psutil.AccessDenied:
                pass

            try:
                mem = proc.memory_info()
                info["memory_info"] = {"rss": mem.rss, "vms": mem.vms}
            except (psutil.AccessDenied, OSError):
                pass

            try:
                info["cwd"] = proc.cwd()
            except (psutil.AccessDenied, OSError):
                pass

            try:
                info["open_files"] = [f.path for f in proc.open_files()]
            except (psutil.AccessDenied, OSError):
                pass

            try:
                info["connections"] = [
                    {
                        "laddr": f"{c.laddr.ip}:{c.laddr.port}" if c.laddr else "",
                        "raddr": f"{c.raddr.ip}:{c.raddr.port}" if c.raddr else "",
                        "status": c.status,
                    }
                    for c in proc.net_connections()
                ]
            except (psutil.AccessDenied, OSError):
                pass

            try:
                info["nice"] = proc.nice()
            except (psutil.AccessDenied, OSError):
                pass

            try:
                info["environ"] = dict(proc.environ())
            except (psutil.AccessDenied, OSError):
                pass

        return info

    # ------------------------------------------------------------------
    # Process control
    # ------------------------------------------------------------------

    @staticmethod
    def kill_process(pid: int) -> bool:
        """Send ``SIGKILL`` to the process with *pid*.

        Returns ``True`` if the signal was delivered successfully, ``False``
        otherwise (e.g. the process no longer exists or permission denied).
        """
        try:
            proc = psutil.Process(pid)
            proc.kill()  # SIGKILL
            proc.wait(timeout=5)
            logger.info("Killed process %d (%s)", pid, proc.name())
            return True
        except psutil.NoSuchProcess:
            logger.warning("Process %d no longer exists", pid)
            return False
        except psutil.AccessDenied:
            logger.error("Permission denied killing process %d", pid)
            # Fall back to raw signal as a last resort.
            try:
                os.kill(pid, signal.SIGKILL)
                logger.info("Killed process %d via os.kill", pid)
                return True
            except OSError as exc:
                logger.error("os.kill(%d) failed: %s", pid, exc)
                return False
        except psutil.TimeoutExpired:
            logger.warning("Process %d did not exit within timeout", pid)
            return False
