"""Human-readable rich output rendering for tyta.

Two Console instances:
- _console: stdout (human-readable output)
- _err_console: stderr (warnings, errors in human mode)

Entry points:
- render_output(envelope, json_mode, command, config): dispatch to per-command renderer or JSON path
- render_error(envelope, json_mode): human → red Panel to stderr; JSON → JSON to stdout
"""

from __future__ import annotations

import json
import sys
from typing import Any

from rich.console import Console
from rich.panel import Panel
from rich.table import Table

from tyta.core.config import (
    DEFAULT_DATE_FORMAT,
    DEFAULT_DISTANCE_UNIT,
    convert_distance,
    format_ts,
)

_console = Console()
_err_console = Console(stderr=True)

_EM_DASH = "—"


def _val(v: Any) -> str:  # noqa: ANN401
    """Return a display string; None → em dash."""
    if v is None:
        return _EM_DASH
    return str(v)


def _get_date_format(cfg: dict) -> str:
    return cfg.get("date_format", DEFAULT_DATE_FORMAT)


def _get_distance_unit(cfg: dict) -> str:
    return cfg.get("distance_unit", DEFAULT_DISTANCE_UNIT)


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


def render_output(
    envelope: dict,
    json_mode: bool,
    command: str,
    config: dict | None = None,
) -> None:
    """Render the success envelope.

    In JSON mode: write indented JSON to stdout.
    In human mode: dispatch to per-command renderer; warnings → stderr.

    Args:
        envelope: Success envelope dict.
        json_mode: If True, output raw JSON to stdout.
        command: Command name used to look up the renderer.
        config: Config dict for formatting preferences (date_format, distance_unit).

    """
    if json_mode:
        print(json.dumps(envelope, indent=2))
        return

    cfg = config or {}
    data = envelope.get("data", {})
    warnings = envelope.get("warnings", [])

    # Emit warnings to stderr first (yellow)
    for w in warnings:
        _err_console.print(f"[yellow]Warning:[/yellow] {w}")

    # Dispatch to per-command renderer
    _dispatch(command, data, cfg)


def _extract_api_message(raw_message: str) -> str:
    """Extract the human-readable description from a Toyota API error message.

    API errors are often of the form:
        "Toyota API error: {<json blob with status.messages[].detailedDescription>}"

    Returns the detailedDescription if parseable, otherwise the original string.
    """
    prefix = "Toyota API error: "
    if not raw_message.startswith(prefix):
        return raw_message
    try:
        blob = json.loads(raw_message[len(prefix) :])
        messages = blob.get("status", {}).get("messages", [])
        if messages:
            desc = messages[0].get("detailedDescription") or messages[0].get("description")
            if desc:
                return desc
    except (json.JSONDecodeError, AttributeError, KeyError):
        pass
    return raw_message


def render_error(envelope: dict, json_mode: bool) -> None:
    """Render an error envelope.

    JSON mode: write JSON to stdout, exit 1.
    Human mode: red Panel to stderr, nothing to stdout, exit 1.
    """
    if json_mode:
        print(json.dumps(envelope, indent=2))
        sys.exit(1)

    error = envelope.get("error", {})
    code = error.get("code", "unknown_error")
    raw_message = error.get("message", "An unknown error occurred.")
    message = _extract_api_message(raw_message)
    _err_console.print(
        Panel(
            f"[bold]{message}[/bold]\n[dim]Error code: {code}[/dim]",
            title="[red]Error[/red]",
            border_style="red",
        )
    )
    sys.exit(1)


# ---------------------------------------------------------------------------
# Dispatcher
# ---------------------------------------------------------------------------

_RENDERERS: dict[str, Any] = {}


def _dispatch(command: str, data: dict, cfg: dict) -> None:
    renderer = _RENDERERS.get(command, _render_fallback)
    renderer(data, cfg)


def _render_fallback(data: dict, cfg: dict) -> None:  # noqa: ARG001
    """Fallback: just print a generic panel with the JSON data."""
    _console.print(Panel(json.dumps(data, indent=2), title="Result"))


# ---------------------------------------------------------------------------
# Per-command renderers
# ---------------------------------------------------------------------------


def _render_auth_login(data: dict, cfg: dict) -> None:
    username = _val(data.get("username"))
    raw_expires = data.get("expires_at")
    expires_at = format_ts(raw_expires, _get_date_format(cfg)) if raw_expires else _EM_DASH
    _console.print(
        Panel(
            f"Logged in as [bold]{username}[/bold]\nToken expires: {expires_at}",
            title="[green]Logged In[/green]",
            border_style="green",
        )
    )


_RENDERERS["auth login"] = _render_auth_login


def _render_auth_logout(data: dict, cfg: dict) -> None:  # noqa: ARG001
    message = data.get("message", "Logged out.")
    _console.print(Panel(message, title="Auth", border_style="blue"))


