"""Seccomp profile generation for containers."""

from __future__ import annotations

import json
import subprocess
import threading
import time
from datetime import UTC, datetime
from pathlib import Path

from gaol.security.exceptions import (
    ProfileGenerationError,
)
from gaol.security.models import (
    SeccompAction,
    SeccompProfile,
    SeccompSyscall,
)

# Default seccomp profile paths
DEFAULT_PROFILES_DIR = Path("/etc/gaol/seccomp")
DOCKER_DEFAULT_PROFILE = Path("/etc/docker/seccomp.json")


# Common syscalls for container workloads
BASELINE_SYSCALLS = [
    # Process management
    "clone",
    "clone3",
    "execve",
    "execveat",
    "fork",
    "vfork",
    "exit",
    "exit_group",
    "wait4",
    "waitid",
    "kill",
    "tgkill",
    "tkill",
    "rt_sigaction",
    "rt_sigprocmask",
    "rt_sigreturn",
    "rt_sigsuspend",
    "sigaltstack",
    # File operations
    "open",
    "openat",
    "openat2",
    "close",
    "read",
    "readv",
    "pread64",
    "preadv",
    "write",
    "writev",
    "pwrite64",
    "pwritev",
    "lseek",
    "fstat",
    "stat",
    "lstat",
    "newfstatat",
    "statx",
    "fstatfs",
    "statfs",
    "access",
    "faccessat",
    "faccessat2",
    "chmod",
    "fchmod",
    "fchmodat",
    "chown",
    "fchown",
    "fchownat",
    "lchown",
    "truncate",
    "ftruncate",
    "link",
    "linkat",
    "unlink",
    "unlinkat",
    "symlink",
    "symlinkat",
    "readlink",
    "readlinkat",
    "rename",
    "renameat",
    "renameat2",
    "mkdir",
    "mkdirat",
    "rmdir",
    "getcwd",
    "chdir",
    "fchdir",
    "umask",
    "dup",
    "dup2",
    "dup3",
    "fcntl",
    "flock",
    "fsync",
    "fdatasync",
    "sync",
    "syncfs",
    "utimensat",
    "futimesat",
    "getdents",
    "getdents64",
    # Memory management
    "brk",
    "mmap",
    "munmap",
    "mremap",
    "mprotect",
    "madvise",
    "mlock",
    "mlock2",
    "munlock",
    "mlockall",
    "munlockall",
    "mincore",
    "msync",
    # Networking
    "socket",
    "socketpair",
    "bind",
    "connect",
    "listen",
    "accept",
    "accept4",
    "getsockname",
    "getpeername",
    "sendto",
    "recvfrom",
    "sendmsg",
    "recvmsg",
    "shutdown",
    "setsockopt",
    "getsockopt",
    "sendmmsg",
    "recvmmsg",
    "select",
    "pselect6",
    "poll",
    "ppoll",
    "epoll_create",
    "epoll_create1",
    "epoll_ctl",
    "epoll_wait",
    "epoll_pwait",
    "epoll_pwait2",
    # Time
    "gettimeofday",
    "clock_gettime",
    "clock_getres",
    "clock_nanosleep",
    "nanosleep",
    "times",
    # User/group
    "getuid",
    "geteuid",
    "getgid",
    "getegid",
    "getgroups",
    "setuid",
    "setgid",
    "setgroups",
    "setreuid",
    "setregid",
    "setresuid",
    "setresgid",
    "getresuid",
    "getresgid",
    # Process info
    "getpid",
    "getppid",
    "gettid",
    "getpgid",
    "setpgid",
    "setsid",
    "getsid",
    # Resource limits
    "getrlimit",
    "setrlimit",
    "prlimit64",
    "getrusage",
    # I/O control
    "ioctl",
    "pipe",
    "pipe2",
    # Event notification
    "eventfd",
    "eventfd2",
    "signalfd",
    "signalfd4",
    "timerfd_create",
    "timerfd_settime",
    "timerfd_gettime",
    "inotify_init",
    "inotify_init1",
    "inotify_add_watch",
    "inotify_rm_watch",
    # Futex (threading)
    "futex",
    "futex_waitv",
    "set_robust_list",
    "get_robust_list",
    # Misc
    "prctl",
    "arch_prctl",
    "set_tid_address",
    "getrandom",
    "uname",
    "sysinfo",
    "capget",
    "capset",
    "seccomp",
    "memfd_create",
    "copy_file_range",
    "fadvise64",
    "readahead",
    "splice",
    "tee",
    "vmsplice",
    "landlock_create_ruleset",
    "landlock_add_rule",
    "landlock_restrict_self",
]

