"""CLI interface for sping."""

import asyncio
import json
import time
from typing import Optional

import typer
from rich.console import Console
from rich.live import Live
from rich.measure import Measurement
from rich.panel import Panel
from rich.segment import Segment
from rich.style import Style
from rich.table import Table

from . import __version__
from .config import Config
from .formatters import (
    format_content_type,
    format_plain_output_line,
    format_probe_data_for_json,
)
from .metrics import Metrics
from .probe import probe_stream
from .ui.colors import ColorPalette, get_latency_color, get_latency_gradient_color


class BarChart:
    """Custom renderable for bar chart that controls exact line width."""

    def __init__(
        self,
        values,
        width: int,
        height: int,
        palette: ColorPalette = ColorPalette.SUNSET,
    ):
        self.values = values
        self.width = width
        self.height = height
        self.palette = palette

    def __rich_console__(self, console, options):
        # Force exact width regardless of content
        max_width = min(self.width, options.max_width or self.width)

        if max_width < 12:
            # Fallback for very narrow
            for _ in range(self.height):
                yield Segment(" " * max_width)
            return

        AXIS_W = 6
        SEP = " │"  # 2 chars
        plot_w = max(1, max_width - AXIS_W - len(SEP))

        if not self.values:
            empty_bar = " " * plot_w
            for _ in range(self.height):
                line = f"{'':>{AXIS_W}}{SEP}{empty_bar}"
                yield Segment(line[:max_width])
            return

        subset = self.values[-plot_w:] if len(self.values) > plot_w else self.values
        if not subset:
            empty_bar = " " * plot_w
            for _ in range(self.height):
                line = f"{'':>{AXIS_W}}{SEP}{empty_bar}"
                yield Segment(line[:max_width])
            return

        vmin, vmax = min(subset), max(subset)

        # Improve y-scale for startup: use 0 as minimum when we have limited data
        # This provides better range initialization and prevents scale issues
        if len(subset) <= 2:
            vmin = 0.0  # Use 0 as minimum for better initial scaling

        span = vmax - vmin

        if span < 1e-9:  # flat values
            bar_chars = "█" * len(subset)
            padded_bar = bar_chars.ljust(plot_w)
            label_text = f"{vmax:.1f}"[:AXIS_W].rjust(AXIS_W)

            for i in range(self.height):
                if i == self.height - 1:
                    # Bottom row with label and gradient bars
                    # For flat values, use medium purple since they're all the same
                    label_color = self._color_for_latency_value(vmax, vmax, vmax)
                    segments = [Segment(f"{label_text}{SEP}", Style(color=label_color))]

                    # Use value-based color for flat values
                    bar_color = self._color_for_latency_value(vmax, vmax, vmax)
                    segments.append(Segment(bar_chars, Style(color=bar_color)))
                    segments.append(Segment(" " * (plot_w - len(bar_chars)), Style()))

                    for seg in segments:
                        yield seg
                    yield Segment.line()
                else:
                    line = f"{'':>{AXIS_W}}{SEP}{padded_bar}"
                    yield Segment(line[:max_width])
                    yield Segment.line()
            return

        # Scale values to heights
        scaled = []
        for v in subset:
            h = max(1, int((v - vmin) / span * (self.height - 1)) + 1)
            scaled.append(h)

        # Build grid
        grid = [[" "] * plot_w for _ in range(self.height)]

        # Improved positioning: fill left-to-right until full, then scroll right
        # This provides intuitive startup behavior and smooth transition to scrolling
        if len(subset) < plot_w:
            start_x = 0  # Left-align when chart is not full (filling phase)
        else:
            start_x = (
                0  # When full, subset is already the rightmost data (scrolling phase)
            )

        for idx, (val, h) in enumerate(zip(subset, scaled)):
            x = start_x + idx
            if 0 <= x < plot_w:
                for y in range(self.height - h, self.height):
                    if 0 <= y < self.height:
                        # Height-based gradient: each bar shows gradient from its bottom to top
                        # y represents the row (0 = top of chart, height-1 = bottom of chart)
                        # For this bar, bottom is at (height - 1), top is at (height - h)

                        bar_bottom = self.height - 1
                        bar_height = h

                        # Position within this specific bar (0.0 = bottom of bar, 1.0 = top of bar)
                        bar_position = (
                            (bar_bottom - y) / (bar_height - 1)
                            if bar_height > 1
                            else 0.0
                        )

                        # Map bar position to gradient based on the bar's height
                        # Short bars only use lower colors, tall bars use full range
                        max_gradient_position = (
                            h / self.height
                        )  # How much of full gradient this bar uses
                        gradient_position = bar_position * max_gradient_position

                        # Apply the gradient position to our color scheme using centralized utility
                        color = get_latency_gradient_color(
                            gradient_position, self.palette
                        )

                        grid[y][x] = (color, "█")

        # Generate output with colored labels
        mid_val = (vmin + vmax) / 2.0
        for i in range(self.height):
            # Determine label and its color
            if i == 0:
                label_val = vmax
                label_text = f"{vmax:.1f}"[:AXIS_W].rjust(AXIS_W)
            elif i == self.height - 1:
                label_val = vmin
                label_text = f"{vmin:.1f}"[:AXIS_W].rjust(AXIS_W)
            elif i == self.height // 2:
                label_val = mid_val
                label_text = f"{mid_val:.1f}"[:AXIS_W].rjust(AXIS_W)
            else:
                label_val = None
                label_text = "".rjust(AXIS_W)

            # Color the label based on its value
            if label_val is not None:
                label_color = self._color_for_latency_value(label_val, vmin, vmax)
                yield Segment(f"{label_text}{SEP}", Style(color=label_color))
            else:
                yield Segment(f"{label_text}{SEP}", Style())

            # Output bar segments with colors
            for x in range(plot_w):
                if x < len(grid[i]) and isinstance(grid[i][x], tuple):
                    color, char = grid[i][x]
                    yield Segment(char, Style(color=color))
                else:
                    yield Segment(" ", Style())

            yield Segment.line()

    def __rich_measure__(self, console, options):
        return Measurement(self.width, self.width)

    def _color_for_latency_value(self, value, min_val, max_val):
        """Get color based on latency value using the chart's palette."""
        return get_latency_color(value, min_val, max_val, self.palette)


