"""ASCII charts — bar, horizontal bar, line, sparkline, pie, progress."""
from __future__ import annotations

import math
from dataclasses import dataclass, field
from typing import Sequence


@dataclass
class DataSeries:
    values: list[float]
    labels: list[str] = field(default_factory=list)
    title: str = ""

    def __post_init__(self):
        if not self.labels:
            self.labels = [str(i) for i in range(len(self.values))]


# ─── Bar chart (vertical) ─────────────────────────────────────────────────────

def bar_chart(
    values: Sequence[float],
    labels: Sequence[str] | None = None,
    title: str = "",
    height: int = 15,
    bar_width: int = 5,
    fill_char: str = "█",
    empty_char: str = " ",
) -> str:
    """Vertical bar chart."""
    if not values:
        return "(no data)"

    labels = list(labels) if labels else [str(i + 1) for i in range(len(values))]
    max_val = max(values) or 1
    min_val = min(values)

    lines: list[str] = []

    if title:
        lines.append(f"  {title}")
        lines.append("")

    # Draw bars top-down
    for row in range(height, 0, -1):
        threshold = min_val + (max_val - min_val) * row / height
        line = ""
        for v in values:
            if v >= threshold:
                line += fill_char * bar_width + " "
            else:
                line += empty_char * bar_width + " "
        # Y-axis label on certain rows
        if row == height:
            y_label = f"{max_val:6.1f} │"
        elif row == height // 2:
            y_label = f"{(max_val + min_val) / 2:6.1f} │"
        elif row == 1:
            y_label = f"{min_val:6.1f} │"
        else:
            y_label = "       │"
        lines.append(y_label + line)

    # X-axis
    col_w = bar_width + 1
    lines.append("       └" + "─" * (col_w * len(values)))

    # Labels
    label_row = "        "
    for lbl in labels:
        label_row += lbl[:bar_width].center(col_w)
    lines.append(label_row)

    return "\n".join(lines)


# ─── Horizontal bar chart ─────────────────────────────────────────────────────

def hbar_chart(
    values: Sequence[float],
    labels: Sequence[str] | None = None,
    title: str = "",
    max_width: int = 40,
    fill_char: str = "█",
    show_values: bool = True,
) -> str:
    """Horizontal bar chart."""
    if not values:
        return "(no data)"

    labels = list(labels) if labels else [str(i + 1) for i in range(len(values))]
    max_val = max(values) or 1
    label_w = max(len(l) for l in labels) + 1

    lines: list[str] = []
    if title:
        lines.append(f"  {title}")
        lines.append("")

    for lbl, v in zip(labels, values):
        bar_len = int(round(v / max_val * max_width))
        bar = fill_char * bar_len
        val_str = f" {v:.1f}" if show_values else ""
        lines.append(f"  {lbl:>{label_w}} │ {bar}{val_str}")

    lines.append("")
    lines.append(f"  {'':>{label_w}} └{'─' * max_width}")

    return "\n".join(lines)


# ─── Line chart ───────────────────────────────────────────────────────────────

def line_chart(
    values: Sequence[float],
    labels: Sequence[str] | None = None,
    title: str = "",
    height: int = 12,
    width: int = 60,
    line_char: str = "●",
    fill_char: str = "·",
) -> str:
    """Line/scatter chart."""
    if not values:
        return "(no data)"

    labels = list(labels) if labels else [str(i + 1) for i in range(len(values))]
    max_val = max(values) or 1
    min_val = min(values)
    val_range = max_val - min_val or 1

    # Scale to height x width grid
    n = len(values)
    x_scale = (width - 1) / max(n - 1, 1)

    # Build grid
    grid = [[" " for _ in range(width)] for _ in range(height)]

    points: list[tuple[int, int]] = []
    for i, v in enumerate(values):
        x = int(round(i * x_scale))
        y = height - 1 - int(round((v - min_val) / val_range * (height - 1)))
        y = max(0, min(height - 1, y))
        x = max(0, min(width - 1, x))
        points.append((x, y))

    # Draw connecting lines
    for i in range(len(points) - 1):
        x0, y0 = points[i]
        x1, y1 = points[i + 1]
        steps = max(abs(x1 - x0), abs(y1 - y0), 1)
        for t in range(steps + 1):
            xi = x0 + int(round(t * (x1 - x0) / steps))
            yi = y0 + int(round(t * (y1 - y0) / steps))
            if 0 <= xi < width and 0 <= yi < height:
                grid[yi][xi] = fill_char

    # Draw data points on top
    for x, y in points:
        grid[y][x] = line_char

    lines: list[str] = []
    if title:
        lines.append(f"  {title}")
        lines.append("")

    for row_i, row in enumerate(grid):
        if row_i == 0:
            y_label = f"{max_val:6.1f} │"
        elif row_i == height // 2:
            y_label = f"{(max_val + min_val) / 2:6.1f} │"
        elif row_i == height - 1:
            y_label = f"{min_val:6.1f} │"
        else:
            y_label = "       │"
        lines.append(y_label + "".join(row))

    lines.append("       └" + "─" * width)

    return "\n".join(lines)