_RENDERERS["auth logout"] = _render_auth_logout


def _render_auth_status(data: dict, cfg: dict) -> None:
    table = Table(show_header=False, box=None, padding=(0, 1))
    table.add_column("Key", style="bold")
    table.add_column("Value")
    authenticated = data.get("authenticated")
    table.add_row(
        "Status",
        "[green]Authenticated[/green]" if authenticated else "[red]Not authenticated[/red]",
    )
    table.add_row("Username", _val(data.get("username")))
    raw_expires = data.get("expires_at")
    expires_str = format_ts(raw_expires, _get_date_format(cfg)) if raw_expires else _EM_DASH
    table.add_row("Expires at", expires_str)
    expired = data.get("expired")
    if expired is None:
        exp_str = _EM_DASH
    elif expired:
        exp_str = "[red]Yes[/red]"
    else:
        exp_str = "[green]No[/green]"
    table.add_row("Expired", exp_str)
    _console.print(Panel(table, title="Auth Status", border_style="blue"))


_RENDERERS["auth status"] = _render_auth_status


def _render_vehicles_list(data: dict, cfg: dict) -> None:  # noqa: ARG001
    vehicles = data.get("vehicles", [])
    if not vehicles:
        _console.print(Panel("No vehicles linked to this account.", title="Vehicles"))
        return
    table = Table(title="Vehicles", show_header=True, header_style="bold")
    table.add_column("VIN")
    table.add_column("Nickname")
    table.add_column("Model")
    table.add_column("Year")
    table.add_column("Color")
    table.add_column("Brand")
    table.add_column("Fuel Type")
    table.add_column("EV")
    for v in vehicles:
        ev = v.get("ev_vehicle")
        ev_str = "Yes" if ev else ("No" if ev is not None else _EM_DASH)
        table.add_row(
            _val(v.get("vin")),
            _val(v.get("nickname")),
            _val(v.get("model_name")),
            _val(v.get("model_year")),
            _val(v.get("color")),
            _val(v.get("brand")),
            _val(v.get("fuel_type")),
            ev_str,
        )
    _console.print(table)


_RENDERERS["vehicles list"] = _render_vehicles_list


def _render_vehicles_use(data: dict, cfg: dict) -> None:  # noqa: ARG001
    vin = data.get("message", "")
    _console.print(Panel(vin, title="[green]Default Vehicle Set[/green]", border_style="green"))


_RENDERERS["vehicles use"] = _render_vehicles_use


def _render_vehicles_capabilities(data: dict, cfg: dict) -> None:  # noqa: ARG001
    """Render two panels: Hardware Capabilities and Remote Service Capabilities."""
    vin = _val(data.get("vin"))
    extended = data.get("extended_capabilities") or {}
    remote = data.get("remote_service_capabilities") or {}

    def _bool_table(flags: dict, title: str) -> None:
        if not flags:
            _console.print(Panel("No flags to display.", title=title, border_style="blue"))
            return
        t = Table(show_header=True, header_style="bold")
        t.add_column("Capability")
        t.add_column("Value")
        for key, val in sorted(flags.items()):
            val_str = (
                "[green]Yes[/green]"
                if val is True
                else ("[red]No[/red]" if val is False else _val(val))
            )
            t.add_row(key, val_str)
        _console.print(Panel(t, title=f"{title} — {vin}", border_style="blue"))

    _bool_table(extended, "Hardware Capabilities")
    _bool_table(remote, "Remote Service Capabilities")


_RENDERERS["vehicles capabilities"] = _render_vehicles_capabilities


def _render_vehicles_features(data: dict, cfg: dict) -> None:  # noqa: ARG001
    """Render Connected Services features panel."""
    vin = _val(data.get("vin"))
    features = data.get("features") or {}

    if not features:
        _console.print(
            Panel(
                "No features to display.", title=f"Connected Services — {vin}", border_style="blue"
            )
        )
        return

    t = Table(show_header=True, header_style="bold")
    t.add_column("Feature")
    t.add_column("Value")
    for key, val in sorted(features.items()):
        val_str = (
            "[green]Yes[/green]"
            if val is True
            else ("[red]No[/red]" if val is False else _val(val))
        )
        t.add_row(key, val_str)
    _console.print(Panel(t, title=f"Connected Services — {vin}", border_style="blue"))


_RENDERERS["vehicles features"] = _render_vehicles_features


