"""AYN Antivirus — Container Scanner.

Scans Docker, Podman, and LXC containers for threats.
Supports: listing containers, scanning container filesystems,
inspecting container processes, checking container images,
and detecting cryptominers/malware inside containers.
"""

from __future__ import annotations

import json
import logging
import re
import shutil
import subprocess
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple

from ayn_antivirus.constants import (
    CRYPTO_MINER_PROCESS_NAMES,
    CRYPTO_POOL_DOMAINS,
    SUSPICIOUS_PORTS,
)
from ayn_antivirus.scanners.base import BaseScanner
from ayn_antivirus.utils.helpers import generate_id

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------

@dataclass
class ContainerInfo:
    """Information about a discovered container."""

    container_id: str
    name: str
    image: str
    status: str          # running, stopped, paused
    runtime: str         # docker, podman, lxc
    created: str
    ports: List[str] = field(default_factory=list)
    mounts: List[str] = field(default_factory=list)
    pid: int = 0         # host PID of container init process
    ip_address: str = ""
    labels: Dict[str, str] = field(default_factory=dict)

    def to_dict(self) -> dict:
        return {
            "container_id": self.container_id,
            "name": self.name,
            "image": self.image,
            "status": self.status,
            "runtime": self.runtime,
            "created": self.created,
            "ports": self.ports,
            "mounts": self.mounts,
            "pid": self.pid,
            "ip_address": self.ip_address,
            "labels": self.labels,
        }


@dataclass
class ContainerThreat:
    """A threat detected inside a container."""

    container_id: str
    container_name: str
    runtime: str
    threat_name: str
    threat_type: str     # virus, malware, miner, spyware, rootkit, misconfiguration
    severity: str        # CRITICAL, HIGH, MEDIUM, LOW
    details: str
    file_path: str = ""
    process_name: str = ""
    timestamp: str = field(
        default_factory=lambda: datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S"),
    )

    def to_dict(self) -> dict:
        return {
            "container_id": self.container_id,
            "container_name": self.container_name,
            "runtime": self.runtime,
            "threat_name": self.threat_name,
            "threat_type": self.threat_type,
            "severity": self.severity,
            "details": self.details,
            "file_path": self.file_path,
            "process_name": self.process_name,
            "timestamp": self.timestamp,
        }


@dataclass
class ContainerScanResult:
    """Result of scanning containers."""

    scan_id: str
    start_time: str
    end_time: str = ""
    containers_found: int = 0
    containers_scanned: int = 0
    threats: List[ContainerThreat] = field(default_factory=list)
    containers: List[ContainerInfo] = field(default_factory=list)
    errors: List[str] = field(default_factory=list)

    @property
    def is_clean(self) -> bool:
        return len(self.threats) == 0

    @property
    def duration_seconds(self) -> float:
        if not self.end_time or not self.start_time:
            return 0.0
        try:
            s = datetime.strptime(self.start_time, "%Y-%m-%d %H:%M:%S")
            e = datetime.strptime(self.end_time, "%Y-%m-%d %H:%M:%S")
            return (e - s).total_seconds()
        except Exception:
            return 0.0

    def to_dict(self) -> dict:
        return {
            "scan_id": self.scan_id,
            "start_time": self.start_time,
            "end_time": self.end_time,
            "containers_found": self.containers_found,
            "containers_scanned": self.containers_scanned,
            "threats_found": len(self.threats),
            "threats": [t.to_dict() for t in self.threats],
            "containers": [c.to_dict() for c in self.containers],
            "errors": self.errors,
            "duration_seconds": self.duration_seconds,
        }


# ---------------------------------------------------------------------------
# Scanner
# ---------------------------------------------------------------------------