async def run(cfg: Config):
    """Main runner for sping."""
    console = Console()
    metrics = Metrics()

    # Track threshold violations for exit codes
    warn_violations = 0
    crit_violations = 0

    # Setup file export if requested
    export_file_handle = None
    if cfg.export_file:
        try:
            export_file_handle = open(cfg.export_file, "a")
        except Exception as e:
            console.print(
                f"[red]Warning: Could not open export file {cfg.export_file}: {e}[/]"
            )

    async def handle_result(r):
        nonlocal warn_violations, crit_violations

        metrics.update(r)

        # Check thresholds for successful probes
        if r.error is None:
            if cfg.crit_threshold and r.latency_ms > cfg.crit_threshold:
                crit_violations += 1
            elif cfg.warn_threshold and r.latency_ms > cfg.warn_threshold:
                warn_violations += 1

        # Export to file if configured
        if export_file_handle:
            try:
                export_file_handle.write(json.dumps(r.to_dict()) + "\n")
                export_file_handle.flush()
            except Exception:
                pass  # Fail silently for export errors

        if cfg.mode == "json":
            probe_data = format_probe_data_for_json(r)
            print(json.dumps(probe_data))
        elif cfg.mode == "plain":
            print(format_plain_output_line(r, cfg))

    results_aiter = probe_stream(cfg)

    try:
        if cfg.mode != "interactive":
            async for r in results_aiter:
                await handle_result(r)
            # summary
            s = metrics.summary()
            summary_lines = [
                f"--- {cfg.url} sping summary ---",
                f"{s['count']} probes, {s['ok']} ok, {s['errors']} errors",
            ]

            if s.get("anomalies", 0) > 0:
                summary_lines.append(f"Outliers detected: {s['anomalies']}")

            latency_line = f"Latency (ms): min {s['min']:.3f} mean {s['mean']:.3f} max {s['max']:.3f}"
            if cfg.show_percentiles and s.get("p50") is not None:
                latency_line += f" p50 {s['p50']:.3f} p90 {s['p90']:.3f} p95 {s['p95']:.3f} p99 {s['p99']:.3f}"
            summary_lines.append(latency_line)

            if s["count"] > 1:
                summary_lines.append(f"Standard deviation: {s.get('stdev', 0):.3f}ms")

            if warn_violations > 0 or crit_violations > 0:
                summary_lines.append(
                    f"Threshold violations: {warn_violations} warnings, {crit_violations} critical"
                )

            console.print("\n".join(summary_lines))

            # Determine exit code
            if crit_violations > 0:
                return 2  # Critical threshold exceeded
            elif warn_violations > 0:
                return 1  # Warning threshold exceeded
            return 0

        # interactive mode continues as before but with outlier detection...
        def compute_top_height() -> int:
            # Reserve ~40% of vertical real estate within bounds
            term_h = console.size.height
            # subtract panel borders & summary lines (approx 6)
            usable = max(5, term_h - 6)
            target = int(usable * 0.4)
            return max(5, min(target, 15))

        def render_panel():
            # Determine width
            width = console.size.width - 4
            if width < 20:
                width = 20
            # Data
            latencies = metrics.last_latencies(width)
            top_height = compute_top_height()

            # Don't start drawing the bar graph until we have at least one poll result
            # This prevents y-scale rendering issues and incorrect bar positioning at startup
            if not latencies:
                # Show empty chart while waiting for first result
                chart = BarChart(
                    [], width - 2, top_height, cfg.color_palette
                )  # -2 for panel padding
            else:
                chart = BarChart(
                    latencies, width - 2, top_height, cfg.color_palette
                )  # -2 for panel padding

            # Calculate time span for the chart
            chart_title = f"sping {cfg.url}"
            if len(latencies) > 1:
                # Get the time span of the data shown in the chart
                time_span_seconds = len(latencies)  # Each bar represents 1 second
                if time_span_seconds >= 60:
                    time_span_minutes = time_span_seconds / 60
                    chart_title += f" (last {time_span_minutes:.1f}m)"
                else:
                    chart_title += f" (last {time_span_seconds}s)"

            stats = metrics.summary()
            stat_text = (
                f"count {stats['count']} [deep_sky_blue1]ok {stats['ok']}[/] [orange_red1]err {stats['errors']}[/] "
                f"min {stats['min']:.1f} [medium_purple1]mean {stats['mean']:.1f}[/] max {stats['max']:.1f}"
                if stats["count"] > 0
                else "(waiting for samples)"
            )
            if stats.get("anomalies", 0) > 0:
                stat_text += f" [red]outliers {stats['anomalies']}[/]"

            top_content = chart
            # Log panel - fixed height with padding for empty rows
            log_panel = Table(box=None, expand=True, show_header=True)
            log_panel.add_column("Seq", justify="left", width=4)
            log_panel.add_column("Time", width=8)
            log_panel.add_column("Host", width=25)
            log_panel.add_column("Response")
            log_panel.add_column("Latency", justify="right", width=8)

            # Get events and pad to always show 10 rows
            events = metrics.tail_events(10)

            # Get full event history for accurate IP change detection
            all_events = list(metrics.events)

            # Calculate min/max latency for consistent coloring (excluding errors)
            valid_latencies = [e.latency_ms for e in events if e.error is None]
            if valid_latencies:
                min_latency = min(valid_latencies)
                max_latency = max(valid_latencies)
            else:
                min_latency = max_latency = 0.0

            # Use centralized latency color function

            for i in range(10):
                if i < len(events):
                    row = events[i]

                    # Format time (HH:MM:SS)
                    import datetime

                    time_str = f"[grey50]{datetime.datetime.fromtimestamp(row.ts).strftime('%H:%M:%S')}[/]"

                    # Host address - highlight IP in pink if it changed from previous
                    host_str = ""
                    if row.host_address:
                        # Find this event in the full history to get accurate previous event
                        prev_ip = None
                        if len(all_events) > 1:
                            # Find the index of current event in full history
                            current_event_idx = None
                            for idx, event in enumerate(all_events):
                                if (
                                    event.ts == row.ts
                                    and event.host_address == row.host_address
                                    and event.latency_ms == row.latency_ms
                                ):
                                    current_event_idx = idx
                                    break

                            # Look for previous event with host_address
                            if current_event_idx is not None and current_event_idx > 0:
                                for j in range(current_event_idx - 1, -1, -1):
                                    if all_events[j].host_address:
                                        prev_ip = all_events[j].host_address
                                        break

                        # Extract hostname and IP from host_address (format: "hostname (ip)")
                        if " (" in row.host_address and row.host_address.endswith(")"):
                            hostname, ip_part = row.host_address.split(" (", 1)
                            ip = ip_part.rstrip(")")

                            # Check if IP changed
                            if prev_ip and prev_ip != row.host_address:
                                # Extract previous IP for comparison
                                if " (" in prev_ip and prev_ip.endswith(")"):
                                    prev_ip_only = prev_ip.split(" (", 1)[1].rstrip(")")
                                    if ip != prev_ip_only:
                                        # IP changed - highlight in pink
                                        host_str = (
                                            f"{hostname} ([bright_magenta]{ip}[/])"
                                        )
                                    else:
                                        host_str = row.host_address
                                else:
                                    # Previous format different, assume change
                                    host_str = f"{hostname} ([bright_magenta]{ip}[/])"
                            else:
                                host_str = row.host_address
                        else:
                            host_str = row.host_address

                    # Response info
                    if row.error:
                        # Show error in orange/red
                        response_info = f"[orange_red1]{row.error}[/]"
                    else:
                        # Build response info: status code (blue) + bytes + content-type
                        response_parts = []

                        # Status - handle TCP vs HTTP differently
                        if row.content_type == "tcp/connection":
                            # TCP connection result
                            if row.status_code == 0:
                                response_parts.append("[deep_sky_blue1]connected[/]")
                            else:
                                response_parts.append("[orange_red1]failed[/]")
                        else:
                            # HTTP status code in deep blue
                            if row.status_code:
                                response_parts.append(
                                    f"[deep_sky_blue1]{row.status_code}[/]"
                                )

                        # Bytes
                        if row.bytes_read > 0:
                            if row.bytes_read >= 1024:
                                response_parts.append(f"{row.bytes_read/1024:.1f}KB")
                            else:
                                response_parts.append(f"{row.bytes_read}B")

                        # Content type
                        if row.content_type:
                            response_parts.append(format_content_type(row.content_type))

                        response_info = (
                            " ".join(response_parts) if response_parts else ""
                        )

                    # Latency with consistent range-based purple coloring
                    latency_str = ""
                    if row.error is None:
                        # Use centralized latency color function
                        latency_color = get_latency_color(
                            row.latency_ms, min_latency, max_latency, cfg.color_palette
                        )
                        latency_str = f"[{latency_color}]{row.latency_ms:.1f}ms[/]"

                    log_panel.add_row(
                        f"[dim blue]{row.seq}[/]",
                        time_str,
                        host_str,
                        response_info,
                        latency_str,
                        style="green" if not row.error else None,
                    )
                else:
                    # Add empty row to maintain fixed height of exactly 10 rows
                    log_panel.add_row("", "", "", "", "")

            # Calculate log panel height: header + 10 rows + padding
            log_height = 12  # header (1) + 10 rows + panel borders

            layout = Table.grid(expand=True)
            layout.add_row(
                Panel(
                    top_content,
                    title=chart_title,
                    height=top_height + 2,
                    padding=(0, 0),
                )
            )
            layout.add_row(Panel(log_panel, title=stat_text, height=log_height))
            return layout

        # Event-driven UI: No polling, updates only when data arrives or terminal resizes
        # Use auto_refresh=False to disable automatic refresh and manually control updates
        with Live(console=console, auto_refresh=False) as live:
            live.update(render_panel())
            live.refresh()  # Initial render

            last_dims = (console.size.width, console.size.height)
            last_update_time = 0.0
            # Convert refresh_rate to throttle interval: higher refresh_rate = less throttling
            # refresh_rate of 4 Hz = 250ms, but we use smaller intervals for better responsiveness
            update_throttle_ms = max(25, min(200, 1000 / max(1, cfg.refresh_rate)))

            def should_update() -> bool:
                """Light throttling to prevent excessive redraws during data bursts."""
                nonlocal last_update_time
                current_time = time.time() * 1000  # Convert to milliseconds
                if current_time - last_update_time >= update_throttle_ms:
                    last_update_time = current_time
                    return True
                return False

            def check_and_handle_resize():
                """Check for terminal resize and update if needed."""
                nonlocal last_dims
                current_dims = (console.size.width, console.size.height)
                if current_dims != last_dims:
                    last_dims = current_dims
                    if should_update():
                        live.update(render_panel())
                        live.refresh()

            try:
                async for r in results_aiter:
                    await handle_result(r)

                    # Check for resize before updating (non-blocking)
                    check_and_handle_resize()

                    # Update UI only when new data arrives and throttling allows
                    if should_update():
                        live.update(render_panel())
                        live.refresh()

                    if cfg.count and r.seq >= cfg.count:
                        break
            except KeyboardInterrupt:
                # Handle Ctrl+C gracefully
                pass

        # Print summary after interactive session
        s = metrics.summary()
        console.print(
            f"--- {cfg.url} sping summary ---\n"
            f"{s['count']} probes, {s['ok']} ok, {s['errors']} errors\n"
            f"Latency (ms): min {s['min']:.3f} mean {s['mean']:.3f} max {s['max']:.3f}"
        )
        return 0

    finally:
        # Clean up file handle
        if export_file_handle:
            export_file_handle.close()


