"""Cacao-powered dashboard reporter."""

from __future__ import annotations

import importlib
import os
import sys
import threading
import time
import webbrowser
from collections.abc import Mapping
from pathlib import Path
from typing import Any, Literal

from sondeo.reporter.console import collect_report_data

Theme = Literal["light", "dark", "auto"]


def _candidate_cacao_paths() -> list[Path]:
    candidates: list[Path] = []

    env_path = os.environ.get("SONDEO_CACAO_PATH")
    if env_path:
        candidates.append(Path(env_path).expanduser())

    # Local fallback for this workspace setup.
    candidates.append(Path("/mnt/c/Users/Juan/Documents/GitHub/Cacao"))

    unique_existing: list[Path] = []
    seen: set[str] = set()
    for path in candidates:
        normalized = str(path.resolve()) if path.exists() else str(path)
        if normalized in seen:
            continue
        seen.add(normalized)
        if path.exists():
            unique_existing.append(path)
    return unique_existing


def _import_cacao() -> None:
    try:
        importlib.import_module("cacao")
        return
    except ModuleNotFoundError as exc:
        original_exc = exc

    for repo_path in _candidate_cacao_paths():
        package_dir = repo_path / "cacao"
        if not package_dir.exists():
            continue

        path_text = str(repo_path)
        if path_text not in sys.path:
            sys.path.insert(0, path_text)

        try:
            importlib.import_module("cacao")
            return
        except ModuleNotFoundError:
            continue

    raise RuntimeError(
        "Could not import Cacao. Install `sondeo[ui]` or set SONDEO_CACAO_PATH "
        "to your local Cacao repository root."
    ) from original_exc


def _coerce_mapping_list(value: Any) -> list[dict[str, Any]]:
    if not isinstance(value, list):
        return []
    result: list[dict[str, Any]] = []
    for item in value:
        if isinstance(item, Mapping):
            result.append(dict(item))
    return result


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


