"""Falco client for gRPC event streaming."""

from __future__ import annotations

import json
import subprocess
import threading
import uuid
from collections.abc import Callable, Iterator
from datetime import UTC, datetime
from pathlib import Path

from gaol.security.exceptions import (
    FalcoConnectionError,
    FalcoNotInstalledError,
    FalcoNotRunningError,
)
from gaol.security.models import (
    ContainerInfo,
    EventSummary,
    FalcoPriority,
    FalcoStatus,
    SecurityEvent,
)

# Default Falco socket path
DEFAULT_SOCKET_PATH = "/run/falco/falco.sock"
DEFAULT_GRPC_ADDRESS = "localhost:5060"


class FalcoClient:
    """Client for interacting with Falco via gRPC or Unix socket.

    This client provides methods to connect to Falco, stream security events,
    and query Falco status. It supports both Unix socket and TCP connections.

    Example:
        client = FalcoClient()
        client.connect()

        for event in client.stream_events():
            print(f"Event: {event.rule} - {event.output}")
    """

    def __init__(
        self,
        socket_path: str | None = None,
        grpc_address: str | None = None,
        use_tls: bool = False,
        ca_cert: str | None = None,
        client_cert: str | None = None,
        client_key: str | None = None,
    ) -> None:
        """Initialize Falco client.

        Args:
            socket_path: Path to Unix socket (default: /run/falco/falco.sock)
            grpc_address: gRPC address for TCP connection (e.g., localhost:5060)
            use_tls: Enable mTLS for TCP connections
            ca_cert: CA certificate path for mTLS
            client_cert: Client certificate path for mTLS
            client_key: Client key path for mTLS
        """
        self._socket_path = socket_path
        self._grpc_address = grpc_address
        self._use_tls = use_tls
        self._ca_cert = ca_cert
        self._client_cert = client_cert
        self._client_key = client_key

        self._connected = False
        self._channel = None
        self._stub = None

        # Background streaming
        self._stream_thread: threading.Thread | None = None
        self._stream_running = False
        self._stream_callback: Callable[[SecurityEvent], None] | None = None

    @property
    def endpoint(self) -> str:
        """Get the connection endpoint."""
        if self._grpc_address:
            return self._grpc_address
        return f"unix://{self._socket_path or DEFAULT_SOCKET_PATH}"

    def connect(self) -> None:
        """Establish connection to Falco.

        This attempts to connect via gRPC. Falls back to checking if Falco
        is running and accessible.

        Raises:
            FalcoNotInstalledError: If Falco is not installed
            FalcoNotRunningError: If Falco service is not running
            FalcoConnectionError: If connection fails
        """
        # Check if Falco is installed
        if not self._is_falco_installed():
            raise FalcoNotInstalledError(
                "Falco is not installed. Run 'gaol security init --auto-install'"
            )

        # Check if Falco is running
        if not self._is_falco_running():
            raise FalcoNotRunningError(
                "Falco service is not running. Start it with 'sudo systemctl start falco'"
            )

        # Check socket/connection availability
        if self._socket_path or not self._grpc_address:
            socket_path = Path(self._socket_path or DEFAULT_SOCKET_PATH)
            if not socket_path.exists():
                raise FalcoConnectionError(
                    f"Falco socket not found at {socket_path}. "
                    "Ensure gRPC output is enabled in Falco configuration."
                )

        self._connected = True

    def disconnect(self) -> None:
        """Close connection to Falco."""
        self.stop_background_stream()
        if self._channel:
            self._channel.close()
            self._channel = None
        self._stub = None
        self._connected = False

    def is_connected(self) -> bool:
        """Check if client is connected."""
        return self._connected

    def _is_falco_installed(self) -> bool:
        """Check if Falco is installed on the system."""
        try:
            result = subprocess.run(
                ["which", "falco"],
                capture_output=True,
                text=True,
                timeout=5,
            )
            return result.returncode == 0
        except (subprocess.TimeoutExpired, FileNotFoundError):
            return False

    def _is_falco_running(self) -> bool:
        """Check if Falco service is running."""
        try:
            result = subprocess.run(
                ["systemctl", "is-active", "falco"],
                capture_output=True,
                text=True,
                timeout=5,
            )
            return result.stdout.strip() == "active"
        except (subprocess.TimeoutExpired, FileNotFoundError):
            # Try checking for falco process directly
            try:
                pgrep_result = subprocess.run(
                    ["pgrep", "-x", "falco"],
                    capture_output=True,
                    text=True,
                    timeout=5,
                )
                return pgrep_result.returncode == 0
            except (subprocess.TimeoutExpired, FileNotFoundError):
                return False

    def get_status(self) -> FalcoStatus:
        """Get Falco service status.

        Returns:
            FalcoStatus with installation and runtime information
        """
        status = FalcoStatus()

        # Check installation
        status.installed = self._is_falco_installed()
        if not status.installed:
            return status

        # Get version
        try:
            result = subprocess.run(
                ["falco", "--version"],
                capture_output=True,
                text=True,
                timeout=5,
            )
            if result.returncode == 0:
                # Parse version from output like "Falco 0.37.1"
                for line in result.stdout.split("\n"):
                    if line.startswith("Falco"):
                        parts = line.split()
                        if len(parts) >= 2:
                            status.version = parts[1]
                        break
        except (subprocess.TimeoutExpired, FileNotFoundError):
            pass

        # Check if running
        status.running = self._is_falco_running()

        # Check gRPC socket
        socket_path = Path(self._socket_path or DEFAULT_SOCKET_PATH)
        if socket_path.exists():
            status.grpc_enabled = True
            status.grpc_socket = str(socket_path)

        # Try to get driver type from falco output
        try:
            result = subprocess.run(
                ["falco", "--list-syscall-events"],
                capture_output=True,
                text=True,
                timeout=10,
            )
            if "modern_ebpf" in result.stderr.lower():
                status.driver_type = "modern_ebpf"
            elif "ebpf" in result.stderr.lower():
                status.driver_type = "ebpf"
            elif "kmod" in result.stderr.lower():
                status.driver_type = "kmod"
        except (subprocess.TimeoutExpired, FileNotFoundError):
            pass

        return status

    def stream_events(
        self,
        filter_priority: FalcoPriority | None = None,
        filter_containers: list[str] | None = None,
        filter_rules: list[str] | None = None,
    ) -> Iterator[SecurityEvent]:
        """Stream security events from Falco.

        This method reads events from Falco's JSON output file or gRPC stream.
        Events are filtered based on the provided criteria.

        Args:
            filter_priority: Minimum priority level to include
            filter_containers: Only include events from these containers (patterns)
            filter_rules: Only include events matching these rule names

        Yields:
            SecurityEvent objects as they arrive

        Raises:
            FalcoConnectionError: If not connected
        """
        if not self._connected:
            raise FalcoConnectionError("Not connected. Call connect() first.")

        # Use falco's JSON output via journalctl
        # This is a simpler approach than gRPC for initial implementation
        try:
            proc = subprocess.Popen(
                [
                    "journalctl",
                    "-u",
                    "falco",
                    "-f",
                    "-o",
                    "cat",
                    "--no-pager",
                ],
                stdout=subprocess.PIPE,
                stderr=subprocess.DEVNULL,
                text=True,
            )

            if proc.stdout is None:
                return

            for line in iter(proc.stdout.readline, ""):
                if not line.strip():
                    continue

                event = self._parse_event_line(line)
                if event is None:
                    continue

                # Apply filters
                if filter_priority and not event.is_at_least_priority(filter_priority):
                    continue

                if filter_containers and not any(
                    event.matches_container(pattern) for pattern in filter_containers
                ):
                    continue

                if filter_rules and event.rule not in filter_rules:
                    continue

                yield event

        except KeyboardInterrupt:
            pass
        finally:
            if proc:
                proc.terminate()

    def _parse_event_line(self, line: str) -> SecurityEvent | None:
        """Parse a Falco event from a log line.

        Args:
            line: Raw log line from Falco

        Returns:
            SecurityEvent or None if parsing fails
        """
        # Try to parse as JSON first
        try:
            if line.strip().startswith("{"):
                data = json.loads(line)
                return self._event_from_json(data)
        except json.JSONDecodeError:
            pass

        # Try to parse Falco's default output format
        # Format: TIMESTAMP PRIORITY RULE OUTPUT
        # Example: 15:30:45.123456789: Warning Sensitive file opened...
        try:
            parts = line.split(": ", 2)
            if len(parts) >= 3:
                timestamp_str = parts[0].strip()
                priority_rule = parts[1].strip()
                output = parts[2].strip()

                # Parse priority and rule from "Warning Sensitive file opened"
                priority_parts = priority_rule.split(" ", 1)
                if len(priority_parts) >= 2:
                    priority_str = priority_parts[0]
                    rule = priority_parts[1]
                else:
                    priority_str = priority_parts[0]
                    rule = "unknown"

                # Parse priority
                try:
                    priority = FalcoPriority.from_string(priority_str)
                except ValueError:
                    priority = FalcoPriority.WARNING

                # Generate UUID
                event_uuid = str(uuid.uuid4())

                # Parse timestamp (may be partial)
                try:
                    # Try full datetime
                    timestamp = datetime.fromisoformat(timestamp_str.replace("Z", "+00:00"))
                except ValueError:
                    # Use current time if parsing fails
                    timestamp = datetime.now(UTC)

                return SecurityEvent(
                    uuid=event_uuid,
                    timestamp=timestamp,
                    rule=rule,
                    priority=priority,
                    output=output,
                )
        except Exception:
            pass

        return None

    def _event_from_json(self, data: dict) -> SecurityEvent:
        """Create SecurityEvent from JSON data.

        Args:
            data: JSON event data from Falco

        Returns:
            SecurityEvent instance
        """
        # Parse container info if present
        container = None
        if "container" in data or "container.id" in data.get("output_fields", {}):
            output_fields = data.get("output_fields", {})
            container = ContainerInfo(
                id=output_fields.get("container.id"),
                name=output_fields.get("container.name"),
                image=output_fields.get("container.image.repository"),
                image_tag=output_fields.get("container.image.tag"),
            )

        # Parse timestamp
        timestamp_str = data.get("time", data.get("timestamp", ""))
        try:
            timestamp = datetime.fromisoformat(timestamp_str.replace("Z", "+00:00"))
        except ValueError:
            timestamp = datetime.now(UTC)

        # Parse priority
        priority_str = data.get("priority", "Warning")
        try:
            priority = FalcoPriority.from_string(priority_str)
        except ValueError:
            priority = FalcoPriority.WARNING

        return SecurityEvent(
            uuid=data.get("uuid", str(uuid.uuid4())),
            timestamp=timestamp,
            rule=data.get("rule", "unknown"),
            priority=priority,
            output=data.get("output", ""),
            output_fields=data.get("output_fields", {}),
            source=data.get("source", "syscall"),
            container=container,
            hostname=data.get("hostname"),
            tags=data.get("tags", []),
        )

    def start_background_stream(
        self,
        callback: Callable[[SecurityEvent], None],
        filter_priority: FalcoPriority | None = None,
        filter_containers: list[str] | None = None,
        filter_rules: list[str] | None = None,
    ) -> threading.Thread:
        """Start event streaming in a background thread.

        Args:
            callback: Function to call for each event
            filter_priority: Minimum priority level
            filter_containers: Container name patterns to include
            filter_rules: Rule names to include

        Returns:
            The background thread
        """
        if self._stream_running:
            return self._stream_thread  # type: ignore

        self._stream_callback = callback
        self._stream_running = True

        def stream_loop() -> None:
            try:
                for event in self.stream_events(
                    filter_priority=filter_priority,
                    filter_containers=filter_containers,
                    filter_rules=filter_rules,
                ):
                    if not self._stream_running:
                        break
                    if self._stream_callback:
                        self._stream_callback(event)
            except FalcoConnectionError:
                pass

        self._stream_thread = threading.Thread(target=stream_loop, daemon=True)
        self._stream_thread.start()
        return self._stream_thread

    def stop_background_stream(self) -> None:
        """Stop background event streaming."""
        self._stream_running = False
        if self._stream_thread:
            self._stream_thread.join(timeout=2.0)
            self._stream_thread = None
        self._stream_callback = None


