import { homedir } from 'os'

import React, { useState, useCallback, useEffect, useRef } from 'react'
import { render, Box, Text, useInput, useApp, useWindowSize } from 'ink'
import { CATEGORY_LABELS, type DateRange, type ProjectSummary, type TaskCategory } from './types.js'
import { formatCost, formatTokens } from './format.js'
import { aggregateModelEfficiency } from './model-efficiency.js'
import { parseAllSessions, filterProjectsByName, setInteractiveScanUI } from './parser.js'
import { findUnpricedModels, loadPricing } from './models.js'
import { getAllProviders } from './providers/index.js'
import { scanAndDetect, type WasteFinding, type WasteAction, type OptimizeResult } from './optimize.js'
import { estimateContextBudget, type ContextBudget } from './context-budget.js'
import { dateKey } from './day-aggregator.js'
import { CompareView } from './compare.js'
import { getPlanUsages, type PlanUsage } from './plan-usage.js'
import { planDisplayName } from './plans.js'
import { formatDayRangeLabel, getDateRange, parseDayFlag, PERIODS, PERIOD_LABELS, shiftDay, type Period } from './cli-date.js'
import { patchStdoutForWindows } from './ink-win.js'

type View = 'dashboard' | 'optimize' | 'compare'

const MIN_WIDE = 90
const ORANGE = '#FF8C42'
const DIM = '#555555'
const GOLD = '#FFD700'
const PLAN_BAR_WIDTH = 10
const HEAVY_PERIODS = new Set<Period>(['30days', 'month', 'all'])

const LANG_DISPLAY_NAMES: Record<string, string> = {
  javascript: 'JavaScript', typescript: 'TypeScript', python: 'Python',
  rust: 'Rust', go: 'Go', java: 'Java', cpp: 'C++', c: 'C', csharp: 'C#',
  ruby: 'Ruby', php: 'PHP', swift: 'Swift', kotlin: 'Kotlin',
  html: 'HTML', css: 'CSS', scss: 'SCSS', json: 'JSON', yaml: 'YAML',
  sql: 'SQL', shell: 'Shell', shellscript: 'Shell Script', bash: 'Bash',
  typescriptreact: 'TSX', javascriptreact: 'JSX',
  markdown: 'Markdown', dockerfile: 'Dockerfile', toml: 'TOML',
}

const PANEL_COLORS = {
  overview: '#FF8C42',
  daily: '#5B9EF5',
  project: '#5BF5A0',
  model: '#E05BF5',
  activity: '#F5C85B',
  tools: '#5BF5E0',
  mcp: '#F55BE0',
  bash: '#F5A05B',
  skills: '#7B68EE',
}

const PROVIDER_COLORS: Record<string, string> = {
  claude: '#FF8C42',
  codex: '#5BF5A0',
  cursor: '#00B4D8',
  'ibm-bob': '#0F62FE',
  opencode: '#A78BFA',
  pi: '#F472B6',
  kimi: '#B6E34A',
  all: '#FF8C42',
}

const CATEGORY_COLORS: Record<TaskCategory, string> = {
  coding: '#5B9EF5',
  debugging: '#F55B5B',
  feature: '#5BF58C',
  refactoring: '#F5E05B',
  testing: '#E05BF5',
  exploration: '#5BF5E0',
  planning: '#7B9EF5',
  delegation: '#F5C85B',
  git: '#CCCCCC',
  'build/deploy': '#5BF5A0',
  conversation: '#888888',
  brainstorming: '#F55BE0',
  general: '#666666',
}

const IMPACT_PANEL_COLORS: Record<string, string> = { high: '#F55B5B', medium: ORANGE, low: DIM }

function toHex(r: number, g: number, b: number): string {
  return '#' + [r, g, b].map(v => Math.round(v).toString(16).padStart(2, '0')).join('')
}

function lerp(a: number, b: number, t: number): number {
  return a + t * (b - a)
}

function gradientColor(pct: number): string {
  if (pct <= 0.33) {
    const t = pct / 0.33
    return toHex(lerp(91, 245, t), lerp(158, 200, t), lerp(245, 91, t))
  }
  if (pct <= 0.66) {
    const t = (pct - 0.33) / 0.33
    return toHex(lerp(245, 255, t), lerp(200, 140, t), lerp(91, 66, t))
  }
  const t = (pct - 0.66) / 0.34
  return toHex(lerp(255, 245, t), lerp(140, 91, t), lerp(66, 91, t))
}

function getPeriodRange(period: Period): { start: Date; end: Date } {
  return getDateRange(period).range
}

function getDayRange(day: string): DateRange {
  return parseDayFlag(day)!.range
}

function isHeavyPeriod(period: Period): boolean {
  return HEAVY_PERIODS.has(period)
}

function nextTick(): Promise<void> {
  return new Promise(resolve => setImmediate(resolve))
}

type Layout = { dashWidth: number; wide: boolean; halfWidth: number; barWidth: number }

function getLayout(columns?: number): Layout {
  const termWidth = columns || parseInt(process.env['COLUMNS'] ?? '') || 80
  const dashWidth = Math.min(160, termWidth)
  const wide = dashWidth >= MIN_WIDE
  const halfWidth = wide ? Math.floor(dashWidth / 2) : dashWidth
  const inner = halfWidth - 4
  const barWidth = Math.max(6, Math.min(10, inner - 30))
  return { dashWidth, wide, halfWidth, barWidth }
}

function HBar({ value, max, width }: { value: number; max: number; width: number }) {
  if (max === 0) return <Text color={DIM}>{'░'.repeat(width)}</Text>
  const filled = Math.round((value / max) * width)
  const fillChars: React.ReactNode[] = []
  for (let i = 0; i < Math.min(filled, width); i++) {
    fillChars.push(<Text key={i} color={gradientColor(i / width)}>{'█'}</Text>)
  }
  return (
    <Text>
      {fillChars}
      <Text color="#333333">{'░'.repeat(Math.max(width - filled, 0))}</Text>
    </Text>
  )
}

const PANEL_CHROME = 4

function Panel({ title, color, children, width }: { title: string; color: string; children: React.ReactNode; width: number }) {
  return (
    <Box flexDirection="column" borderStyle="round" borderColor={color} paddingX={1} width={width} overflowX="hidden">
      <Text bold color={color}>{title}</Text>
      {children}
    </Box>
  )
}

function fit(s: string, n: number): string {
  return s.length > n ? s.slice(0, n) : s.padEnd(n)
}

function renderPlanBar(percentUsed: number, width: number): string {
  if (percentUsed <= 100) {
    const capped = Math.max(0, percentUsed)
    const filled = Math.round((capped / 100) * width)
    return `${'▓'.repeat(filled)}${'░'.repeat(Math.max(0, width - filled))}`
  }
  const factor = percentUsed / 100
  const chevrons = Math.min(4, Math.max(1, Math.floor(Math.log10(factor)) + 1))
  return `${'▓'.repeat(width)}${'▶'.repeat(chevrons)}`
}

function planLabel(planUsage: PlanUsage): string {
  const name = planDisplayName(planUsage.plan.id)
  return planUsage.plan.id === 'custom' ? `${name} (${planUsage.plan.provider})` : name
}

function planColor(planUsage: PlanUsage): string {
  return planUsage.status === 'over'
    ? '#F55B5B'
    : planUsage.status === 'near'
      ? ORANGE
      : '#5BF58C'
}