app = typer.Typer()


def version_callback(value: bool):
    """Callback for --version option."""
    if value:
        console = Console()
        console.print(f"sping version {__version__}")
        raise typer.Exit()


@app.command()
def main(
    url: str = typer.Argument(..., help="URL to probe"),
    interval: float = typer.Option(1.0, help="Interval between probes in seconds"),
    count: Optional[int] = typer.Option(None, help="Number of probes to send"),
    timeout: float = typer.Option(10.0, help="Request timeout in seconds"),
    method: str = typer.Option("HEAD", help="HTTP method to use"),
    json_output: bool = typer.Option(False, "--json", help="JSON output mode"),
    plain: bool = typer.Option(False, "--plain", help="Plain text output mode"),
    body: bool = typer.Option(False, "--body", help="Include full body transfer time"),
    no_keepalive: bool = typer.Option(
        False, "--no-keepalive", help="Disable persistent connections"
    ),
    resolve_once: bool = typer.Option(
        False, "--resolve-once", help="Resolve DNS only once"
    ),
    user_agent: Optional[str] = typer.Option(
        None, "--user-agent", help="Custom User-Agent string"
    ),
    auth: Optional[str] = typer.Option(
        None, "--auth", help="Authentication: user:pass or bearer:token"
    ),
    insecure: bool = typer.Option(False, "--insecure", help="Skip TLS verification"),
    warn: Optional[float] = typer.Option(
        None, "--warn", help="Warning threshold in ms"
    ),
    crit: Optional[float] = typer.Option(
        None, "--crit", help="Critical threshold in ms"
    ),
    export_file: Optional[str] = typer.Option(
        None, "--export-file", help="Export JSON results to file"
    ),
    percentiles: bool = typer.Option(
        False, "--percentiles", help="Show percentiles in summary"
    ),
    refresh_rate: float = typer.Option(
        4.0,
        "--refresh-rate",
        help="UI update throttling in Hz (higher = more responsive, lower = less CPU)",
    ),
    palette: ColorPalette = typer.Option(
        ColorPalette.SUNSET,
        "--palette",
        help="Color palette: sunset (warm oranges/reds), ocean (blues/teals), forest (greens), volcano (reds/oranges), galaxy (purples/magentas), arctic (cool blues/whites), neon (bright electric), monochrome (grays)",
    ),
    ipv4: bool = typer.Option(False, "--ipv4", help="Force IPv4 only"),
    ipv6: bool = typer.Option(False, "--ipv6", help="Force IPv6 only"),
    version: Optional[bool] = typer.Option(
        None, "--version", callback=version_callback, help="Show version and exit"
    ),
) -> None:
    """Modern HTTP/TCP latency monitoring tool with terminal UI."""
    # Validate conflicting options
    if ipv4 and ipv6:
        console = Console()
        console.print("[red]Error: --ipv4 and --ipv6 cannot be used together[/]")
        raise typer.Exit(1)

    # Preprocess URL - add http:// if no scheme provided
    from urllib.parse import urlparse

    parsed = urlparse(url)
    if not parsed.scheme:
        url = f"http://{url}"
    elif parsed.scheme == "tcp":
        # Validate TCP URL format
        if not parsed.hostname or not parsed.port:
            console = Console()
            console.print(
                "[red]Error: TCP URLs must include both hostname and port (e.g., tcp://example.com:80)[/]"
            )
            raise typer.Exit(1)

    # Determine mode
    if json_output:
        mode = "json"
    elif plain:
        mode = "plain"
    else:
        mode = "interactive"

    cfg = Config(
        url=url,
        interval=interval,
        count=count,
        timeout=timeout,
        method=method.upper(),
        include_body=body,
        mode=mode,
        keepalive=not no_keepalive,
        resolve_once=resolve_once,
        user_agent=user_agent,
        auth=auth,
        insecure=insecure,
        warn_threshold=warn,
        crit_threshold=crit,
        export_file=export_file,
        show_percentiles=percentiles,
        ipv4_only=ipv4,
        ipv6_only=ipv6,
        refresh_rate=refresh_rate,
        color_palette=palette,
    )

    exit_code = asyncio.run(run(cfg))
    if exit_code:
        raise typer.Exit(exit_code)


if __name__ == "__main__":
    app()
