"""Log/build output compressor for test and compiler output.

This module compresses build and test output which can be 10,000+ lines
with only 5-10 actual errors. Typical compression: 10-50x.

Supported formats:
- pytest output
- npm/yarn output
- cargo/rustc output
- make/gcc output
- generic log format (ERROR, WARN, INFO)

Compression Strategy:
1. Detect log format (pytest, npm, cargo, etc.)
2. Extract all ERROR/FAIL lines with context
3. Extract first stack trace completely
4. Deduplicate repeated warnings
5. Summarize: [247 INFO lines, 12 WARN lines omitted]

Key Patterns to Preserve:
- First error (often root cause)
- Last error (sometimes the real failure)
- Stack traces
- Exit codes
- Test summary lines
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from enum import Enum


class LogFormat(Enum):
    """Detected log format."""

    PYTEST = "pytest"
    NPM = "npm"
    CARGO = "cargo"
    MAKE = "make"
    JEST = "jest"
    GENERIC = "generic"


class LogLevel(Enum):
    """Log level for categorization."""

    ERROR = "error"
    FAIL = "fail"
    WARN = "warn"
    INFO = "info"
    DEBUG = "debug"
    TRACE = "trace"
    UNKNOWN = "unknown"


@dataclass(eq=False)
class LogLine:
    """A single log line with metadata."""

    line_number: int
    content: str
    level: LogLevel = LogLevel.UNKNOWN
    is_stack_trace: bool = False
    is_summary: bool = False
    score: float = 0.0

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, LogLine):
            return NotImplemented
        return self.line_number == other.line_number

    def __hash__(self) -> int:
        return hash(self.line_number)


@dataclass
class LogCompressorConfig:
    """Configuration for log compression."""

    # Error handling
    max_errors: int = 10
    error_context_lines: int = 3
    keep_first_error: bool = True
    keep_last_error: bool = True

    # Stack trace handling
    max_stack_traces: int = 3
    stack_trace_max_lines: int = 20

    # Warning handling
    max_warnings: int = 5
    dedupe_warnings: bool = True

    # Summary handling
    keep_summary_lines: bool = True

    # Global limits
    max_total_lines: int = 100

    # CCR integration
    enable_ccr: bool = True
    min_lines_for_ccr: int = 50


class LogCompressor:
    """Compresses build/test log output.

    Example:
        >>> compressor = LogCompressor()
        >>> result = compressor.compress(pytest_output)
        >>> print(result.compressed)  # Just errors + summary
    """

    # Format detection patterns
    _FORMAT_PATTERNS = {
        LogFormat.PYTEST: [
            re.compile(r"^={3,} (FAILURES|ERRORS|test session|short test summary)"),
            re.compile(r"^(PASSED|FAILED|ERROR|SKIPPED)\s+\["),
            re.compile(r"^collected \d+ items?"),
        ],
        LogFormat.NPM: [
            re.compile(r"^npm (ERR!|WARN|info|http)"),
            re.compile(r"^(>|added|removed) .+ packages?"),
        ],
        LogFormat.CARGO: [
            re.compile(r"^\s*(Compiling|Finished|Running|error\[E\d+\])"),
            re.compile(r"^warning: .+"),
        ],
        LogFormat.JEST: [
            re.compile(r"^(PASS|FAIL)\s+.+\.test\.(js|ts)"),
            re.compile(r"^Test Suites:"),
        ],
        LogFormat.MAKE: [
            re.compile(r"^make(\[\d+\])?: "),
            re.compile(r"^(gcc|g\+\+|clang).*-o "),
        ],
    }

    # Level detection patterns
    _LEVEL_PATTERNS = {
        LogLevel.ERROR: re.compile(r"\b(ERROR|error|Error|FATAL|fatal|Fatal|CRITICAL|critical)\b"),
        LogLevel.FAIL: re.compile(r"\b(FAIL|FAILED|fail|failed|Fail|Failed)\b"),
        LogLevel.WARN: re.compile(r"\b(WARN|WARNING|warn|warning|Warn|Warning)\b"),
        LogLevel.INFO: re.compile(r"\b(INFO|info|Info)\b"),
        LogLevel.DEBUG: re.compile(r"\b(DEBUG|debug|Debug)\b"),
        LogLevel.TRACE: re.compile(r"\b(TRACE|trace|Trace)\b"),
    }

    # Stack trace patterns
    _STACK_TRACE_PATTERNS = [
        re.compile(r"^\s*Traceback \(most recent call last\)"),
        re.compile(r'^\s*File ".+", line \d+'),
        re.compile(r"^\s*at .+\(.+:\d+:\d+\)"),  # JS stack trace
        re.compile(r"^\s+at [\w.$]+\("),  # Java stack trace
        re.compile(r"^\s*--> .+:\d+:\d+"),  # Rust error
        re.compile(r"^\s*\d+:\s+0x[0-9a-f]+"),  # Go stack trace
    ]

    # Summary line patterns
    _SUMMARY_PATTERNS = [
        re.compile(r"^={3,}"),  # pytest separators
        re.compile(r"^-{3,}"),
        re.compile(r"^\d+ (passed|failed|skipped|error|warning)"),
        re.compile(r"^(Tests?|Suites?):?\s+\d+"),
        re.compile(r"^(TOTAL|Total|Summary)"),
        re.compile(r"^(Build|Compile|Test).*(succeeded|failed|complete)"),
    ]

    def __init__(self, config: LogCompressorConfig | None = None):
        """Initialize log compressor.

        Args:
            config: Compression configuration.
        """
        self.config = config or LogCompressorConfig()

    def compress(self, content: str, context: str = "") -> LogCompressionResult:
        """Compress log output.

        Args:
            content: Raw log output.
            context: User query context (unused for now).

        Returns:
            LogCompressionResult with compressed output and metadata.
        """
        lines = content.split("\n")

        if len(lines) < self.config.min_lines_for_ccr:
            return LogCompressionResult(
                compressed=content,
                original=content,
                original_line_count=len(lines),
                compressed_line_count=len(lines),
                format_detected=LogFormat.GENERIC,
                compression_ratio=1.0,
            )

        # Detect format
        log_format = self._detect_format(lines)

        # Parse and categorize lines
        log_lines = self._parse_lines(lines)

        # Select important lines
        selected = self._select_lines(log_lines)

        # Format output with summaries
        compressed, stats = self._format_output(selected, log_lines)

        ratio = len(compressed) / max(len(content), 1)

        # Store in CCR if significant compression
        cache_key = None
        if self.config.enable_ccr and ratio < 0.5:
            cache_key = self._store_in_ccr(content, compressed, len(lines))
            if cache_key:
                compressed += f"\n[{len(lines)} lines compressed. hash={cache_key}]"

        return LogCompressionResult(
            compressed=compressed,
            original=content,
            original_line_count=len(lines),
            compressed_line_count=len(selected),
            format_detected=log_format,
            compression_ratio=ratio,
            cache_key=cache_key,
            stats=stats,
        )

    def _detect_format(self, lines: list[str]) -> LogFormat:
        """Detect the log format."""
        sample = lines[:100]  # Check first 100 lines

        format_scores: dict[LogFormat, int] = {}
        for log_format, patterns in self._FORMAT_PATTERNS.items():
            score = 0
            for line in sample:
                for pattern in patterns:
                    if pattern.search(line):
                        score += 1
                        break
            if score > 0:
                format_scores[log_format] = score

        if not format_scores:
            return LogFormat.GENERIC

        return max(format_scores, key=lambda k: format_scores[k])

    def _parse_lines(self, lines: list[str]) -> list[LogLine]:
        """Parse lines and categorize by level."""
        log_lines: list[LogLine] = []
        in_stack_trace = False
        stack_trace_lines = 0

        for i, line in enumerate(lines):
            log_line = LogLine(line_number=i, content=line)

            # Detect level
            for level, pattern in self._LEVEL_PATTERNS.items():
                if pattern.search(line):
                    log_line.level = level
                    break

            # Detect stack trace
            for pattern in self._STACK_TRACE_PATTERNS:
                if pattern.search(line):
                    in_stack_trace = True
                    stack_trace_lines = 0
                    break

            if in_stack_trace:
                log_line.is_stack_trace = True
                stack_trace_lines += 1
                # End stack trace after max lines or empty line
                if stack_trace_lines > self.config.stack_trace_max_lines or not line.strip():
                    in_stack_trace = False

            # Detect summary lines
            for pattern in self._SUMMARY_PATTERNS:
                if pattern.search(line):
                    log_line.is_summary = True
                    break

            # Score line by importance
            log_line.score = self._score_line(log_line)

            log_lines.append(log_line)

        return log_lines

    def _score_line(self, log_line: LogLine) -> float:
        """Score a line by importance."""
        score = 0.0

        # Level-based scoring
        level_scores = {
            LogLevel.ERROR: 1.0,
            LogLevel.FAIL: 1.0,
            LogLevel.WARN: 0.5,
            LogLevel.INFO: 0.1,
            LogLevel.DEBUG: 0.05,
            LogLevel.TRACE: 0.02,
            LogLevel.UNKNOWN: 0.1,
        }
        score += level_scores.get(log_line.level, 0.1)

        # Boost stack traces
        if log_line.is_stack_trace:
            score += 0.3

        # Boost summary lines
        if log_line.is_summary:
            score += 0.4

        return min(1.0, score)

    def _select_lines(self, log_lines: list[LogLine]) -> list[LogLine]:
        """Select important lines to keep."""
        selected: list[LogLine] = []

        # Group by category
        errors: list[LogLine] = []
        fails: list[LogLine] = []
        warnings: list[LogLine] = []
        stack_traces: list[list[LogLine]] = []
        summaries: list[LogLine] = []
        current_stack: list[LogLine] = []

        for log_line in log_lines:
            if log_line.level == LogLevel.ERROR:
                errors.append(log_line)
            elif log_line.level == LogLevel.FAIL:
                fails.append(log_line)
            elif log_line.level == LogLevel.WARN:
                warnings.append(log_line)

            if log_line.is_stack_trace:
                current_stack.append(log_line)
            elif current_stack:
                stack_traces.append(current_stack)
                current_stack = []

            if log_line.is_summary:
                summaries.append(log_line)

        if current_stack:
            stack_traces.append(current_stack)

        # Select errors (first, last, highest scoring)
        if errors:
            selected_errors = self._select_with_first_last(errors, self.config.max_errors)
            selected.extend(selected_errors)

        # Select fails
        if fails:
            selected_fails = self._select_with_first_last(fails, self.config.max_errors)
            selected.extend(selected_fails)

        # Select warnings (dedupe if configured)
        if warnings:
            if self.config.dedupe_warnings:
                warnings = self._dedupe_similar(warnings)
            selected.extend(warnings[: self.config.max_warnings])

        # Select stack traces
        for stack in stack_traces[: self.config.max_stack_traces]:
            selected.extend(stack[: self.config.stack_trace_max_lines])

        # Always include summary lines
        if self.config.keep_summary_lines:
            selected.extend(summaries)

        # Add context lines around errors
        selected = self._add_context(log_lines, selected)

        # Sort by line number and dedupe
        selected = sorted(set(selected), key=lambda x: x.line_number)

        # Limit total lines
        if len(selected) > self.config.max_total_lines:
            # Keep most important lines
            selected = sorted(selected, key=lambda x: x.score, reverse=True)
            selected = selected[: self.config.max_total_lines]
            selected = sorted(selected, key=lambda x: x.line_number)

        return selected

    def _select_with_first_last(self, lines: list[LogLine], max_count: int) -> list[LogLine]:
        """Select lines keeping first and last."""
        if len(lines) <= max_count:
            return lines

        selected: list[LogLine] = []

        if self.config.keep_first_error and lines:
            selected.append(lines[0])

        if self.config.keep_last_error and lines and lines[-1] not in selected:
            selected.append(lines[-1])

        # Fill remaining with highest scoring
        remaining = max_count - len(selected)
        if remaining > 0:
            candidates = [line for line in lines if line not in selected]
            candidates = sorted(candidates, key=lambda x: x.score, reverse=True)
            selected.extend(candidates[:remaining])

        return selected

    def _dedupe_similar(self, lines: list[LogLine]) -> list[LogLine]:
        """Remove duplicate/similar lines."""
        seen_patterns: set[str] = set()
        deduped: list[LogLine] = []

        for line in lines:
            # Normalize: remove numbers, paths for comparison
            normalized = re.sub(r"\d+", "N", line.content)
            normalized = re.sub(r"/[\w/]+/", "/PATH/", normalized)
            normalized = re.sub(r"0x[0-9a-f]+", "ADDR", normalized)

            if normalized not in seen_patterns:
                seen_patterns.add(normalized)
                deduped.append(line)

        return deduped

    def _add_context(self, all_lines: list[LogLine], selected: list[LogLine]) -> list[LogLine]:
        """Add context lines around selected lines."""
        selected_indices = {line.line_number for line in selected}
        context_indices: set[int] = set()

        for idx in selected_indices:
            # Add lines before
            for i in range(max(0, idx - self.config.error_context_lines), idx):
                context_indices.add(i)
            # Add lines after
            for i in range(
                idx + 1,
                min(len(all_lines), idx + self.config.error_context_lines + 1),
            ):
                context_indices.add(i)

        # Add context lines to selected
        for idx in context_indices:
            if idx not in selected_indices and idx < len(all_lines):
                selected.append(all_lines[idx])

        return selected

    def _format_output(
        self, selected: list[LogLine], all_lines: list[LogLine]
    ) -> tuple[str, dict[str, int]]:
        """Format selected lines with summary stats."""
        # Count categories
        stats: dict[str, int] = {
            "errors": sum(1 for line in all_lines if line.level == LogLevel.ERROR),
            "fails": sum(1 for line in all_lines if line.level == LogLevel.FAIL),
            "warnings": sum(1 for line in all_lines if line.level == LogLevel.WARN),
            "info": sum(1 for line in all_lines if line.level == LogLevel.INFO),
            "total": len(all_lines),
            "selected": len(selected),
        }

        # Build output
        output_lines = [line.content for line in selected]

        # Add summary of omitted lines
        omitted = len(all_lines) - len(selected)
        if omitted > 0:
            summary_parts = []
            for level_name, count in [
                ("ERROR", stats["errors"]),
                ("FAIL", stats["fails"]),
                ("WARN", stats["warnings"]),
                ("INFO", stats["info"]),
            ]:
                if count > 0:
                    summary_parts.append(f"{count} {level_name}")

            if summary_parts:
                summary = f"[{omitted} lines omitted: {', '.join(summary_parts)}]"
                output_lines.append(summary)

        return "\n".join(output_lines), stats

    def _store_in_ccr(self, original: str, compressed: str, original_count: int) -> str | None:
        """Store original in CCR for later retrieval."""
        try:
            from ..cache.compression_store import get_compression_store

            store = get_compression_store()
            return store.store(
                original,
                compressed,
                original_item_count=original_count,
            )
        except ImportError:
            return None
        except Exception:
            return None


@dataclass
class LogCompressionResult:
    """Result of log compression."""

    compressed: str
    original: str
    original_line_count: int
    compressed_line_count: int
    format_detected: LogFormat
    compression_ratio: float
    cache_key: str | None = None
    stats: dict[str, int] = field(default_factory=dict)

    @property
    def tokens_saved_estimate(self) -> int:
        """Estimate tokens saved (rough: 1 token per 4 chars)."""
        chars_saved = len(self.original) - len(self.compressed)
        return max(0, chars_saved // 4)

    @property
    def lines_omitted(self) -> int:
        """Number of lines omitted."""
        return self.original_line_count - self.compressed_line_count
