from __future__ import annotations

from collections.abc import Callable, Mapping, Sequence
from dataclasses import dataclass, field, replace

from health_client import AccountSnapshot, HealthStatus
from limit_warning import WINDOW_5H, WINDOW_7D, breached_windows

_HOT_USED_PCT = 80
_STALE_AFTER_S = 180.0
AlertKey = tuple[str, str, str]


def _noop_dismiss_alert(_kind: str, _tool: str, _slug: str) -> None:
    return


def _noop_toggle_lock(_tool: str, _slug: str) -> None:
    return


@dataclass(frozen=True)
class AccountInput:
    tool: str
    slug: str
    alias: str
    plan: str | None
    is_default: bool
    snapshot: AccountSnapshot | None
    checked_at: float | None
    busy_actions: frozenset[str]
    account_caps: Mapping[str, int] = field(default_factory=dict)
    locked: bool = False


@dataclass(frozen=True)
class ProviderInput:
    tool: str
    label: str
    accounts: tuple[AccountInput, ...]


@dataclass(frozen=True)
class QuotaBarVM:
    window_label: str
    fraction: float
    left_pct: int
    percent_text: str
    reset_text: str
    level: str
    cap_stop_at_pct: int | None = None


@dataclass(frozen=True)
class AccountVM:
    tool: str
    slug: str
    alias: str
    plan_text: str
    is_default: bool
    health: str
    health_text: str
    attention: bool
    primary: QuotaBarVM | None
    secondary: QuotaBarVM | None
    checked_text: str
    primary_action: str | None
    primary_action_label: str
    busy_actions: frozenset[str]
    locked: bool = False


@dataclass(frozen=True)
class AlertVM:
    kind: str
    tool: str
    slug: str
    title_text: str
    detail_text: str
    action: str | None
    action_label: str
    action_slug: str | None

    @property
    def dismissal_key(self) -> AlertKey:
        return self.kind, self.tool, self.slug


@dataclass(frozen=True)
class SummaryLineVM:
    text_left: str
    text_right: str


@dataclass(frozen=True)
class AlertsTabVM:
    alerts: tuple[AlertVM, ...]
    healthy_lines: tuple[SummaryLineVM, ...]
    empty_heading: str
    empty_detail: str


@dataclass(frozen=True)
class TabVM:
    id: str
    label: str
    badge_count: int
    badge_level: str


@dataclass(frozen=True)
class ProviderTabVM:
    tool: str
    accounts: tuple[AccountVM, ...]
    add_label: str


@dataclass(frozen=True)
class DashboardVM:
    tabs: tuple[TabVM, ...]
    alerts: AlertsTabVM
    providers: tuple[ProviderTabVM, ...]
    active_tab: str
    add_tools: tuple[tuple[str, str], ...]
    refresh_text: str
    busy_add_tools: frozenset[str] = frozenset()


@dataclass(frozen=True)
class DashboardCallbacks:
    on_set_default: Callable[[str, str], None]
    on_reload: Callable[[str | None, str | None], None]
    on_repair: Callable[[str, str], None]
    on_rename: Callable[[str, str], None]
    on_remove: Callable[[str, str], None]
    on_add: Callable[[str], None]
    on_open_settings: Callable[[], None]
    on_set_limits: Callable[[str, str], None]
    on_tab_changed: Callable[[str], None]
    on_dismiss_alert: Callable[[str, str, str], None] = _noop_dismiss_alert
    on_toggle_lock: Callable[[str, str], None] = _noop_toggle_lock


