"""Log collection from containers.

This module provides the LogCollector class for retrieving and parsing
logs from containers across different runtimes.
"""

from __future__ import annotations

import logging
import re
import threading
from datetime import datetime
from typing import TYPE_CHECKING, Callable

from gaol.logs.models import (
    LogConfig,
    LogEntry,
    LogLevel,
    LogSource,
    parse_log_level,
)

if TYPE_CHECKING:
    from gaol.runtimes.base import ContainerRuntime

logger = logging.getLogger(__name__)


class LogCollector:
    """Collects and parses logs from containers.

    The collector can retrieve logs on-demand or run in the background
    to continuously collect logs from one or more containers.

    Example:
        >>> from gaol.runtimes import get_runtime
        >>> from gaol.logs import LogCollector
        >>> runtime = get_runtime("docker")
        >>> collector = LogCollector(runtime)
        >>> entries = collector.collect("my-container", tail=100)
        >>> for entry in entries:
        ...     print(f"[{entry.level}] {entry.message}")
    """

    def __init__(
        self,
        runtime: ContainerRuntime,
        config: LogConfig | None = None,
    ) -> None:
        """Initialize the log collector.

        Args:
            runtime: Container runtime to collect logs from.
            config: Log collection configuration.
        """
        self.runtime = runtime
        self.config = config or LogConfig()
        self._callbacks: list[Callable[[LogEntry], None]] = []
        self._running = False
        self._threads: dict[str, threading.Thread] = {}
        self._last_timestamps: dict[str, datetime] = {}

    def add_callback(self, callback: Callable[[LogEntry], None]) -> None:
        """Add a callback to receive log entries.

        Callbacks are invoked for each new log entry collected.

        Args:
            callback: Function to call with each LogEntry.
        """
        self._callbacks.append(callback)

    def remove_callback(self, callback: Callable[[LogEntry], None]) -> None:
        """Remove a previously added callback.

        Args:
            callback: The callback to remove.
        """
        if callback in self._callbacks:
            self._callbacks.remove(callback)

    def collect(
        self,
        container_name: str,
        *,
        tail: int | None = None,
        since: str | datetime | None = None,
        container_id: str | None = None,
    ) -> list[LogEntry]:
        """Collect logs from a container.

        Args:
            container_name: Name of the container.
            tail: Number of recent lines to retrieve.
            since: Retrieve logs since this time.
            container_id: Container ID (uses name if not provided).

        Returns:
            List of parsed log entries.
        """
        cid = container_id or container_name

        # Convert datetime to string format for runtime
        since_str = None
        if since:
            if isinstance(since, datetime):
                since_str = since.isoformat()
            else:
                since_str = since

        try:
            raw_logs = self.runtime.logs(cid, tail=tail, since=since_str)
        except Exception as e:
            logger.warning(f"Failed to collect logs from {container_name}: {e}")
            return []

        return self._parse_logs(raw_logs, container_name, cid)

    def _parse_logs(
        self,
        raw_logs: str,
        container_name: str,
        container_id: str,
    ) -> list[LogEntry]:
        """Parse raw log output into LogEntry objects.

        Args:
            raw_logs: Raw log string from runtime.
            container_name: Container name for entries.
            container_id: Container ID for entries.

        Returns:
            List of parsed LogEntry objects.
        """
        entries = []

        for line in raw_logs.splitlines():
            if not line.strip():
                continue

            entry = self._parse_log_line(line, container_name, container_id)
            entries.append(entry)

            # Invoke callbacks
            for callback in self._callbacks:
                try:
                    callback(entry)
                except Exception as e:
                    logger.error(f"Log callback error: {e}")

        return entries

    def _parse_log_line(
        self,
        line: str,
        container_name: str,
        container_id: str,
    ) -> LogEntry:
        """Parse a single log line.

        Attempts to extract timestamp, level, and message from common
        log formats.

        Args:
            line: Raw log line.
            container_name: Container name.
            container_id: Container ID.

        Returns:
            Parsed LogEntry.
        """
        timestamp = datetime.now()
        message = line
        source = self._detect_source()
        level = LogLevel.UNKNOWN

        # Try to extract timestamp from common formats
        # Docker format: 2025-01-12T10:30:45.123456789Z message
        docker_match = re.match(
            r'^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?)\s+(.*)$',
            line,
        )
        if docker_match:
            try:
                ts_str = docker_match.group(1).rstrip('Z')
                # Handle nanoseconds by truncating to microseconds
                if '.' in ts_str and len(ts_str.split('.')[-1]) > 6:
                    ts_str = ts_str[:ts_str.index('.') + 7]
                timestamp = datetime.fromisoformat(ts_str)
                message = docker_match.group(2)
            except ValueError:
                pass

        # Try systemd journal format: Mon 2025-01-12 10:30:45 UTC message
        journal_match = re.match(
            r'^(?:\w{3}\s+)?(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\w+\s+(.*)$',
            line,
        )
        if journal_match and message == line:
            try:
                timestamp = datetime.strptime(
                    journal_match.group(1),
                    "%Y-%m-%d %H:%M:%S"
                )
                message = journal_match.group(2)
            except ValueError:
                pass

        # Parse log level from message
        if self.config.parse_levels:
            level = parse_log_level(message)

        return LogEntry(
            container_id=container_id,
            container_name=container_name,
            runtime=self.runtime.name,
            timestamp=timestamp,
            message=message,
            level=level,
            source=source,
            raw=line,
        )

    def _detect_source(self) -> LogSource:
        """Detect log source based on runtime.

        Returns:
            LogSource based on runtime type.
        """
        runtime_sources = {
            "docker": LogSource.STDOUT,
            "nspawn": LogSource.JOURNAL,
            "libvirt": LogSource.FILE,
        }
        return runtime_sources.get(self.runtime.name, LogSource.UNKNOWN)

    def start_streaming(
        self,
        container_name: str,
        container_id: str | None = None,
    ) -> None:
        """Start streaming logs from a container in the background.

        Logs are collected periodically and sent to registered callbacks.

        Args:
            container_name: Name of the container.
            container_id: Container ID (uses name if not provided).
        """
        if container_name in self._threads:
            logger.warning(f"Already streaming logs for {container_name}")
            return

        self._running = True
        cid = container_id or container_name

        def stream_worker():
            while self._running and container_name in self._threads:
                try:
                    # Get last timestamp for incremental collection
                    since = self._last_timestamps.get(container_name)

                    entries = self.collect(
                        container_name,
                        container_id=cid,
                        since=since,
                    )

                    # Update last timestamp
                    if entries:
                        self._last_timestamps[container_name] = entries[-1].timestamp

                except Exception as e:
                    logger.error(f"Error streaming logs for {container_name}: {e}")

                # Wait before next collection
                import time
                time.sleep(self.config.collect_interval)

        thread = threading.Thread(
            target=stream_worker,
            name=f"log-collector-{container_name}",
            daemon=True,
        )
        self._threads[container_name] = thread
        thread.start()
        logger.info(f"Started log streaming for {container_name}")

    def stop_streaming(self, container_name: str) -> None:
        """Stop streaming logs for a container.

        Args:
            container_name: Name of the container.
        """
        if container_name in self._threads:
            thread = self._threads.pop(container_name)
            # Thread will exit on next iteration when it checks _threads
            thread.join(timeout=self.config.collect_interval + 1)
            logger.info(f"Stopped log streaming for {container_name}")

    def stop_all(self) -> None:
        """Stop all log streaming."""
        self._running = False
        for name in list(self._threads.keys()):
            self.stop_streaming(name)

    @property
    def is_streaming(self) -> bool:
        """Check if any containers are being streamed."""
        return bool(self._threads)

    @property
    def streaming_containers(self) -> list[str]:
        """Get list of containers being streamed."""
        return list(self._threads.keys())