function planStatusText(planUsage: PlanUsage): string {
  if (planUsage.status === 'under') {
    return `Well within plan. Projected month: ${formatCost(planUsage.projectedMonthUsd)} (reset in ${planUsage.daysUntilReset} days).`
  }
  if (planUsage.status === 'near') {
    return `Approaching plan limit. Projected month: ${formatCost(planUsage.projectedMonthUsd)} (reset in ${planUsage.daysUntilReset} days).`
  }
  return `${(planUsage.spentApiEquivalentUsd / Math.max(planUsage.budgetUsd, 1)).toFixed(1)}x your subscription value. Projected month: ${formatCost(planUsage.projectedMonthUsd)} (reset in ${planUsage.daysUntilReset} days).`
}

function Overview({ projects, label, width, planUsages }: { projects: ProjectSummary[]; label: string; width: number; planUsages?: PlanUsage[] }) {
  const totalCost = projects.reduce((s, p) => s + p.totalCostUSD, 0)
  const totalSavings = projects.reduce((s, p) => s + p.totalSavingsUSD, 0)
  const totalCalls = projects.reduce((s, p) => s + p.totalApiCalls, 0)
  const totalSessions = projects.reduce((s, p) => s + p.sessions.length, 0)
  const allSessions = projects.flatMap(p => p.sessions)
  const totalInput = allSessions.reduce((s, sess) => s + sess.totalInputTokens, 0)
  const totalOutput = allSessions.reduce((s, sess) => s + sess.totalOutputTokens, 0)
  const totalCacheRead = allSessions.reduce((s, sess) => s + sess.totalCacheReadTokens, 0)
  const totalCacheWrite = allSessions.reduce((s, sess) => s + sess.totalCacheWriteTokens, 0)
  const allInputTokens = totalInput + totalCacheRead + totalCacheWrite
  const cacheHit = allInputTokens > 0
    ? (totalCacheRead / allInputTokens) * 100 : 0
  const activePlanUsages = planUsages ?? []

  return (
    <Box flexDirection="column" borderStyle="round" borderColor={PANEL_COLORS.overview} paddingX={1} width={width}>
      <Text wrap="truncate-end">
        <Text bold color={ORANGE}>CodeBurn</Text>
        <Text dimColor>  {label}</Text>
      </Text>
      <Text wrap="truncate-end">
        <Text bold color={GOLD}>{formatCost(totalCost)}</Text>
        <Text dimColor> cost   </Text>
        <Text bold>{totalCalls.toLocaleString()}</Text>
        <Text dimColor> calls   </Text>
        <Text bold>{String(totalSessions)}</Text>
        <Text dimColor> sessions   </Text>
        <Text bold>{cacheHit.toFixed(1)}%</Text>
        <Text dimColor> cache hit</Text>
      </Text>
      <Text dimColor wrap="truncate-end">
        {formatTokens(totalInput)} in   {formatTokens(totalOutput)} out   {formatTokens(totalCacheRead)} cached   {formatTokens(totalCacheWrite)} written
      </Text>
      {totalSavings > 0 && (
        <Text wrap="truncate-end">
          <Text color="green">{formatCost(totalSavings)}</Text>
          <Text dimColor> saved by local models</Text>
        </Text>
      )}
      {activePlanUsages.length > 0 && (
        <>
          {activePlanUsages.map(planUsage => {
            const color = planColor(planUsage)
            return (
              <React.Fragment key={planUsage.plan.provider}>
                <Text wrap="truncate-end">
                  <Text color={color}>{planLabel(planUsage)}: {formatCost(planUsage.spentApiEquivalentUsd)} API-equivalent vs {formatCost(planUsage.budgetUsd)} plan</Text>
                  <Text>  </Text>
                  <Text color={color}>{renderPlanBar(planUsage.percentUsed, PLAN_BAR_WIDTH)}</Text>
                  <Text> </Text>
                  <Text bold color={color}>{planUsage.percentUsed.toFixed(1)}%</Text>
                </Text>
                <Text dimColor wrap="truncate-end">{planStatusText(planUsage)}</Text>
              </React.Fragment>
            )
          })}
        </>
      )}
    </Box>
  )
}

function DailyActivity({ projects, days = 14, pw, bw }: { projects: ProjectSummary[]; days?: number; pw: number; bw: number }) {
  const dailyCosts: Record<string, number> = {}
  const dailyCalls: Record<string, number> = {}
  for (const project of projects) {
    for (const session of project.sessions) {
      for (const turn of session.turns) {
        if (!turn.timestamp) continue
        const day = dateKey(turn.timestamp)
        dailyCosts[day] = (dailyCosts[day] ?? 0) + turn.assistantCalls.reduce((s, c) => s + c.costUSD, 0)
        dailyCalls[day] = (dailyCalls[day] ?? 0) + turn.assistantCalls.length
      }
    }
  }
  const sortedDays = days !== undefined ? Object.keys(dailyCosts).sort().slice(-days) : Object.keys(dailyCosts).sort()
  const maxCost = Math.max(...sortedDays.map(d => dailyCosts[d] ?? 0))

  return (
    <Panel title="Daily Activity" color={PANEL_COLORS.daily} width={pw}>
      <Text dimColor wrap="truncate-end">{''.padEnd(6 + bw)}{'cost'.padStart(8)}{'calls'.padStart(6)}</Text>
      {sortedDays.map(day => (
        <Text key={day} wrap="truncate-end">
          <Text dimColor>{day.slice(5)} </Text>
          <HBar value={dailyCosts[day] ?? 0} max={maxCost} width={bw} />
          <Text color={GOLD}>{formatCost(dailyCosts[day] ?? 0).padStart(8)}</Text>
          <Text>{String(dailyCalls[day] ?? 0).padStart(6)}</Text>
        </Text>
      ))}
    </Panel>
  )
}

const _home = homedir()
const _homePrefix = _home.endsWith('/') ? _home : _home + '/'