# Dangerous syscalls that should typically be blocked
DANGEROUS_SYSCALLS = [
    "ptrace",
    "process_vm_readv",
    "process_vm_writev",
    "kexec_load",
    "kexec_file_load",
    "reboot",
    "swapon",
    "swapoff",
    "mount",
    "umount",
    "umount2",
    "pivot_root",
    "init_module",
    "finit_module",
    "delete_module",
    "ioperm",
    "iopl",
    "acct",
    "settimeofday",
    "adjtimex",
    "clock_adjtime",
    "clock_settime",
    "stime",
    "quotactl",
    "quotactl_fd",
    "personality",
    "unshare",
    "setns",
    "kcmp",
    "lookup_dcookie",
    "perf_event_open",
    "bpf",
    "userfaultfd",
    "open_by_handle_at",
    "name_to_handle_at",
]


class SeccompProfileGenerator:
    """Generator for seccomp security profiles.

    This class can generate seccomp profiles in OCI format, either from
    defaults, baselines, or by learning from Falco events.

    Example:
        generator = SeccompProfileGenerator()
        profile = generator.generate_default("my-container")
        generator.save_profile(profile, Path("/etc/seccomp/my-container.json"))
    """

    def __init__(self, profiles_dir: Path | None = None) -> None:
        """Initialize profile generator.

        Args:
            profiles_dir: Directory for storing profiles
        """
        self._profiles_dir = profiles_dir or DEFAULT_PROFILES_DIR
        self._learning_thread: threading.Thread | None = None
        self._learning_stop = threading.Event()
        self._learned_syscalls: set[str] = set()

    @property
    def profiles_dir(self) -> Path:
        """Get the profiles directory."""
        return self._profiles_dir

    def is_seccomp_available(self) -> bool:
        """Check if seccomp is available on this system.

        Returns:
            True if seccomp is available
        """
        # Check for seccomp support in kernel
        try:
            with open("/proc/sys/kernel/seccomp/actions_avail") as f:
                return bool(f.read().strip())
        except FileNotFoundError:
            pass

        # Alternative check via prctl
        try:
            result = subprocess.run(
                ["grep", "-c", "Seccomp", "/proc/self/status"],
                capture_output=True,
                text=True,
                timeout=5,
            )
            return result.returncode == 0 and int(result.stdout.strip()) > 0
        except (subprocess.TimeoutExpired, FileNotFoundError, ValueError):
            return False

    def generate_default(self, name: str = "default") -> SeccompProfile:
        """Generate a default seccomp profile.

        Creates a profile with a reasonable set of allowed syscalls
        for typical container workloads.

        Args:
            name: Profile name

        Returns:
            SeccompProfile with default syscalls allowed
        """
        return SeccompProfile(
            name=name,
            description="Default gaol seccomp profile",
            defaultAction=SeccompAction.SCMP_ACT_ERRNO,
            architectures=["SCMP_ARCH_X86_64", "SCMP_ARCH_AARCH64"],
            syscalls=[
                SeccompSyscall(
                    names=BASELINE_SYSCALLS.copy(),
                    action=SeccompAction.SCMP_ACT_ALLOW,
                    comment="Baseline syscalls for container operation",
                ),
            ],
            generated_at=datetime.now(UTC),
            generated_from="gaol-default",
        )

    def generate_restrictive(self, name: str = "restrictive") -> SeccompProfile:
        """Generate a restrictive seccomp profile.

        Creates a more restrictive profile that blocks additional
        syscalls. Suitable for high-security environments.

        Args:
            name: Profile name

        Returns:
            SeccompProfile with minimal syscalls allowed
        """
        # Start with baseline and remove some potentially dangerous ones
        restricted_syscalls = [
            s
            for s in BASELINE_SYSCALLS
            if s
            not in [
                "ptrace",
                "personality",
                "kcmp",
            ]
        ]

        return SeccompProfile(
            name=name,
            description="Restrictive gaol seccomp profile",
            defaultAction=SeccompAction.SCMP_ACT_KILL_PROCESS,
            architectures=["SCMP_ARCH_X86_64", "SCMP_ARCH_AARCH64"],
            syscalls=[
                SeccompSyscall(
                    names=restricted_syscalls,
                    action=SeccompAction.SCMP_ACT_ALLOW,
                    comment="Minimal syscalls for restricted container operation",
                ),
            ],
            generated_at=datetime.now(UTC),
            generated_from="gaol-restrictive",
        )

    def generate_from_docker_default(self) -> SeccompProfile | None:
        """Generate profile based on Docker's default seccomp profile.

        Returns:
            SeccompProfile or None if Docker profile not found
        """
        if not DOCKER_DEFAULT_PROFILE.exists():
            return None

        try:
            with open(DOCKER_DEFAULT_PROFILE) as f:
                data = json.load(f)

            syscalls = []
            for syscall_entry in data.get("syscalls", []):
                names = syscall_entry.get("names", [])
                action_str = syscall_entry.get("action", "SCMP_ACT_ALLOW")
                try:
                    action = SeccompAction(action_str)
                except ValueError:
                    action = SeccompAction.SCMP_ACT_ALLOW

                syscalls.append(SeccompSyscall(names=names, action=action))

            return SeccompProfile(
                name="docker-default",
                description="Profile based on Docker default",
                defaultAction=SeccompAction(
                    data.get("defaultAction", "SCMP_ACT_ERRNO")
                ),
                architectures=data.get(
                    "architectures", ["SCMP_ARCH_X86_64", "SCMP_ARCH_AARCH64"]
                ),
                syscalls=syscalls,
                generated_at=datetime.now(UTC),
                generated_from=str(DOCKER_DEFAULT_PROFILE),
            )

        except (json.JSONDecodeError, KeyError):
            return None

    def start_learning(
        self,
        container_name: str,
        duration: int = 300,
    ) -> None:
        """Start learning syscalls from a running container.

        This uses Falco to monitor syscalls made by the container
        and builds a profile based on observed behavior.

        Args:
            container_name: Name of container to monitor
            duration: Learning duration in seconds (default 5 minutes)

        Raises:
            ProfileGenerationError: If learning fails to start
        """
        if self._learning_thread and self._learning_thread.is_alive():
            raise ProfileGenerationError("Learning already in progress")

        self._learning_stop.clear()
        self._learned_syscalls.clear()

        def learn_loop() -> None:
            end_time = time.time() + duration
            try:
                # Use strace to capture syscalls if available
                proc = subprocess.Popen(
                    [
                        "strace",
                        "-f",
                        "-e",
                        "trace=all",
                        "-c",
                        "-p",
                        self._get_container_pid(container_name),
                    ],
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    text=True,
                )

                while time.time() < end_time and not self._learning_stop.is_set():
                    time.sleep(1)

                proc.terminate()
                proc.wait(timeout=5)

                # Parse strace output for syscall names
                output = proc.stderr.read() if proc.stderr else ""
                for line in output.split("\n"):
                    parts = line.split()
                    if len(parts) >= 2 and parts[0].isdigit():
                        # Format: count errors total syscall_name
                        syscall_name = parts[-1]
                        if syscall_name.isalpha():
                            self._learned_syscalls.add(syscall_name)

            except (subprocess.SubprocessError, FileNotFoundError):
                # Fall back to using Falco events if strace not available
                self._learn_from_falco(container_name, duration)

        self._learning_thread = threading.Thread(target=learn_loop, daemon=True)
        self._learning_thread.start()

    def _get_container_pid(self, container_name: str) -> str:
        """Get the PID of a container's main process.

        Args:
            container_name: Container name

        Returns:
            PID as string

        Raises:
            ProfileGenerationError: If PID cannot be determined
        """
        try:
            # Try docker
            result = subprocess.run(
                ["docker", "inspect", "-f", "{{.State.Pid}}", container_name],
                capture_output=True,
                text=True,
                timeout=10,
            )
            if result.returncode == 0:
                return result.stdout.strip()
        except (subprocess.SubprocessError, FileNotFoundError):
            pass

        raise ProfileGenerationError(f"Cannot determine PID for container {container_name}")

    def _learn_from_falco(self, _container_name: str, _duration: int) -> None:
        """Learn syscalls from Falco events.

        Args:
            _container_name: Container name (unused until Falco integration)
            _duration: Duration in seconds (unused until Falco integration)
        """
        # TODO: Integrate with FalcoClient to stream events
        # For now, just use the baseline syscalls
        self._learned_syscalls.update(BASELINE_SYSCALLS)

    def stop_learning(self) -> SeccompProfile:
        """Stop learning and generate profile from learned syscalls.

        Returns:
            SeccompProfile based on learned syscalls
        """
        self._learning_stop.set()

        if self._learning_thread:
            self._learning_thread.join(timeout=5)
            self._learning_thread = None

        # Generate profile from learned syscalls
        if not self._learned_syscalls:
            # Fall back to default if nothing learned
            return self.generate_default("learned")

        return SeccompProfile(
            name="learned",
            description="Profile generated from observed syscalls",
            defaultAction=SeccompAction.SCMP_ACT_ERRNO,
            architectures=["SCMP_ARCH_X86_64", "SCMP_ARCH_AARCH64"],
            syscalls=[
                SeccompSyscall(
                    names=sorted(self._learned_syscalls),
                    action=SeccompAction.SCMP_ACT_ALLOW,
                    comment="Syscalls observed during learning period",
                ),
            ],
            generated_at=datetime.now(UTC),
            generated_from="learning",
        )

    def is_learning(self) -> bool:
        """Check if learning is in progress.

        Returns:
            True if learning is active
        """
        return self._learning_thread is not None and self._learning_thread.is_alive()

    def save_profile(self, profile: SeccompProfile, path: Path | None = None) -> Path:
        """Save profile to a JSON file.

        Args:
            profile: Profile to save
            path: Path to save to (defaults to profiles_dir/name.json)

        Returns:
            Path where profile was saved
        """
        if path is None:
            self._profiles_dir.mkdir(parents=True, exist_ok=True)
            safe_name = "".join(
                c if c.isalnum() or c in "-_" else "_" for c in profile.name
            )
            path = self._profiles_dir / f"{safe_name}.json"

        path.parent.mkdir(parents=True, exist_ok=True)

        with open(path, "w") as f:
            json.dump(profile.to_oci_json(), f, indent=2)

        return path

    def load_profile(self, path: Path) -> SeccompProfile:
        """Load profile from a JSON file.

        Args:
            path: Path to profile file

        Returns:
            SeccompProfile

        Raises:
            FileNotFoundError: If file doesn't exist
            ProfileGenerationError: If profile is invalid
        """
        if not path.exists():
            raise FileNotFoundError(f"Profile not found: {path}")

        try:
            with open(path) as f:
                data = json.load(f)

            syscalls = []
            for entry in data.get("syscalls", []):
                syscalls.append(
                    SeccompSyscall(
                        names=entry.get("names", []),
                        action=SeccompAction(entry.get("action", "SCMP_ACT_ALLOW")),
                    )
                )

            return SeccompProfile(
                name=path.stem,
                defaultAction=SeccompAction(data.get("defaultAction", "SCMP_ACT_ERRNO")),
                architectures=data.get(
                    "architectures", ["SCMP_ARCH_X86_64", "SCMP_ARCH_AARCH64"]
                ),
                syscalls=syscalls,
            )

        except (json.JSONDecodeError, KeyError, ValueError) as e:
            raise ProfileGenerationError(f"Invalid profile: {e}") from e

    def list_profiles(self) -> list[Path]:
        """List all saved profiles.

        Returns:
            List of profile file paths
        """
        if not self._profiles_dir.exists():
            return []

        return sorted(self._profiles_dir.glob("*.json"))


# Global generator instance
_generator: SeccompProfileGenerator | None = None


def get_seccomp_generator() -> SeccompProfileGenerator:
    """Get the global seccomp profile generator."""
    global _generator
    if _generator is None:
        _generator = SeccompProfileGenerator()
    return _generator