def _render_vehicles_subscriptions(data: dict, cfg: dict) -> None:
    """Render subscriptions table."""
    vin = _val(data.get("vin"))
    subs = data.get("subscriptions") or []
    date_fmt = _get_date_format(cfg)

    if not subs:
        _console.print(
            Panel("No subscriptions found.", title=f"Subscriptions — {vin}", border_style="blue")
        )
        return

    t = Table(title=f"Subscriptions — {vin}", show_header=True, header_style="bold")
    t.add_column("Product")
    t.add_column("Status")
    t.add_column("Start")
    t.add_column("End")
    t.add_column("Auto-Renew")
    t.add_column("Days Remaining")

    for sub in subs:
        product = _val(sub.get("productName") or sub.get("displayProductName"))
        status = _val(sub.get("status"))
        start_raw = sub.get("subscriptionStartDate")
        end_raw = sub.get("subscriptionEndDate")
        start_str = format_ts(str(start_raw), date_fmt) if start_raw else _EM_DASH
        end_str = format_ts(str(end_raw), date_fmt) if end_raw else _EM_DASH
        auto_renew = sub.get("autoRenew")
        auto_renew_str = (
            "Yes" if auto_renew is True else ("No" if auto_renew is False else _EM_DASH)
        )
        days_remaining = sub.get("subscriptionRemainingDays")
        days_str = _val(days_remaining)
        t.add_row(product, status, start_str, end_str, auto_renew_str, days_str)

    _console.print(t)


_RENDERERS["vehicles subscriptions"] = _render_vehicles_subscriptions


def _fmt_quantity(q: dict | None) -> str:
    if not isinstance(q, dict):
        return _val(q)
    value = q.get("value")
    unit = q.get("unit", "")
    if value is None:
        return _EM_DASH
    return f"{value} {unit}".strip()


def _render_status(data: dict, cfg: dict) -> None:
    """Render four stacked panels: Telemetry, Location, Lock Status, Health."""
    date_fmt = _get_date_format(cfg)

    # Telemetry
    telemetry = data.get("telemetry")
    if telemetry is None:
        _console.print(Panel("Not available", title="Telemetry", border_style="blue"))
    else:
        t = Table(show_header=False, box=None, padding=(0, 1))
        t.add_column("Key", style="bold")
        t.add_column("Value")
        t.add_row("Fuel Type", _val(telemetry.get("fuelType")))
        t.add_row(
            "Fuel Level",
            _val(telemetry.get("fuelLevel")) + "%"
            if telemetry.get("fuelLevel") is not None
            else _EM_DASH,
        )
        t.add_row("Odometer", _fmt_quantity(telemetry.get("odometer")))
        t.add_row("Range Remaining", _fmt_quantity(telemetry.get("distanceToEmpty")))
        raw_ts = telemetry.get("timestamp")
        t.add_row("Last Updated", format_ts(raw_ts, date_fmt) if raw_ts else _EM_DASH)
        _console.print(Panel(t, title="Telemetry", border_style="blue"))

    # Location
    location = data.get("location")
    vehicle_loc = (location or {}).get("vehicleLocation") if location else None
    if vehicle_loc is None:
        _console.print(Panel("Not available", title="Location", border_style="blue"))
    else:
        t = Table(show_header=False, box=None, padding=(0, 1))
        t.add_column("Key", style="bold")
        t.add_column("Value")
        t.add_row("Latitude", _val(vehicle_loc.get("latitude")))
        t.add_row("Longitude", _val(vehicle_loc.get("longitude")))
        t.add_row("Display Name", _val(vehicle_loc.get("displayName")))
        raw_loc_ts = vehicle_loc.get("locationAcquisitionDatetime")
        t.add_row("Last Updated", format_ts(raw_loc_ts, date_fmt) if raw_loc_ts else _EM_DASH)
        _console.print(Panel(t, title="Location", border_style="blue"))

    # Lock Status — from remote_status.vehicleStatus
    remote = data.get("remote_status") or {}
    vehicle_status = remote.get("vehicleStatus") or []
    lock_rows: list[tuple[str, str]] = []
    for category in vehicle_status:
        for section in category.get("sections") or []:
            label = (
                section.get("section", "").replace("carstatus_item_", "").replace("_", " ").title()
            )
            values = [
                v.get("value", "").replace("carstatus_", "") for v in section.get("values") or []
            ]
            lock_rows.append((label, ", ".join(values)))

    # Caution count and occurrence date from remote_status
    caution_count = remote.get("cautionOverallCount")
    occurrence_date = remote.get("occurrenceDate")

    if not lock_rows and caution_count is None and not occurrence_date:
        _console.print(Panel("Not available", title="Lock Status", border_style="blue"))
    else:
        t = Table(show_header=False, box=None, padding=(0, 1))
        t.add_column("Key", style="bold")
        t.add_column("Value")
        for label, value in lock_rows:
            t.add_row(label, value)
        if caution_count is not None:
            t.add_row("Caution Count", str(caution_count))
        if occurrence_date:
            t.add_row("Occurrence Date", format_ts(occurrence_date, date_fmt))
        _console.print(Panel(t, title="Lock Status", border_style="blue"))

    # Health
    health = data.get("health")
    if health is None:
        _console.print(Panel("Not available", title="Health", border_style="blue"))
    else:
        warnings = health.get("warning") or []
        if not warnings:
            _console.print(Panel("No warnings", title="Health", border_style="blue"))
        else:
            content = "\n".join(str(w) for w in warnings)
            _console.print(Panel(content, title="Health", border_style="yellow"))


