"""
Console reporter — print profiling results to terminal.

Usage:
    from sondeo import report

    report()                   # Print all collected results as text
    report(format="json")      # Output as JSON
    report(format="markdown")  # Output as Markdown (CI-friendly)
"""

from __future__ import annotations

import json
from collections.abc import Iterable, Mapping
from typing import Any, Literal

from sondeo.profiler import logging as logging_mod
from sondeo.profiler import memory as memory_mod
from sondeo.profiler import timer as timer_mod
from sondeo.profiler import trace as trace_mod

ReportFormat = Literal["text", "json", "markdown"]


def _coerce_str_iter(values: Iterable[str] | None) -> tuple[str, ...]:
    if values is None:
        return ()
    if isinstance(values, str):
        return (values,)
    return tuple(values)


def _call_depth(node: trace_mod.CallNode) -> int:
    if not node.children:
        return 1
    return 1 + max(_call_depth(child) for child in node.children)


def _format_timeline(events: list[logging_mod.TimelineEvent], *, color: bool) -> list[str]:
    if not events:
        return []

    lines: list[str] = []
    start_ts = events[0].timestamp
    for event in events:
        delta_ms = (event.timestamp - start_ts) * 1000.0
        level = logging_mod.colorize_level(event.level, enabled=color)
        cid = f" cid={event.correlation_id}" if event.correlation_id else ""
        lines.append(
            f"  +{delta_ms:>8.2f}ms [{level}] [{event.event_type:<7}] "
            f"{event.message} ({event.module}{cid})"
        )

    return lines


def _format_trace_nodes(
    nodes: list[dict[str, Any]],
    *,
    indent: int = 0,
    min_ms: float = 0.1,
) -> list[str]:
    lines: list[str] = []
    prefix = "  " * indent
    for node in nodes:
        elapsed_ms = float(node.get("elapsed_ms", 0.0))
        if elapsed_ms < min_ms:
            continue
        name = str(node.get("name", "<unknown>"))
        file_name = str(node.get("file", ""))
        line_no = int(node.get("line", 0))
        lines.append(f"{prefix}{name} ({elapsed_ms:.2f}ms) [{file_name}:{line_no}]")
        children = node.get("children", [])
        if isinstance(children, list):
            lines.extend(_format_trace_nodes(children, indent=indent + 1, min_ms=min_ms))
    return lines


def collect_report_data(
    *,
    include_modules: Iterable[str] | None = None,
    exclude_modules: Iterable[str] | None = None,
    levels: Iterable[str] | None = None,
    pattern: str | None = None,
    show_timeline: bool = True,
    show_traces: bool = True,
) -> dict[str, Any]:
    """Collect profiling data from all profiler backends."""
    timings = timer_mod.get_results()
    memory = memory_mod.get_results()
    traces = trace_mod.get_results() if show_traces else []
    async_task_stats = timer_mod.get_async_task_stats()
    timeline = (
        logging_mod.get_events(
            include_modules=_coerce_str_iter(include_modules),
            exclude_modules=_coerce_str_iter(exclude_modules),
            levels=_coerce_str_iter(levels) or None,
            pattern=pattern,
        )
        if show_timeline
        else []
    )
    trace_hints = [hint for trace_result in traces for hint in trace_result.contention_hints]
    timing_hints = timer_mod.get_concurrency_hints()
    hints: list[str] = []
    for hint in [*trace_hints, *timing_hints]:
        if hint not in hints:
            hints.append(hint)

    slowest = max(timings, key=lambda result: result.elapsed_ms, default=None)
    peak_memory = max((result.peak_mb for result in memory), default=0.0)
    deepest_stack = max(
        (_call_depth(child) for trace in traces for child in trace.root.children),
        default=0,
    )
    thread_count = len({result.thread_id for result in timings if result.thread_id is not None})

    summary: dict[str, Any] = {
        "total_time_ms": round(sum(result.elapsed_ms for result in timings), 3),
        "peak_memory_mb": round(peak_memory, 3),
        "deepest_call_stack": deepest_stack,
        "timing_count": len(timings),
        "memory_count": len(memory),
        "trace_count": len(traces),
        "timeline_count": len(timeline),
        "thread_count": thread_count,
        "async_task_count": len(async_task_stats),
        "hint_count": len(hints),
        "slowest_function": None,
    }

    if slowest is not None:
        summary["slowest_function"] = {
            "name": slowest.name,
            "elapsed_ms": round(slowest.elapsed_ms, 3),
            "calls": slowest.calls,
            "correlation_id": slowest.correlation_id,
        }

    return {
        "summary": summary,
        "timings": [
            {
                "name": result.name,
                "elapsed_ms": round(result.elapsed_ms, 3),
                "calls": result.calls,
                "started_at": result.started_at,
                "ended_at": result.ended_at,
                "correlation_id": result.correlation_id,
                "thread_id": result.thread_id,
                "thread_name": result.thread_name,
                "task_id": result.task_id,
                "task_name": result.task_name,
                "is_async": result.is_async,
                "contention_hint": result.contention_hint,
            }
            for result in timings
        ],
        "memory": [
            {
                "name": result.name,
                "peak_mb": round(result.peak_mb, 3),
                "current_mb": round(result.current_mb, 3),
                "allocated_mb": round(result.allocated_mb, 3),
                "correlation_id": result.correlation_id,
                "top_allocations": [
                    {
                        "location": location,
                        "size_mb": round(size_mb, 3),
                    }
                    for location, size_mb in result.top_allocations
                ],
            }
            for result in memory
        ],
        "async_tasks": [
            {
                "task_id": stat.task_id,
                "task_name": stat.task_name,
                "total_ms": round(stat.total_ms, 3),
                "calls": stat.calls,
                "slowest_ms": round(stat.slowest_ms, 3),
                "functions": list(stat.functions),
            }
            for stat in async_task_stats
        ],
        "hints": hints,
        "traces": [trace_result.to_dict() for trace_result in traces],
        "timeline": [event.to_dict() for event in timeline],
    }