export function shortProject(absPath: string): string {
  const normalized = absPath.replace(/\\/g, '/')
  let path: string
  if (normalized === _home) path = ''
  else if (normalized.startsWith(_homePrefix)) path = normalized.slice(_homePrefix.length)
  else path = normalized
  path = path.replace(/^\/+/, '')
  path = path.replace(/^private\/tmp\/[^/]+\/[^/]+\//, '').replace(/^private\/tmp\//, '').replace(/^tmp\//, '')
  if (!path) return 'home'
  const parts = path.split('/').filter(Boolean)
  if (parts.length <= 3) return parts.join('/')
  return parts.slice(-3).join('/')
}

const PROJECT_COL_AVG = 7
const PROJECT_COL_BASE_WIDTH = 30
const PROJECT_COL_WITH_OVERHEAD_WIDTH = 40

function ProjectBreakdown({ projects, pw, bw, budgets, rows = 14 }: { projects: ProjectSummary[]; pw: number; bw: number; budgets?: Map<string, ContextBudget>; rows?: number }) {
  const maxCost = Math.max(...projects.map(p => p.totalCostUSD))
  const hasBudgets = budgets && budgets.size > 0
  const nw = Math.max(8, pw - bw - (hasBudgets ? PROJECT_COL_WITH_OVERHEAD_WIDTH : PROJECT_COL_BASE_WIDTH))
  return (
    <Panel title="By Project" color={PANEL_COLORS.project} width={pw}>
      <Text dimColor wrap="truncate-end">
        {''.padEnd(bw + 1 + nw)}{'cost'.padStart(8)}{'avg/s'.padStart(PROJECT_COL_AVG)}{'sess'.padStart(6)}{hasBudgets ? 'overhead'.padStart(10) : ''}
      </Text>
      {projects.slice(0, rows).map((project, i) => {
        const budget = budgets?.get(project.project)
        const avgCost = project.sessions.length > 0
          ? formatCost(project.totalCostUSD / project.sessions.length)
          : '-'
        return (
          <Text key={`${project.project}-${i}`} wrap="truncate-end">
            <HBar value={project.totalCostUSD} max={maxCost} width={bw} />
            <Text dimColor> {fit(shortProject(project.projectPath), nw)}</Text>
            <Text color={GOLD}>{formatCost(project.totalCostUSD).padStart(8)}</Text>
            <Text color={GOLD}>{avgCost.padStart(PROJECT_COL_AVG)}</Text>
            <Text>{String(project.sessions.length).padStart(6)}</Text>
            {hasBudgets && <Text color="#7B9EF5">{(budget ? formatTokens(budget.total) : '-').padStart(10)}</Text>}
          </Text>
        )
      })}
    </Panel>
  )
}

const MODEL_COL_COST = 8
const MODEL_COL_CACHE = 7
const MODEL_COL_CALLS = 7
const MODEL_COL_ONESHOT = 7
const MODEL_NAME_WIDTH = 14
const MIN_EDIT_TURNS_FOR_RATE = 5

function ModelBreakdown({ projects, pw, bw }: { projects: ProjectSummary[]; pw: number; bw: number }) {
  const modelTotals: Record<string, { calls: number; costUSD: number; freshInput: number; cacheRead: number; cacheWrite: number }> = {}
  const modelEfficiency = aggregateModelEfficiency(projects)
  for (const project of projects) {
    for (const session of project.sessions) {
      for (const [model, data] of Object.entries(session.modelBreakdown)) {
        if (!modelTotals[model]) modelTotals[model] = { calls: 0, costUSD: 0, freshInput: 0, cacheRead: 0, cacheWrite: 0 }
        modelTotals[model].calls += data.calls
        modelTotals[model].costUSD += data.costUSD
        modelTotals[model].freshInput += data.tokens.inputTokens
        modelTotals[model].cacheRead += data.tokens.cacheReadInputTokens
        modelTotals[model].cacheWrite += data.tokens.cacheCreationInputTokens
      }
    }
  }
  const sorted = Object.entries(modelTotals).sort(([, a], [, b]) => b.costUSD - a.costUSD)
  const maxCost = sorted[0]?.[1]?.costUSD ?? 0
  const unpriced = findUnpricedModels(Object.entries(modelTotals).map(([model, d]) => ({
    model,
    calls: d.calls,
    cost: d.costUSD,
    tokens: d.freshInput + d.cacheRead + d.cacheWrite,
  })))

  return (
    <Panel title="By Model" color={PANEL_COLORS.model} width={pw}>
      <Text dimColor wrap="truncate-end">{''.padEnd(bw + 1 + MODEL_NAME_WIDTH)}{'cost'.padStart(MODEL_COL_COST)}{'cache'.padStart(MODEL_COL_CACHE)}{'calls'.padStart(MODEL_COL_CALLS)}{'1-shot'.padStart(MODEL_COL_ONESHOT)}</Text>
      {sorted.map(([model, data], i) => {
        const totalInput = data.freshInput + data.cacheRead + data.cacheWrite
        const cacheHit = totalInput > 0 ? (data.cacheRead / totalInput) * 100 : 0
        const cacheLabel = totalInput > 0 ? `${cacheHit.toFixed(1)}%` : '-'
        const efficiency = modelEfficiency.get(model)
        const oneShotLabel = efficiency && efficiency.editTurns >= MIN_EDIT_TURNS_FOR_RATE && efficiency.oneShotRate !== null
          ? `${efficiency.oneShotRate.toFixed(1)}%`
          : '-'
        return (
          <Text key={`${model}-${i}`} wrap="truncate-end">
            <HBar value={data.costUSD} max={maxCost} width={bw} />
            <Text> {fit(model, MODEL_NAME_WIDTH)}</Text>
            <Text color={GOLD}>{formatCost(data.costUSD).padStart(MODEL_COL_COST)}</Text>
            <Text>{cacheLabel.padStart(MODEL_COL_CACHE)}</Text>
            <Text>{String(data.calls).padStart(MODEL_COL_CALLS)}</Text>
            <Text>{oneShotLabel.padStart(MODEL_COL_ONESHOT)}</Text>
          </Text>
        )
      })}
      {unpriced.length > 0 && (
        <Text color="yellow" wrap="truncate-end">
          {`! ${unpriced.length} model${unpriced.length === 1 ? '' : 's'} unpriced at $0, fix: codeburn model-alias (${unpriced.slice(0, 2).map(u => u.model).join(', ')}${unpriced.length > 2 ? ', ...' : ''})`}
        </Text>
      )}
    </Panel>
  )
}

const SKILL_SUB_ROWS_LIMIT = 5

function ActivityBreakdown({ projects, pw, bw }: { projects: ProjectSummary[]; pw: number; bw: number }) {
  const categoryTotals: Record<string, { turns: number; costUSD: number; editTurns: number; oneShotTurns: number }> = {}
  const skillTotals: Record<string, { turns: number; costUSD: number; editTurns: number; oneShotTurns: number }> = {}
  for (const project of projects) {
    for (const session of project.sessions) {
      for (const [cat, data] of Object.entries(session.categoryBreakdown)) {
        if (!categoryTotals[cat]) categoryTotals[cat] = { turns: 0, costUSD: 0, editTurns: 0, oneShotTurns: 0 }
        categoryTotals[cat].turns += data.turns
        categoryTotals[cat].costUSD += data.costUSD
        categoryTotals[cat].editTurns += data.editTurns
        categoryTotals[cat].oneShotTurns += data.oneShotTurns
      }
      for (const [skill, data] of Object.entries(session.skillBreakdown ?? {})) {
        if (!skillTotals[skill]) skillTotals[skill] = { turns: 0, costUSD: 0, editTurns: 0, oneShotTurns: 0 }
        skillTotals[skill].turns += data.turns
        skillTotals[skill].costUSD += data.costUSD
        skillTotals[skill].editTurns += data.editTurns
        skillTotals[skill].oneShotTurns += data.oneShotTurns
      }
    }
  }
  const sorted = Object.entries(categoryTotals).sort(([, a], [, b]) => b.costUSD - a.costUSD)
  const sortedSkills = Object.entries(skillTotals).sort(([, a], [, b]) => b.costUSD - a.costUSD).slice(0, SKILL_SUB_ROWS_LIMIT)
  const maxCost = sorted[0]?.[1]?.costUSD ?? 0
  return (
    <Panel title="By Activity" color={PANEL_COLORS.activity} width={pw}>
      <Text dimColor wrap="truncate-end">{''.padEnd(bw + 14)}{'cost'.padStart(8)}{'turns'.padStart(6)}{'1-shot'.padStart(7)}</Text>
      {sorted.flatMap(([cat, data]) => {
        const oneShotPct = data.editTurns > 0 ? Math.round((data.oneShotTurns / data.editTurns) * 100) + '%' : '-'
        const rows = [
          <Text key={cat} wrap="truncate-end">
            <HBar value={data.costUSD} max={maxCost} width={bw} />
            <Text color={CATEGORY_COLORS[cat as TaskCategory] ?? '#666666'}> {fit(CATEGORY_LABELS[cat as TaskCategory] ?? cat, 13)}</Text>
            <Text color={GOLD}>{formatCost(data.costUSD).padStart(8)}</Text>
            <Text>{String(data.turns).padStart(6)}</Text>
            <Text color={data.editTurns === 0 ? DIM : oneShotPct === '100%' ? '#5BF58C' : ORANGE}>{String(oneShotPct).padStart(7)}</Text>
          </Text>,
        ]
        if (cat === 'general' && sortedSkills.length > 0) {
          for (const [skill, sd] of sortedSkills) {
            const subPct = sd.editTurns > 0 ? Math.round((sd.oneShotTurns / sd.editTurns) * 100) + '%' : '-'
            rows.push(
              <Text key={`${cat}:${skill}`} wrap="truncate-end" dimColor>
                <HBar value={sd.costUSD} max={maxCost} width={bw} />
                <Text> {fit(`  /${skill}`, 13)}</Text>
                <Text>{formatCost(sd.costUSD).padStart(8)}</Text>
                <Text>{String(sd.turns).padStart(6)}</Text>
                <Text>{String(subPct).padStart(7)}</Text>
              </Text>,
            )
          }
        }
        return rows
      })}
    </Panel>
  )
}

function ToolBreakdown({ projects, pw, bw, title, filterPrefix }: { projects: ProjectSummary[]; pw: number; bw: number; title?: string; filterPrefix?: string }) {
  const toolTotals: Record<string, number> = {}
  for (const project of projects) {
    for (const session of project.sessions) {
      for (const [tool, data] of Object.entries(session.toolBreakdown)) {
        if (filterPrefix) { if (!tool.startsWith(filterPrefix)) continue } else { if (tool.startsWith('lang:')) continue }
        toolTotals[tool] = (toolTotals[tool] ?? 0) + data.calls
      }
    }
  }
  const sorted = Object.entries(toolTotals).sort(([, a], [, b]) => b - a)
  const maxCalls = sorted[0]?.[1] ?? 0
  const nw = Math.max(6, pw - bw - 15)
  return (
    <Panel title={title ?? 'Core Tools'} color={PANEL_COLORS.tools} width={pw}>
      <Text dimColor wrap="truncate-end">{''.padEnd(bw + 1 + nw)}{'calls'.padStart(7)}</Text>
      {sorted.slice(0, 10).map(([tool, calls]) => {
        const raw = filterPrefix ? tool.slice(filterPrefix.length) : tool
        const display = filterPrefix ? (LANG_DISPLAY_NAMES[raw] ?? raw) : raw
        return (
          <Text key={tool} wrap="truncate-end">
            <HBar value={calls} max={maxCalls} width={bw} />
            <Text> {fit(display, nw)}</Text>
            <Text>{String(calls).padStart(7)}</Text>
          </Text>
        )
      })}
    </Panel>
  )
}


function McpBreakdown({ projects, pw, bw }: { projects: ProjectSummary[]; pw: number; bw: number }) {
  const mcpTotals: Record<string, number> = {}
  for (const project of projects) { for (const session of project.sessions) { for (const [server, data] of Object.entries(session.mcpBreakdown)) { mcpTotals[server] = (mcpTotals[server] ?? 0) + data.calls } } }
  const sorted = Object.entries(mcpTotals).sort(([, a], [, b]) => b - a)
  if (sorted.length === 0) return <Panel title="MCP Servers" color={PANEL_COLORS.mcp} width={pw}><Text dimColor>No MCP usage</Text></Panel>
  const maxCalls = sorted[0]?.[1] ?? 0
  const nw = Math.max(6, pw - bw - 15)
  return (
    <Panel title="MCP Servers" color={PANEL_COLORS.mcp} width={pw}>
      <Text dimColor wrap="truncate-end">{''.padEnd(bw + 1 + nw)}{'calls'.padStart(6)}</Text>
      {sorted.slice(0, 8).map(([server, calls]) => (
        <Text key={server} wrap="truncate-end"><HBar value={calls} max={maxCalls} width={bw} /><Text> {fit(server, nw)}</Text><Text>{String(calls).padStart(6)}</Text></Text>
      ))}
    </Panel>
  )
}

function BashBreakdown({ projects, pw, bw }: { projects: ProjectSummary[]; pw: number; bw: number }) {
  const bashTotals: Record<string, number> = {}
  for (const project of projects) { for (const session of project.sessions) { for (const [cmd, data] of Object.entries(session.bashBreakdown)) { bashTotals[cmd] = (bashTotals[cmd] ?? 0) + data.calls } } }
  const sorted = Object.entries(bashTotals).sort(([, a], [, b]) => b - a)
  if (sorted.length === 0) return <Panel title="Shell Commands" color={PANEL_COLORS.bash} width={pw}><Text dimColor>No shell commands</Text></Panel>
  const maxCalls = sorted[0]?.[1] ?? 0
  const nw = Math.max(6, pw - bw - 15)
  return (
    <Panel title="Shell Commands" color={PANEL_COLORS.bash} width={pw}>
      <Text dimColor wrap="truncate-end">{''.padEnd(bw + 1 + nw)}{'calls'.padStart(7)}</Text>
      {sorted.slice(0, 10).map(([cmd, calls]) => (
        <Text key={cmd} wrap="truncate-end"><HBar value={calls} max={maxCalls} width={bw} /><Text> {fit(cmd, nw)}</Text><Text>{String(calls).padStart(7)}</Text></Text>
      ))}
    </Panel>
  )
}

function SkillsAndAgents({ projects, pw, bw }: { projects: ProjectSummary[]; pw: number; bw: number }) {
  const merged: Record<string, { uses: number; cost: number }> = {}
  for (const project of projects) { for (const session of project.sessions) {
    for (const [skill, d] of Object.entries(session.skillBreakdown)) { const e = merged[skill] ?? { uses: 0, cost: 0 }; e.uses += d.turns; e.cost += d.costUSD; merged[skill] = e }
    for (const [agent, d] of Object.entries(session.subagentBreakdown)) { const e = merged[agent] ?? { uses: 0, cost: 0 }; e.uses += d.calls; e.cost += d.costUSD; merged[agent] = e }
  } }
  const sorted = Object.entries(merged).sort(([, a], [, b]) => b.cost - a.cost)
  if (sorted.length === 0) return <Panel title="Skills & Agents" color={PANEL_COLORS.skills} width={pw}><Text dimColor>No skill/agent usage</Text></Panel>
  const maxCost = sorted[0]?.[1]?.cost ?? 0
  const nw = Math.max(6, pw - bw - 22)
  return (
    <Panel title="Skills & Agents" color={PANEL_COLORS.skills} width={pw}>
      <Text dimColor wrap="truncate-end">{''.padEnd(bw + 1 + nw)}{'uses'.padStart(6)}{'cost'.padStart(8)}</Text>
      {sorted.slice(0, 10).map(([name, d]) => (
        <Text key={name} wrap="truncate-end"><HBar value={d.cost} max={maxCost} width={bw} /><Text> {fit(name, nw)}</Text><Text>{String(d.uses).padStart(6)}</Text><Text color={GOLD}>{formatCost(d.cost).padStart(8)}</Text></Text>
      ))}
    </Panel>
  )
}

// Claude Code only: real subagent-transcript spend by agentType
// (workflow-subagent / Explore / general-purpose / …). Returns null when there
// are no agent transcripts, so it never shows for other providers.
function ClaudeAgentTypes({ projects, pw, bw }: { projects: ProjectSummary[]; pw: number; bw: number }) {
  const merged: Record<string, { uses: number; cost: number }> = {}
  for (const project of projects) { for (const session of project.sessions) {
    if (!session.agentType) continue
    const e = merged[session.agentType] ?? { uses: 0, cost: 0 }
    e.uses += session.apiCalls; e.cost += session.totalCostUSD; merged[session.agentType] = e
  } }
  const sorted = Object.entries(merged).sort(([, a], [, b]) => b.cost - a.cost)
  if (sorted.length === 0) return null
  const maxCost = sorted[0]?.[1]?.cost ?? 0
  const nw = Math.max(6, pw - bw - 22)
  return (
    <Panel title="Claude Agent Types" color={PANEL_COLORS.skills} width={pw}>
      <Text dimColor wrap="truncate-end">{''.padEnd(bw + 1 + nw)}{'calls'.padStart(6)}{'cost'.padStart(8)}</Text>
      {sorted.slice(0, 10).map(([name, d]) => (
        <Text key={name} wrap="truncate-end"><HBar value={d.cost} max={maxCost} width={bw} /><Text> {fit(name, nw)}</Text><Text>{String(d.uses).padStart(6)}</Text><Text color={GOLD}>{formatCost(d.cost).padStart(8)}</Text></Text>
      ))}
    </Panel>
  )
}

const PROVIDER_DISPLAY_NAMES: Record<string, string> = {
  all: 'All',
  claude: 'Claude',
  codex: 'Codex',
  cursor: 'Cursor',
  'ibm-bob': 'IBM Bob',
  opencode: 'OpenCode',
  pi: 'Pi',
  kimi: 'Kimi',
}
function getProviderDisplayName(name: string): string { return PROVIDER_DISPLAY_NAMES[name] ?? name }

function PeriodTabs({ active, providerName, showProvider }: { active: Period; providerName?: string; showProvider?: boolean }) {
  return (
    <Box justifyContent="space-between" paddingX={1}>
      <Box gap={1}>
        {PERIODS.map(p => (
          <Text key={p} bold={active === p} color={active === p ? ORANGE : DIM}>
            {active === p ? `[ ${PERIOD_LABELS[p]} ]` : `  ${PERIOD_LABELS[p]}  `}
          </Text>
        ))}
      </Box>
      {showProvider && providerName && (
        <Box><Text color={DIM}>|  </Text><Text color={ORANGE} bold>[p]</Text><Text bold color={PROVIDER_COLORS[providerName] ?? ORANGE}> {getProviderDisplayName(providerName)}</Text></Box>
      )}
    </Box>
  )
}

/// Header for an action's intended destination. Helps users distinguish a
/// permanent CLAUDE.md rule from a one-time session opener so they don't
/// accidentally bake a single-run constraint into their project's permanent
/// instructions. Issue #277.
function actionDestinationHeader(action: WasteAction): string {
  switch (action.type) {
    case 'file-content':
      return `── Suggested ${action.path} addition `.padEnd(64, '─')
    case 'command':
      return '── Run this command '.padEnd(64, '─')
    case 'paste': {
      switch (action.destination) {
        case 'claude-md':
          return '── Suggested CLAUDE.md addition (permanent rule) '.padEnd(64, '─')
        case 'session-opener':
          return '── One-time session opener (do not add to CLAUDE.md) '.padEnd(64, '─')
        case 'prompt':
          return '── Ask Claude in the current session '.padEnd(64, '─')
        case 'shell-config':
          return '── Add to your shell config '.padEnd(64, '─')
        default:
          return '── Suggested action '.padEnd(64, '─')
      }
    }
  }
}

function FindingAction({ action }: { action: WasteAction }) {
  const lines = action.type === 'file-content' ? action.content.split('\n') : action.type === 'command' ? action.text.split('\n') : [action.text]
  const header = actionDestinationHeader(action)
  return (
    <>
      <Text color={ORANGE}>{header}</Text>
      <Text dimColor>{action.label}</Text>
      {lines.map((line, i) => <Text key={i} color="#5BF5E0">  {line}</Text>)}
    </>
  )
}

function FindingPanel({ index, finding, costRate, width }: { index: number; finding: WasteFinding; costRate: number; width: number }) {
  const costSaved = finding.tokensSaved * costRate
  const color = IMPACT_PANEL_COLORS[finding.impact] ?? DIM
  const label = finding.impact.charAt(0).toUpperCase() + finding.impact.slice(1)
  const trendBadge = finding.trend === 'improving' ? ' improving \u2193' : ''
  return (
    <Box flexDirection="column" borderStyle="round" borderColor={color} paddingX={1} width={width}>
      <Text wrap="truncate-end">
        <Text bold>{index}. {finding.title}</Text>
        <Text>  </Text>
        <Text color={color}>{label}</Text>
        {trendBadge && <Text color="#5BF5A0">{trendBadge}</Text>}
      </Text>
      <Text dimColor wrap="wrap">{finding.explanation}</Text>
      <Text color={GOLD}>Savings: ~{formatTokens(finding.tokensSaved)} tokens (~{formatCost(costSaved)})</Text>
      <Text> </Text>
      <FindingAction action={finding.fix} />
    </Box>
  )
}

const GRADE_COLORS: Record<string, string> = { A: '#5BF5A0', B: '#5BF5A0', C: GOLD, D: ORANGE, F: '#F55B5B' }

// Each finding panel takes ~6-8 lines. Show 3 at a time so the window fits a
// 30-line terminal alongside the optimize header + status bar; users page
// with j/k. Without this cap, 4 new detectors + 7 originals scrolled findings
// off the alt-buffer top and the user couldn't see the StatusBar at all.
const FINDINGS_WINDOW_SIZE = 3

function OptimizeView({ findings, costRate, projects, label, width, healthScore, healthGrade, cursor }: { findings: WasteFinding[]; costRate: number; projects: ProjectSummary[]; label: string; width: number; healthScore: number; healthGrade: string; cursor: number }) {
  const periodCost = projects.reduce((s, p) => s + p.totalCostUSD, 0)
  const totalTokens = findings.reduce((s, f) => s + f.tokensSaved, 0)
  const totalCost = totalTokens * costRate
  const pctRaw = periodCost > 0 ? (totalCost / periodCost) * 100 : 0
  const pct = pctRaw >= 1 ? pctRaw.toFixed(0) : pctRaw.toFixed(1)
  const gradeColor = GRADE_COLORS[healthGrade] ?? DIM
  const total = findings.length
  const start = total === 0 ? 0 : Math.min(cursor, Math.max(0, total - FINDINGS_WINDOW_SIZE))
  const end = Math.min(start + FINDINGS_WINDOW_SIZE, total)
  const visible = findings.slice(start, end)
  return (
    <Box flexDirection="column" width={width}>
      <Box flexDirection="column" borderStyle="round" borderColor={ORANGE} paddingX={1} width={width}>
        <Text wrap="truncate-end">
          <Text bold color={ORANGE}>CodeBurn Optimize</Text>
          <Text dimColor>  {label}   Setup: </Text>
          <Text bold color={gradeColor}>{healthGrade}</Text>
          <Text dimColor> ({healthScore}/100)</Text>
        </Text>
        <Text color="#5BF5A0" wrap="truncate-end">Savings: ~{formatTokens(totalTokens)} tokens (~{formatCost(totalCost)}, ~{pct}% of spend)</Text>
        {total > FINDINGS_WINDOW_SIZE && (
          <Text dimColor>Showing {start + 1}–{end} of {total} · j/k to scroll</Text>
        )}
      </Box>
      {visible.map((f, i) => <FindingPanel key={start + i} index={start + i + 1} finding={f} costRate={costRate} width={width} />)}
      <Box paddingX={1} width={width}><Text dimColor>Token estimates are approximate.</Text></Box>
    </Box>
  )
}

function StatusBar({ width, showProvider, view, findingCount, optimizeAvailable, compareAvailable, customRange, dayMode }: { width: number; showProvider?: boolean; view?: View; findingCount?: number; optimizeAvailable?: boolean; compareAvailable?: boolean; customRange?: boolean; dayMode?: boolean }) {
  const isOptimize = view === 'optimize'
  return (
    <Box borderStyle="round" borderColor={DIM} width={width} justifyContent="center" paddingX={1}>
      <Text>
        {isOptimize
          ? <><Text color={ORANGE} bold>b</Text><Text dimColor> back   </Text><Text color={ORANGE} bold>j</Text><Text dimColor>/</Text><Text color={ORANGE} bold>k</Text><Text dimColor> scroll   </Text></>
          : dayMode
            ? <><Text color={ORANGE} bold>{'<'}</Text><Text color={ORANGE}>{'>'}</Text><Text dimColor> day   </Text></>
            : !customRange
            ? <><Text color={ORANGE} bold>{'<'}</Text><Text color={ORANGE}>{'>'}</Text><Text dimColor> switch   </Text></>
            : null}
        <Text color={ORANGE} bold>q</Text><Text dimColor> quit</Text>
        {!customRange && !isOptimize && (
          <>
            <Text dimColor>   </Text><Text color={ORANGE} bold>1</Text><Text dimColor> today   </Text>
            <Text color={ORANGE} bold>2</Text><Text dimColor> week   </Text>
            <Text color={ORANGE} bold>3</Text><Text dimColor> 30 days   </Text>
            <Text color={ORANGE} bold>4</Text><Text dimColor> month   </Text>
            <Text color={ORANGE} bold>5</Text><Text dimColor> 6 months</Text>
          </>
        )}
        {!customRange && !isOptimize && (
          <>
            <Text dimColor>   </Text><Text color={ORANGE} bold>d</Text><Text dimColor>{dayMode ? ' exit day' : ' yesterday'}</Text>
          </>
        )}
        {!isOptimize && optimizeAvailable && (
          <><Text dimColor>   </Text><Text color={ORANGE} bold>o</Text><Text dimColor> optimize</Text>{findingCount != null && findingCount > 0 ? <Text color="#F55B5B"> ({findingCount})</Text> : null}</>
        )}
        {!isOptimize && compareAvailable && (
          <><Text dimColor>   </Text><Text color={ORANGE} bold>c</Text><Text dimColor> compare</Text></>
        )}
        {showProvider && (<><Text dimColor>   </Text><Text color={ORANGE} bold>p</Text><Text dimColor> provider</Text></>)}
      </Text>
    </Box>
  )
}

function Row({ wide, width, children }: { wide: boolean; width: number; children: React.ReactNode }) {
  if (wide) return <Box width={width}>{children}</Box>
  return <>{children}</>
}

function DashboardContent({ projects, period, columns, activeProvider, budgets, planUsages, label, dayMode }: { projects: ProjectSummary[]; period: Period; columns?: number; activeProvider?: string; budgets?: Map<string, ContextBudget>; planUsages?: PlanUsage[]; label?: string; dayMode?: boolean }) {
  const { dashWidth, wide, halfWidth, barWidth } = getLayout(columns)
  const isCursor = activeProvider === 'cursor'
  const activeLabel = label ?? PERIOD_LABELS[period]
  if (projects.length === 0) return <Panel title="CodeBurn" color={ORANGE} width={dashWidth}><Text dimColor>No usage data found for {activeLabel}.</Text></Panel>
  const pw = wide ? halfWidth : dashWidth
  const days = dayMode ? 1 : period === 'all' ? undefined : (period === 'month' || period === '30days' ? 31 : 14)
  // A provider-scoped plan (e.g. SuperGrok) only makes sense on its own
  // provider tab, where the shown cost matches the plan's spend. Hide it on
  // every other tab, including All, so its budget isn't compared to spend it
  // doesn't cover.
  const visiblePlanUsages = (planUsages ?? []).filter(p => p.plan.provider === (activeProvider ?? 'all'))
  return (
    <Box flexDirection="column" width={dashWidth}>
      <Overview projects={projects} label={activeLabel} width={dashWidth} planUsages={visiblePlanUsages} />
      <Row wide={wide} width={dashWidth}><DailyActivity projects={projects} days={days} pw={pw} bw={barWidth} /><ProjectBreakdown projects={projects} pw={pw} bw={barWidth} budgets={budgets} rows={dayMode ? 8 : period === 'all' ? 14 : period === 'month' || period === '30days' ? 14 : 8} /></Row>
      <Row wide={wide} width={dashWidth}><ActivityBreakdown projects={projects} pw={pw} bw={barWidth} /><ModelBreakdown projects={projects} pw={pw} bw={barWidth} /></Row>
      {isCursor ? (
        <ToolBreakdown projects={projects} pw={dashWidth} bw={barWidth} title="Languages" filterPrefix="lang:" />
      ) : (
        <><Row wide={wide} width={dashWidth}><ToolBreakdown projects={projects} pw={pw} bw={barWidth} /><BashBreakdown projects={projects} pw={pw} bw={barWidth} /></Row><Row wide={wide} width={dashWidth}><SkillsAndAgents projects={projects} pw={pw} bw={barWidth} /><McpBreakdown projects={projects} pw={pw} bw={barWidth} /></Row><Row wide={wide} width={dashWidth}><ClaudeAgentTypes projects={projects} pw={pw} bw={barWidth} /></Row></>
      )}
    </Box>
  )
}

function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider, initialPlanUsages, refreshSeconds, projectFilter, excludeFilter, customRange, customRangeLabel, initialDay }: {
  initialProjects: ProjectSummary[]
  initialPeriod: Period
  initialProvider: string
  initialPlanUsages?: PlanUsage[]
  refreshSeconds?: number
  projectFilter?: string[]
  excludeFilter?: string[]
  customRange?: DateRange | null
  customRangeLabel?: string
  initialDay?: string
}) {
  const { exit } = useApp()
  const [period, setPeriod] = useState<Period>(initialPeriod)
  const [projects, setProjects] = useState<ProjectSummary[]>(initialProjects)
  const [loading, setLoading] = useState(false)
  const [activeProvider, setActiveProvider] = useState(initialProvider)
  const [detectedProviders, setDetectedProviders] = useState<string[]>([])
  const [view, setView] = useState<View>('dashboard')
  const [optimizeResult, setOptimizeResult] = useState<OptimizeResult | null>(null)
  const [optimizeLoading, setOptimizeLoading] = useState(false)
  const [projectBudgets, setProjectBudgets] = useState<Map<string, ContextBudget>>(new Map())
  const [planUsages, setPlanUsages] = useState<PlanUsage[]>(initialPlanUsages ?? [])
  const [dayDate, setDayDate] = useState<string | null>(initialDay ?? null)
  // Cursor for the OptimizeView's findings window. Reset whenever the user
  // leaves the optimize view OR the underlying findings change so a long
  // findings list never strands the user past the new array length.
  const [findingsCursor, setFindingsCursor] = useState(0)
  const isDayMode = dayDate != null
  const isCustomRange = customRange != null && !isDayMode
  const { columns } = useWindowSize()
  const { dashWidth } = getLayout(columns)
  const multipleProviders = detectedProviders.length > 1
  const optimizeAvailable = !isCustomRange && (activeProvider === 'all' || activeProvider === 'claude')
  const modelCount = new Set(
    projects.flatMap(p => p.sessions.flatMap(s => Object.keys(s.modelBreakdown)))
  ).size
  const compareAvailable = modelCount >= 2
  const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
  const reloadGenerationRef = useRef(0)
  const reloadInFlightRef = useRef(false)
  const currentReloadRef = useRef<{ period: Period; provider: string; day: string | null } | null>(null)
  const pendingReloadRef = useRef<{ period: Period; provider: string; day: string | null } | null>(null)
  const findingCount = optimizeResult?.findings.length ?? 0

  useEffect(() => {
    let cancelled = false
    async function detect() {
      const found: string[] = []
      for (const p of await getAllProviders()) { const s = await p.discoverSessions(); if (s.length > 0) found.push(p.name) }
      if (!cancelled) setDetectedProviders(found)
    }
    detect()
    return () => { cancelled = true }
  }, [])

  useEffect(() => {
    let cancelled = false
    async function loadBudgets() {
      const budgets = new Map<string, ContextBudget>()
      for (const project of projects.slice(0, 8)) {
        if (cancelled) return
        if (!project.projectPath.startsWith('/')) continue
        budgets.set(project.project, await estimateContextBudget(project.projectPath))
      }
      if (!cancelled) setProjectBudgets(budgets)
    }
    loadBudgets()
    return () => { cancelled = true }
  }, [projects])

  const reloadData = useCallback(async (p: Period, prov: string, day: string | null = null) => {
    if (reloadInFlightRef.current) {
      const current = currentReloadRef.current
      if (current?.period === p && current.provider === prov && current.day === day) {
        pendingReloadRef.current = null
        return
      }
      reloadGenerationRef.current++
      pendingReloadRef.current = { period: p, provider: prov, day }
      return
    }
    reloadInFlightRef.current = true
    currentReloadRef.current = { period: p, provider: prov, day }
    const generation = ++reloadGenerationRef.current
    setLoading(true)
    setOptimizeLoading(false)
    setOptimizeResult(null)
    try {
      if (!day && isHeavyPeriod(p)) {
        setProjects([])
        setProjectBudgets(new Map())
        await nextTick()
        if (reloadGenerationRef.current !== generation) return
      }
      const range = day ? getDayRange(day) : getPeriodRange(p)
      const data = await parseAllSessions(range, prov)
      if (reloadGenerationRef.current !== generation) return

      const filteredProjects = filterProjectsByName(data, projectFilter, excludeFilter)
      if (reloadGenerationRef.current !== generation) return

      setProjects(filteredProjects)
      const usage = await getPlanUsages()
      if (reloadGenerationRef.current !== generation) return
      setPlanUsages(usage)
    } catch (error) {
      console.error(error)
    } finally {
      if (reloadGenerationRef.current === generation) {
        setLoading(false)
      }
      reloadInFlightRef.current = false
      currentReloadRef.current = null
      const pending = pendingReloadRef.current
      pendingReloadRef.current = null
      if (pending) {
        void reloadData(pending.period, pending.provider, pending.day)
      }
    }
  }, [projectFilter, excludeFilter])

  const currentRange = useCallback(() => {
    return dayDate ? getDayRange(dayDate) : getPeriodRange(period)
  }, [dayDate, period])

  const loadOptimizeResult = useCallback(async () => {
    if (!optimizeAvailable || projects.length === 0 || optimizeLoading) return
    setView('optimize')
    setFindingsCursor(0)
    if (optimizeResult) return

    const generation = reloadGenerationRef.current
    setOptimizeLoading(true)
    try {
      const result = await scanAndDetect(projects, currentRange())
      if (reloadGenerationRef.current === generation) setOptimizeResult(result)
    } catch (error) {
      console.error(error)
    } finally {
      if (reloadGenerationRef.current === generation) setOptimizeLoading(false)
    }
  }, [optimizeAvailable, projects, currentRange, optimizeLoading, optimizeResult])

  useEffect(() => {
    if (!refreshSeconds || refreshSeconds <= 0) return
    if (!dayDate && isHeavyPeriod(period)) return
    const id = setInterval(() => { reloadData(period, activeProvider, dayDate) }, refreshSeconds * 1000)
    return () => clearInterval(id)
  }, [refreshSeconds, period, activeProvider, dayDate, reloadData])

  const switchPeriod = useCallback((np: Period) => {
    if (np === period && !dayDate) return
    // Clear projects + flip loading synchronously so the dashboard never
    // renders the new period label over the old period's numbers between
    // setPeriod() and the reloadData() promise resolving. Without this,
    // there's a frame-to-hundreds-of-ms window where users saw wrong
    // figures captioned with the new period.
    setPeriod(np)
    setDayDate(null)
    setProjects([])
    setLoading(true)
    if (debounceRef.current) clearTimeout(debounceRef.current)
    debounceRef.current = setTimeout(() => { reloadData(np, activeProvider, null) }, 600)
  }, [period, activeProvider, dayDate, reloadData])

  const switchPeriodImmediate = useCallback(async (np: Period) => {
    if (np === period && !dayDate) return
    setPeriod(np)
    setDayDate(null)
    setProjects([])
    setLoading(true)
    if (debounceRef.current) clearTimeout(debounceRef.current)
    await reloadData(np, activeProvider, null)
  }, [period, activeProvider, dayDate, reloadData])

  const switchDay = useCallback(async (nextDay: string) => {
    const today = parseDayFlag('today')!.day
    const clampedDay = nextDay > today ? today : nextDay
    if (clampedDay === dayDate) return
    setDayDate(clampedDay)
    setProjects([])
    setLoading(true)
    setView('dashboard')
    if (debounceRef.current) clearTimeout(debounceRef.current)
    await reloadData(period, activeProvider, clampedDay)
  }, [period, activeProvider, dayDate, reloadData])

  const enterYesterday = useCallback(async () => {
    const yesterday = parseDayFlag('yesterday')!.day
    await switchDay(yesterday)
  }, [switchDay])

  useInput((input, key) => {
    if (input === 'q') { exit(); return }
    if (input === 'o' && view === 'dashboard' && optimizeAvailable) { void loadOptimizeResult(); return }
    if ((input === 'b' || key.escape) && view === 'optimize') { setView('dashboard'); setFindingsCursor(0); return }
    if (view === 'optimize') {
      const total = optimizeResult?.findings.length ?? 0
      const maxStart = Math.max(0, total - FINDINGS_WINDOW_SIZE)
      if (input === 'j' || key.downArrow) { setFindingsCursor(c => Math.min(c + 1, maxStart)); return }
      if (input === 'k' || key.upArrow)   { setFindingsCursor(c => Math.max(c - 1, 0)); return }
      return
    }
    if (input === 'c' && compareAvailable && view === 'dashboard') { setView('compare'); return }
    if ((input === 'b' || key.escape) && view === 'compare') { setView('dashboard'); return }
    if (input === 'p' && multipleProviders && view !== 'compare') {
      const opts = ['all', ...detectedProviders]; const next = opts[(opts.indexOf(activeProvider) + 1) % opts.length]
      setActiveProvider(next); setView('dashboard')
      if (debounceRef.current) clearTimeout(debounceRef.current)
      reloadData(period, next, dayDate); return
    }
    // Period switches reload the underlying data. Disable them while the
    // compare view is mounted; the compare view re-aggregates from
    // `projects` and would visibly change underneath the user without any
    // affordance back to the dashboard. Press `b` or Esc to return first.
    if (view === 'compare') return
    if (!customRange && input === 'd') {
      if (dayDate) {
        setDayDate(null)
        setProjects([])
        setLoading(true)
        void reloadData(period, activeProvider, null)
      } else {
        void enterYesterday()
      }
      return
    }
    // Also disable while a custom --from/--to range is in effect. Switching
    // period would silently abandon the user's explicit range and reload
    // standard period data; the period tab strip is hidden in this mode so
    // users have no expectation that 1-5 should do anything.
    if (isCustomRange) return
    if (dayDate) {
      if (key.leftArrow) { void switchDay(shiftDay(dayDate, -1)); return }
      if (key.rightArrow || key.tab) { void switchDay(shiftDay(dayDate, 1)); return }
      if (key.escape || input === 'b') {
        setDayDate(null)
        setProjects([])
        setLoading(true)
        void reloadData(period, activeProvider, null)
        return
      }
    }
    const idx = PERIODS.indexOf(period)
    if (key.leftArrow) switchPeriod(PERIODS[(idx - 1 + PERIODS.length) % PERIODS.length]!)
    else if (key.rightArrow || key.tab) switchPeriod(PERIODS[(idx + 1) % PERIODS.length]!)
    else if (input === '1') switchPeriodImmediate('today')
    else if (input === '2') switchPeriodImmediate('week')
    else if (input === '3') switchPeriodImmediate('30days')
    else if (input === '4') switchPeriodImmediate('month')
    else if (input === '5') switchPeriodImmediate('all')
  })

  const headerLabel = dayDate ? formatDayRangeLabel(dayDate) : customRangeLabel ?? PERIOD_LABELS[period]

  if (loading || optimizeLoading) {
    return (
      <Box flexDirection="column" width={dashWidth}>
        {!isCustomRange && !isDayMode && <PeriodTabs active={period} providerName={activeProvider} showProvider={view !== 'compare' && multipleProviders} />}
        {isDayMode && <DayBanner label={headerLabel} width={dashWidth} />}
        {isCustomRange && <CustomRangeBanner label={headerLabel} width={dashWidth} />}
        {view === 'compare'
          ? <Box flexDirection="column" paddingX={2} paddingY={1}>
              <Box flexDirection="column" borderStyle="round" borderColor={ORANGE} paddingX={1}>
                <Text bold color={ORANGE}>Model Comparison</Text>
                <Text> </Text>
                <Text dimColor>Loading {headerLabel} model data...</Text>
              </Box>
            </Box>
          : view === 'optimize'
            ? <Panel title="CodeBurn Optimize" color={ORANGE} width={dashWidth}><Text dimColor>Scanning {headerLabel}...</Text></Panel>
            : <Panel title="CodeBurn" color={ORANGE} width={dashWidth}><Text dimColor>Loading {headerLabel}...</Text></Panel>}
        {view !== 'compare' && <StatusBar width={dashWidth} showProvider={multipleProviders} view={view} findingCount={0} optimizeAvailable={false} compareAvailable={false} customRange={isCustomRange} dayMode={isDayMode} />}
      </Box>
    )
  }

  return (
    <Box flexDirection="column" width={dashWidth}>
      {!isCustomRange && !isDayMode && <PeriodTabs active={period} providerName={activeProvider} showProvider={multipleProviders && view !== 'compare'} />}
      {isDayMode && <DayBanner label={headerLabel} width={dashWidth} />}
      {isCustomRange && <CustomRangeBanner label={headerLabel} width={dashWidth} />}
      {view === 'compare'
        ? <CompareView projects={projects} onBack={() => setView('dashboard')} />
        : view === 'optimize' && optimizeResult
          ? <OptimizeView findings={optimizeResult.findings} costRate={optimizeResult.costRate} projects={projects} label={headerLabel} width={dashWidth} healthScore={optimizeResult.healthScore} healthGrade={optimizeResult.healthGrade} cursor={findingsCursor} />
          : <DashboardContent projects={projects} period={period} columns={columns} activeProvider={activeProvider} budgets={projectBudgets} planUsages={planUsages} label={headerLabel} dayMode={isDayMode} />}
      {view !== 'compare' && <StatusBar width={dashWidth} showProvider={multipleProviders} view={view} findingCount={findingCount} optimizeAvailable={optimizeAvailable} compareAvailable={compareAvailable} customRange={isCustomRange} dayMode={isDayMode} />}
    </Box>
  )
}

