###############################################################################
# (c) Copyright 2020-2026 CERN for the benefit of the LHCb Collaboration      #
#                                                                             #
# This software is distributed under the terms of the GNU General Public      #
# Licence version 3 (GPL Version 3), copied verbatim in the file "COPYING".   #
#                                                                             #
# In applying this licence, CERN does not waive the privileges and immunities #
# granted to it by virtue of its status as an Intergovernmental Organization  #
# or submit itself to any jurisdiction.                                       #
###############################################################################
from __future__ import annotations

import re
from collections import Counter
from dataclasses import dataclass, field

import click
from LbAPCommon.validators import explain_log
from LbAPCommon.validators.logs import _split_by_level

# ISO 8601 timestamps (e.g. "2026-02-25T14:35:22.183Z") are not recognised by
# LbAPCommon's MESSAGE_RE which expects "YYYY-MM-DD HH:MM:SS TZ".  Normalise
# them so _split_by_level can match the lines.
_ISO_TS_RE = re.compile(
    r"^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})\.\d+Z ", re.MULTILINE
)

# ---------------------------------------------------------------------------
# Structured data models
# ---------------------------------------------------------------------------


@dataclass
class LogMessageSummary:
    """Summary of log messages at a single severity level."""

    level: str
    total_count: int
    messages: list[tuple[str, int]] = field(default_factory=list)
    suppressed_count: int = 0
    suppressed_unique: int = 0


@dataclass
class LogAdvice:
    """A category of advice items (errors, suggestions, or explanations)."""

    heading: str
    severity: str  # "error", "warning", "info"
    items: list[tuple[str, list[int] | None]] = field(default_factory=list)


@dataclass
class LogAnalysis:
    """Complete analysis result for a Gaudi log file."""

    summaries: list[LogMessageSummary] = field(default_factory=list)
    errors: LogAdvice | None = None
    suggestions: LogAdvice | None = None
    explanations: LogAdvice | None = None
    has_fatal_issues: bool = False
    repetitive_messages: list[tuple[str, str, int]] = field(default_factory=list)
    """Messages repeated excessively: (level, message, count)."""


# ---------------------------------------------------------------------------
# Pure analysis function (no side effects)
# ---------------------------------------------------------------------------


def _normalise_timestamps(log_text: str) -> str:
    """Convert ISO 8601 timestamps to the format LbAPCommon expects."""
    return _ISO_TS_RE.sub(r"\1 \2 UTC ", log_text)


def analyse_log(log_text: str, suppress: int = 5) -> LogAnalysis:
    """Analyse a Gaudi log and return structured results.

    Args:
        log_text: Full text content of the log file.
        suppress: Minimum instance count to show WARNING/ERROR messages.
            Messages appearing fewer times are counted but not listed.

    Returns:
        A :class:`LogAnalysis` with message summaries and advice.
    """
    log_text = _normalise_timestamps(log_text)
    log_messages = _split_by_level(log_text)

    # Build message summaries for FATAL, ERROR, WARNING
    summaries: list[LogMessageSummary] = []
    for level, min_count in [("FATAL", 0), ("ERROR", 0), ("WARNING", 0)]:
        entries = log_messages[level]
        if not entries:
            continue
        counted = sorted(
            Counter(message for _, _, _, _, message in entries).items(),
            key=lambda x: x[1],
            reverse=True,
        )
        shown = [(msg, cnt) for msg, cnt in counted if cnt >= min_count]
        suppressed = [(msg, cnt) for msg, cnt in counted if cnt < min_count]
        summaries.append(
            LogMessageSummary(
                level=level,
                total_count=len(entries),
                messages=shown,
                suppressed_count=sum(cnt for _, cnt in suppressed),
                suppressed_unique=len(suppressed),
            )
        )

    # Get explanations, suggestions, errors from LbAPCommon
    raw_explanations, raw_suggestions, raw_errors = explain_log(log_text)

    errors = (
        LogAdvice(heading="Errors detected", severity="error", items=raw_errors)
        if raw_errors
        else None
    )
    suggestions = (
        LogAdvice(
            heading="Advice about warnings and errors",
            severity="warning",
            items=raw_suggestions,
        )
        if raw_suggestions
        else None
    )
    explanations = (
        LogAdvice(
            heading="General explanations", severity="info", items=raw_explanations
        )
        if raw_explanations
        else None
    )

    # Detect repetitive messages (any level, threshold: 10 occurrences)
    repetitive: list[tuple[str, str, int]] = []
    repetitive_threshold = 10
    for level in ("FATAL", "ERROR", "WARNING"):
        entries = log_messages[level]
        if not entries:
            continue
        counted = Counter(message for _, _, _, _, message in entries)
        for msg, cnt in counted.most_common():
            if cnt >= repetitive_threshold:
                repetitive.append((level, msg, cnt))

    has_fatal = bool(log_messages["FATAL"]) or bool(raw_errors)

    return LogAnalysis(
        summaries=summaries,
        errors=errors,
        suggestions=suggestions,
        explanations=explanations,
        has_fatal_issues=has_fatal,
        repetitive_messages=repetitive,
    )


# ---------------------------------------------------------------------------
# Rich rendering
# ---------------------------------------------------------------------------


