from __future__ import annotations

from dataclasses import dataclass
from typing import Iterable

from .profiler import ProfileNode, RequestTrace


@dataclass(slots=True)
class DisplayNode:
    start_ms: float
    duration_ms: float
    percent_total: float
    self_time_ms: float
    label: str
    tree_label: str
    depth: int = 0
    tree_prefix: str = ""
    is_aggregate: bool = False
    count: int = 1


def _render_bar(
    start_ms: float,
    duration_ms: float,
    total_ms: float,
    width: int,
) -> str:
    if width <= 0:
        return ""

    canvas = [" "] * width
    if total_ms <= 0:
        return "".join(canvas)

    start_col = min(width - 1, int((start_ms / total_ms) * width))
    span_col = max(1, int((duration_ms / total_ms) * width))
    end_col = min(width, start_col + span_col)
    for index in range(start_col, end_col):
        canvas[index] = "#"
    return "".join(canvas)


def _format_label(node: ProfileNode) -> str:
    if node.filename == "<request>":
        return node.name
    return f"{node.filename}:{node.lineno} {node.name}"


def _self_time_ms(node: ProfileNode) -> float:
    return node.self_time_ns / 1_000_000


def _percent_total(duration_ns: int, total_ns: int) -> float:
    if total_ns == 0:
        return 0.0
    return (duration_ns / total_ns) * 100


def _is_chain_compressible(
    node: ProfileNode,
    total_ns: int,
    chain_self_below_percent: float,
) -> bool:
    if node.filename == "<request>":
        return False
    if len(node.children) != 1:
        return False
    return (
        _percent_total(node.self_time_ns, total_ns)
        < chain_self_below_percent
    )


def _collapse_chain(
    node: ProfileNode,
    total_ns: int,
    chain_self_below_percent: float,
) -> list[ProfileNode]:
    chain = [node]
    current = node
    while _is_chain_compressible(
        current,
        total_ns,
        chain_self_below_percent,
    ):
        current = current.children[0]
        chain.append(current)
    return chain


def _tree_prefix(
    ancestor_has_next: tuple[bool, ...],
    is_last_child: bool,
) -> str:
    parts: list[str] = []
    for has_next in ancestor_has_next:
        parts.append("|  " if has_next else "   ")
    parts.append("`- " if is_last_child else "|- ")
    return "".join(parts)


def _build_display_node(
    chain: list[ProfileNode],
    root_started_ns: int,
    total_ns: int,
    tree_prefix: str,
) -> DisplayNode:
    first_node = chain[0]
    terminal_node = chain[-1]
    label = _format_label(terminal_node)
    return DisplayNode(
        start_ms=(first_node.started_ns - root_started_ns) / 1_000_000,
        duration_ms=first_node.duration_ns / 1_000_000,
        percent_total=_percent_total(first_node.duration_ns, total_ns),
        self_time_ms=sum(_self_time_ms(node) for node in chain),
        label=label,
        tree_label=f"{tree_prefix}{label}",
        depth=first_node.depth,
        tree_prefix=tree_prefix,
    )


def _aggregate_tree_prefix(depth: int) -> str:
    if depth <= 0:
        return ""
    return "|  " * (depth - 1) + "+- "


def _aggregate_small_nodes(
    nodes: Iterable[DisplayNode],
) -> list[DisplayNode]:
    grouped: dict[tuple[int, str], DisplayNode] = {}
    for item in nodes:
        key = (item.depth, item.label)
        aggregated = grouped.get(key)
        if aggregated is None:
            label = f"{item.label} ({item.count} calls consolidated)"
            grouped[key] = DisplayNode(
                start_ms=item.start_ms,
                duration_ms=item.duration_ms,
                percent_total=item.percent_total,
                self_time_ms=item.self_time_ms,
                label=label,
                tree_label=(
                    f"{_aggregate_tree_prefix(item.depth)}{label}"
                ),
                depth=item.depth,
                tree_prefix=_aggregate_tree_prefix(item.depth),
                is_aggregate=True,
                count=1,
            )
            continue

        aggregated.start_ms = min(aggregated.start_ms, item.start_ms)
        aggregated.duration_ms += item.duration_ms
        aggregated.percent_total += item.percent_total
        aggregated.self_time_ms += item.self_time_ms
        aggregated.count += 1
        label = f"{item.label} ({aggregated.count} calls consolidated)"
        aggregated.label = label
        aggregated.tree_label = (
            f"{_aggregate_tree_prefix(item.depth)}{label}"
        )

    return sorted(grouped.values(), key=lambda item: item.start_ms)