class ContainerScanner(BaseScanner):
    """Scans Docker, Podman, and LXC containers for security threats.

    Gracefully degrades when a container runtime is not installed — only
    the available runtimes are exercised.
    """

    _SAFE_CONTAINER_ID = re.compile(r'^[a-zA-Z0-9][a-zA-Z0-9_.\-]*$')

    def __init__(self) -> None:
        self._docker_cmd = self._find_command("docker")
        self._podman_cmd = self._find_command("podman")
        self._lxc_cmd = self._find_command("lxc-ls")
        self._incus_cmd = self._find_command("incus")
        self._available_runtimes: List[str] = []
        if self._incus_cmd:
            self._available_runtimes.append("incus")
        if self._docker_cmd:
            self._available_runtimes.append("docker")
        if self._podman_cmd:
            self._available_runtimes.append("podman")
        if self._lxc_cmd:
            self._available_runtimes.append("lxc")

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

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

    @property
    def description(self) -> str:
        return (
            "Scans Docker, Podman, and LXC containers for malware, "
            "miners, and misconfigurations"
        )

    @property
    def available_runtimes(self) -> List[str]:
        return list(self._available_runtimes)

    def scan(self, target: Any = "all") -> ContainerScanResult:
        """Scan all containers or a specific one.

        Parameters
        ----------
        target:
            ``"all"``, a runtime name (``"docker"``, ``"podman"``,
            ``"lxc"``), or a container ID / name.
        """
        result = ContainerScanResult(
            scan_id=generate_id()[:16],
            start_time=datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S"),
        )

        if not self._available_runtimes:
            result.errors.append(
                "No container runtimes found (docker/podman/lxc not installed)"
            )
            result.end_time = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
            return result

        target = str(target)
        if target in ("all", "docker", "podman", "lxc"):
            runtime = "all" if target == "all" else target
            containers = self.list_containers(
                runtime=runtime, include_stopped=True,
            )
        else:
            containers = self._find_container(target)

        result.containers = containers
        result.containers_found = len(containers)

        for container in containers:
            try:
                threats = self._scan_container(container)
                result.threats.extend(threats)
                result.containers_scanned += 1
            except Exception as exc:
                msg = f"Error scanning {container.name}: {exc}"
                result.errors.append(msg)
                self._log_error("Error scanning container %s: %s", container.name, exc)

        result.end_time = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
        return result

    def scan_container(self, container_id: str) -> ContainerScanResult:
        """Convenience — scan a single container by ID or name."""
        cid = self._sanitize_id(container_id)
        return self.scan(target=cid)

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    @staticmethod
    def _find_command(cmd: str) -> Optional[str]:
        return shutil.which(cmd)

    @staticmethod
    def _run_cmd(
        cmd: List[str],
        timeout: int = 30,
    ) -> Tuple[str, str, int]:
        """Run a shell command and return ``(stdout, stderr, returncode)``."""
        try:
            proc = subprocess.run(
                cmd, capture_output=True, text=True, timeout=timeout,
            )
            return proc.stdout.strip(), proc.stderr.strip(), proc.returncode
        except subprocess.TimeoutExpired:
            return "", f"Command timed out: {' '.join(cmd)}", -1
        except FileNotFoundError:
            return "", f"Command not found: {cmd[0]}", -1
        except Exception as exc:
            return "", str(exc), -1

    def _sanitize_id(self, container_id: str) -> str:
        """Sanitize container ID/name for safe use in subprocess commands.

        Raises :class:`ValueError` if the ID contains invalid characters
        or exceeds length limits.
        """
        cid = container_id.strip()
        if not cid or len(cid) > 128:
            raise ValueError(f"Invalid container ID length: {len(cid)}")
        if not self._SAFE_CONTAINER_ID.match(cid):
            raise ValueError(f"Invalid container ID characters: {cid!r}")
        return cid

    # ------------------------------------------------------------------
    # Container discovery
    # ------------------------------------------------------------------

    def list_containers(
        self,
        runtime: str = "all",
        include_stopped: bool = False,
    ) -> List[ContainerInfo]:
        """List all containers across available runtimes."""
        containers: List[ContainerInfo] = []
        runtimes = (
            self._available_runtimes if runtime == "all" else [runtime]
        )

        for rt in runtimes:
            if rt == "incus" and self._incus_cmd:
                containers.extend(self._list_incus(include_stopped))
            elif rt == "docker" and self._docker_cmd:
                containers.extend(self._list_docker(include_stopped))
            elif rt == "podman" and self._podman_cmd:
                containers.extend(self._list_podman(include_stopped))
            elif rt == "lxc" and self._lxc_cmd:
                containers.extend(self._list_lxc())

        return containers

    # -- Docker --------------------------------------------------------

    def _list_docker(self, include_stopped: bool = False) -> List[ContainerInfo]:
        fmt = (
            "{{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}"
            "\t{{.CreatedAt}}\t{{.Ports}}"
        )
        cmd = [self._docker_cmd, "ps", "--format", fmt, "--no-trunc"]
        if include_stopped:
            cmd.append("-a")
        stdout, stderr, rc = self._run_cmd(cmd)
        if rc != 0:
            self._log_warning("Docker ps failed: %s", stderr)
            return []

        containers: List[ContainerInfo] = []
        for line in stdout.splitlines():
            if not line.strip():
                continue
            parts = line.split("\t")
            if len(parts) < 4:
                continue
            cid = parts[0][:12]
            name = parts[1] if len(parts) > 1 else ""
            image = parts[2] if len(parts) > 2 else ""
            status_str = parts[3] if len(parts) > 3 else ""
            created = parts[4] if len(parts) > 4 else ""
            ports_str = parts[5] if len(parts) > 5 else ""

            status = (
                "running" if "Up" in status_str
                else "stopped" if "Exited" in status_str
                else "unknown"
            )
            ports = (
                [p.strip() for p in ports_str.split(",") if p.strip()]
                if ports_str else []
            )
            info = self._inspect_docker(cid)
            containers.append(ContainerInfo(
                container_id=cid,
                name=name,
                image=image,
                status=status,
                runtime="docker",
                created=created,
                ports=ports,
                mounts=info.get("mounts", []),
                pid=info.get("pid", 0),
                ip_address=info.get("ip", ""),
                labels=info.get("labels", {}),
            ))
        return containers

    def _inspect_docker(self, container_id: str) -> Dict[str, Any]:
        cid = self._sanitize_id(container_id)
        cmd = [self._docker_cmd, "inspect", cid]
        stdout, _, rc = self._run_cmd(cmd, timeout=10)
        if rc != 0:
            return {}
        try:
            data = json.loads(stdout)
            if not data:
                return {}
            c = data[0]
            state = c.get("State", {})
            network = c.get("NetworkSettings", {})
            mounts = [m.get("Source", "") for m in c.get("Mounts", [])]
            ip = ""
            for net_info in network.get("Networks", {}).values():
                if net_info.get("IPAddress"):
                    ip = net_info["IPAddress"]
                    break
            return {
                "pid": state.get("Pid", 0),
                "ip": ip,
                "mounts": mounts,
                "labels": c.get("Config", {}).get("Labels", {}),
            }
        except (json.JSONDecodeError, KeyError, IndexError):
            return {}

    # -- Podman --------------------------------------------------------

    def _list_podman(self, include_stopped: bool = False) -> List[ContainerInfo]:
        cmd = [self._podman_cmd, "ps", "--format", "json"]
        if include_stopped:
            cmd.append("-a")
        stdout, stderr, rc = self._run_cmd(cmd)
        if rc != 0:
            self._log_warning("Podman ps failed: %s", stderr)
            return []
        try:
            data = json.loads(stdout) if stdout else []
        except json.JSONDecodeError:
            return []

        containers: List[ContainerInfo] = []
        for c in data:
            cid = str(c.get("Id", ""))[:12]
            names = c.get("Names", [])
            name = names[0] if names else cid
            status_str = str(c.get("State", c.get("Status", "")))
            status = (
                "running" if status_str.lower() in ("running", "up")
                else "stopped"
            )
            ports_list: List[str] = []
            for p in c.get("Ports", []) or []:
                if isinstance(p, dict):
                    ports_list.append(
                        f"{p.get('hostPort', '')}:{p.get('containerPort', '')}"
                    )
                else:
                    ports_list.append(str(p))
            containers.append(ContainerInfo(
                container_id=cid,
                name=name,
                image=c.get("Image", ""),
                status=status,
                runtime="podman",
                created=str(c.get("Created", c.get("CreatedAt", ""))),
                ports=ports_list,
                pid=c.get("Pid", 0),
                labels=c.get("Labels", {}),
            ))
        return containers

    # -- LXC -----------------------------------------------------------

    def _list_lxc(self) -> List[ContainerInfo]:
        stdout, stderr, rc = self._run_cmd(
            [self._lxc_cmd, "--fancy", "-F", "name,state,ipv4,pid"],
        )
        if rc != 0:
            self._log_warning("LXC list failed: %s", stderr)
            return []
        containers: List[ContainerInfo] = []
        for line in stdout.splitlines()[1:]:  # skip header
            parts = line.split()
            if len(parts) < 2:
                continue
            name = parts[0]
            state = parts[1].lower()
            ip = parts[2] if len(parts) > 2 and parts[2] != "-" else ""
            pid = (
                int(parts[3])
                if len(parts) > 3 and parts[3].isdigit()
                else 0
            )
            containers.append(ContainerInfo(
                container_id=name,
                name=name,
                image="lxc",
                status="running" if state == "running" else "stopped",
                runtime="lxc",
                created="",
                pid=pid,
                ip_address=ip,
            ))
        return containers

    # -- Incus ---------------------------------------------------------

    def _list_incus(self, include_stopped: bool = False) -> List[ContainerInfo]:
        """List Incus containers (and optionally VMs)."""
        cmd = [self._incus_cmd, "list", "--format", "json"]
        stdout, stderr, rc = self._run_cmd(cmd, timeout=15)
        if rc != 0:
            self._log_warning("Incus list failed: %s", stderr)
            return []
        try:
            data = json.loads(stdout) if stdout else []
        except json.JSONDecodeError:
            return []

        containers: List[ContainerInfo] = []
        for c in data:
            status_str = c.get("status", "").lower()
            if not include_stopped and status_str != "running":
                continue

            name = c.get("name", "")
            ctype = c.get("type", "container")

            # Extract IPv4 addresses from the network state.
            ip_address = ""
            net_state = c.get("state", {}).get("network", {}) or {}
            for iface_name, iface in net_state.items():
                if iface_name == "lo":
                    continue
                for addr in iface.get("addresses", []):
                    if addr.get("family") == "inet" and addr.get("scope") == "global":
                        ip_address = addr.get("address", "")
                        break
                if ip_address:
                    break

            # Fallback: try expanded_devices for static IP.
            if not ip_address:
                devices = c.get("expanded_devices", {})
                for dev in devices.values():
                    if dev.get("type") == "nic" and dev.get("ipv4.address"):
                        ip_address = dev["ipv4.address"]
                        break

            config = c.get("config", {})
            image_desc = config.get("image.description", "")
            image_os = config.get("image.os", "")
            image_release = config.get("image.release", "")
            image = image_desc or f"{image_os} {image_release}".strip() or ctype

            # Proxy ports (Incus proxy devices act like port mappings).
            ports: List[str] = []
            for dev_name, dev in c.get("expanded_devices", {}).items():
                if dev.get("type") == "proxy":
                    listen = dev.get("listen", "")
                    connect = dev.get("connect", "")
                    if listen and connect:
                        ports.append(f"{listen} -> {connect}")

            containers.append(ContainerInfo(
                container_id=name,
                name=name,
                image=image,
                status="running" if status_str == "running" else "stopped",
                runtime="incus",
                created=c.get("created_at", ""),
                ports=ports,
                ip_address=ip_address,
                labels={
                    k: v for k, v in config.items()
                    if not k.startswith("volatile.")
                    and not k.startswith("image.")
                },
            ))
        return containers

    def _inspect_incus(self, container_name: str) -> Dict[str, Any]:
        """Inspect an Incus container for security-relevant config."""
        name = self._sanitize_id(container_name)
        cmd = [self._incus_cmd, "config", "show", name]
        stdout, _, rc = self._run_cmd(cmd, timeout=10)
        if rc != 0:
            return {}
        try:
            import yaml
            data = yaml.safe_load(stdout) or {}
        except Exception:
            # Fallback: parse key lines manually.
            data = {}
            for line in stdout.splitlines():
                line = line.strip()
                if ": " in line:
                    k, v = line.split(": ", 1)
                    data[k.strip()] = v.strip()
        return data

    # ------------------------------------------------------------------
    # Container lookup
    # ------------------------------------------------------------------

    def _find_container(self, identifier: str) -> List[ContainerInfo]:
        """Find a container by ID prefix or name across all runtimes."""
        safe_id = self._sanitize_id(identifier)
        all_containers = self.list_containers(
            runtime="all", include_stopped=True,
        )
        return [
            c for c in all_containers
            if safe_id in c.container_id
            or safe_id.lower() == c.name.lower()
        ]

    # ------------------------------------------------------------------
    # Scanning pipeline
    # ------------------------------------------------------------------

    def _scan_container(
        self, container: ContainerInfo,
    ) -> List[ContainerThreat]:
        """Run all checks on a single container."""
        threats: List[ContainerThreat] = []

        # Running-only checks
        if container.status == "running":
            threats.extend(self._check_processes(container))
            threats.extend(self._check_network(container))

        # Always check these
        threats.extend(self._check_filesystem(container))
        threats.extend(self._check_misconfigurations(container))
        if container.runtime == "incus":
            threats.extend(self._check_incus_security(container))
        else:
            threats.extend(self._check_image(container))

        return threats

    # -- Process checks ------------------------------------------------

    def _check_processes(
        self, container: ContainerInfo,
    ) -> List[ContainerThreat]:
        """Check running processes inside the container for miners / malware."""
        threats: List[ContainerThreat] = []
        cmd_prefix = self._get_exec_prefix(container)
        if not cmd_prefix:
            return threats

        # Try ``ps aux`` inside the container.
        stdout, _, rc = self._run_cmd(cmd_prefix + ["ps", "aux"], timeout=15)
        if rc != 0:
            # Fallback: ``docker|podman top``.
            if container.runtime in ("docker", "podman"):
                rt_cmd = (
                    self._docker_cmd
                    if container.runtime == "docker"
                    else self._podman_cmd
                )
                stdout, _, rc = self._run_cmd(
                    [rt_cmd, "top", container.container_id,
                     "-eo", "pid,user,%cpu,%mem,comm,args"],
                    timeout=15,
                )
            if rc != 0:
                return threats

        miner_names_lower = {n.lower() for n in CRYPTO_MINER_PROCESS_NAMES}

        for line in stdout.splitlines()[1:]:  # skip header
            parts = line.split()
            if len(parts) < 6:
                continue

            process_name = parts[-1].split("/")[-1].lower()
            full_cmd = (
                " ".join(parts[10:])
                if len(parts) > 10
                else " ".join(parts[5:])
            )

            # Known miner process names
            for miner in miner_names_lower:
                if miner in process_name or miner in full_cmd.lower():
                    threats.append(ContainerThreat(
                        container_id=container.container_id,
                        container_name=container.name,
                        runtime=container.runtime,
                        threat_name=f"CryptoMiner.Container.{miner.title()}",
                        threat_type="miner",
                        severity="CRITICAL",
                        details=(
                            f"Crypto miner process '{process_name}' detected "
                            f"inside container. CMD: {full_cmd[:200]}"
                        ),
                        process_name=process_name,
                    ))
                    break

            # High CPU usage
            try:
                cpu = float(parts[2])
                if cpu > 80:
                    threats.append(ContainerThreat(
                        container_id=container.container_id,
                        container_name=container.name,
                        runtime=container.runtime,
                        threat_name="HighCPU.Container.SuspiciousProcess",
                        threat_type="miner",
                        severity="HIGH",
                        details=(
                            f"Process '{process_name}' using {cpu}% CPU "
                            f"inside container. Possible cryptominer."
                        ),
                        process_name=process_name,
                    ))
            except (ValueError, IndexError):
                pass

            # Reverse shells
            _SHELL_INDICATORS = [
                "nc -e", "ncat -e", "bash -i", "/dev/tcp/",
                "python -c 'import socket",
                "perl -e 'use Socket",
                "ruby -rsocket",
                "php -r '$sock",
            ]
            for indicator in _SHELL_INDICATORS:
                if indicator in full_cmd:
                    threats.append(ContainerThreat(
                        container_id=container.container_id,
                        container_name=container.name,
                        runtime=container.runtime,
                        threat_name="ReverseShell.Container",
                        threat_type="malware",
                        severity="CRITICAL",
                        details=(
                            f"Reverse shell detected inside container: "
                            f"{full_cmd[:200]}"
                        ),
                        process_name=process_name,
                    ))
                    break

        return threats

    # -- Network checks ------------------------------------------------

    def _check_network(
        self, container: ContainerInfo,
    ) -> List[ContainerThreat]:
        """Check container network connections for suspicious activity."""
        threats: List[ContainerThreat] = []
        cmd_prefix = self._get_exec_prefix(container)
        if not cmd_prefix:
            return threats

        stdout, _, rc = self._run_cmd(
            cmd_prefix + ["sh", "-c", "ss -tnp 2>/dev/null || netstat -tnp 2>/dev/null"],
            timeout=15,
        )
        if rc != 0 or not stdout:
            return threats

        pool_domains_lower = {d.lower() for d in CRYPTO_POOL_DOMAINS}

        for line in stdout.splitlines():
            line_lower = line.lower()
            for pool in pool_domains_lower:
                if pool in line_lower:
                    threats.append(ContainerThreat(
                        container_id=container.container_id,
                        container_name=container.name,
                        runtime=container.runtime,
                        threat_name=f"MiningPool.Container.{pool}",
                        threat_type="miner",
                        severity="CRITICAL",
                        details=(
                            f"Container connecting to mining pool: "
                            f"{line.strip()[:200]}"
                        ),
                    ))
            for port in SUSPICIOUS_PORTS:
                if f":{port}" in line:
                    threats.append(ContainerThreat(
                        container_id=container.container_id,
                        container_name=container.name,
                        runtime=container.runtime,
                        threat_name=f"SuspiciousPort.Container.{port}",
                        threat_type="malware",
                        severity="MEDIUM",
                        details=(
                            f"Container connection on suspicious port {port}: "
                            f"{line.strip()[:200]}"
                        ),
                    ))

        return threats

    # -- Filesystem checks ---------------------------------------------

    def _check_filesystem(
        self, container: ContainerInfo,
    ) -> List[ContainerThreat]:
        """Scan container filesystem for suspicious files."""
        threats: List[ContainerThreat] = []

        if container.runtime == "incus":
            return self._check_filesystem_via_exec(container)

        if container.runtime not in ("docker", "podman"):
            return threats
        runtime_cmd = (
            self._docker_cmd
            if container.runtime == "docker"
            else self._podman_cmd
        )
        if not runtime_cmd:
            return threats

        # -- suspicious scripts in temp directories --------------------
        check_dirs = ["/tmp", "/var/tmp", "/dev/shm", "/root", "/home"]
        for check_dir in check_dirs:
            cmd = [
                runtime_cmd, "exec", container.container_id,
                "find", check_dir, "-maxdepth", "3", "-type", "f",
                "-name", "*.sh", "-o", "-name", "*.py",
                "-o", "-name", "*.elf", "-o", "-name", "*.bin",
            ]
            stdout, _, rc = self._run_cmd(cmd, timeout=15)
            if rc != 0 or not stdout:
                continue
            for fpath in stdout.splitlines()[:50]:
                fpath = fpath.strip()
                if not fpath:
                    continue
                cat_cmd = [
                    runtime_cmd, "exec", container.container_id,
                    "head", "-c", "8192", fpath,
                ]
                content, _, crc = self._run_cmd(cat_cmd, timeout=10)
                if crc != 0:
                    continue
                ct_lower = content.lower()

                if "stratum+tcp://" in ct_lower or "stratum+ssl://" in ct_lower:
                    threats.append(ContainerThreat(
                        container_id=container.container_id,
                        container_name=container.name,
                        runtime=container.runtime,
                        threat_name="MinerConfig.Container",
                        threat_type="miner",
                        severity="CRITICAL",
                        details=(
                            f"Mining configuration found in container: {fpath}"
                        ),
                        file_path=fpath,
                    ))

                _MALICIOUS_PATTERNS = [
                    "eval(base64_decode(",
                    "exec(base64.b64decode(",
                    "import socket;socket.socket",
                    "/dev/tcp/",
                    "bash -i >& /dev/tcp/",
                ]
                if any(s in ct_lower for s in _MALICIOUS_PATTERNS):
                    threats.append(ContainerThreat(
                        container_id=container.container_id,
                        container_name=container.name,
                        runtime=container.runtime,
                        threat_name="MaliciousScript.Container",
                        threat_type="malware",
                        severity="HIGH",
                        details=(
                            f"Suspicious script pattern in container file: "
                            f"{fpath}"
                        ),
                        file_path=fpath,
                    ))

        # -- unexpected SUID binaries ----------------------------------
        _EXPECTED_SUID = {
            "/usr/bin/passwd", "/usr/bin/su", "/usr/bin/sudo",
            "/usr/bin/newgrp", "/usr/bin/chfn", "/usr/bin/chsh",
            "/usr/bin/gpasswd", "/bin/su", "/bin/mount", "/bin/umount",
            "/usr/bin/mount", "/usr/bin/umount",
        }
        cmd = [
            runtime_cmd, "exec", container.container_id,
            "find", "/", "-maxdepth", "4", "-perm", "-4000", "-type", "f",
        ]
        stdout, _, rc = self._run_cmd(cmd, timeout=20)
        if rc == 0 and stdout:
            for fpath in stdout.splitlines():
                fpath = fpath.strip()
                if fpath and fpath not in _EXPECTED_SUID:
                    threats.append(ContainerThreat(
                        container_id=container.container_id,
                        container_name=container.name,
                        runtime=container.runtime,
                        threat_name="UnexpectedSUID.Container",
                        threat_type="rootkit",
                        severity="MEDIUM",
                        details=(
                            f"Unexpected SUID binary in container: {fpath}"
                        ),
                        file_path=fpath,
                    ))

        return threats

    # -- Misconfiguration checks ---------------------------------------

    def _check_misconfigurations(
        self, container: ContainerInfo,
    ) -> List[ContainerThreat]:
        """Check container for security misconfigurations."""
        threats: List[ContainerThreat] = []

        # Incus misconfigs are handled in _check_incus_security.
        if container.runtime not in ("docker", "podman"):
            return threats
        runtime_cmd = (
            self._docker_cmd
            if container.runtime == "docker"
            else self._podman_cmd
        )
        if not runtime_cmd:
            return threats

        stdout, _, rc = self._run_cmd(
            [runtime_cmd, "inspect", container.container_id],
        )
        if rc != 0:
            return threats
        try:
            data = json.loads(stdout)
            if not data:
                return threats
            c = data[0]
        except (json.JSONDecodeError, IndexError):
            return threats

        host_config = c.get("HostConfig", {})
        config = c.get("Config", {})

        # Running as root
        user = config.get("User", "")
        if not user or user in ("root", "0"):
            threats.append(ContainerThreat(
                container_id=container.container_id,
                container_name=container.name,
                runtime=container.runtime,
                threat_name="RunAsRoot.Container",
                threat_type="misconfiguration",
                severity="MEDIUM",
                details=(
                    "Container running as root user. "
                    "Use a non-root user for better isolation."
                ),
            ))

        # Privileged mode
        if host_config.get("Privileged", False):
            threats.append(ContainerThreat(
                container_id=container.container_id,
                container_name=container.name,
                runtime=container.runtime,
                threat_name="PrivilegedMode.Container",
                threat_type="misconfiguration",
                severity="CRITICAL",
                details=(
                    "Container running in privileged mode! "
                    "This grants full host access."
                ),
            ))

        # Host network
        if host_config.get("NetworkMode", "") == "host":
            threats.append(ContainerThreat(
                container_id=container.container_id,
                container_name=container.name,
                runtime=container.runtime,
                threat_name="HostNetwork.Container",
                threat_type="misconfiguration",
                severity="HIGH",
                details=(
                    "Container using host network mode. "
                    "This bypasses network isolation."
                ),
            ))

        # Host PID namespace
        if host_config.get("PidMode") == "host":
            threats.append(ContainerThreat(
                container_id=container.container_id,
                container_name=container.name,
                runtime=container.runtime,
                threat_name="HostPID.Container",
                threat_type="misconfiguration",
                severity="HIGH",
                details=(
                    "Container sharing host PID namespace. "
                    "Container can see all host processes."
                ),
            ))

        # Dangerous capabilities
        _DANGEROUS_CAPS = {
            "SYS_ADMIN", "SYS_PTRACE", "NET_ADMIN", "SYS_RAWIO",
            "DAC_OVERRIDE", "SYS_MODULE", "NET_RAW",
        }
        added_caps = set(host_config.get("CapAdd", []) or [])
        for cap in sorted(added_caps & _DANGEROUS_CAPS):
            threats.append(ContainerThreat(
                container_id=container.container_id,
                container_name=container.name,
                runtime=container.runtime,
                threat_name=f"DangerousCap.Container.{cap}",
                threat_type="misconfiguration",
                severity="HIGH",
                details=f"Container has dangerous capability: {cap}",
            ))

        # Sensitive host mounts
        _SENSITIVE_MOUNTS = {
            "/", "/etc", "/var/run/docker.sock", "/proc", "/sys",
            "/dev", "/root", "/home",
        }
        for mount in c.get("Mounts", []):
            src = mount.get("Source", "")
            if src in _SENSITIVE_MOUNTS:
                threats.append(ContainerThreat(
                    container_id=container.container_id,
                    container_name=container.name,
                    runtime=container.runtime,
                    threat_name="SensitiveMount.Container",
                    threat_type="misconfiguration",
                    severity="HIGH",
                    details=(
                        f"Container mounts sensitive host path: "
                        f"{src} -> {mount.get('Destination', '')}"
                    ),
                ))

        # No resource limits
        mem_limit = host_config.get("Memory", 0)
        cpu_quota = host_config.get("CpuQuota", 0)
        if mem_limit == 0 and cpu_quota == 0:
            threats.append(ContainerThreat(
                container_id=container.container_id,
                container_name=container.name,
                runtime=container.runtime,
                threat_name="NoResourceLimits.Container",
                threat_type="misconfiguration",
                severity="LOW",
                details=(
                    "Container has no memory or CPU limits. "
                    "A compromised container could consume all host resources."
                ),
            ))

        # Security profiles disabled
        security_opt = host_config.get("SecurityOpt", []) or []
        for opt in security_opt:
            opt_str = str(opt)
            if "apparmor=unconfined" in opt_str or "seccomp=unconfined" in opt_str:
                threats.append(ContainerThreat(
                    container_id=container.container_id,
                    container_name=container.name,
                    runtime=container.runtime,
                    threat_name="SecurityDisabled.Container",
                    threat_type="misconfiguration",
                    severity="HIGH",
                    details=f"Container security profile disabled: {opt}",
                ))

        return threats

    # -- Image checks --------------------------------------------------

    @staticmethod
    def _check_image(
        container: ContainerInfo,
    ) -> List[ContainerThreat]:
        """Check if the container image has known issues."""
        threats: List[ContainerThreat] = []

        # Using :latest or untagged image
        image_name = container.image.split("/")[-1]
        if container.image.endswith(":latest") or ":" not in image_name:
            threats.append(ContainerThreat(
                container_id=container.container_id,
                container_name=container.name,
                runtime=container.runtime,
                threat_name="LatestTag.Container",
                threat_type="misconfiguration",
                severity="LOW",
                details=(
                    f"Container using ':latest' or untagged image "
                    f"'{container.image}'. "
                    f"Pin to a specific version for reproducibility."
                ),
            ))

        return threats

    # -- Incus security checks -----------------------------------------

    def _check_incus_security(
        self, container: ContainerInfo,
    ) -> List[ContainerThreat]:
        """Check Incus container security configuration."""
        threats: List[ContainerThreat] = []
        labels = container.labels or {}

        # security.privileged = true
        if labels.get("security.privileged") == "true":
            threats.append(ContainerThreat(
                container_id=container.container_id,
                container_name=container.name,
                runtime="incus",
                threat_name="PrivilegedMode.Incus",
                threat_type="misconfiguration",
                severity="CRITICAL",
                details=(
                    "Incus container running in privileged mode! "
                    "This grants full host access."
                ),
            ))

        # security.nesting=true is required for Docker-in-Incus setups
        # (e.g. Dokploy). Only flag it when combined with privileged mode.
        if (
            labels.get("security.nesting") == "true"
            and labels.get("security.privileged") == "true"
        ):
            threats.append(ContainerThreat(
                container_id=container.container_id,
                container_name=container.name,
                runtime="incus",
                threat_name="PrivilegedNesting.Incus",
                threat_type="misconfiguration",
                severity="CRITICAL",
                details=(
                    "Container has security.nesting=true AND "
                    "security.privileged=true. This is a dangerous "
                    "combination allowing full host escape."
                ),
            ))

        # Check for Docker inside Incus (nested Docker).
        if container.status == "running" and self._incus_cmd:
            name = self._sanitize_id(container.name)
            stdout, _, rc = self._run_cmd(
                [self._incus_cmd, "exec", name, "--", "docker", "ps", "-q"],
                timeout=10,
            )
            if rc == 0 and stdout.strip():
                n_docker = len(stdout.strip().splitlines())
                # Not a threat — informational label stored in labels
                # for the dashboard to display.
                container.labels["_nested_docker_containers"] = str(n_docker)

        return threats

    def _check_filesystem_via_exec(
        self, container: ContainerInfo,
    ) -> List[ContainerThreat]:
        """Scan an Incus container's filesystem via ``incus exec``."""
        threats: List[ContainerThreat] = []
        if container.status != "running" or not self._incus_cmd:
            return threats

        name = self._sanitize_id(container.name)
        check_dirs = ["/tmp", "/var/tmp", "/dev/shm", "/root"]

        for check_dir in check_dirs:
            cmd = [
                self._incus_cmd, "exec", name, "--",
                "find", check_dir, "-maxdepth", "3", "-type", "f",
                "-name", "*.sh", "-o", "-name", "*.py",
                "-o", "-name", "*.elf", "-o", "-name", "*.bin",
            ]
            stdout, _, rc = self._run_cmd(cmd, timeout=15)
            if rc != 0 or not stdout:
                continue
            for fpath in stdout.splitlines()[:50]:
                fpath = fpath.strip()
                if not fpath:
                    continue
                cat_cmd = [
                    self._incus_cmd, "exec", name, "--",
                    "head", "-c", "8192", fpath,
                ]
                content, _, crc = self._run_cmd(cat_cmd, timeout=10)
                if crc != 0:
                    continue
                ct_lower = content.lower()

                if "stratum+tcp://" in ct_lower or "stratum+ssl://" in ct_lower:
                    threats.append(ContainerThreat(
                        container_id=container.container_id,
                        container_name=container.name,
                        runtime="incus",
                        threat_name="MinerConfig.Incus",
                        threat_type="miner",
                        severity="CRITICAL",
                        details=f"Mining config found in container: {fpath}",
                        file_path=fpath,
                    ))

                _MALICIOUS_PATTERNS = [
                    "eval(base64_decode(",
                    "exec(base64.b64decode(",
                    "import socket;socket.socket",
                    "/dev/tcp/",
                    "bash -i >& /dev/tcp/",
                ]
                if any(s in ct_lower for s in _MALICIOUS_PATTERNS):
                    threats.append(ContainerThreat(
                        container_id=container.container_id,
                        container_name=container.name,
                        runtime="incus",
                        threat_name="MaliciousScript.Incus",
                        threat_type="malware",
                        severity="HIGH",
                        details=f"Suspicious script in container file: {fpath}",
                        file_path=fpath,
                    ))
        return threats

    # ------------------------------------------------------------------
    # Exec prefix
    # ------------------------------------------------------------------

    def _get_exec_prefix(
        self, container: ContainerInfo,
    ) -> Optional[List[str]]:
        """Get the command prefix to execute commands inside a container."""
        if container.status != "running":
            return None
        cid = self._sanitize_id(container.container_id)
        if container.runtime == "incus" and self._incus_cmd:
            name = self._sanitize_id(container.name)
            return [self._incus_cmd, "exec", name, "--"]
        if container.runtime == "docker" and self._docker_cmd:
            return [self._docker_cmd, "exec", cid]
        if container.runtime == "podman" and self._podman_cmd:
            return [self._podman_cmd, "exec", cid]
        if container.runtime == "lxc":
            lxc_attach = shutil.which("lxc-attach")
            if lxc_attach:
                name = self._sanitize_id(container.name)
                return [lxc_attach, "-n", name, "--"]
        return None

    # ------------------------------------------------------------------
    # Utility methods
    # ------------------------------------------------------------------

    def get_container_logs(
        self,
        container_id: str,
        runtime: str = "docker",
        lines: int = 100,
    ) -> str:
        """Get recent logs from a container."""
        cid = self._sanitize_id(container_id)
        if runtime == "incus" and self._incus_cmd:
            stdout, _, rc = self._run_cmd(
                [self._incus_cmd, "exec", cid, "--",
                 "journalctl", "--no-pager", "-n", str(lines)],
                timeout=15,
            )
            return stdout if rc == 0 else ""
        if runtime in ("docker", "podman"):
            cmd_bin = (
                self._docker_cmd if runtime == "docker" else self._podman_cmd
            )
            if not cmd_bin:
                return ""
            stdout, _, rc = self._run_cmd(
                [cmd_bin, "logs", "--tail", str(lines), cid],
                timeout=15,
            )
            return stdout if rc == 0 else ""
        return ""

    def get_container_stats(
        self,
        container_id: str,
        runtime: str = "docker",
    ) -> Dict[str, Any]:
        """Get resource usage stats for a container."""
        cid = self._sanitize_id(container_id)
        if runtime in ("docker", "podman"):
            cmd_bin = (
                self._docker_cmd if runtime == "docker" else self._podman_cmd
            )
            if not cmd_bin:
                return {}
            fmt = (
                '{"cpu":"{{.CPUPerc}}","mem":"{{.MemUsage}}",'
                '"net":"{{.NetIO}}","block":"{{.BlockIO}}",'
                '"pids":"{{.PIDs}}"}'
            )
            stdout, _, rc = self._run_cmd(
                [cmd_bin, "stats", cid, "--no-stream", "--format", fmt],
                timeout=15,
            )
            if rc != 0:
                return {}
            try:
                return json.loads(stdout)
            except json.JSONDecodeError:
                return {}
        return {}