def _render_text_report(data: Mapping[str, Any], *, color: bool) -> str:
    lines: list[str] = []
    summary = data.get("summary", {})
    timings = data.get("timings", [])
    memory = data.get("memory", [])
    async_tasks = data.get("async_tasks", [])
    hints = data.get("hints", [])
    traces = data.get("traces", [])
    timeline = data.get("timeline", [])

    lines.append("")
    lines.append("=" * 60)
    lines.append("  SONDEO PROFILING REPORT")
    lines.append("=" * 60)

    lines.append("")
    lines.append("  SUMMARY")
    lines.append("  " + "-" * 56)
    lines.append(f"  Total time:       {summary.get('total_time_ms', 0.0):>10.2f}ms")
    lines.append(f"  Peak memory:      {summary.get('peak_memory_mb', 0.0):>10.2f}MB")
    lines.append(f"  Deepest callstack:{summary.get('deepest_call_stack', 0):>10}")
    lines.append(f"  Threads observed: {summary.get('thread_count', 0):>10}")
    lines.append(f"  Async tasks:      {summary.get('async_task_count', 0):>10}")

    slowest = summary.get("slowest_function")
    if isinstance(slowest, Mapping):
        lines.append(
            f"  Slowest function: {str(slowest.get('name', '<unknown>'))} "
            f"({float(slowest.get('elapsed_ms', 0.0)):.2f}ms)"
        )
    else:
        lines.append("  Slowest function: n/a")

    if isinstance(timings, list) and timings:
        lines.append("")
        lines.append("  TIMING")
        lines.append("  " + "-" * 56)
        for timing in timings:
            name = str(timing.get("name", "<unknown>"))
            elapsed_ms = float(timing.get("elapsed_ms", 0.0))
            calls = int(timing.get("calls", 1))
            calls_str = f" ({calls} calls)" if calls > 1 else ""
            thread_name = str(timing.get("thread_name") or "")
            task_name = str(timing.get("task_name") or "")
            mode = "async" if bool(timing.get("is_async")) else "sync"
            extra_parts = [mode]
            if thread_name:
                extra_parts.append(thread_name)
            if task_name:
                extra_parts.append(task_name)
            extra = ", ".join(extra_parts)
            lines.append(f"  {name:<30} {elapsed_ms:>10.2f}ms{calls_str} [{extra}]")

    if isinstance(memory, list) and memory:
        lines.append("")
        lines.append("  MEMORY")
        lines.append("  " + "-" * 56)
        for mem in memory:
            name = str(mem.get("name", "<unknown>"))
            peak_mb = float(mem.get("peak_mb", 0.0))
            alloc_mb = float(mem.get("allocated_mb", 0.0))
            lines.append(f"  {name:<40} peak={peak_mb:.2f}MB  alloc={alloc_mb:.2f}MB")

    if isinstance(async_tasks, list) and async_tasks:
        lines.append("")
        lines.append("  ASYNC TASKS")
        lines.append("  " + "-" * 56)
        for task in async_tasks:
            lines.append(
                "  "
                f"{str(task.get('task_name', '<task>')):<28} "
                f"total={float(task.get('total_ms', 0.0)):>8.2f}ms  "
                f"slowest={float(task.get('slowest_ms', 0.0)):>8.2f}ms  "
                f"calls={int(task.get('calls', 0))}"
            )

    if isinstance(hints, list) and hints:
        lines.append("")
        lines.append("  CONCURRENCY HINTS")
        lines.append("  " + "-" * 56)
        for hint in hints:
            lines.append(f"  - {hint}")

    if isinstance(traces, list) and traces:
        lines.append("")
        lines.append("  CALL TREE")
        lines.append("  " + "-" * 56)
        for idx, trace in enumerate(traces, start=1):
            lines.append(
                f"  Trace #{idx} "
                f"(calls={int(trace.get('total_calls', 0))}, "
                f"elapsed={float(trace.get('elapsed_ms', 0.0)):.2f}ms)"
            )
            children = trace.get("children", [])
            if isinstance(children, list):
                tree_lines = _format_trace_nodes(children)
                if tree_lines:
                    lines.extend(f"    {line}" for line in tree_lines)
                else:
                    lines.append("    <no calls above threshold>")

    if isinstance(timeline, list) and timeline:
        lines.append("")
        lines.append("  TIMELINE")
        lines.append("  " + "-" * 56)
        timeline_events = [
            logging_mod.TimelineEvent(
                timestamp=float(item.get("timestamp", 0.0)),
                event_type=str(item.get("event_type", "log")),
                level=logging_mod.normalize_level(str(item.get("level", "info"))),
                module=str(item.get("module", "__main__")),
                message=str(item.get("message", "")),
                correlation_id=item.get("correlation_id"),
                data=dict(item.get("data", {})) if isinstance(item.get("data"), Mapping) else {},
            )
            for item in timeline
            if isinstance(item, Mapping)
        ]
        lines.extend(_format_timeline(timeline_events, color=color))

    no_timings = not isinstance(timings, list) or not timings
    no_memory = not isinstance(memory, list) or not memory
    no_async_tasks = not isinstance(async_tasks, list) or not async_tasks
    no_hints = not isinstance(hints, list) or not hints
    no_traces = not isinstance(traces, list) or not traces
    no_timeline = not isinstance(timeline, list) or not timeline
    if no_timings and no_memory and no_async_tasks and no_hints and no_traces and no_timeline:
        lines.append("")
        lines.append("  No profiling or logging data collected.")
        lines.append("  Use @profile, @memprof, timer(), trace(), @log, or log().")

    lines.append("")
    lines.append("=" * 60)
    lines.append("")
    return "\n".join(lines)