_RENDERERS["status"] = _render_status


def _render_electric_status(data: dict, cfg: dict) -> None:
    date_fmt = _get_date_format(cfg)
    es = data.get("electric_status") or {}
    t = Table(show_header=False, box=None, padding=(0, 1))
    t.add_column("Key", style="bold")
    t.add_column("Value")
    t.add_row("Battery %", _val(es.get("battery_level")))
    t.add_row("Charging State", _val(es.get("charging_status")))
    t.add_row("Range", _val(es.get("ev_range")))
    t.add_row("Completion Time", _val(es.get("charge_completion_time")))
    raw_ts = es.get("last_updated")
    t.add_row("Last Updated", format_ts(raw_ts, date_fmt) if raw_ts else _EM_DASH)
    _console.print(Panel(t, title="Electric Status", border_style="blue"))


_RENDERERS["electric status"] = _render_electric_status


def _render_electric_refresh(data: dict, cfg: dict) -> None:  # noqa: ARG001
    # Main success panel to stdout
    _console.print(
        Panel(
            "Real-time EV status refresh requested.",
            title="[green]Refresh Requested[/green]",
            border_style="green",
        )
    )
    # 12V battery warning to stderr as bold yellow Panel
    _err_console.print(
        Panel(
            "[bold yellow]Frequent use of real-time EV status refresh drains the vehicle's 12V battery.[/bold yellow]",
            title="[yellow]Warning[/yellow]",
            border_style="yellow",
        )
    )


_RENDERERS["electric refresh"] = _render_electric_refresh


def _render_electric_charge_now(data: dict, cfg: dict) -> None:  # noqa: ARG001
    _console.print(
        Panel(
            "Charge now command sent.",
            title="[green]Charge Now[/green]",
            border_style="green",
        )
    )


_RENDERERS["electric charge-now"] = _render_electric_charge_now


def _render_electric_schedule(data: dict, cfg: dict) -> None:  # noqa: ARG001
    t = Table(show_header=False, box=None, padding=(0, 1))
    t.add_column("Key", style="bold")
    t.add_column("Value")
    t.add_row("Day", _val(data.get("day")))
    t.add_row("Start", _val(data.get("start")))
    t.add_row("End", _val(data.get("end")))
    t.add_row("Charge Type", _val(data.get("charge_type")))
    _console.print(Panel(t, title="[green]Charging Schedule Set[/green]", border_style="green"))


_RENDERERS["electric reserve-charge"] = _render_electric_schedule
_RENDERERS["electric set-charging-time"] = _render_electric_schedule


def _render_climate_status(data: dict, cfg: dict) -> None:
    date_fmt = _get_date_format(cfg)
    cs = data.get("climate_status") or {}
    status_code = cs.get("status")
    running_str = "Yes" if status_code else ("No" if status_code is not None else _EM_DASH)
    t = Table(show_header=False, box=None, padding=(0, 1))
    t.add_column("Key", style="bold")
    t.add_column("Value")
    t.add_row("Running", running_str)
    t.add_row("Type", _val(cs.get("type")))

    # Current temperature
    curr_temp = cs.get("currentTemperature")
    if curr_temp and isinstance(curr_temp, dict):
        curr_val = curr_temp.get("value")
        curr_unit = curr_temp.get("unit", "")
        curr_ts = curr_temp.get("timestamp")
        temp_str = f"{curr_val} \u00b0{curr_unit}" if curr_val is not None else _EM_DASH
        if curr_ts:
            temp_str += f" (as of {format_ts(curr_ts, date_fmt)})"
        t.add_row("Current Temp", temp_str)
    else:
        t.add_row("Current Temp", _EM_DASH)

    # Target temperature
    target_temp = cs.get("targetTemperature")
    if target_temp and isinstance(target_temp, dict):
        tgt_val = target_temp.get("value")
        tgt_unit = target_temp.get("unit", "")
        tgt_str = f"{tgt_val} \u00b0{tgt_unit}" if tgt_val is not None else _EM_DASH
        t.add_row("Target Temp", tgt_str)
    else:
        t.add_row("Target Temp", _EM_DASH)

    # Started at
    started_at = cs.get("startedAt")
    t.add_row("Started At", format_ts(started_at, date_fmt) if started_at else _EM_DASH)

    # Duration
    duration = cs.get("duration")
    t.add_row("Duration", f"{duration} min" if duration is not None else _EM_DASH)

    # Defoggers
    options = cs.get("options")
    if options and isinstance(options, dict):
        front = options.get("frontDefogger")
        rear = options.get("rearDefogger")
        t.add_row("Front Defogger", "On" if front else ("Off" if front is not None else _EM_DASH))
        t.add_row("Rear Defogger", "On" if rear else ("Off" if rear is not None else _EM_DASH))
    else:
        t.add_row("Front Defogger", _EM_DASH)
        t.add_row("Rear Defogger", _EM_DASH)

    _console.print(Panel(t, title="Climate Status", border_style="blue"))


