"""AppArmor profile generation for containers."""

from __future__ import annotations

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

from gaol.security.exceptions import (
    AppArmorNotAvailableError,
    ProfileGenerationError,
)
from gaol.security.models import AppArmorMode, AppArmorProfile

# Default AppArmor profile paths
APPARMOR_PROFILES_DIR = Path("/etc/apparmor.d")
GAOL_PROFILES_DIR = APPARMOR_PROFILES_DIR / "gaol"
APPARMOR_PARSER = Path("/sbin/apparmor_parser")


# Default capabilities for containers
DEFAULT_CAPABILITIES_ALLOW = [
    "chown",
    "dac_override",
    "fowner",
    "fsetid",
    "kill",
    "setgid",
    "setuid",
    "setpcap",
    "net_bind_service",
    "sys_chroot",
    "mknod",
    "audit_write",
    "setfcap",
]

DEFAULT_CAPABILITIES_DENY = [
    "sys_admin",
    "sys_module",
    "sys_rawio",
    "sys_pacct",
    "sys_nice",
    "sys_resource",
    "sys_time",
    "sys_tty_config",
    "mac_admin",
    "mac_override",
    "net_admin",
    "syslog",
    "dac_read_search",
    "linux_immutable",
    "ipc_lock",
    "ipc_owner",
    "sys_ptrace",
    "sys_boot",
    "lease",
    "wake_alarm",
    "block_suspend",
]

# Default file access patterns
DEFAULT_FILE_READ = [
    "/",
    "/bin/**",
    "/lib/**",
    "/lib64/**",
    "/usr/**",
    "/etc/**",
    "/proc/**",
    "/sys/**",
    "/dev/null",
    "/dev/zero",
    "/dev/random",
    "/dev/urandom",
    "/dev/tty",
    "/dev/pts/**",
]

DEFAULT_FILE_WRITE = [
    "/tmp/**",
    "/var/tmp/**",
    "/run/**",
    "/dev/null",
    "/dev/tty",
    "/dev/pts/**",
]

DEFAULT_FILE_EXEC = [
    "/bin/**",
    "/usr/bin/**",
    "/usr/local/bin/**",
    "/lib/**",
    "/lib64/**",
    "/usr/lib/**",
]

DEFAULT_FILE_DENY = [
    "/etc/shadow",
    "/etc/gshadow",
    "/etc/sudoers",
    "/etc/sudoers.d/**",
    "/root/.ssh/**",
]

DEFAULT_NETWORK_ALLOW = [
    "inet stream",
    "inet dgram",
    "inet6 stream",
    "inet6 dgram",
    "unix stream",
    "unix dgram",
]