def build_dashboard_vm(
    providers: Sequence[ProviderInput],
    *,
    now: float,
    last_used_tab: str | None,
    warning_threshold_pct: int,
    busy_add_tools: frozenset[str] = frozenset(),
    dismissed_alert_keys: frozenset[AlertKey] = frozenset(),
) -> DashboardVM:
    provider_vms: list[ProviderTabVM] = []
    provider_tabs: list[TabVM] = []
    broken_alerts: list[AlertVM] = []
    breach_alerts: list[AlertVM] = []
    healthy_lines: list[SummaryLineVM] = []
    all_account_vms: list[AccountVM] = []
    newest_checked: float | None = None

    for provider in providers:
        account_vms: list[AccountVM] = []
        broken_on_tab = False
        attention_count = 0
        for account in provider.accounts:
            vm, breached = _account_vm(
                account, now=now, warning_threshold_pct=warning_threshold_pct
            )
            account_vms.append(vm)
            all_account_vms.append(vm)
            if account.checked_at is not None and (
                newest_checked is None or account.checked_at > newest_checked
            ):
                newest_checked = account.checked_at
            if vm.health == "broken":
                broken_on_tab = True
                broken_alerts.append(_broken_alert(account))
            elif breached and account.snapshot is not None:
                breach_alerts.append(
                    _breach_alert(
                        provider,
                        account,
                        account.snapshot,
                        breached,
                        now=now,
                        warning_threshold_pct=warning_threshold_pct,
                    )
                )
            else:
                healthy_lines.append(_summary_line(vm))
            if vm.attention:
                attention_count += 1
        provider_vms.append(
            ProviderTabVM(
                tool=provider.tool,
                accounts=tuple(account_vms),
                add_label=f"＋ Add {provider.label} account",
            )
        )
        provider_tabs.append(
            TabVM(
                id=provider.tool,
                label=provider.label,
                badge_count=attention_count,
                badge_level=_badge_level(attention_count, has_broken=broken_on_tab),
            )
        )

    alerts = tuple(
        alert
        for alert in (*broken_alerts, *breach_alerts)
        if alert.dismissal_key not in dismissed_alert_keys
    )
    has_broken_alert = any(alert.kind == "broken" for alert in alerts)
    alerts_tab = TabVM(
        id="alerts",
        label="⚠ Alerts",
        badge_count=len(alerts),
        badge_level=_badge_level(len(alerts), has_broken=has_broken_alert),
    )
    empty_heading, empty_detail = _empty_state(alerts, all_account_vms)
    alerts_vm = AlertsTabVM(
        alerts=alerts,
        healthy_lines=tuple(healthy_lines),
        empty_heading=empty_heading,
        empty_detail=empty_detail,
    )
    return DashboardVM(
        tabs=(alerts_tab, *provider_tabs),
        alerts=alerts_vm,
        providers=tuple(provider_vms),
        active_tab=_active_tab(providers, alerts, last_used_tab),
        add_tools=tuple((provider.tool, provider.label) for provider in providers),
        busy_add_tools=busy_add_tools,
        refresh_text=(
            f"⟳ {_ago_text(now - newest_checked)}"
            if newest_checked is not None
            else "⟳ never"
        ),
    )


def _account_vm(
    account: AccountInput, *, now: float, warning_threshold_pct: int
) -> tuple[AccountVM, frozenset[str]]:
    snapshot = account.snapshot
    evaluated = _evaluated_snapshot(snapshot, account.checked_at, now) if snapshot is not None else None
    breached = (
        breached_windows(evaluated, five_hour_threshold_pct=warning_threshold_pct)
        if evaluated is not None
        else frozenset()
    )
    health, health_text = _health(evaluated)
    primary: QuotaBarVM | None = None
    secondary: QuotaBarVM | None = None
    caps = account.account_caps
    if snapshot is not None:
        if snapshot.primary_used_pct is not None:
            primary = _quota_bar(
                "5h",
                snapshot.primary_used_pct,
                snapshot.primary_reset_at,
                breached=WINDOW_5H in breached,
                now=now,
                cap_stop_at_pct=caps.get(WINDOW_5H),
            )
        if snapshot.secondary_used_pct is not None:
            secondary = _quota_bar(
                "mo" if account.tool == "grok" else "7d",
                snapshot.secondary_used_pct,
                snapshot.secondary_reset_at,
                breached=WINDOW_7D in breached,
                now=now,
                cap_stop_at_pct=caps.get(WINDOW_7D),
            )
    action, action_label = _primary_action(account, evaluated, health)
    vm = AccountVM(
        tool=account.tool,
        slug=account.slug,
        alias=account.alias,
        plan_text=account.plan or "unknown",
        is_default=account.is_default,
        health=health,
        health_text=health_text,
        attention=health != "ok" or bool(breached),
        primary=primary,
        secondary=secondary,
        checked_text=_checked_text(account.checked_at, now),
        primary_action=action,
        primary_action_label=action_label,
        busy_actions=account.busy_actions,
        locked=account.locked,
    )
    return vm, breached