def _render_markdown_report(data: Mapping[str, Any]) -> str:
    summary = data.get("summary", {})
    timings = data.get("timings", [])
    memory = data.get("memory", [])
    async_tasks = data.get("async_tasks", [])
    hints = data.get("hints", [])
    traces = data.get("traces", [])
    timeline = data.get("timeline", [])

    lines: list[str] = []
    lines.append("# Sondeo Profiling Report")
    lines.append("")
    lines.append("## Summary")
    lines.append("")
    lines.append("| Metric | Value |")
    lines.append("| --- | --- |")
    lines.append(f"| Total time | {float(summary.get('total_time_ms', 0.0)):.2f} ms |")
    lines.append(f"| Peak memory | {float(summary.get('peak_memory_mb', 0.0)):.2f} MB |")
    lines.append(f"| Deepest call stack | {int(summary.get('deepest_call_stack', 0))} |")
    lines.append(f"| Threads observed | {int(summary.get('thread_count', 0))} |")
    lines.append(f"| Async tasks | {int(summary.get('async_task_count', 0))} |")
    slowest = summary.get("slowest_function")
    if isinstance(slowest, Mapping):
        lines.append(
            "| Slowest function | "
            f"{str(slowest.get('name', '<unknown>'))} "
            f"({float(slowest.get('elapsed_ms', 0.0)):.2f} ms) |"
        )
    else:
        lines.append("| Slowest function | n/a |")

    lines.append("")
    lines.append("## Timing")
    lines.append("")
    if isinstance(timings, list) and timings:
        lines.append("| Function | Elapsed (ms) | Calls |")
        lines.append("| --- | ---: | ---: |")
        for timing in timings:
            lines.append(
                f"| {str(timing.get('name', '<unknown>'))} | "
                f"{float(timing.get('elapsed_ms', 0.0)):.3f} | "
                f"{int(timing.get('calls', 1))} |"
            )
    else:
        lines.append("_No timing data._")

    lines.append("")
    lines.append("## Memory")
    lines.append("")
    if isinstance(memory, list) and memory:
        lines.append("| Function | Peak (MB) | Current (MB) | Allocated (MB) |")
        lines.append("| --- | ---: | ---: | ---: |")
        for mem in memory:
            lines.append(
                f"| {str(mem.get('name', '<unknown>'))} | "
                f"{float(mem.get('peak_mb', 0.0)):.3f} | "
                f"{float(mem.get('current_mb', 0.0)):.3f} | "
                f"{float(mem.get('allocated_mb', 0.0)):.3f} |"
            )
    else:
        lines.append("_No memory data._")

    lines.append("")
    lines.append("## Async Tasks")
    lines.append("")
    if isinstance(async_tasks, list) and async_tasks:
        lines.append("| Task | Total (ms) | Slowest (ms) | Calls |")
        lines.append("| --- | ---: | ---: | ---: |")
        for task in async_tasks:
            lines.append(
                f"| {str(task.get('task_name', '<task>'))} | "
                f"{float(task.get('total_ms', 0.0)):.3f} | "
                f"{float(task.get('slowest_ms', 0.0)):.3f} | "
                f"{int(task.get('calls', 0))} |"
            )
    else:
        lines.append("_No async task data._")

    lines.append("")
    lines.append("## Call Tree")
    lines.append("")
    if isinstance(traces, list) and traces:
        for idx, trace in enumerate(traces, start=1):
            lines.append(
                f"### Trace {idx} "
                f"(calls={int(trace.get('total_calls', 0))}, "
                f"elapsed={float(trace.get('elapsed_ms', 0.0)):.2f}ms)"
            )
            lines.append("")
            children = trace.get("children", [])
            tree_lines = _format_trace_nodes(children) if isinstance(children, list) else []
            if tree_lines:
                lines.append("```text")
                lines.extend(tree_lines)
                lines.append("```")
            else:
                lines.append("_No call tree data._")
            lines.append("")
    else:
        lines.append("_No trace data._")

    lines.append("")
    lines.append("## Timeline")
    lines.append("")
    if isinstance(timeline, list) and timeline:
        lines.append("| Time | Level | Type | Message | Module |")
        lines.append("| --- | --- | --- | --- | --- |")
        first_ts = float(timeline[0].get("timestamp", 0.0))
        for event in timeline:
            ts = float(event.get("timestamp", 0.0))
            delta = (ts - first_ts) * 1000.0
            level = str(event.get("level", "info")).upper()
            etype = str(event.get("event_type", "log"))
            message = str(event.get("message", "")).replace("|", "\\|")
            module = str(event.get("module", "__main__")).replace("|", "\\|")
            lines.append(f"| +{delta:.2f}ms | {level} | {etype} | {message} | {module} |")
    else:
        lines.append("_No timeline events._")

    lines.append("")
    lines.append("## Concurrency Hints")
    lines.append("")
    if isinstance(hints, list) and hints:
        for hint in hints:
            lines.append(f"- {hint}")
    else:
        lines.append("_No contention or deadlock hints._")

    lines.append("")
    return "\n".join(lines)