class AppArmorProfileGenerator:
    """Generator for AppArmor security profiles.

    This class can generate AppArmor profiles for containers, either from
    defaults or by learning from Falco events.

    Example:
        generator = AppArmorProfileGenerator()
        if generator.is_apparmor_available():
            profile = generator.generate_default("my-container")
            generator.install_profile(profile)
    """

    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 GAOL_PROFILES_DIR
        self._learning_thread: threading.Thread | None = None
        self._learning_stop = threading.Event()
        self._learned_files: dict[str, set[str]] = {
            "read": set(),
            "write": set(),
            "exec": set(),
        }
        self._learned_capabilities: set[str] = set()
        self._learned_network: set[str] = set()

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

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

        Returns:
            True if AppArmor is available and enabled
        """
        # Check if AppArmor filesystem is mounted
        try:
            with open("/proc/mounts") as f:
                mounts = f.read()
                if "securityfs" not in mounts:
                    return False
        except FileNotFoundError:
            return False

        # Check for apparmor status
        apparmor_status = Path("/sys/module/apparmor")
        if not apparmor_status.exists():
            return False

        # Check if apparmor_parser is available
        if not APPARMOR_PARSER.exists():
            # Try to find it elsewhere
            try:
                result = subprocess.run(
                    ["which", "apparmor_parser"],
                    capture_output=True,
                    text=True,
                    timeout=5,
                )
                return result.returncode == 0
            except (subprocess.TimeoutExpired, FileNotFoundError):
                return False

        return True

    def get_loaded_profiles(self) -> list[str]:
        """Get list of currently loaded AppArmor profiles.

        Returns:
            List of profile names
        """
        profiles = []
        profiles_file = Path("/sys/kernel/security/apparmor/profiles")

        if profiles_file.exists():
            try:
                with open(profiles_file) as f:
                    for line in f:
                        # Format: profile_name (mode)
                        match = re.match(r"^([^\s]+)", line.strip())
                        if match:
                            profiles.append(match.group(1))
            except PermissionError:
                pass

        return profiles

    def generate_default(self, name: str) -> AppArmorProfile:
        """Generate a default AppArmor profile for a container.

        Args:
            name: Profile name (typically container name)

        Returns:
            AppArmorProfile with default settings
        """
        # Sanitize name for AppArmor
        safe_name = self._sanitize_profile_name(name)

        return AppArmorProfile(
            name=safe_name,
            mode=AppArmorMode.ENFORCE,
            description=f"Default gaol AppArmor profile for {name}",
            capabilities_allow=DEFAULT_CAPABILITIES_ALLOW.copy(),
            capabilities_deny=DEFAULT_CAPABILITIES_DENY.copy(),
            file_read=DEFAULT_FILE_READ.copy(),
            file_write=DEFAULT_FILE_WRITE.copy(),
            file_exec=DEFAULT_FILE_EXEC.copy(),
            file_deny=DEFAULT_FILE_DENY.copy(),
            network_allow=DEFAULT_NETWORK_ALLOW.copy(),
            generated_at=datetime.now(UTC),
            generated_from="gaol-default",
        )

    def generate_restrictive(self, name: str) -> AppArmorProfile:
        """Generate a restrictive AppArmor profile.

        Creates a more restrictive profile suitable for
        high-security environments.

        Args:
            name: Profile name

        Returns:
            AppArmorProfile with restrictive settings
        """
        safe_name = self._sanitize_profile_name(name)

        # Minimal capabilities
        restrictive_caps = ["chown", "setgid", "setuid", "net_bind_service"]

        # Minimal file access
        restrictive_read = [
            "/bin/**",
            "/lib/**",
            "/lib64/**",
            "/usr/bin/**",
            "/usr/lib/**",
            "/etc/passwd",
            "/etc/group",
            "/etc/nsswitch.conf",
            "/etc/hosts",
            "/etc/resolv.conf",
            "/proc/self/**",
            "/dev/null",
            "/dev/urandom",
        ]

        restrictive_write = [
            "/tmp/**",
            "/dev/null",
        ]

        restrictive_exec = [
            "/bin/**",
            "/usr/bin/**",
        ]

        return AppArmorProfile(
            name=safe_name,
            mode=AppArmorMode.ENFORCE,
            description=f"Restrictive gaol AppArmor profile for {name}",
            capabilities_allow=restrictive_caps,
            capabilities_deny=DEFAULT_CAPABILITIES_DENY.copy(),
            file_read=restrictive_read,
            file_write=restrictive_write,
            file_exec=restrictive_exec,
            file_deny=DEFAULT_FILE_DENY.copy(),
            network_allow=["inet stream", "inet dgram"],  # Only IPv4
            generated_at=datetime.now(UTC),
            generated_from="gaol-restrictive",
        )

    def _sanitize_profile_name(self, name: str) -> str:
        """Sanitize a name for use as an AppArmor profile name.

        Args:
            name: Original name

        Returns:
            Sanitized name
        """
        # Replace invalid characters
        safe = re.sub(r"[^a-zA-Z0-9_.-]", "_", name)
        # Ensure it starts with a letter or underscore
        if safe and not safe[0].isalpha() and safe[0] != "_":
            safe = f"gaol_{safe}"
        return safe

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

        This monitors the container's file access, capability usage,
        and network activity to build a custom profile.

        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_files = {"read": set(), "write": set(), "exec": set()}
        self._learned_capabilities.clear()
        self._learned_network.clear()

        def learn_loop() -> None:
            end_time = time.time() + duration

            # Try using aa-notify or audit log to learn patterns
            audit_log = Path("/var/log/audit/audit.log")
            if audit_log.exists():
                self._learn_from_audit(container_name, end_time)
            else:
                # Fall back to using Falco events
                self._learn_from_falco(container_name, end_time)

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

    def _learn_from_audit(self, container_name: str, end_time: float) -> None:
        """Learn from audit log.

        Args:
            container_name: Container name
            end_time: When to stop learning
        """
        audit_log = Path("/var/log/audit/audit.log")

        try:
            # Tail the audit log and look for AppArmor denials
            proc = subprocess.Popen(
                ["tail", "-f", str(audit_log)],
                stdout=subprocess.PIPE,
                stderr=subprocess.DEVNULL,
                text=True,
            )

            while time.time() < end_time and not self._learning_stop.is_set():
                if proc.stdout:
                    line = proc.stdout.readline()
                    if not line:
                        time.sleep(0.1)
                        continue

                    # Look for AppArmor denials related to our container
                    if "apparmor" in line.lower() and container_name in line:
                        self._parse_audit_line(line)

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

        except (subprocess.SubprocessError, FileNotFoundError):
            pass

    def _parse_audit_line(self, line: str) -> None:
        """Parse an audit log line for AppArmor events.

        Args:
            line: Audit log line
        """
        # Parse file access
        name_match = re.search(r'name="([^"]+)"', line)
        if name_match:
            path = name_match.group(1)

            if "requested_mask" in line:
                if "r" in line:
                    self._learned_files["read"].add(path)
                if "w" in line:
                    self._learned_files["write"].add(path)
                if "x" in line:
                    self._learned_files["exec"].add(path)

        # Parse capability
        cap_match = re.search(r"capname=(\w+)", line)
        if cap_match:
            self._learned_capabilities.add(cap_match.group(1))

        # Parse network
        if "sock_type=" in line:
            sock_match = re.search(r'family="(\w+)".*sock_type="(\w+)"', line)
            if sock_match:
                family = sock_match.group(1).lower()
                sock_type = sock_match.group(2).lower()
                self._learned_network.add(f"{family} {sock_type}")

    def _learn_from_falco(self, _container_name: str, _end_time: float) -> None:
        """Learn from Falco events.

        Args:
            _container_name: Container name (unused until Falco integration)
            _end_time: When to stop learning (unused until Falco integration)
        """
        # TODO: Integrate with FalcoClient for real learning
        # For now, populate with defaults
        self._learned_files["read"].update(DEFAULT_FILE_READ)
        self._learned_files["write"].update(DEFAULT_FILE_WRITE)
        self._learned_files["exec"].update(DEFAULT_FILE_EXEC)
        self._learned_capabilities.update(DEFAULT_CAPABILITIES_ALLOW)
        self._learned_network.update(DEFAULT_NETWORK_ALLOW)

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

        Returns:
            AppArmorProfile based on learned access patterns
        """
        self._learning_stop.set()

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

        # Build profile from learned patterns
        if not any(self._learned_files.values()):
            # Fall back to default if nothing learned
            return self.generate_default("learned")

        # Generalize file paths (e.g., /tmp/foo123 -> /tmp/**)
        file_read = self._generalize_paths(self._learned_files["read"])
        file_write = self._generalize_paths(self._learned_files["write"])
        file_exec = self._generalize_paths(self._learned_files["exec"])

        return AppArmorProfile(
            name="learned",
            mode=AppArmorMode.ENFORCE,
            description="Profile generated from observed access patterns",
            capabilities_allow=sorted(self._learned_capabilities) or DEFAULT_CAPABILITIES_ALLOW,
            capabilities_deny=DEFAULT_CAPABILITIES_DENY.copy(),
            file_read=sorted(file_read),
            file_write=sorted(file_write),
            file_exec=sorted(file_exec),
            file_deny=DEFAULT_FILE_DENY.copy(),
            network_allow=sorted(self._learned_network) or DEFAULT_NETWORK_ALLOW,
            generated_at=datetime.now(UTC),
            generated_from="learning",
        )

    def _generalize_paths(self, paths: set[str]) -> set[str]:
        """Generalize specific file paths to patterns.

        Args:
            paths: Set of specific file paths

        Returns:
            Set of generalized patterns
        """
        generalized = set()

        for path in paths:
            # Convert specific files to directory patterns
            # e.g., /tmp/foo123 -> /tmp/**
            parts = path.split("/")
            if len(parts) > 2:
                # Keep top-level directories, generalize deeper paths
                if any(c.isdigit() for c in parts[-1]) or len(parts[-1]) > 20:
                    # Looks like a temp file, generalize
                    generalized.add("/".join(parts[:-1]) + "/**")
                else:
                    generalized.add(path)
            else:
                generalized.add(path)

        return generalized

    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: AppArmorProfile, path: Path | None = None) -> Path:
        """Save profile to a file.

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

        Returns:
            Path where profile was saved
        """
        if path is None:
            self._profiles_dir.mkdir(parents=True, exist_ok=True)
            path = self._profiles_dir / profile.name

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

        with open(path, "w") as f:
            f.write(profile.to_apparmor_text())

        return path

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

        This parses an AppArmor profile file and reconstructs
        the AppArmorProfile object.

        Args:
            path: Path to profile file

        Returns:
            AppArmorProfile

        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:
                content = f.read()

            # Parse profile name
            name_match = re.search(r"profile\s+(\S+)", content)
            if not name_match:
                raise ProfileGenerationError("Could not parse profile name")

            name = name_match.group(1)

            # Parse capabilities
            caps_allow = re.findall(r"^\s*capability\s+(\w+),", content, re.MULTILINE)
            caps_deny = re.findall(r"^\s*deny\s+capability\s+(\w+),", content, re.MULTILINE)

            # Parse file access
            file_read = re.findall(r"^\s*(\S+)\s+r,", content, re.MULTILINE)
            file_write = re.findall(r"^\s*(\S+)\s+w,", content, re.MULTILINE)
            file_exec = re.findall(r"^\s*(\S+)\s+ix,", content, re.MULTILINE)
            file_deny = re.findall(r"^\s*deny\s+(\S+)\s+rwx,", content, re.MULTILINE)

            # Parse network
            network = re.findall(r"^\s*network\s+([^,]+),", content, re.MULTILINE)

            return AppArmorProfile(
                name=name,
                mode=AppArmorMode.ENFORCE,
                capabilities_allow=caps_allow,
                capabilities_deny=caps_deny,
                file_read=file_read,
                file_write=file_write,
                file_exec=file_exec,
                file_deny=file_deny,
                network_allow=network,
            )

        except Exception as e:
            raise ProfileGenerationError(f"Failed to parse profile: {e}") from e

    def install_profile(self, profile: AppArmorProfile) -> bool:
        """Install and load an AppArmor profile.

        Args:
            profile: Profile to install

        Returns:
            True if installation succeeded

        Raises:
            AppArmorNotAvailableError: If AppArmor is not available
            ProfileGenerationError: If installation fails
        """
        if not self.is_apparmor_available():
            raise AppArmorNotAvailableError()

        # Save profile to AppArmor directory
        profile_path = APPARMOR_PROFILES_DIR / profile.name
        profile_path.parent.mkdir(parents=True, exist_ok=True)

        with open(profile_path, "w") as f:
            f.write(profile.to_apparmor_text())

        # Load profile using apparmor_parser
        try:
            result = subprocess.run(
                ["apparmor_parser", "-r", "-W", str(profile_path)],
                capture_output=True,
                text=True,
                timeout=30,
            )
            if result.returncode != 0:
                raise ProfileGenerationError(
                    f"Failed to load profile: {result.stderr}"
                )
            return True

        except subprocess.TimeoutExpired as e:
            raise ProfileGenerationError("Profile loading timed out") from e
        except FileNotFoundError as e:
            raise ProfileGenerationError("apparmor_parser not found") from e

    def unload_profile(self, name: str) -> bool:
        """Unload an AppArmor profile.

        Args:
            name: Profile name to unload

        Returns:
            True if unloading succeeded
        """
        if not self.is_apparmor_available():
            return False

        try:
            result = subprocess.run(
                ["apparmor_parser", "-R", f"/etc/apparmor.d/{name}"],
                capture_output=True,
                text=True,
                timeout=30,
            )
            return result.returncode == 0

        except (subprocess.TimeoutExpired, FileNotFoundError):
            return False

    def set_profile_mode(self, name: str, mode: AppArmorMode) -> bool:
        """Set the mode of a loaded profile.

        Args:
            name: Profile name
            mode: New mode (enforce/complain)

        Returns:
            True if mode was changed successfully
        """
        if not self.is_apparmor_available():
            return False

        try:
            if mode == AppArmorMode.ENFORCE:
                cmd = ["aa-enforce", f"/etc/apparmor.d/{name}"]
            elif mode == AppArmorMode.COMPLAIN:
                cmd = ["aa-complain", f"/etc/apparmor.d/{name}"]
            else:
                return False

            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=30,
            )
            return result.returncode == 0

        except (subprocess.TimeoutExpired, FileNotFoundError):
            return False

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

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

        return sorted(self._profiles_dir.iterdir())


# Global generator instance
_generator: AppArmorProfileGenerator | None = None


def get_apparmor_generator() -> AppArmorProfileGenerator:
    """Get the global AppArmor profile generator."""
    global _generator
    if _generator is None:
        _generator = AppArmorProfileGenerator()
    return _generator
