"""Falco rule management for gaol."""

from __future__ import annotations

import re
import shutil
import subprocess
from pathlib import Path

import yaml

from gaol.security.exceptions import (
    RuleExistsError,
    RuleNotFoundError,
    RuleValidationError,
)
from gaol.security.models import FalcoPriority, RuleList, SecurityRule

# Default paths for Falco rules
DEFAULT_RULES_DIR = Path("/etc/falco/rules.d")
GAOL_RULES_DIR = DEFAULT_RULES_DIR / "gaol"
BUILTIN_RULES_DIR = Path(__file__).parent / "builtin_rules"


class RuleManager:
    """Manager for Falco security rules.

    This class handles creating, updating, deleting, and managing
    Falco rules, including gaol's built-in rules.

    Example:
        manager = RuleManager()
        rules = manager.list_rules()
        manager.create_rule(SecurityRule(
            name="my_rule",
            condition="container and proc.name=bash",
            output="Shell spawned in container",
            priority=FalcoPriority.WARNING,
        ))
    """

    def __init__(
        self,
        rules_dir: Path | None = None,
        gaol_dir: Path | None = None,
    ) -> None:
        """Initialize rule manager.

        Args:
            rules_dir: Base Falco rules.d directory
            gaol_dir: Directory for gaol-managed rules
        """
        self._rules_dir = rules_dir or DEFAULT_RULES_DIR
        self._gaol_dir = gaol_dir or GAOL_RULES_DIR

    @property
    def rules_dir(self) -> Path:
        """Get the rules directory."""
        return self._rules_dir

    @property
    def gaol_dir(self) -> Path:
        """Get the gaol rules directory."""
        return self._gaol_dir

    def _ensure_dirs(self) -> None:
        """Ensure rule directories exist."""
        self._rules_dir.mkdir(parents=True, exist_ok=True)
        self._gaol_dir.mkdir(parents=True, exist_ok=True)

    def _get_rule_file(self, name: str) -> Path:
        """Get the path for a rule file.

        Args:
            name: Rule name

        Returns:
            Path to the rule's YAML file
        """
        safe_name = re.sub(r"[^a-zA-Z0-9_-]", "_", name)
        return self._gaol_dir / f"{safe_name}.yaml"

    def _load_rule_from_file(self, path: Path) -> list[SecurityRule]:
        """Load rules from a YAML file.

        Args:
            path: Path to YAML file

        Returns:
            List of SecurityRule objects
        """
        if not path.exists():
            return []

        with open(path) as f:
            data = yaml.safe_load(f)

        if data is None:
            return []

        rules = []
        items = data if isinstance(data, list) else [data]

        for item in items:
            if "rule" in item:
                try:
                    rule = SecurityRule(
                        name=item["rule"],
                        description=item.get("desc", ""),
                        condition=item.get("condition", ""),
                        output=item.get("output", ""),
                        priority=FalcoPriority.from_string(
                            item.get("priority", "Warning")
                        ),
                        source=item.get("source", "syscall"),
                        tags=item.get("tags", []),
                        enabled=item.get("enabled", True),
                    )
                    rules.append(rule)
                except (ValueError, KeyError):
                    continue

        return rules

    def _save_rule_to_file(self, rule: SecurityRule, path: Path) -> None:
        """Save a rule to a YAML file.

        Args:
            rule: Rule to save
            path: Path to save to
        """
        self._ensure_dirs()

        # Create Falco YAML format
        rule_data = [rule.to_falco_yaml()]

        with open(path, "w") as f:
            yaml.safe_dump(rule_data, f, default_flow_style=False, sort_keys=False)

    def list_rules(self, include_builtin: bool = True) -> list[SecurityRule]:
        """List all managed rules.

        Args:
            include_builtin: Include built-in gaol rules

        Returns:
            List of all rules
        """
        rules = []

        # Load gaol-managed rules
        if self._gaol_dir.exists():
            for path in self._gaol_dir.glob("*.yaml"):
                rules.extend(self._load_rule_from_file(path))

        # Optionally include built-in rules (from package)
        if include_builtin and BUILTIN_RULES_DIR.exists():
            for path in BUILTIN_RULES_DIR.glob("*.yaml"):
                builtin_rules = self._load_rule_from_file(path)
                # Mark as builtin and check if already installed
                for rule in builtin_rules:
                    if not any(r.name == rule.name for r in rules):
                        rules.append(rule)

        return rules

    def get_rule(self, name: str) -> SecurityRule:
        """Get a specific rule by name.

        Args:
            name: Rule name

        Returns:
            The requested rule

        Raises:
            RuleNotFoundError: If rule doesn't exist
        """
        # Check gaol-managed rules first
        rule_file = self._get_rule_file(name)
        if rule_file.exists():
            rules = self._load_rule_from_file(rule_file)
            for rule in rules:
                if rule.name == name:
                    return rule

        # Check all rule files
        if self._gaol_dir.exists():
            for path in self._gaol_dir.glob("*.yaml"):
                rules = self._load_rule_from_file(path)
                for rule in rules:
                    if rule.name == name:
                        return rule

        # Check built-in rules
        if BUILTIN_RULES_DIR.exists():
            for path in BUILTIN_RULES_DIR.glob("*.yaml"):
                rules = self._load_rule_from_file(path)
                for rule in rules:
                    if rule.name == name:
                        return rule

        raise RuleNotFoundError(name)

    def create_rule(self, rule: SecurityRule) -> Path:
        """Create a new rule.

        Args:
            rule: Rule to create

        Returns:
            Path to the created rule file

        Raises:
            RuleExistsError: If rule already exists
            RuleValidationError: If rule is invalid
        """
        # Check if rule already exists
        try:
            self.get_rule(rule.name)
            raise RuleExistsError(rule.name)
        except RuleNotFoundError:
            pass

        # Validate rule
        errors = self.validate_rule(rule)
        if errors:
            raise RuleValidationError(rule.name, errors)

        # Save rule
        rule_file = self._get_rule_file(rule.name)
        self._save_rule_to_file(rule, rule_file)

        return rule_file

    def update_rule(self, name: str, rule: SecurityRule) -> Path:
        """Update an existing rule.

        Args:
            name: Name of rule to update
            rule: New rule data

        Returns:
            Path to the updated rule file

        Raises:
            RuleNotFoundError: If rule doesn't exist
            RuleValidationError: If new rule data is invalid
        """
        # Ensure rule exists
        self.get_rule(name)

        # Validate new rule data
        errors = self.validate_rule(rule)
        if errors:
            raise RuleValidationError(rule.name, errors)

        # If name changed, delete old file
        old_file = self._get_rule_file(name)
        new_file = self._get_rule_file(rule.name)

        if old_file.exists() and old_file != new_file:
            old_file.unlink()

        # Save updated rule
        self._save_rule_to_file(rule, new_file)

        return new_file

    def delete_rule(self, name: str) -> None:
        """Delete a rule.

        Args:
            name: Name of rule to delete

        Raises:
            RuleNotFoundError: If rule doesn't exist
        """
        # Ensure rule exists
        self.get_rule(name)

        # Find and delete rule file
        rule_file = self._get_rule_file(name)
        if rule_file.exists():
            rule_file.unlink()
            return

        # Search all rule files
        if self._gaol_dir.exists():
            for path in self._gaol_dir.glob("*.yaml"):
                rules = self._load_rule_from_file(path)
                if any(r.name == name for r in rules):
                    # If this is the only rule in the file, delete it
                    if len(rules) == 1:
                        path.unlink()
                    else:
                        # Remove just this rule and save
                        remaining = [r for r in rules if r.name != name]
                        rule_data = [r.to_falco_yaml() for r in remaining]
                        with open(path, "w") as f:
                            yaml.safe_dump(
                                rule_data, f, default_flow_style=False, sort_keys=False
                            )
                    return

        raise RuleNotFoundError(name)

    def enable_rule(self, name: str) -> None:
        """Enable a rule.

        Args:
            name: Name of rule to enable

        Raises:
            RuleNotFoundError: If rule doesn't exist
        """
        rule = self.get_rule(name)
        rule.enabled = True
        self.update_rule(name, rule)

    def disable_rule(self, name: str) -> None:
        """Disable a rule.

        Args:
            name: Name of rule to disable

        Raises:
            RuleNotFoundError: If rule doesn't exist
        """
        rule = self.get_rule(name)
        rule.enabled = False
        self.update_rule(name, rule)

    def validate_rule(self, rule: SecurityRule) -> list[str]:
        """Validate a rule.

        Args:
            rule: Rule to validate

        Returns:
            List of validation errors (empty if valid)
        """
        errors = []

        # Check required fields
        if not rule.name:
            errors.append("Rule name is required")

        if not rule.condition:
            errors.append("Rule condition is required")

        if not rule.output:
            errors.append("Rule output is required")

        # Validate name format
        if rule.name and not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", rule.name):
            errors.append(
                "Rule name must start with a letter or underscore "
                "and contain only alphanumeric characters and underscores"
            )

        # Basic condition syntax check - check for unbalanced parentheses
        if rule.condition and rule.condition.count("(") != rule.condition.count(")"):
            errors.append("Unbalanced parentheses in condition")

        return errors

    def import_rules(self, path: Path) -> int:
        """Import rules from a YAML file.

        Args:
            path: Path to YAML file to import

        Returns:
            Number of rules imported

        Raises:
            FileNotFoundError: If file doesn't exist
        """
        if not path.exists():
            raise FileNotFoundError(f"Rule file not found: {path}")

        rules = self._load_rule_from_file(path)
        imported = 0

        for rule in rules:
            try:
                self.create_rule(rule)
                imported += 1
            except RuleExistsError:
                # Skip existing rules
                pass
            except RuleValidationError:
                # Skip invalid rules
                pass

        return imported

    def export_rules(self, path: Path, rules: list[SecurityRule] | None = None) -> int:
        """Export rules to a YAML file.

        Args:
            path: Path to save rules to
            rules: Rules to export (defaults to all managed rules)

        Returns:
            Number of rules exported
        """
        if rules is None:
            rules = self.list_rules(include_builtin=False)

        if not rules:
            return 0

        # Create RuleList and export
        rule_list = RuleList(rules=rules)
        rule_data = rule_list.to_falco_yaml()

        with open(path, "w") as f:
            yaml.safe_dump(rule_data, f, default_flow_style=False, sort_keys=False)

        return len(rules)

    def install_builtin_rules(self) -> int:
        """Install gaol's built-in security rules.

        Returns:
            Number of rules installed
        """
        if not BUILTIN_RULES_DIR.exists():
            return 0

        installed = 0
        for path in BUILTIN_RULES_DIR.glob("*.yaml"):
            # Copy to gaol rules directory
            dest = self._gaol_dir / path.name
            self._ensure_dirs()

            if not dest.exists():
                shutil.copy(path, dest)
                rules = self._load_rule_from_file(path)
                installed += len(rules)

        return installed

    def reload_falco_rules(self) -> bool:
        """Signal Falco to reload its rules.

        Returns:
            True if reload signal was sent successfully
        """
        try:
            # Send SIGHUP to falco to reload rules
            result = subprocess.run(
                ["pkill", "-HUP", "falco"],
                capture_output=True,
                timeout=5,
            )
            return result.returncode == 0
        except (subprocess.TimeoutExpired, FileNotFoundError):
            return False


# Global rule manager instance
_rule_manager: RuleManager | None = None


def get_rule_manager() -> RuleManager:
    """Get the global rule manager instance."""
    global _rule_manager
    if _rule_manager is None:
        _rule_manager = RuleManager()
    return _rule_manager