_RENDERERS["climate status"] = _render_climate_status


def _render_climate_settings(data: dict, cfg: dict) -> None:  # noqa: ARG001
    cs = data.get("climate_settings") or {}
    settings_on = cs.get("settingsOn")
    on_str = "Yes" if settings_on else ("No" if settings_on is not None else _EM_DASH)
    temp = cs.get("temperature")
    unit = cs.get("temperatureUnit", "")
    temp_str = f"{temp} \u00b0{unit}" if temp is not None else _EM_DASH
    t = Table(show_header=False, box=None, padding=(0, 1))
    t.add_column("Key", style="bold")
    t.add_column("Value")
    t.add_row("Settings On", on_str)
    t.add_row("Temperature", temp_str)
    t.add_row(
        "Min Temp",
        f"{cs.get('minTemp')} \u00b0{unit}" if cs.get("minTemp") is not None else _EM_DASH,
    )
    t.add_row(
        "Max Temp",
        f"{cs.get('maxTemp')} \u00b0{unit}" if cs.get("maxTemp") is not None else _EM_DASH,
    )
    _console.print(Panel(t, title="Climate Settings", border_style="blue"))

    # AC operations — one panel per category
    ac_ops = cs.get("acOperations") or []
    for op in ac_ops:
        if not isinstance(op, dict):
            continue
        category_name = op.get("categoryDisplayName") or op.get("categoryName") or "AC Operation"
        params = op.get("acParameters") or []
        if not params:
            continue
        param_table = Table(show_header=True, header_style="bold")
        param_table.add_column("Parameter")
        param_table.add_column("Enabled")
        param_table.add_column("Available")
        for param in params:
            if not isinstance(param, dict):
                continue
            name = param.get("displayName") or param.get("name") or _EM_DASH
            enabled = param.get("enabled")
            available = param.get("available")
            enabled_str = "Yes" if enabled else ("No" if enabled is not None else _EM_DASH)
            available_str = "Yes" if available else ("No" if available is not None else _EM_DASH)
            param_table.add_row(name, enabled_str, available_str)
        _console.print(Panel(param_table, title=f"AC: {category_name}", border_style="cyan"))


_RENDERERS["climate settings"] = _render_climate_settings


def _render_climate_set(data: dict, cfg: dict) -> None:  # noqa: ARG001
    _console.print(
        Panel(
            "Climate settings updated.",
            title="[green]Climate Settings[/green]",
            border_style="green",
        )
    )


_RENDERERS["climate set"] = _render_climate_set


def _render_climate_on(data: dict, cfg: dict) -> None:  # noqa: ARG001
    _console.print(
        Panel(
            "Climate control started.",
            title="[green]Climate On[/green]",
            border_style="green",
        )
    )


_RENDERERS["climate on"] = _render_climate_on


def _render_climate_off(data: dict, cfg: dict) -> None:  # noqa: ARG001
    _console.print(
        Panel(
            "Climate control stopped.",
            title="[green]Climate Off[/green]",
            border_style="green",
        )
    )


_RENDERERS["climate off"] = _render_climate_off


def _fmt_duration(seconds: int | None) -> str:
    if seconds is None:
        return _EM_DASH
    h, rem = divmod(int(seconds), 3600)
    m = rem // 60
    return f"{h}h {m}m" if h else f"{m}m"


def _fmt_distance(meters: int | float | None, distance_unit: str = DEFAULT_DISTANCE_UNIT) -> str:
    if meters is None:
        return _EM_DASH
    value, unit = convert_distance(meters, distance_unit)
    if value is None:
        return _EM_DASH
    return f"{value:.1f} {unit}"


def _fmt_latlon(lat: float | None, lon: float | None) -> str:
    if lat is None or lon is None:
        return _EM_DASH
    return f"{lat:.4f}, {lon:.4f}"


def _route_summary(route: list) -> str:
    """Compute % EV, % highway, % overspeed from route waypoints."""
    if not route:
        return _EM_DASH
    total = len(route)
    ev_count = sum(1 for w in route if w.get("isEv"))
    hw_count = sum(1 for w in route if w.get("highway"))
    os_count = sum(1 for w in route if w.get("overspeed"))
    ev_pct = round(ev_count / total * 100)
    hw_pct = round(hw_count / total * 100)
    os_pct = round(os_count / total * 100)
    return f"EV {ev_pct}% | HW {hw_pct}% | OS {os_pct}%"