class FalcoEventBuffer:
    """Thread-safe buffer for Falco events with filtering and aggregation.

    This class provides a circular buffer for storing events with methods
    for filtering, aggregation, and summary generation.
    """

    def __init__(self, max_size: int = 10000) -> None:
        """Initialize event buffer.

        Args:
            max_size: Maximum number of events to store
        """
        self._events: list[SecurityEvent] = []
        self._max_size = max_size
        self._lock = threading.Lock()

    def add(self, event: SecurityEvent) -> None:
        """Add event to buffer.

        Args:
            event: Event to add
        """
        with self._lock:
            self._events.append(event)
            if len(self._events) > self._max_size:
                self._events.pop(0)

    def get_recent(
        self,
        count: int = 100,
        container: str | None = None,
        min_priority: FalcoPriority | None = None,
        rule: str | None = None,
    ) -> list[SecurityEvent]:
        """Get recent events with optional filtering.

        Args:
            count: Maximum number of events to return
            container: Filter by container name pattern
            min_priority: Minimum priority level
            rule: Filter by rule name

        Returns:
            List of matching events, most recent first
        """
        with self._lock:
            events = list(reversed(self._events))

        # Apply filters
        filtered: list[SecurityEvent] = []
        for event in events:
            if len(filtered) >= count:
                break

            if min_priority and not event.is_at_least_priority(min_priority):
                continue

            if container and not event.matches_container(container):
                continue

            if rule and event.rule != rule:
                continue

            filtered.append(event)

        return filtered

    def get_summary(self) -> EventSummary:
        """Get summary of events in buffer.

        Returns:
            EventSummary with aggregated statistics
        """
        with self._lock:
            events = list(self._events)

        if not events:
            return EventSummary()

        summary = EventSummary(
            total_events=len(events),
            time_range_start=events[0].timestamp,
            time_range_end=events[-1].timestamp,
        )

        for event in events:
            # Count by priority
            priority_key = event.priority.value
            summary.events_by_priority[priority_key] = (
                summary.events_by_priority.get(priority_key, 0) + 1
            )

            # Count by rule
            summary.events_by_rule[event.rule] = (
                summary.events_by_rule.get(event.rule, 0) + 1
            )

            # Count by container
            if event.container and event.container.name:
                container_name = event.container.name
                summary.events_by_container[container_name] = (
                    summary.events_by_container.get(container_name, 0) + 1
                )

        return summary

    def clear(self) -> None:
        """Clear all events from buffer."""
        with self._lock:
            self._events.clear()

    def __len__(self) -> int:
        """Return number of events in buffer."""
        with self._lock:
            return len(self._events)


# Global client instance
_client: FalcoClient | None = None


def get_falco_client() -> FalcoClient:
    """Get the global Falco client instance."""
    global _client
    if _client is None:
        _client = FalcoClient()
    return _client
