"""Log aggregation and analysis system for OpenClaw trading agents.

This module provides the LogAnalyzer class for collecting, indexing, and analyzing
JSONL log files generated by the loguru logging system.
"""

from __future__ import annotations

import csv
import json
import re
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any


@dataclass
class LogEntry:
    """A single parsed log entry.

    Attributes:
        timestamp: ISO format timestamp
        level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
        message: Log message content
        module: Source module name
        function: Source function name
        line: Source line number
        extra: Additional structured data (may contain agent_id, trade_id, etc.)
        exception: Exception information if present
        raw: Original JSON line
    """

    timestamp: datetime
    level: str
    message: str
    module: str
    function: str
    line: int
    extra: dict[str, Any] = field(default_factory=dict)
    exception: str | None = None
    raw: str = ""

    @property
    def agent_id(self) -> str | None:
        """Extract agent_id from extra fields if present."""
        return self.extra.get("agent_id")

    @property
    def trade_id(self) -> str | None:
        """Extract trade_id from extra fields if present."""
        return self.extra.get("trade_id")

    def to_dict(self) -> dict[str, Any]:
        """Convert entry to dictionary."""
        return {
            "timestamp": self.timestamp.isoformat(),
            "level": self.level,
            "message": self.message,
            "module": self.module,
            "function": self.function,
            "line": self.line,
            "extra": self.extra,
            "exception": self.exception,
        }

    def matches_text(self, query: str) -> bool:
        """Check if entry matches a text query (case-insensitive)."""
        query_lower = query.lower()
        return (
            query_lower in self.message.lower()
            or query_lower in self.module.lower()
            or query_lower in self.function.lower()
            or query_lower in str(self.extra).lower()
        )


@dataclass
class ErrorPattern:
    """Detected error pattern in logs.

    Attributes:
        pattern: Description of the error pattern
        count: Number of occurrences
        sample_messages: Sample error messages
        first_occurrence: First timestamp seen
        last_occurrence: Last timestamp seen
        affected_agents: Set of agent IDs affected
    """

    pattern: str
    count: int = 0
    sample_messages: list[str] = field(default_factory=list)
    first_occurrence: datetime | None = None
    last_occurrence: datetime | None = None
    affected_agents: set[str] = field(default_factory=set)

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary."""
        return {
            "pattern": self.pattern,
            "count": self.count,
            "sample_messages": self.sample_messages[:5],  # Limit samples
            "first_occurrence": (
                self.first_occurrence.isoformat() if self.first_occurrence else None
            ),
            "last_occurrence": (
                self.last_occurrence.isoformat() if self.last_occurrence else None
            ),
            "affected_agents": list(self.affected_agents),
        }


@dataclass
class LogReport:
    """Log analysis report.

    Attributes:
        start_time: Analysis period start
        end_time: Analysis period end
        total_entries: Total log entries analyzed
        level_counts: Count by log level
        agent_counts: Count by agent
        error_patterns: Detected error patterns
        summary: Text summary
    """

    start_time: datetime
    end_time: datetime
    total_entries: int
    level_counts: dict[str, int]
    agent_counts: dict[str, int]
    error_patterns: list[ErrorPattern]
    summary: str = ""

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary."""
        return {
            "start_time": self.start_time.isoformat(),
            "end_time": self.end_time.isoformat(),
            "total_entries": self.total_entries,
            "level_counts": self.level_counts,
            "agent_counts": self.agent_counts,
            "error_patterns": [p.to_dict() for p in self.error_patterns],
            "summary": self.summary,
        }

    def to_json(self, indent: int = 2) -> str:
        """Convert to JSON string."""
        return json.dumps(self.to_dict(), indent=indent, ensure_ascii=False)