function DayBanner({ label, width }: { label: string; width: number }) {
  return (
    <Box width={width} paddingX={1} marginBottom={1}>
      <Text color={ORANGE} bold>{label}</Text>
    </Box>
  )
}

function CustomRangeBanner({ label, width }: { label: string; width: number }) {
  return (
    <Box width={width} paddingX={1} marginBottom={1}>
      <Text dimColor>Custom range: </Text>
      <Text color={ORANGE} bold>{label}</Text>
    </Box>
  )
}

function StaticDashboard({ projects, period, activeProvider, planUsages, label, dayMode }: { projects: ProjectSummary[]; period: Period; activeProvider?: string; planUsages?: PlanUsage[]; label?: string; dayMode?: boolean }) {
  const { columns } = useWindowSize()
  const { dashWidth } = getLayout(columns)
  return (
    <Box flexDirection="column" width={dashWidth}>
      {dayMode ? <DayBanner label={label ?? PERIOD_LABELS[period]} width={dashWidth} /> : <PeriodTabs active={period} />}
      <DashboardContent projects={projects} period={period} columns={columns} activeProvider={activeProvider} planUsages={planUsages} label={label} dayMode={dayMode} />
    </Box>
  )
}

export async function renderDashboard(period: Period = 'week', provider: string = 'all', refreshSeconds?: number, projectFilter?: string[], excludeFilter?: string[], customRange?: DateRange | null, customRangeLabel?: string, initialDay?: string): Promise<void> {
  // Interactive Ink UI: it renders to the same terminal and has its own in-frame
  // loading state, so the CLI scan-progress line must stay silent for its whole
  // lifetime (initial scan and every 30s auto-refresh, including the
  // getPlanUsages → parseAllSessions path). Plain CLI commands are unaffected.
  setInteractiveScanUI()
  await loadPricing()
  const dayRange = initialDay ? getDayRange(initialDay) : null
  const range = dayRange ?? customRange ?? getPeriodRange(period)
  const filteredProjects = filterProjectsByName(await parseAllSessions(range, provider), projectFilter, excludeFilter)
  const planUsages = await getPlanUsages()
  const isTTY = process.stdin.isTTY && process.stdout.isTTY
  const label = initialDay ? formatDayRangeLabel(initialDay) : customRangeLabel
  patchStdoutForWindows()
  if (isTTY) {
    const { waitUntilExit } = render(
      <InteractiveDashboard initialProjects={filteredProjects} initialPeriod={period} initialProvider={provider} initialPlanUsages={planUsages} refreshSeconds={refreshSeconds} projectFilter={projectFilter} excludeFilter={excludeFilter} customRange={customRange} customRangeLabel={customRangeLabel} initialDay={initialDay} />
    )
    await waitUntilExit()
  } else {
    const { unmount } = render(<StaticDashboard projects={filteredProjects} period={period} activeProvider={provider} planUsages={planUsages} label={label} dayMode={initialDay != null} />, { patchConsole: false })
    // Non-interactive one-shot output: ink schedules the frame through a
    // throttled render, so yield a tick to let it flush to stdout before
    // unmounting. Unmounting synchronously can race the flush and drop output.
    await new Promise<void>(resolve => setTimeout(resolve, 0))
    unmount()
  }
}