# ─── Sparkline ────────────────────────────────────────────────────────────────

SPARK_CHARS = "▁▂▃▄▅▆▇█"

def sparkline(values: Sequence[float], title: str = "") -> str:
    """Single-line sparkline using Unicode block characters."""
    if not values:
        return ""
    mn = min(values)
    mx = max(values) or 1
    rng = mx - mn or 1
    chars = "".join(SPARK_CHARS[int((v - mn) / rng * 7)] for v in values)
    return (f"{title}: " if title else "") + chars + f"  [{mn:.1f}–{mx:.1f}]"


# ─── Pie chart (text) ─────────────────────────────────────────────────────────

def pie_chart(values: Sequence[float], labels: Sequence[str] | None = None, title: str = "") -> str:
    """Text-based pie representation (legend with proportions)."""
    if not values:
        return "(no data)"

    labels = list(labels) if labels else [str(i + 1) for i in range(len(values))]
    total = sum(values) or 1
    chars = "█▓▒░"

    lines: list[str] = []
    if title:
        lines.append(f"  {title}")

    # ASCII pie using a 2:1 character ratio approximation
    radius = 8
    cx, cy = radius, radius // 2
    pie_lines = [list(" " * (radius * 2 + 2)) for _ in range(radius + 1)]

    # Calculate cumulative angles
    angles: list[float] = []
    cumulative = 0.0
    for v in values:
        cumulative += v / total * 360
        angles.append(cumulative)

    # Fill the pie
    fill_chars = ["█", "▓", "▒", "░", "▪", "▫", "◼", "◻"]
    for y in range(-cy, cy + 1):
        for x in range(-radius, radius + 1):
            # Scale x because terminal chars are ~2:1
            if (x / radius) ** 2 + (y / cy) ** 2 <= 1.0:
                angle = (math.degrees(math.atan2(-y, x)) + 360) % 360
                for idx, thresh in enumerate(angles):
                    if angle < thresh:
                        gy = y + cy
                        gx = x + radius
                        if 0 <= gy < len(pie_lines) and 0 <= gx < len(pie_lines[0]):
                            pie_lines[gy][gx] = fill_chars[idx % len(fill_chars)]
                        break

    for row in pie_lines:
        lines.append("  " + "".join(row))

    lines.append("")
    # Legend
    for i, (lbl, v) in enumerate(zip(labels, values)):
        pct = v / total * 100
        lines.append(f"  {fill_chars[i % len(fill_chars)]} {lbl}: {pct:.1f}%  ({v:.1f})")

    return "\n".join(lines)


# ─── Progress bar ─────────────────────────────────────────────────────────────

def progress_bar(
    value: float,
    total: float = 100,
    width: int = 40,
    label: str = "",
    fill_char: str = "█",
    empty_char: str = "░",
) -> str:
    """Single progress bar."""
    pct = min(value / total, 1.0) if total else 0
    filled = int(pct * width)
    bar = fill_char * filled + empty_char * (width - filled)
    pct_str = f"{pct * 100:.1f}%"
    prefix = f"{label}: " if label else ""
    return f"{prefix}[{bar}] {pct_str}  ({value:.0f}/{total:.0f})"


def progress_bars(items: list[tuple[str, float, float]], width: int = 30) -> str:
    """Multiple labeled progress bars."""
    lines = []
    for label, val, total in items:
        lines.append(progress_bar(val, total, width=width, label=label))
    return "\n".join(lines)