def render_report(
    data: Mapping[str, Any],
    *,
    format: ReportFormat = "text",
    color: bool = True,
) -> str | dict[str, Any]:
    """Render already-collected report data into text, JSON, or Markdown."""
    normalized_data = dict(data)
    if format == "json":
        return normalized_data
    if format == "markdown":
        return _render_markdown_report(normalized_data)
    return _render_text_report(normalized_data, color=color)


def _clear_all_results() -> None:
    timer_mod.clear_results()
    memory_mod.clear_results()
    trace_mod.clear_results()
    logging_mod.clear_events()


def report(
    *,
    format: ReportFormat = "text",
    clear: bool = True,
    include_modules: Iterable[str] | None = None,
    exclude_modules: Iterable[str] | None = None,
    levels: Iterable[str] | None = None,
    pattern: str | None = None,
    color: bool = True,
    show_timeline: bool = True,
    show_traces: bool = True,
    print_output: bool = True,
) -> str | dict[str, Any]:
    """Print or return all collected profiling results.

    Args:
        format: Output format — "text", "json", or "markdown".
        clear: Whether to clear results after reporting.
        include_modules: Only include timeline events from matching module prefixes.
        exclude_modules: Exclude timeline events from matching module prefixes.
        levels: Include only timeline events matching these levels.
        pattern: Include timeline events whose message or module contains this text.
        color: Whether to colorize timeline level labels in text output.
        show_timeline: Whether to include timeline events in the report.
        show_traces: Whether to include trace call-tree data in the report.
        print_output: Whether to print rendered output to stdout.

    Returns:
        Formatted string (text/markdown) or dict (json).
    """
    data = collect_report_data(
        include_modules=include_modules,
        exclude_modules=exclude_modules,
        levels=levels,
        pattern=pattern,
        show_timeline=show_timeline,
        show_traces=show_traces,
    )

    rendered = render_report(data, format=format, color=color)

    if print_output:
        if format == "json":
            print(json.dumps(rendered, indent=2))
        else:
            print(rendered)

    if clear:
        _clear_all_results()

    return rendered