def _evaluated_snapshot(
    snapshot: AccountSnapshot, checked_at: float | None, now: float
) -> AccountSnapshot:
    fresh = checked_at is not None and now - checked_at <= _STALE_AFTER_S
    if (
        fresh
        and snapshot.status is HealthStatus.UNKNOWN
        and snapshot.primary_used_pct is not None
        and snapshot.secondary_used_pct is not None
    ):
        return replace(snapshot, status=HealthStatus.OK)
    return snapshot


def _health(snapshot: AccountSnapshot | None) -> tuple[str, str]:
    if snapshot is None or snapshot.status is HealthStatus.UNKNOWN:
        return "unknown", "Checking"
    if snapshot.status is HealthStatus.BROKEN:
        return "broken", "Needs repair"
    return "ok", "Healthy"


def _primary_action(
    account: AccountInput, evaluated: AccountSnapshot | None, health: str
) -> tuple[str | None, str]:
    if account.locked:
        return None, ""
    if health == "broken":
        return "repair", "Repair…"
    if (
        evaluated is not None
        and evaluated.status is HealthStatus.OK
        and not account.is_default
        and not account.locked
    ):
        return "set_default", "Set as default"
    return None, ""


def _quota_bar(
    window_label: str,
    used_pct: int,
    reset_at: float | None,
    *,
    breached: bool,
    now: float,
    cap_stop_at_pct: int | None = None,
) -> QuotaBarVM:
    bounded = _bounded(used_pct)
    if breached:
        level = "crit"
    elif bounded >= _HOT_USED_PCT:
        level = "hot"
    else:
        level = "ok"
    delta = _reset_delta(reset_at, now)
    left_pct = 100 - bounded
    percent_text = f"{left_pct}% left"
    return QuotaBarVM(
        window_label=window_label,
        fraction=left_pct / 100,
        left_pct=left_pct,
        percent_text=percent_text,
        reset_text=f"↺ {delta}" if delta is not None else "",
        level=level,
        cap_stop_at_pct=cap_stop_at_pct,
    )


def _broken_alert(account: AccountInput) -> AlertVM:
    return AlertVM(
        kind="broken",
        tool=account.tool,
        slug=account.slug,
        title_text=f"{account.tool}/{account.slug} auth expired",
        detail_text="health checks paused",
        action="repair",
        action_label="Repair…",
        action_slug=None,
    )


def _breach_alert(
    provider: ProviderInput,
    account: AccountInput,
    snapshot: AccountSnapshot,
    breached: frozenset[str],
    *,
    now: float,
    warning_threshold_pct: int,
) -> AlertVM:
    windows: list[tuple[int, int, str, str, float | None]] = []
    if WINDOW_5H in breached and snapshot.primary_used_pct is not None:
        windows.append(
            (
                _bounded(snapshot.primary_used_pct),
                0,
                WINDOW_5H,
                "5h window",
                snapshot.primary_reset_at,
            )
        )
    if WINDOW_7D in breached and snapshot.secondary_used_pct is not None:
        label = "billing period" if account.tool == "grok" else "7d window"
        windows.append(
            (
                _bounded(snapshot.secondary_used_pct),
                1,
                WINDOW_7D,
                label,
                snapshot.secondary_reset_at,
            )
        )
    pct, _, window, window_label, reset_at = max(windows, key=lambda w: (w[0], -w[1]))

    target = _switch_target(
        provider,
        account.slug,
        window=window,
        warning_threshold_pct=warning_threshold_pct,
        now=now,
    )
    detail_parts: list[str] = []
    delta = _reset_delta(reset_at, now)
    if delta is not None:
        detail_parts.append(f"resets in {delta}")
    if target is not None:
        target_account, target_left_pct = target
        detail_parts.append(f"{target_account.alias} has {target_left_pct}% left")
    return AlertVM(
        kind="breach",
        tool=account.tool,
        slug=account.slug,
        title_text=f"{account.tool}/{account.slug} {100 - pct}% left of {window_label}",
        detail_text=" · ".join(detail_parts),
        action="switch" if target is not None else None,
        action_label=f"Switch to {target[0].alias}" if target is not None else "",
        action_slug=target[0].slug if target is not None else None,
    )