class MultiRuntimeCollector:
    """Collects logs from containers across multiple runtimes.

    Provides a unified interface for log collection regardless of
    which runtime a container is using.

    Example:
        >>> from gaol.logs import MultiRuntimeCollector
        >>> from gaol.store import get_container_store
        >>> collector = MultiRuntimeCollector()
        >>> entries = collector.collect_all(tail=50)
    """

    def __init__(self, config: LogConfig | None = None) -> None:
        """Initialize multi-runtime collector.

        Args:
            config: Log collection configuration.
        """
        self.config = config or LogConfig()
        self._collectors: dict[str, LogCollector] = {}
        self._callbacks: list[Callable[[LogEntry], None]] = []

    def _get_collector(self, runtime_name: str) -> LogCollector:
        """Get or create a collector for a runtime.

        Args:
            runtime_name: Name of the runtime.

        Returns:
            LogCollector for the runtime.
        """
        if runtime_name not in self._collectors:
            from gaol.runtimes import get_runtime

            runtime = get_runtime(runtime_name)
            collector = LogCollector(runtime, self.config)

            # Forward callbacks
            for callback in self._callbacks:
                collector.add_callback(callback)

            self._collectors[runtime_name] = collector

        return self._collectors[runtime_name]

    def add_callback(self, callback: Callable[[LogEntry], None]) -> None:
        """Add a callback to all collectors.

        Args:
            callback: Function to call with each LogEntry.
        """
        self._callbacks.append(callback)
        for collector in self._collectors.values():
            collector.add_callback(callback)

    def collect(
        self,
        container_name: str,
        runtime: str | None = None,
        **kwargs,
    ) -> list[LogEntry]:
        """Collect logs from a container.

        If runtime is not specified, attempts to find it from the store.

        Args:
            container_name: Name of the container.
            runtime: Runtime name (auto-detected if not provided).
            **kwargs: Additional arguments passed to collect().

        Returns:
            List of log entries.
        """
        if not runtime:
            from gaol.store import get_container_store

            store = get_container_store()
            stored = store.get(container_name)
            if stored:
                runtime = stored.runtime
            else:
                # Try each runtime
                for rt_name in ["docker", "nspawn", "libvirt"]:
                    try:
                        collector = self._get_collector(rt_name)
                        entries = collector.collect(container_name, **kwargs)
                        if entries:
                            return entries
                    except Exception:
                        continue
                return []

        collector = self._get_collector(runtime)
        return collector.collect(container_name, **kwargs)

    def collect_all(
        self,
        containers: list[str] | None = None,
        **kwargs,
    ) -> list[LogEntry]:
        """Collect logs from all containers or specified ones.

        Args:
            containers: List of container names (all if not provided).
            **kwargs: Additional arguments passed to collect().

        Returns:
            List of log entries sorted by timestamp.
        """
        from gaol.store import get_container_store

        store = get_container_store()
        all_entries: list[LogEntry] = []

        target_containers = containers or [s.config.name for s in store.list()]

        for name in target_containers:
            try:
                entries = self.collect(name, **kwargs)
                all_entries.extend(entries)
            except Exception as e:
                logger.warning(f"Failed to collect logs from {name}: {e}")

        # Sort by timestamp
        all_entries.sort(key=lambda e: e.timestamp)

        return all_entries

    def stop_all(self) -> None:
        """Stop all log streaming."""
        for collector in self._collectors.values():
            collector.stop_all()