def _render_trips_list(data: dict, cfg: dict) -> None:
    raw = data.get("trips") or {}
    trip_list = raw.get("payload", {}).get("trips", []) if isinstance(raw, dict) else []
    routes_flag = any(trip.get("route") for trip in trip_list)
    date_fmt = _get_date_format(cfg)
    dist_unit = _get_distance_unit(cfg)
    limit = data.get("limit", 10)
    page = data.get("page", 1)
    total = data.get("total")

    if not trip_list:
        _console.print(Panel("No trips found for the selected period.", title="Trips"))
        return

    table = Table(title="Trips", show_header=True, header_style="bold")
    table.add_column("ID")
    table.add_column("Date")
    table.add_column("Duration")
    table.add_column("Distance")
    table.add_column("Start (lat, lon)")
    table.add_column("End (lat, lon)")
    table.add_column("Score")
    if routes_flag:
        table.add_column("Route")

    for trip in trip_list:
        s = trip.get("summary") or {}
        scores = trip.get("scores") or {}
        start_ts = s.get("startTs")
        trip_id = _val(trip.get("id"))
        row = [
            trip_id,
            format_ts(start_ts, date_fmt) if start_ts else _EM_DASH,
            _fmt_duration(s.get("duration")),
            _fmt_distance(s.get("length"), dist_unit),
            _fmt_latlon(s.get("startLat"), s.get("startLon")),
            _fmt_latlon(s.get("endLat"), s.get("endLon")),
            _val(scores.get("global")),
        ]
        if routes_flag:
            row.append(_route_summary(trip.get("route") or []))
        table.add_row(*row)
    _console.print(table)

    # Route flag hint
    if routes_flag:
        _console.print("[dim]Use --json to see full waypoint arrays per trip.[/dim]")

    # Pagination footer
    if total is not None and total > limit:
        start_item = (page - 1) * limit + 1
        end_item = min(page * limit, total)
        _console.print(f"[dim]Page {page} ({start_item}–{end_item} of {total})[/dim]")


_RENDERERS["trips list"] = _render_trips_list