def render_log_analysis(analysis: LogAnalysis, console) -> None:
    """Render a LogAnalysis using Rich panels and tables.

    Args:
        analysis: The structured analysis to render.
        console: A ``rich.console.Console`` instance.
    """
    from rich.markdown import Markdown
    from rich.panel import Panel
    from rich.text import Text

    if not analysis.summaries and not any(
        [analysis.errors, analysis.suggestions, analysis.explanations]
    ):
        return

    # Message summary — one panel per severity level
    if analysis.summaries:
        level_styles = {"FATAL": "red", "ERROR": "dark_orange", "WARNING": "yellow"}

        for summary in analysis.summaries:
            style = level_styles.get(summary.level, "white")
            body = Text()
            for i, (msg, count) in enumerate(summary.messages[:10]):
                if i:
                    body.append("\n")
                body.append(f"  {count}x  ", style="bold")
                body.append(msg)
            if summary.suppressed_count:
                if body:
                    body.append("\n")
                body.append(
                    f"  … +{summary.suppressed_count} more "
                    f"({summary.suppressed_unique} unique)",
                    style="dim",
                )
            console.print(
                Panel(
                    body,
                    title=f"{summary.level} ({summary.total_count})",
                    title_align="left",
                    border_style=style,
                    padding=(0, 1),
                )
            )

    # Advice panels
    for advice in [analysis.errors, analysis.suggestions, analysis.explanations]:
        if advice is None:
            continue

        border_style = {
            "error": "red",
            "warning": "yellow",
            "info": "green",
        }.get(advice.severity, "white")
        title_style = {
            "error": "bold red",
            "warning": "bold yellow",
            "info": "bold green",
        }.get(advice.severity, "bold")

        parts: list = []
        for message_text, lines in advice.items:
            if lines is None:
                line_info = "[dim]No line information available[/dim]"
            elif len(lines) <= 5:
                line_info = f"[cyan]Line{'s' if len(lines) > 1 else ''}: {', '.join(map(str, lines))}[/cyan]"
            else:
                shown = ", ".join(map(str, lines[:5]))
                line_info = f"[cyan]Lines: {shown} and {len(lines) - 5} other(s)[/cyan]"
            parts.append(line_info)
            parts.append(Markdown(message_text.strip()))
            parts.append("")  # spacing

        from rich.console import Group

        console.print(
            Panel(
                Group(*parts),
                title=f"[{title_style}]{advice.heading}[/{title_style}]",
                border_style=border_style,
            )
        )

    # Repetitive message warning
    if analysis.repetitive_messages:
        from rich.console import Group
        from rich.panel import Panel

        lines = []
        for level, msg, cnt in analysis.repetitive_messages:
            lines.append(
                f"  [bold]{level}[/bold] ({cnt}x): [dim]{msg[:80]}{'...' if len(msg) > 80 else ''}[/dim]"
            )
        lines.append(
            "\n[yellow]Repetitive log messages can create huge log files "
            "and indicate a configuration issue.[/yellow]"
        )
        console.print(
            Panel(
                "\n".join(lines),
                title="[bold yellow]Repetitive messages detected[/bold yellow]",
                border_style="yellow",
            )
        )


# ---------------------------------------------------------------------------
# Legacy compatibility shim (click-based output for lb-ap-legacy)
# ---------------------------------------------------------------------------


def show_log_advice(log_text, suppress=5):
    """Analyse a log and display advice using click output.

    This is a backward-compatible wrapper around :func:`analyse_log` that
    renders results via ``click.echo``/``click.secho``.  It is used by the
    legacy CLI (``lb-ap-legacy parse-log``) and preserves its exact behaviour:
    raises :class:`click.ClickException` when fatal issues are found.
    """
    analysis = analyse_log(log_text, suppress=suppress)

    # Message summaries
    level_formatting = {
        "FATAL": {"fg": "red", "bold": True},
        "ERROR": {"fg": "red"},
        "WARNING": {"fg": "yellow"},
    }
    for summary in analysis.summaries:
        fmt = level_formatting.get(summary.level, {})
        click.secho(
            f"    Found {summary.total_count} {summary.level} message(s)", **fmt
        )
        for message, count in summary.messages:
            click.echo(f'        * {count} instances of "{message}"')
        if summary.suppressed_count:
            click.echo(
                f"        and {summary.suppressed_count} other(s) "
                f"({summary.suppressed_unique} unique). "
                'Pass "--suppress=0" to show all messages'
            )
    click.echo()

    # Advice sections
    for advice, label, fg in [
        (analysis.errors, "Errors have been detected!", "red"),
        (analysis.suggestions, "Advice about warnings and errors:", "yellow"),
        (analysis.explanations, "General explanations", "green"),
    ]:
        if advice is None:
            continue
        click.secho(label, fg=fg, bold=(fg == "red"))
        for message_text, lines in advice.items:
            if lines is None:
                heading = "  * No line information available"
            else:
                heading = "  * Line" + ("s" if len(lines) > 1 else "") + ":"
                heading += f" {', '.join(map(str, lines[:5]))}"
                if len(lines) > 5:
                    heading += f" and {len(lines) - 5} other(s)"
            click.secho(heading, fg="cyan")
            # Plain-text rendering of the markdown advice
            click.echo("    " + message_text.strip().replace("\n", "\n    "))

    if analysis.has_fatal_issues:
        raise click.ClickException("Found issues in log")
