"""Log search and analysis utilities.

This module provides advanced search, filtering, and analysis
capabilities for container logs.
"""

from __future__ import annotations

import re
from collections import Counter
from datetime import datetime, timedelta
from typing import Callable

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


class LogSearcher:
    """Advanced log search and analysis.

    Provides filtering, aggregation, and pattern analysis
    for log entries.

    Example:
        >>> from gaol.logs import LogSearcher, get_log_store
        >>> store = get_log_store()
        >>> entries = store.query_all()
        >>> searcher = LogSearcher(entries)
        >>> errors = searcher.filter_by_level(LogLevel.ERROR)
        >>> top_errors = searcher.find_top_patterns(n=10)
    """

    def __init__(self, entries: list[LogEntry]) -> None:
        """Initialize the searcher with log entries.

        Args:
            entries: List of log entries to search.
        """
        self.entries = entries

    def filter(self, predicate: Callable[[LogEntry], bool]) -> list[LogEntry]:
        """Filter entries using a custom predicate.

        Args:
            predicate: Function that returns True for entries to keep.

        Returns:
            Filtered list of entries.
        """
        return [e for e in self.entries if predicate(e)]

    def filter_by_level(
        self,
        *levels: LogLevel,
        min_level: LogLevel | None = None,
    ) -> list[LogEntry]:
        """Filter entries by log level.

        Args:
            *levels: Specific levels to include.
            min_level: Minimum level (ERROR includes CRITICAL).

        Returns:
            Filtered list of entries.
        """
        level_order = [
            LogLevel.DEBUG,
            LogLevel.INFO,
            LogLevel.WARNING,
            LogLevel.ERROR,
            LogLevel.CRITICAL,
        ]

        if min_level:
            min_idx = level_order.index(min_level) if min_level in level_order else 0
            allowed = set(level_order[min_idx:])
            return [e for e in self.entries if e.level in allowed]

        if levels:
            allowed = set(levels)
            return [e for e in self.entries if e.level in allowed]

        return list(self.entries)

    def filter_by_container(self, *containers: str) -> list[LogEntry]:
        """Filter entries by container name.

        Args:
            *containers: Container names to include.

        Returns:
            Filtered list of entries.
        """
        allowed = set(containers)
        return [e for e in self.entries if e.container_name in allowed]

    def filter_by_time(
        self,
        since: datetime | None = None,
        until: datetime | None = None,
    ) -> list[LogEntry]:
        """Filter entries by time range.

        Args:
            since: Start time (inclusive).
            until: End time (inclusive).

        Returns:
            Filtered list of entries.
        """
        result = self.entries
        if since:
            result = [e for e in result if e.timestamp >= since]
        if until:
            result = [e for e in result if e.timestamp <= until]
        return result

    def search(
        self,
        pattern: str,
        regex: bool = False,
        case_sensitive: bool = False,
    ) -> list[LogEntry]:
        """Search entries by text pattern.

        Args:
            pattern: Text pattern to search for.
            regex: Treat pattern as regular expression.
            case_sensitive: Case-sensitive matching.

        Returns:
            List of matching entries.
        """
        if regex:
            flags = 0 if case_sensitive else re.IGNORECASE
            compiled = re.compile(pattern, flags)
            return [e for e in self.entries if compiled.search(e.message)]

        if case_sensitive:
            return [e for e in self.entries if pattern in e.message]

        pattern_lower = pattern.lower()
        return [e for e in self.entries if pattern_lower in e.message.lower()]

    def grep(self, pattern: str, context: int = 0) -> list[tuple[LogEntry, list[LogEntry], list[LogEntry]]]:
        """Search with context lines (like grep -C).

        Args:
            pattern: Text pattern to search for.
            context: Number of context lines before and after.

        Returns:
            List of (matching_entry, before_entries, after_entries) tuples.
        """
        results = []
        indices = [i for i, e in enumerate(self.entries) if pattern.lower() in e.message.lower()]

        for idx in indices:
            before = self.entries[max(0, idx - context):idx]
            after = self.entries[idx + 1:idx + 1 + context]
            results.append((self.entries[idx], before, after))

        return results

    def find_top_patterns(
        self,
        n: int = 10,
        min_count: int = 2,
    ) -> list[tuple[str, int]]:
        """Find most common log message patterns.

        Normalizes messages by removing numbers and UUIDs to
        identify recurring patterns.

        Args:
            n: Number of top patterns to return.
            min_count: Minimum occurrences to include.

        Returns:
            List of (pattern, count) tuples sorted by count.
        """
        patterns: Counter[str] = Counter()

        for entry in self.entries:
            normalized = self._normalize_message(entry.message)
            patterns[normalized] += 1

        return [
            (pattern, count)
            for pattern, count in patterns.most_common(n)
            if count >= min_count
        ]

    def _normalize_message(self, message: str) -> str:
        """Normalize a message for pattern detection.

        Replaces numbers, UUIDs, IPs, timestamps with placeholders.

        Args:
            message: The message to normalize.

        Returns:
            Normalized message string.
        """
        # Replace UUIDs
        normalized = re.sub(
            r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
            '<UUID>',
            message,
            flags=re.IGNORECASE,
        )

        # Replace IP addresses
        normalized = re.sub(
            r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b',
            '<IP>',
            normalized,
        )

        # Replace timestamps
        normalized = re.sub(
            r'\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?',
            '<TIMESTAMP>',
            normalized,
        )

        # Replace long hex strings (hashes, IDs)
        normalized = re.sub(r'\b[0-9a-f]{12,}\b', '<HEX>', normalized, flags=re.IGNORECASE)

        # Replace remaining numbers
        normalized = re.sub(r'\b\d+\b', '<N>', normalized)

        return normalized

    def count_by_level(self) -> dict[LogLevel, int]:
        """Count entries by log level.

        Returns:
            Dictionary mapping levels to counts.
        """
        counts: dict[LogLevel, int] = {}
        for entry in self.entries:
            counts[entry.level] = counts.get(entry.level, 0) + 1
        return counts

    def count_by_container(self) -> dict[str, int]:
        """Count entries by container.

        Returns:
            Dictionary mapping container names to counts.
        """
        counts: dict[str, int] = {}
        for entry in self.entries:
            counts[entry.container_name] = counts.get(entry.container_name, 0) + 1
        return counts

    def count_by_hour(self) -> dict[str, int]:
        """Count entries by hour.

        Returns:
            Dictionary mapping hour strings to counts.
        """
        counts: dict[str, int] = {}
        for entry in self.entries:
            hour = entry.timestamp.strftime("%Y-%m-%d %H:00")
            counts[hour] = counts.get(hour, 0) + 1
        return dict(sorted(counts.items()))

    def get_error_summary(self) -> dict:
        """Get a summary of errors and warnings.

        Returns:
            Dictionary with error analysis.
        """
        errors = self.filter_by_level(LogLevel.ERROR, LogLevel.CRITICAL)
        warnings = self.filter_by_level(LogLevel.WARNING)

        error_patterns = LogSearcher(errors).find_top_patterns(5)
        warning_patterns = LogSearcher(warnings).find_top_patterns(5)

        return {
            "total_errors": len(errors),
            "total_warnings": len(warnings),
            "errors_by_container": LogSearcher(errors).count_by_container(),
            "top_error_patterns": error_patterns,
            "top_warning_patterns": warning_patterns,
            "first_error": errors[0] if errors else None,
            "last_error": errors[-1] if errors else None,
        }

    def get_timeline(
        self,
        bucket_minutes: int = 5,
    ) -> list[dict]:
        """Get log volume timeline.

        Args:
            bucket_minutes: Size of time buckets.

        Returns:
            List of dicts with timestamp and counts.
        """
        if not self.entries:
            return []

        buckets: dict[datetime, dict[str, int]] = {}
        delta = timedelta(minutes=bucket_minutes)

        for entry in self.entries:
            # Round down to bucket
            bucket = entry.timestamp.replace(
                minute=(entry.timestamp.minute // bucket_minutes) * bucket_minutes,
                second=0,
                microsecond=0,
            )
            if bucket not in buckets:
                buckets[bucket] = {"total": 0, "errors": 0}
            buckets[bucket]["total"] += 1
            if entry.level in (LogLevel.ERROR, LogLevel.CRITICAL):
                buckets[bucket]["errors"] += 1

        return [
            {"timestamp": ts, **counts}
            for ts, counts in sorted(buckets.items())
        ]


def create_query(
    since: str | datetime | None = None,
    until: str | datetime | None = None,
    containers: list[str] | None = None,
    levels: list[str] | None = None,
    pattern: str | None = None,
    regex: bool = False,
    limit: int | None = None,
) -> LogQuery:
    """Create a LogQuery from CLI-style arguments.

    Args:
        since: Start time (datetime, ISO string, or relative like "1h", "30m").
        until: End time (datetime, ISO string, or relative).
        containers: List of container names.
        levels: List of level names (debug, info, warning, error, critical).
        pattern: Search pattern.
        regex: Treat pattern as regex.
        limit: Maximum entries to return.

    Returns:
        Configured LogQuery object.
    """
    parsed_since = _parse_time(since) if since else None
    parsed_until = _parse_time(until) if until else None

    parsed_levels = None
    if levels:
        parsed_levels = [LogLevel(l.lower()) for l in levels if l.upper() in LogLevel.__members__]

    return LogQuery(
        since=parsed_since,
        until=parsed_until,
        containers=containers,
        levels=parsed_levels,
        pattern=pattern,
        regex=regex,
        limit=limit,
    )


def _parse_time(value: str | datetime) -> datetime:
    """Parse a time value from various formats.

    Supports:
    - datetime objects (passthrough)
    - ISO format strings
    - Relative times: "1h", "30m", "2d", "1w"

    Args:
        value: Time value to parse.

    Returns:
        Parsed datetime.
    """
    if isinstance(value, datetime):
        return value

    # Try relative time
    match = re.match(r'^(\d+)([mhdw])$', value.lower())
    if match:
        amount = int(match.group(1))
        unit = match.group(2)
        now = datetime.now()

        if unit == 'm':
            return now - timedelta(minutes=amount)
        elif unit == 'h':
            return now - timedelta(hours=amount)
        elif unit == 'd':
            return now - timedelta(days=amount)
        elif unit == 'w':
            return now - timedelta(weeks=amount)

    # Try ISO format
    try:
        return datetime.fromisoformat(value)
    except ValueError:
        pass

    # Try common formats
    for fmt in [
        "%Y-%m-%d %H:%M:%S",
        "%Y-%m-%d %H:%M",
        "%Y-%m-%d",
        "%d/%m/%Y %H:%M:%S",
        "%d/%m/%Y",
    ]:
        try:
            return datetime.strptime(value, fmt)
        except ValueError:
            continue

    raise ValueError(f"Cannot parse time: {value}")