def _render_trips_show(data: dict, cfg: dict) -> None:
    """Render full trip detail: summary, scores, HDC, behaviours, route breakdown."""
    raw = data.get("trip") or {}
    trip = raw.get("payload") or {}
    date_fmt = _get_date_format(cfg)
    dist_unit = _get_distance_unit(cfg)

    s = trip.get("summary") or {}
    scores = trip.get("scores") or {}
    hdc = trip.get("hdc")
    behaviours = trip.get("behaviours") or []
    route = trip.get("route") or []

    # Summary panel
    t = Table(show_header=False, box=None, padding=(0, 1))
    t.add_column("Key", style="bold")
    t.add_column("Value")
    start_ts = s.get("startTs")
    end_ts = s.get("endTs")
    t.add_row("Start", format_ts(start_ts, date_fmt) if start_ts else _EM_DASH)
    t.add_row("End", format_ts(end_ts, date_fmt) if end_ts else _EM_DASH)
    t.add_row("Duration", _fmt_duration(s.get("duration")))
    t.add_row("Distance", _fmt_distance(s.get("length"), dist_unit))
    t.add_row(
        "Max Speed", f"{s.get('maxSpeed')} km/h" if s.get("maxSpeed") is not None else _EM_DASH
    )
    t.add_row(
        "Avg Speed",
        f"{s.get('averageSpeed')} km/h" if s.get("averageSpeed") is not None else _EM_DASH,
    )
    t.add_row("Countries", ", ".join(s.get("countries") or []) or _EM_DASH)
    night = s.get("nightTrip")
    t.add_row("Night Trip", "Yes" if night else ("No" if night is not None else _EM_DASH))
    t.add_row("Fuel Consumption", _val(s.get("fuelConsumption")))
    highway_pct = (
        round(s["lengthHighway"] / s["length"] * 100)
        if s.get("length") and s.get("lengthHighway") is not None
        else None
    )
    overspeed_pct = (
        round(s["lengthOverspeed"] / s["length"] * 100)
        if s.get("length") and s.get("lengthOverspeed") is not None
        else None
    )
    t.add_row("Highway %", f"{highway_pct}%" if highway_pct is not None else _EM_DASH)
    t.add_row("Overspeed %", f"{overspeed_pct}%" if overspeed_pct is not None else _EM_DASH)
    _console.print(Panel(t, title="Trip Summary", border_style="blue"))

    # Scores panel
    if scores:
        st = Table(show_header=False, box=None, padding=(0, 1))
        st.add_column("Key", style="bold")
        st.add_column("Value")
        st.add_row("Global", _val(scores.get("global")))
        st.add_row("Acceleration", _val(scores.get("acceleration")))
        st.add_row("Braking", _val(scores.get("braking")))
        st.add_row("Advice", _val(scores.get("advice")))
        if scores.get("constantSpeed") is not None:
            st.add_row("Constant Speed", _val(scores.get("constantSpeed")))
        _console.print(Panel(st, title="Scores", border_style="blue"))

    # HDC panel
    if hdc:
        ht = Table(show_header=False, box=None, padding=(0, 1))
        ht.add_column("Key", style="bold")
        ht.add_column("Value")
        ht.add_row("EV Time", _fmt_duration(hdc.get("evTime")))
        ht.add_row("EV Distance", _fmt_distance(hdc.get("evDistance"), dist_unit))
        ht.add_row("Charge Time", _fmt_duration(hdc.get("chargeTime")))
        ht.add_row("Charge Distance", _fmt_distance(hdc.get("chargeDist"), dist_unit))
        ht.add_row("Eco Time", _fmt_duration(hdc.get("ecoTime")))
        ht.add_row("Eco Distance", _fmt_distance(hdc.get("ecoDist"), dist_unit))
        ht.add_row("Power Time", _fmt_duration(hdc.get("powerTime")))
        ht.add_row("Power Distance", _fmt_distance(hdc.get("powerDist"), dist_unit))
        _console.print(Panel(ht, title="HDC (Hybrid Drive Cycles)", border_style="blue"))

    # Behaviours table
    if behaviours:
        bt = Table(show_header=True, header_style="bold")
        bt.add_column("Time")
        bt.add_column("Type")
        bt.add_column("Coaching Params")
        for b in behaviours:
            ts = b.get("ts")
            btype = _val(b.get("type"))
            params = b.get("coachingMsgParams") or []
            params_str = (
                ", ".join(
                    f"{p.get('name')}={p.get('value')} {p.get('unit') or ''}".strip()
                    for p in params
                )
                if params
                else _EM_DASH
            )
            bt.add_row(
                format_ts(ts, date_fmt) if ts else _EM_DASH,
                btype,
                params_str,
            )
        _console.print(Panel(bt, title="Behaviours", border_style="blue"))

    # Route breakdown
    if route:
        total_wp = len(route)
        ev_count = sum(1 for w in route if w.get("isEv"))
        hw_count = sum(1 for w in route if w.get("highway"))
        os_count = sum(1 for w in route if w.get("overspeed"))
        rt = Table(show_header=False, box=None, padding=(0, 1))
        rt.add_column("Key", style="bold")
        rt.add_column("Value")
        rt.add_row("Total Waypoints", str(total_wp))
        rt.add_row("% EV", f"{round(ev_count / total_wp * 100)}%")
        rt.add_row("% Highway", f"{round(hw_count / total_wp * 100)}%")
        rt.add_row("% Overspeed", f"{round(os_count / total_wp * 100)}%")
        _console.print(Panel(rt, title="Route Breakdown", border_style="blue"))


_RENDERERS["trips show"] = _render_trips_show


def _render_telemetry(data: dict, cfg: dict) -> None:
    """Render raw telemetry: fuel, odometer, battery."""
    date_fmt = _get_date_format(cfg)
    telemetry = data.get("telemetry")
    vin = _val(data.get("vin"))
    if telemetry is None:
        _console.print(Panel("Not available", title=f"Telemetry — {vin}", border_style="blue"))
        return
    t = Table(show_header=False, box=None, padding=(0, 1))
    t.add_column("Key", style="bold")
    t.add_column("Value")
    t.add_row("Fuel Type", _val(telemetry.get("fuelType")))
    fuel_level = telemetry.get("fuelLevel")
    t.add_row("Fuel Level", f"{fuel_level}%" if fuel_level is not None else _EM_DASH)
    t.add_row("Odometer", _fmt_quantity(telemetry.get("odometer")))
    t.add_row("Range Remaining", _fmt_quantity(telemetry.get("distanceToEmpty")))
    raw_ts = telemetry.get("timestamp")
    t.add_row("Last Updated", format_ts(raw_ts, date_fmt) if raw_ts else _EM_DASH)
    _console.print(Panel(t, title=f"Telemetry — {vin}", border_style="blue"))


_RENDERERS["telemetry"] = _render_telemetry


def _render_location(data: dict, cfg: dict) -> None:
    """Render raw location: GPS coordinates and timestamp."""
    date_fmt = _get_date_format(cfg)
    location = data.get("location")
    vin = _val(data.get("vin"))
    vehicle_loc = (location or {}).get("vehicleLocation") if location else None
    if vehicle_loc is None:
        _console.print(Panel("Not available", title=f"Location — {vin}", border_style="blue"))
        return
    t = Table(show_header=False, box=None, padding=(0, 1))
    t.add_column("Key", style="bold")
    t.add_column("Value")
    t.add_row("Latitude", _val(vehicle_loc.get("latitude")))
    t.add_row("Longitude", _val(vehicle_loc.get("longitude")))
    t.add_row("Display Name", _val(vehicle_loc.get("displayName")))
    raw_loc_ts = vehicle_loc.get("locationAcquisitionDatetime")
    t.add_row("Last Updated", format_ts(raw_loc_ts, date_fmt) if raw_loc_ts else _EM_DASH)
    _console.print(Panel(t, title=f"Location — {vin}", border_style="blue"))