def _build_display_tree(
    node: ProfileNode,
    root_started_ns: int,
    total_ns: int,
    chain_self_below_percent: float,
    ancestor_has_next: tuple[bool, ...] = (),
    is_root: bool = False,
) -> list[DisplayNode]:
    if is_root:
        root_label = _format_label(node)
        items = [
            DisplayNode(
                start_ms=0.0,
                duration_ms=node.duration_ns / 1_000_000,
                percent_total=100.0,
                self_time_ms=_self_time_ms(node),
                label=root_label,
                tree_label=root_label,
                depth=0,
            )
        ]
        child_count = len(node.children)
        for index, child in enumerate(node.children):
            items.extend(
                _build_display_tree(
                    child,
                    root_started_ns,
                    total_ns,
                    chain_self_below_percent,
                    ancestor_has_next=(index != child_count - 1,),
                )
            )
        return items

    chain = _collapse_chain(node, total_ns, chain_self_below_percent)
    terminal = chain[-1]
    is_last_child = not ancestor_has_next[-1] if ancestor_has_next else True
    tree_prefix = _tree_prefix(ancestor_has_next[:-1], is_last_child)
    items = [
        _build_display_node(
            chain,
            root_started_ns,
            total_ns,
            tree_prefix,
        )
    ]

    child_count = len(terminal.children)
    for index, child in enumerate(terminal.children):
        items.extend(
            _build_display_tree(
                child,
                root_started_ns,
                total_ns,
                chain_self_below_percent,
                ancestor_has_next=ancestor_has_next + (
                    index != child_count - 1,
                ),
            )
        )

    return items


def render_trace(
    trace: RequestTrace,
    *,
    width: int = 60,
    hotspot_count: int = 5,
    collapse_below_percent: float = 0.1,
    collapse_chain_self_below_percent: float = 0.1,
) -> str:
    total_ns = trace.duration_ns
    total_ms = total_ns / 1_000_000
    tree_nodes = _build_display_tree(
        trace.root,
        trace.root.started_ns,
        total_ns,
        collapse_chain_self_below_percent,
        is_root=True,
    )

    non_root_nodes = tree_nodes[1:]
    small_nodes = [
        item
        for item in non_root_nodes
        if item.percent_total < collapse_below_percent
    ]
    visible_nodes = [
        item
        for item in non_root_nodes
        if item.percent_total >= collapse_below_percent
    ]
    aggregated_small_nodes = _aggregate_small_nodes(small_nodes)

    display_nodes = list(visible_nodes)
    display_nodes.extend(aggregated_small_nodes)
    display_nodes.sort(key=lambda item: item.start_ms)

    hottest_node = max(
        display_nodes,
        key=lambda item: item.self_time_ms,
        default=None,
    )
    hottest_self = sorted(
        display_nodes,
        key=lambda item: item.self_time_ms,
        reverse=True,
    )[:hotspot_count]

    lines = [
        (
            f"Request {trace.status_code} {trace.method} "
            f"{trace.path} took {total_ms:.2f} ms"
        ),
        "Hotspots",
    ]

    if hottest_self:
        for item in hottest_self:
            lines.append(
                "  "
                f"{item.duration_ms:7.2f} ms total | "
                f"{item.self_time_ms:7.2f} ms self  "
                f"{item.tree_label}"
            )
    else:
        lines.append("  no Python frames were captured")

    lines.append("Waterfall")
    root_item = tree_nodes[0]
    waterfall_nodes = [root_item, *display_nodes]
    for item in waterfall_nodes:
        marker = ""
        if hottest_node is item:
            marker = "  <<< hottest"

        bar = _render_bar(item.start_ms, item.duration_ms, total_ms, width)
        lines.append(
            f"{item.start_ms:8.2f} ms |{bar}|"
            f" {item.duration_ms:8.2f} ms"
            f" {item.percent_total:5.1f}% {item.tree_label}{marker}"
        )

    collapsed_count = sum(item.count for item in aggregated_small_nodes)
    if collapsed_count:
        lines.append(
            f"Collapsed low-impact calls: {collapsed_count} below "
            f"{collapse_below_percent:.1f}% each"
        )

    if trace.dropped_calls:
        lines.append(f"Dropped calls: {trace.dropped_calls}")

    return "\n".join(lines)