class LogAnalyzer:
    """Log aggregation and analysis system.

    The LogAnalyzer provides efficient indexing and search capabilities
    for JSONL log files generated by the OpenClaw logging system.

    Example:
        analyzer = LogAnalyzer()

        # Load logs from a date range
        analyzer.load_logs(
            start_date=datetime(2024, 1, 1),
            end_date=datetime(2024, 1, 31)
        )

        # Search for specific errors
        errors = analyzer.filter_by_level("ERROR")

        # Get trade audit trail
        trail = analyzer.get_trade_audit_trail("T001")

        # Generate report
        report = analyzer.generate_log_report()
    """

    # Log levels ordered by severity
    LOG_LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]

    def __init__(self, log_dir: str | Path = "logs") -> None:
        """Initialize the log analyzer.

        Args:
            log_dir: Directory containing JSONL log files
        """
        self.log_dir = Path(log_dir)
        self._entries: list[LogEntry] = []

        # Indexes for efficient filtering
        self._index_by_level: dict[str, set[int]] = {}
        self._index_by_agent: dict[str, set[int]] = {}
        self._index_by_trade: dict[str, set[int]] = {}
        self._index_by_time: list[tuple[datetime, int]] = []  # (timestamp, entry_index)

    def _parse_jsonl_line(self, line: str) -> LogEntry | None:
        """Parse a single JSONL line into a LogEntry.

        Args:
            line: JSON string line

        Returns:
            LogEntry or None if parsing fails
        """
        try:
            data = json.loads(line)

            # Parse timestamp
            timestamp_str = data.get("timestamp", "")
            try:
                timestamp = datetime.fromisoformat(timestamp_str)
            except (ValueError, TypeError):
                timestamp = datetime.now()

            # Extract extra fields
            extra = data.get("extra", {})

            return LogEntry(
                timestamp=timestamp,
                level=data.get("level", "INFO"),
                message=data.get("message", ""),
                module=data.get("module", ""),
                function=data.get("function", ""),
                line=data.get("line", 0),
                extra=extra,
                exception=data.get("exception"),
                raw=line,
            )
        except (json.JSONDecodeError, KeyError):
            return None

    def _build_indexes(self) -> None:
        """Rebuild indexes from current entries."""
        self._index_by_level.clear()
        self._index_by_agent.clear()
        self._index_by_trade.clear()
        self._index_by_time.clear()

        for idx, entry in enumerate(self._entries):
            # Index by level
            level = entry.level
            if level not in self._index_by_level:
                self._index_by_level[level] = set()
            self._index_by_level[level].add(idx)

            # Index by agent
            if entry.agent_id:
                agent_id = entry.agent_id
                if agent_id not in self._index_by_agent:
                    self._index_by_agent[agent_id] = set()
                self._index_by_agent[agent_id].add(idx)

            # Index by trade
            if entry.trade_id:
                trade_id = entry.trade_id
                if trade_id not in self._index_by_trade:
                    self._index_by_trade[trade_id] = set()
                self._index_by_trade[trade_id].add(idx)

            # Index by time (maintain sorted order)
            self._index_by_time.append((entry.timestamp, idx))

        # Sort time index
        self._index_by_time.sort(key=lambda x: x[0])

    def load_logs(
        self,
        start_date: datetime | None = None,
        end_date: datetime | None = None,
        log_pattern: str = "openclaw_*.jsonl",
    ) -> int:
        """Load JSONL log files from the log directory.

        Args:
            start_date: Optional start date filter (inclusive)
            end_date: Optional end date filter (inclusive)
            log_pattern: Glob pattern for log files

        Returns:
            Number of entries loaded
        """
        self._entries.clear()

        if not self.log_dir.exists():
            return 0

        # Adjust end_date to include the entire day if it has no time component
        if end_date and end_date.hour == 0 and end_date.minute == 0 and end_date.second == 0:
            from datetime import timedelta
            end_date = end_date + timedelta(days=1) - timedelta(microseconds=1)

        # Find all matching log files
        log_files = list(self.log_dir.glob(log_pattern))

        for log_file in log_files:
            # Try to extract date from filename (openclaw_YYYY-MM-DD.jsonl)
            date_match = re.search(r"(\d{4}-\d{2}-\d{2})", log_file.name)
            if date_match:
                file_date = datetime.strptime(date_match.group(1), "%Y-%m-%d")

                # Skip files outside date range
                if start_date and file_date.date() < start_date.date():
                    continue
                if end_date and file_date.date() > end_date.date():
                    continue

            # Parse log file
            try:
                with open(log_file, "r", encoding="utf-8") as f:
                    for line in f:
                        line = line.strip()
                        if line:
                            entry = self._parse_jsonl_line(line)
                            if entry:
                                # Additional time filtering for entries within files
                                if start_date and entry.timestamp < start_date:
                                    continue
                                if end_date and entry.timestamp > end_date:
                                    continue
                                self._entries.append(entry)
            except (IOError, OSError):
                continue

        # Build indexes
        self._build_indexes()

        return len(self._entries)

    def add_entry(self, entry: LogEntry) -> None:
        """Add a single log entry (useful for testing).

        Args:
            entry: LogEntry to add
        """
        idx = len(self._entries)
        self._entries.append(entry)

        # Update indexes
        level = entry.level
        if level not in self._index_by_level:
            self._index_by_level[level] = set()
        self._index_by_level[level].add(idx)

        if entry.agent_id:
            agent_id = entry.agent_id
            if agent_id not in self._index_by_agent:
                self._index_by_agent[agent_id] = set()
            self._index_by_agent[agent_id].add(idx)

        if entry.trade_id:
            trade_id = entry.trade_id
            if trade_id not in self._index_by_trade:
                self._index_by_trade[trade_id] = set()
            self._index_by_trade[trade_id].add(idx)

        self._index_by_time.append((entry.timestamp, idx))
        self._index_by_time.sort(key=lambda x: x[0])

    def search_logs(
        self,
        query: str,
        filters: dict[str, Any] | None = None,
    ) -> list[LogEntry]:
        """Search logs with full-text query and optional filters.

        Args:
            query: Text to search for (case-insensitive)
            filters: Optional filters (level, agent_id, start_time, end_time)

        Returns:
            List of matching LogEntry objects
        """
        filters = filters or {}

        # Start with candidate indices if filters are provided
        candidates: set[int] | None = None

        # Apply level filter using index
        if "level" in filters:
            level = filters["level"]
            level_indices = self._index_by_level.get(level, set())
            candidates = level_indices if candidates is None else candidates & level_indices

        # Apply agent filter using index
        if "agent_id" in filters:
            agent_id = filters["agent_id"]
            agent_indices = self._index_by_agent.get(agent_id, set())
            candidates = agent_indices if candidates is None else candidates & agent_indices

        # Get entries to search
        if candidates is not None:
            entries_to_search = [self._entries[i] for i in sorted(candidates)]
        else:
            entries_to_search = self._entries

        results = []
        for entry in entries_to_search:
            # Apply time filters
            if "start_time" in filters:
                if entry.timestamp < filters["start_time"]:
                    continue
            if "end_time" in filters:
                if entry.timestamp > filters["end_time"]:
                    continue

            # Apply text search
            if entry.matches_text(query):
                results.append(entry)

        return results

    def filter_by_agent(self, agent_id: str) -> list[LogEntry]:
        """Filter logs by agent ID.

        Args:
            agent_id: Agent identifier

        Returns:
            List of LogEntry objects for the agent
        """
        indices = self._index_by_agent.get(agent_id, set())
        return [self._entries[i] for i in sorted(indices)]

    def filter_by_level(self, level: str, min_level: bool = False) -> list[LogEntry]:
        """Filter logs by log level.

        Args:
            level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
            min_level: If True, include all logs at this level or higher severity

        Returns:
            List of matching LogEntry objects
        """
        if not min_level:
            indices = self._index_by_level.get(level, set())
            return [self._entries[i] for i in sorted(indices)]

        # Get all levels at or above the specified level
        try:
            level_idx = self.LOG_LEVELS.index(level)
            target_levels = self.LOG_LEVELS[level_idx:]
        except ValueError:
            return []

        all_indices: set[int] = set()
        for lvl in target_levels:
            all_indices.update(self._index_by_level.get(lvl, set()))

        return [self._entries[i] for i in sorted(all_indices)]

    def filter_by_time_range(
        self,
        start_time: datetime | None = None,
        end_time: datetime | None = None,
    ) -> list[LogEntry]:
        """Filter logs by time range.

        Args:
            start_time: Start of time range (inclusive)
            end_time: End of time range (inclusive)

        Returns:
            List of LogEntry objects within the time range
        """
        if not self._index_by_time:
            return []

        # Binary search for start index
        start_idx = 0
        end_idx = len(self._index_by_time)

        if start_time:
            left, right = 0, len(self._index_by_time)
            while left < right:
                mid = (left + right) // 2
                if self._index_by_time[mid][0] < start_time:
                    left = mid + 1
                else:
                    right = mid
            start_idx = left

        if end_time:
            left, right = 0, len(self._index_by_time)
            while left < right:
                mid = (left + right) // 2
                if self._index_by_time[mid][0] <= end_time:
                    left = mid + 1
                else:
                    right = mid
            end_idx = left

        return [self._entries[i] for _, i in self._index_by_time[start_idx:end_idx]]

    def get_error_stats(self) -> dict[str, Any]:
        """Get error statistics from logs.

        Returns:
            Dictionary with error statistics
        """
        error_entries = self.filter_by_level("ERROR")
        critical_entries = self.filter_by_level("CRITICAL")
        warning_entries = self.filter_by_level("WARNING")

        all_errors = error_entries + critical_entries

        # Group by error pattern (module:function)
        pattern_counts: dict[str, ErrorPattern] = {}

        for entry in all_errors:
            pattern_key = f"{entry.module}:{entry.function}"

            if pattern_key not in pattern_counts:
                pattern_counts[pattern_key] = ErrorPattern(
                    pattern=f"{entry.module}:{entry.function}"
                )

            pattern = pattern_counts[pattern_key]
            pattern.count += 1

            if len(pattern.sample_messages) < 5:
                pattern.sample_messages.append(entry.message)

            if entry.agent_id:
                pattern.affected_agents.add(entry.agent_id)

            if pattern.first_occurrence is None or entry.timestamp < pattern.first_occurrence:
                pattern.first_occurrence = entry.timestamp
            if pattern.last_occurrence is None or entry.timestamp > pattern.last_occurrence:
                pattern.last_occurrence = entry.timestamp

        # Sort by count descending
        sorted_patterns = sorted(
            pattern_counts.values(),
            key=lambda p: p.count,
            reverse=True,
        )

        return {
            "total_errors": len(error_entries),
            "total_critical": len(critical_entries),
            "total_warnings": len(warning_entries),
            "unique_patterns": len(sorted_patterns),
            "top_patterns": [p.to_dict() for p in sorted_patterns[:10]],
            "affected_agents": list(
                set.union(
                    *[p.affected_agents for p in sorted_patterns]
                ) if sorted_patterns else set()
            ),
        }

    def get_trade_audit_trail(self, trade_id: str) -> list[LogEntry]:
        """Get complete audit trail for a trade.

        Args:
            trade_id: Trade identifier

        Returns:
            Chronologically sorted list of LogEntry objects for the trade
        """
        indices = self._index_by_trade.get(trade_id, set())
        entries = [self._entries[i] for i in indices]
        return sorted(entries, key=lambda e: e.timestamp)

    def get_agent_activity(
        self,
        agent_id: str,
        start_time: datetime | None = None,
        end_time: datetime | None = None,
    ) -> dict[str, Any]:
        """Get activity summary for an agent.

        Args:
            agent_id: Agent identifier
            start_time: Optional start time filter
            end_time: Optional end time filter

        Returns:
            Dictionary with agent activity summary
        """
        entries = self.filter_by_agent(agent_id)

        # Apply time filters
        if start_time:
            entries = [e for e in entries if e.timestamp >= start_time]
        if end_time:
            entries = [e for e in entries if e.timestamp <= end_time]

        if not entries:
            return {
                "agent_id": agent_id,
                "total_entries": 0,
                "level_counts": {},
                "time_range": None,
            }

        level_counts: dict[str, int] = {}
        for entry in entries:
            level_counts[entry.level] = level_counts.get(entry.level, 0) + 1

        timestamps = [e.timestamp for e in entries]

        return {
            "agent_id": agent_id,
            "total_entries": len(entries),
            "level_counts": level_counts,
            "time_range": {
                "start": min(timestamps).isoformat(),
                "end": max(timestamps).isoformat(),
            },
            "error_count": level_counts.get("ERROR", 0) + level_counts.get("CRITICAL", 0),
        }

    def generate_log_report(
        self,
        start_time: datetime | None = None,
        end_time: datetime | None = None,
    ) -> LogReport:
        """Generate a comprehensive log analysis report.

        Args:
            start_time: Optional start time for report period
            end_time: Optional end time for report period

        Returns:
            LogReport object
        """
        # Get entries in time range
        if start_time or end_time:
            entries = self.filter_by_time_range(start_time, end_time)
        else:
            entries = self._entries.copy()

        if not entries:
            return LogReport(
                start_time=start_time or datetime.now(),
                end_time=end_time or datetime.now(),
                total_entries=0,
                level_counts={},
                agent_counts={},
                error_patterns=[],
                summary="No log entries found for the specified period.",
            )

        # Count by level
        level_counts: dict[str, int] = {}
        for entry in entries:
            level_counts[entry.level] = level_counts.get(entry.level, 0) + 1

        # Count by agent
        agent_counts: dict[str, int] = {}
        for entry in entries:
            if entry.agent_id:
                agent_counts[entry.agent_id] = agent_counts.get(entry.agent_id, 0) + 1

        # Detect error patterns
        error_stats = self.get_error_stats()
        error_patterns = [
            ErrorPattern(
                pattern=p["pattern"],
                count=p["count"],
                sample_messages=p["sample_messages"],
                affected_agents=set(p["affected_agents"]),
            )
            for p in error_stats.get("top_patterns", [])
        ]

        # Calculate time range from entries
        timestamps = [e.timestamp for e in entries]
        actual_start = min(timestamps)
        actual_end = max(timestamps)

        # Generate summary
        total_errors = level_counts.get("ERROR", 0) + level_counts.get("CRITICAL", 0)
        total_warnings = level_counts.get("WARNING", 0)

        if total_errors > 0:
            summary = f"Alert: {total_errors} error(s) detected across {len(agent_counts)} agent(s)."
        elif total_warnings > 0:
            summary = f"Warning: {total_warnings} warning(s) logged. {len(entries)} total entries."
        else:
            summary = f"Healthy: {len(entries)} log entries processed without errors."

        return LogReport(
            start_time=start_time or actual_start,
            end_time=end_time or actual_end,
            total_entries=len(entries),
            level_counts=level_counts,
            agent_counts=agent_counts,
            error_patterns=error_patterns,
            summary=summary,
        )

    def export_to_csv(
        self,
        filepath: str | Path,
        entries: list[LogEntry] | None = None,
    ) -> None:
        """Export logs to CSV file.

        Args:
            filepath: Output file path
            entries: Optional list of entries to export (default: all)
        """
        entries = entries or self._entries
        path = Path(filepath)
        path.parent.mkdir(parents=True, exist_ok=True)

        with open(path, "w", newline="", encoding="utf-8") as f:
            writer = csv.writer(f)
            writer.writerow([
                "timestamp", "level", "message", "module",
                "function", "line", "agent_id", "trade_id", "extra"
            ])

            for entry in entries:
                writer.writerow([
                    entry.timestamp.isoformat(),
                    entry.level,
                    entry.message,
                    entry.module,
                    entry.function,
                    entry.line,
                    entry.agent_id or "",
                    entry.trade_id or "",
                    json.dumps(entry.extra) if entry.extra else "",
                ])

    def export_to_json(
        self,
        filepath: str | Path,
        entries: list[LogEntry] | None = None,
    ) -> None:
        """Export logs to JSON file.

        Args:
            filepath: Output file path
            entries: Optional list of entries to export (default: all)
        """
        entries = entries or self._entries
        path = Path(filepath)
        path.parent.mkdir(parents=True, exist_ok=True)

        data = [entry.to_dict() for entry in entries]
        path.write_text(
            json.dumps(data, indent=2, ensure_ascii=False, default=str),
            encoding="utf-8"
        )

    def get_unique_agents(self) -> list[str]:
        """Get list of unique agent IDs from logs.

        Returns:
            Sorted list of agent IDs
        """
        return sorted(self._index_by_agent.keys())

    def get_unique_trades(self) -> list[str]:
        """Get list of unique trade IDs from logs.

        Returns:
            Sorted list of trade IDs
        """
        return sorted(self._index_by_trade.keys())

    def clear(self) -> None:
        """Clear all loaded logs and indexes."""
        self._entries.clear()
        self._index_by_level.clear()
        self._index_by_agent.clear()
        self._index_by_trade.clear()
        self._index_by_time.clear()

    @property
    def entry_count(self) -> int:
        """Return the number of loaded log entries."""
        return len(self._entries)

    @property
    def time_range(self) -> tuple[datetime, datetime] | None:
        """Return the time range of loaded logs."""
        if not self._index_by_time:
            return None
        return (self._index_by_time[0][0], self._index_by_time[-1][0])