_RENDERERS["location"] = _render_location


def _render_remote_status(data: dict, cfg: dict) -> None:
    """Render raw remote/lock status."""
    date_fmt = _get_date_format(cfg)
    remote = data.get("remote_status") or {}
    vin = _val(data.get("vin"))
    vehicle_status = remote.get("vehicleStatus") or []
    lock_rows: list[tuple[str, str]] = []
    for category in vehicle_status:
        for section in category.get("sections") or []:
            label = (
                section.get("section", "").replace("carstatus_item_", "").replace("_", " ").title()
            )
            values = [
                v.get("value", "").replace("carstatus_", "") for v in section.get("values") or []
            ]
            lock_rows.append((label, ", ".join(values)))

    caution_count = remote.get("cautionOverallCount")
    occurrence_date = remote.get("occurrenceDate")

    if not lock_rows and caution_count is None and not occurrence_date:
        _console.print(Panel("Not available", title=f"Remote Status — {vin}", border_style="blue"))
        return
    t = Table(show_header=False, box=None, padding=(0, 1))
    t.add_column("Key", style="bold")
    t.add_column("Value")
    for label, value in lock_rows:
        t.add_row(label, value)
    if caution_count is not None:
        t.add_row("Caution Count", str(caution_count))
    if occurrence_date:
        t.add_row("Occurrence Date", format_ts(occurrence_date, date_fmt))
    _console.print(Panel(t, title=f"Remote Status — {vin}", border_style="blue"))


_RENDERERS["remote"] = _render_remote_status


def _render_health(data: dict, cfg: dict) -> None:  # noqa: ARG001
    """Render vehicle health: warning lights and alerts."""
    health = data.get("health")
    vin = _val(data.get("vin"))
    if health is None:
        _console.print(Panel("Not available", title=f"Health — {vin}", border_style="blue"))
        return
    health_warnings = health.get("warning") or []
    if not health_warnings:
        _console.print(Panel("No warnings", title=f"Health — {vin}", border_style="green"))
    else:
        content = "\n".join(str(w) for w in health_warnings)
        _console.print(Panel(content, title=f"Health — {vin}", border_style="yellow"))


_RENDERERS["health"] = _render_health


def _render_remote(data: dict, cfg: dict) -> None:  # noqa: ARG001
    message = data.get("message", "Command sent.")
    _console.print(
        Panel(
            message,
            title="[green]Remote Command[/green]",
            border_style="green",
        )
    )


_RENDERERS["remote lock"] = _render_remote
_RENDERERS["remote unlock"] = _render_remote
_RENDERERS["remote horn"] = _render_remote
_RENDERERS["remote find"] = _render_remote
_RENDERERS["remote headlights on"] = _render_remote
_RENDERERS["remote headlights off"] = _render_remote
_RENDERERS["remote hazard on"] = _render_remote
_RENDERERS["remote hazard off"] = _render_remote


def _render_config_show(data: dict, cfg: dict) -> None:  # noqa: ARG001
    config = data.get("config") or {}
    table = Table(show_header=True, header_style="bold")
    table.add_column("Key")
    table.add_column("Value")
    table.add_column("Default")
    rows = [
        (
            "default_vin",
            str(config.get("default_vin")) if config.get("default_vin") else _EM_DASH,
            "(none)",
        ),
        ("date_format", str(config.get("date_format", DEFAULT_DATE_FORMAT)), DEFAULT_DATE_FORMAT),
        (
            "distance_unit",
            str(config.get("distance_unit", DEFAULT_DISTANCE_UNIT)),
            DEFAULT_DISTANCE_UNIT,
        ),
    ]
    for key, value, default in rows:
        table.add_row(key, value, default)
    _console.print(Panel(table, title="Config", border_style="blue"))


_RENDERERS["config show"] = _render_config_show


def _render_config_set(data: dict, cfg: dict) -> None:  # noqa: ARG001
    key = data.get("key", "")
    value = data.get("value", "")
    _console.print(
        Panel(
            f"[bold]{key}[/bold] = {value}",
            title="[green]Config Updated[/green]",
            border_style="green",
        )
    )


_RENDERERS["config set"] = _render_config_set


# Expose console instances for use in tests
def get_console() -> Console:
    return _console


def get_err_console() -> Console:
    return _err_console