def _build_dashboard_app(
    ui: Any,
    chart: Any,
    report_data: Mapping[str, Any],
    *,
    theme: Theme,
) -> Any:
    summary = dict(report_data.get("summary", {})) if isinstance(report_data, Mapping) else {}
    timings = _coerce_mapping_list(report_data.get("timings"))
    memory = _coerce_mapping_list(report_data.get("memory"))
    async_tasks = _coerce_mapping_list(report_data.get("async_tasks"))
    hints = report_data.get("hints")
    traces = _coerce_mapping_list(report_data.get("traces"))
    timeline = _coerce_mapping_list(report_data.get("timeline"))

    timing_rows = sorted(timings, key=lambda item: float(item.get("elapsed_ms", 0.0)), reverse=True)
    timing_table_rows: list[dict[str, Any]] = []
    for row in timing_rows:
        calls = int(row.get("calls", 1)) or 1
        elapsed_ms = float(row.get("elapsed_ms", 0.0))
        timing_table_rows.append(
            {
                "function": str(row.get("name", "<unknown>")),
                "elapsed_ms": round(elapsed_ms, 3),
                "calls": calls,
                "avg_ms": round(elapsed_ms / calls, 3),
            }
        )

    flame_rows = timing_table_rows[:15]

    memory_rows: list[dict[str, Any]] = []
    alloc_rows: list[dict[str, Any]] = []
    for row in memory:
        name = str(row.get("name", "<unknown>"))
        memory_rows.append(
            {
                "function": name,
                "peak_mb": round(float(row.get("peak_mb", 0.0)), 3),
                "current_mb": round(float(row.get("current_mb", 0.0)), 3),
                "allocated_mb": round(float(row.get("allocated_mb", 0.0)), 3),
            }
        )
        for alloc in _coerce_mapping_list(row.get("top_allocations"))[:10]:
            alloc_rows.append(
                {
                    "function": name,
                    "size_mb": round(float(alloc.get("size_mb", 0.0)), 3),
                    "location": str(alloc.get("location", "")),
                }
            )

    timeline_rows: list[dict[str, Any]] = []
    sorted_timeline = sorted(timeline, key=lambda event: float(event.get("timestamp", 0.0)))
    first_ts = float(sorted_timeline[0].get("timestamp", 0.0)) if sorted_timeline else 0.0
    for event in sorted_timeline:
        ts = float(event.get("timestamp", 0.0))
        timeline_rows.append(
            {
                "offset_ms": round((ts - first_ts) * 1000.0, 2),
                "level": str(event.get("level", "info")).upper(),
                "type": str(event.get("event_type", "log")),
                "message": str(event.get("message", "")),
                "module": str(event.get("module", "")),
                "correlation_id": str(event.get("correlation_id") or ""),
            }
        )
    hint_rows: list[dict[str, str]] = []
    if isinstance(hints, list):
        for hint in hints:
            hint_rows.append({"hint": str(hint)})

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

    app = ui.App(title="Sondeo Dashboard", theme=theme)

    with app.page("/"):
        ui.title("Sondeo Dashboard")
        ui.text("Interactive profiling dashboard powered by Cacao.", color="dimmed")
        ui.spacer(size=2)

        with ui.row():
            with ui.col(span=3):
                ui.metric("Total Time", f"{float(summary.get('total_time_ms', 0.0)):.2f} ms")
            with ui.col(span=3):
                ui.metric("Peak Memory", f"{float(summary.get('peak_memory_mb', 0.0)):.2f} MB")
            with ui.col(span=3):
                ui.metric("Deepest Stack", int(summary.get("deepest_call_stack", 0)))
            with ui.col(span=3):
                ui.metric("Slowest Function", slowest_text)

        with ui.row():
            with ui.col(span=6):
                with ui.card(title="Async Task Tracking"):
                    if async_tasks:
                        ui.table(
                            async_tasks,
                            columns=["task_name", "total_ms", "slowest_ms", "calls"],
                            searchable=True,
                            page_size=10,
                        )
                    else:
                        ui.text("No async task data collected yet.", color="dimmed")
            with ui.col(span=6):
                with ui.card(title="Deadlock / Contention Hints"):
                    if hint_rows:
                        ui.table(hint_rows, columns=["hint"], searchable=False, paginate=False)
                    else:
                        ui.text("No contention hints generated.", color="dimmed")

        ui.divider()

        with ui.row():
            with ui.col(span=8):
                with ui.card(title="Timing Panel"):
                    if timing_table_rows:
                        ui.table(
                            timing_table_rows,
                            columns=["function", "elapsed_ms", "calls", "avg_ms"],
                            searchable=True,
                            page_size=15,
                        )
                    else:
                        ui.text("No timing data collected yet.", color="dimmed")
            with ui.col(span=4):
                with ui.card(title="Flamegraph"):
                    if flame_rows:
                        chart.bar(
                            flame_rows,
                            x="function",
                            y="elapsed_ms",
                            horizontal=True,
                            grouped=False,
                            height=360,
                        )
                    else:
                        ui.text("No timing data available for flamegraph.", color="dimmed")

        ui.spacer(size=2)

        with ui.row():
            with ui.col(span=6):
                with ui.card(title="Memory Panel"):
                    if memory_rows:
                        ui.table(
                            memory_rows,
                            columns=["function", "peak_mb", "current_mb", "allocated_mb"],
                            searchable=True,
                            page_size=10,
                        )
                    else:
                        ui.text("No memory data collected yet.", color="dimmed")
            with ui.col(span=6):
                with ui.card(title="Top Allocation Lines"):
                    if alloc_rows:
                        ui.table(
                            alloc_rows,
                            columns=["function", "size_mb", "location"],
                            searchable=True,
                            page_size=10,
                        )
                    else:
                        ui.text("No allocation line data available.", color="dimmed")

        ui.spacer(size=2)

        with ui.card(title="Call Tree Panel"):
            if traces:
                with ui.accordion(mode="multiple"):
                    for index, trace in enumerate(traces, start=1):
                        header = (
                            f"Trace {index} "
                            f"(elapsed={float(trace.get('elapsed_ms', 0.0)):.2f}ms, "
                            f"calls={int(trace.get('total_calls', 0))})"
                        )
                        with ui.accordion_item(header, default_open=index == 1):
                            tree_lines = _trace_lines(_coerce_mapping_list(trace.get("children")))
                            ui.code(
                                "\n".join(tree_lines) if tree_lines else "<no call tree nodes>",
                                language="text",
                            )
            else:
                ui.text("No trace data collected yet.", color="dimmed")

        ui.spacer(size=2)

        with ui.card(title="Timeline View"):
            if timeline_rows:
                ui.table(
                    timeline_rows,
                    columns=["offset_ms", "level", "type", "message", "module", "correlation_id"],
                    searchable=True,
                    page_size=20,
                )
            else:
                ui.text("No timeline events recorded yet.", color="dimmed")

    return app


def _open_browser_later(url: str) -> None:
    time.sleep(0.35)
    webbrowser.open(url)


def serve_report(
    *,
    data: Mapping[str, Any] | None = None,
    host: str = "127.0.0.1",
    port: int = 8000,
    theme: Theme = "auto",
    open_browser: bool = True,
) -> str:
    """Run the Sondeo dashboard using Cacao.

    Args:
        data: Pre-collected report data. Uses in-memory Sondeo data when omitted.
        host: Host to bind Cacao server.
        port: Port to bind Cacao server.
        theme: Cacao theme mode.
        open_browser: Open browser automatically.

    Returns:
        Dashboard URL.
    """
    _import_cacao()
    ui = importlib.import_module("cacao.server.ui")
    chart = importlib.import_module("cacao.server.chart")

    report_data = dict(data) if data is not None else collect_report_data()
    app = _build_dashboard_app(ui, chart, report_data, theme=theme)

    url = f"http://{host}:{port}"
    if open_browser:
        threading.Thread(target=_open_browser_later, args=(url,), daemon=True).start()

    print(f"Sondeo dashboard running at {url} (powered by Cacao)")
    app.run(host=host, port=port, reload=False)
    return url