def _switch_target(
    provider: ProviderInput,
    exclude_slug: str,
    *,
    window: str,
    warning_threshold_pct: int,
    now: float,
) -> tuple[AccountInput, int] | None:
    best: tuple[AccountInput, int] | None = None
    best_key: tuple[int, int, int] | None = None
    for index, candidate in enumerate(provider.accounts):
        if (
            candidate.slug == exclude_slug
            or candidate.is_default
            or candidate.locked
            or candidate.snapshot is None
        ):
            continue
        evaluated = _evaluated_snapshot(candidate.snapshot, candidate.checked_at, now)
        if evaluated.status is not HealthStatus.OK:
            continue
        if breached_windows(evaluated, five_hour_threshold_pct=warning_threshold_pct):
            continue
        used_pct = (
            evaluated.primary_used_pct
            if window == WINDOW_5H
            else evaluated.secondary_used_pct
        )
        if used_pct is None:
            continue
        other_pct = (
            evaluated.secondary_used_pct
            if window == WINDOW_5H
            else evaluated.primary_used_pct
        )
        other_key = _bounded(other_pct) if other_pct is not None else 101
        key = (_bounded(used_pct), other_key, index)
        if best_key is None or key < best_key:
            best = (candidate, 100 - _bounded(used_pct))
            best_key = key
    return best


def _summary_line(vm: AccountVM) -> SummaryLineVM:
    marker = "…" if vm.health == "unknown" else "✓"
    left = f"{marker} {vm.tool}/{vm.slug}"
    if vm.locked:
        left = f"{left} 🔒"
    if vm.is_default:
        left = f"{left} ★"
    if vm.health == "unknown":
        return SummaryLineVM(text_left=left, text_right="checking")
    parts = [
        f"{bar.window_label} {bar.percent_text}"
        for bar in (vm.primary, vm.secondary)
        if bar is not None
    ]
    return SummaryLineVM(text_left=left, text_right=" · ".join(parts))


def _empty_state(
    alerts: tuple[AlertVM, ...], accounts: Sequence[AccountVM]
) -> tuple[str, str]:
    if alerts:
        return "", ""
    if not accounts:
        return "No accounts yet", "Use ＋ Add account below to connect a provider"
    if any(vm.health == "unknown" for vm in accounts):
        return "", ""
    return "All good ✓", _empty_detail(accounts)


def _empty_detail(accounts: Sequence[AccountVM]) -> str:
    count = len(accounts)
    parts = [f"{count} account" if count == 1 else f"{count} accounts"]
    for window_label in ("5h", "7d", "mo"):
        values = [
            bar.left_pct
            for vm in accounts
            for bar in (vm.primary, vm.secondary)
            if bar is not None and bar.window_label == window_label
        ]
        if values:
            parts.append(f"lowest {window_label} {min(values)}% left")
    return " · ".join(parts)


def _badge_level(count: int, *, has_broken: bool) -> str:
    if has_broken:
        return "err"
    if count > 0:
        return "warn"
    return ""


def _active_tab(
    providers: Sequence[ProviderInput],
    alerts: tuple[AlertVM, ...],
    last_used_tab: str | None,
) -> str:
    if alerts:
        return "alerts"
    provider_ids = {provider.tool for provider in providers}
    if last_used_tab is not None and last_used_tab in provider_ids:
        return last_used_tab
    if providers:
        return providers[0].tool
    return "alerts"


def _checked_text(checked_at: float | None, now: float) -> str:
    if checked_at is None:
        return "never refreshed"
    age = now - checked_at
    text = f"⟳ {_ago_text(age)}"
    if age > _STALE_AFTER_S:
        text = f"{text} (stale)"
    return text


def _ago_text(age_seconds: float) -> str:
    seconds = max(0, int(age_seconds))
    if seconds < 60:
        return f"{seconds}s ago"
    if seconds < 3600:
        return f"{seconds // 60}m ago"
    if seconds < 86400:
        return f"{seconds // 3600}h ago"
    return f"{seconds // 86400}d ago"


def _reset_delta(reset_at: float | None, now: float) -> str | None:
    if reset_at is None:
        return None
    seconds = int(reset_at - now)
    if seconds <= 0:
        return None
    if seconds < 3600:
        minutes = max(1, (seconds + 59) // 60)
        return f"{minutes}m"
    if seconds < 86400:
        hours = seconds // 3600
        minutes = (seconds % 3600) // 60
        if minutes == 0:
            return f"{hours}h"
        return f"{hours}h{minutes}m"
    days = seconds // 86400
    hours = (seconds % 86400) // 3600
    if hours == 0:
        return f"{days}d"
    return f"{days}d{hours}h"


def _bounded(used_pct: int) -> int:
    return max(0, min(100, used_pct))
