import { Hype } from '@because/hype'
import { define, stylesToCSS } from '@because/forge'
import { baseStyles, ToolScript, theme } from '@because/toes/tools'
import { existsSync, readdirSync, statSync } from 'fs'
import { join } from 'path'
import type { Child } from 'hono/jsx'

// ============================================================================
// Configuration
// ============================================================================

const DATA_HISTORY_MAX_DAYS = 30 // Keep 30 days of data size history
const SAMPLE_INTERVAL_MS = 10_000 // How often to sample process metrics
const HISTORY_MAX_SAMPLES = 60 // Keep 10 minutes of history at 10s intervals

const APPS_DIR = process.env.APPS_DIR!
const TOES_DIR = process.env.TOES_DIR!
const TOES_URL = process.env.TOES_URL!

// ============================================================================
// Types
// ============================================================================

interface App {
  name: string
  state: string
  port?: number
  pid?: number
  tool?: boolean
}

interface AppMetrics extends App {
  cpu?: number
  dataSize?: number
  memory?: number
  rss?: number
}

interface HistorySample {
  timestamp: number
  cpu: number
  memory: number
  rss: number
}

interface DataSample {
  date: string
  bytes: number
}

interface ProcessMetrics {
  pid: number
  cpu: number
  memory: number
  rss: number
}

interface SystemMetrics {
  cpu: number
  ram: { used: number, total: number, percent: number }
  disk: { used: number, total: number, percent: number }
}

// ============================================================================
// Process Metrics Collection
// ============================================================================

const appHistory = new Map<string, HistorySample[]>() // app name -> history
const dataHistory = new Map<string, DataSample[]>() // app name -> daily data size
const metricsCache = new Map<number, ProcessMetrics>()

function getDataHistory(appName: string): DataSample[] {
  return dataHistory.get(appName) ?? []
}

function getHistory(appName: string): HistorySample[] {
  return appHistory.get(appName) ?? []
}

function getProcessMetrics(pid: number): ProcessMetrics | undefined {
  return metricsCache.get(pid)
}

function recordHistory(appName: string, cpu: number, memory: number, rss: number): void {
  const history = appHistory.get(appName) ?? []
  history.push({
    timestamp: Date.now(),
    cpu,
    memory,
    rss,
  })

  // Keep only the last N samples
  while (history.length > HISTORY_MAX_SAMPLES) {
    history.shift()
  }

  appHistory.set(appName, history)
}

async function sampleProcessMetrics(): Promise<void> {
  try {
    const proc = Bun.spawn(['ps', '-eo', 'pid,pcpu,pmem,rss'], {
      stdout: 'pipe',
      stderr: 'ignore',
    })

    const text = await new Response(proc.stdout).text()
    const lines = text.trim().split('\n').slice(1) // Skip header

    metricsCache.clear()

    for (const line of lines) {
      const parts = line.trim().split(/\s+/)
      if (parts.length >= 4) {
        const pid = parseInt(parts[0]!, 10)
        const cpu = parseFloat(parts[1]!)
        const memory = parseFloat(parts[2]!)
        const rss = parseInt(parts[3]!, 10) // KB

        if (!isNaN(pid) && pid > 0) {
          metricsCache.set(pid, { pid, cpu, memory, rss })
        }
      }
    }
  } catch (err) {
    console.error('Failed to sample process metrics:', err)
  }
}

function recordDataSize(appName: string): void {
  const today = new Date().toISOString().slice(0, 10)
  const history = dataHistory.get(appName) ?? []
  const bytes = getDataSize(appName)

  // Update today's entry or add new one
  const existing = history.find(s => s.date === today)
  if (existing) {
    existing.bytes = bytes
  } else {
    history.push({ date: today, bytes })
  }

  // Keep only the last N days
  while (history.length > DATA_HISTORY_MAX_DAYS) {
    history.shift()
  }

  dataHistory.set(appName, history)
}

async function sampleAndRecordHistory(): Promise<void> {
  await sampleProcessMetrics()

  try {
    const res = await fetch(`${TOES_URL}/api/apps`)
    if (!res.ok) return
    const apps = await res.json() as App[]

    // Record process history for running apps
    for (const app of apps) {
      if (app.pid && app.state === 'running') {
        const metrics = getProcessMetrics(app.pid)
        if (metrics) {
          recordHistory(app.name, metrics.cpu, metrics.memory, metrics.rss)
        }
      }
    }

    // Record data size history for all apps (filesystem-based, not process-based)
    for (const app of apps) {
      recordDataSize(app.name)
    }
  } catch {
    // Ignore errors
  }
}

// Start sampling on module load
sampleAndRecordHistory()
setInterval(sampleAndRecordHistory, SAMPLE_INTERVAL_MS)

// ============================================================================
// Data Size
// ============================================================================

function getDataSize(appName: string): number {
  let total = 0
  const appDir = join(APPS_DIR, appName)
  if (existsSync(appDir)) total += dirSize(appDir)
  const dataDir = join(TOES_DIR, appName)
  if (existsSync(dataDir)) total += dirSize(dataDir)
  return total
}

function dirSize(dir: string): number {
  let total = 0
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
    const path = join(dir, entry.name)
    if (entry.isDirectory()) {
      total += dirSize(path)
    } else {
      total += statSync(path).size
    }
  }
  return total
}

// ============================================================================
// API Client
// ============================================================================

async function fetchApps(): Promise<App[]> {
  try {
    const res = await fetch(`${TOES_URL}/api/apps`)
    if (!res.ok) return []
    return await res.json() as App[]
  } catch {
    return []
  }
}

async function getAppMetrics(): Promise<AppMetrics[]> {
  const apps = await fetchApps()
  return apps.map(app => {
    const metrics = app.pid ? getProcessMetrics(app.pid) : undefined
    return {
      ...app,
      cpu: metrics?.cpu,
      dataSize: getDataSize(app.name),
      memory: metrics?.memory,
      rss: metrics?.rss,
    }
  })
}

async function getAppMetricsByName(name: string): Promise<AppMetrics | undefined> {
  const apps = await fetchApps()
  const app = apps.find(a => a.name === name)
  if (!app) return undefined

  const metrics = app.pid ? getProcessMetrics(app.pid) : undefined
  return {
    ...app,
    cpu: metrics?.cpu,
    dataSize: getDataSize(app.name),
    memory: metrics?.memory,
    rss: metrics?.rss,
  }
}

// ============================================================================
// Styled Components
// ============================================================================

const Container = define('Container', {
  fontFamily: theme('fonts-sans'),
  padding: '20px',
  paddingTop: 0,
  maxWidth: '900px',
  margin: '0 auto',
  color: theme('colors-text'),
})

const Table = define('Table', {
  base: 'table',
  width: '100%',
  borderCollapse: 'collapse',
  fontSize: '14px',
  fontFamily: theme('fonts-mono'),
})

const Th = define('Th', {
  base: 'th',
  textAlign: 'left',
  padding: '10px 12px',
  borderBottom: `2px solid ${theme('colors-border')}`,
  color: theme('colors-textMuted'),
  fontSize: '12px',
  fontWeight: 'normal',
  textTransform: 'uppercase',
  letterSpacing: '0.5px',
})

const ThRight = define('ThRight', {
  base: 'th',
  textAlign: 'right',
  padding: '10px 12px',
  borderBottom: `2px solid ${theme('colors-border')}`,
  color: theme('colors-textMuted'),
  fontSize: '12px',
  fontWeight: 'normal',
  textTransform: 'uppercase',
  letterSpacing: '0.5px',
})

const Tr = define('Tr', {
  base: 'tr',
  states: {
    ':hover': {
      backgroundColor: theme('colors-bgHover'),
    },
  },
})

const Td = define('Td', {
  base: 'td',
  padding: '10px 12px',
  borderBottom: `1px solid ${theme('colors-border')}`,
})

const TdRight = define('TdRight', {
  base: 'td',
  padding: '10px 12px',
  borderBottom: `1px solid ${theme('colors-border')}`,
  textAlign: 'right',
})

const StatusBadge = define('StatusBadge', {
  base: 'span',
  fontSize: '12px',
  padding: '2px 6px',
  borderRadius: theme('radius-md'),
})

const ToolBadge = define('ToolBadge', {
  base: 'span',
  fontSize: '11px',
  marginLeft: '6px',
  color: theme('colors-textMuted'),
})

const Summary = define('Summary', {
  marginTop: '20px',
  padding: '12px 15px',
  backgroundColor: theme('colors-bgElement'),
  borderRadius: theme('radius-md'),
  fontSize: '13px',
  color: theme('colors-textMuted'),
})

const EmptyState = define('EmptyState', {
  padding: '40px 20px',
  textAlign: 'center',
  color: theme('colors-textMuted'),
})

const Tab = define('Tab', {
  base: 'a',
  padding: '8px 16px',
  fontSize: '13px',
  fontFamily: theme('fonts-sans'),
  color: theme('colors-textMuted'),
  textDecoration: 'none',
  borderBottom: '2px solid transparent',
  cursor: 'pointer',
  states: {
    ':hover': {
      color: theme('colors-text'),
    },
  },
})

const TabActive = define('TabActive', {
  base: 'a',
  padding: '8px 16px',
  fontSize: '13px',
  fontFamily: theme('fonts-sans'),
  color: theme('colors-text'),
  textDecoration: 'none',
  borderBottom: `2px solid ${theme('colors-primary')}`,
  fontWeight: 'bold',
  cursor: 'default',
})

const TabBar = define('TabBar', {
  display: 'flex',
  gap: '4px',
  borderBottom: `1px solid ${theme('colors-border')}`,
  marginBottom: '15px',
})

const ChartsContainer = define('ChartsContainer', {
  marginTop: '24px',
  display: 'grid',
  gridTemplateColumns: 'repeat(2, 1fr)',
  gap: '20px',
})

const ChartCard = define('ChartCard', {
  backgroundColor: theme('colors-bgElement'),
  borderRadius: theme('radius-md'),
  padding: '16px',
  border: `1px solid ${theme('colors-border')}`,
})

const ChartTitle = define('ChartTitle', {
  margin: '0 0 12px 0',
  fontSize: '13px',
  fontWeight: 600,
  color: theme('colors-textMuted'),
  textTransform: 'uppercase',
  letterSpacing: '0.5px',
})

const ChartWrapper = define('ChartWrapper', {
  position: 'relative',
  height: '150px',
})

const GaugeLabel = define('GaugeLabel', {
  fontSize: '13px',
  fontWeight: 600,
  color: theme('colors-textMuted'),
  textTransform: 'uppercase',
  letterSpacing: '0.5px',
})

const GaugeValueText = define('GaugeValueText', {
  textAlign: 'center',
  fontSize: '20px',
  fontWeight: 'bold',
  marginTop: '-4px',
  color: theme('colors-text'),
})

const GaugesCard = define('GaugesCard', {
  display: 'flex',
  flexDirection: 'column',
  alignItems: 'center',
  gap: '8px',
  background: theme('colors-bgElement'),
  border: `1px solid ${theme('colors-border')}`,
  borderRadius: theme('radius-md'),
  padding: '24px',
})

const GaugesGrid = define('GaugesGrid', {
  display: 'flex',
  justifyContent: 'center',
  gap: '40px',
  padding: '20px 0',
})

const NoDataMessage = define('NoDataMessage', {
  display: 'flex',
  alignItems: 'center',
  justifyContent: 'center',
  height: '100%',
  color: theme('colors-textMuted'),
  fontSize: '13px',
})

// ============================================================================
// Helpers
// ============================================================================

function formatBytes(bytes?: number): string {
  if (bytes === undefined) return '-'
  if (bytes < 1024) return `${bytes} B`
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
  if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`
  return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`
}

function formatPercent(value?: number): string {
  if (value === undefined) return '-'
  return `${value.toFixed(1)}%`
}

function formatRss(kb?: number): string {
  if (kb === undefined) return '-'
  if (kb < 1024) return `${kb} KB`
  if (kb < 1024 * 1024) return `${(kb / 1024).toFixed(1)} MB`
  return `${(kb / 1024 / 1024).toFixed(2)} GB`
}

async function fetchSystemMetrics(): Promise<SystemMetrics> {
  try {
    const res = await fetch(`${TOES_URL}/api/system/metrics`)
    if (res.ok) return await res.json() as SystemMetrics
  } catch {}
  return { cpu: 0, ram: { used: 0, total: 0, percent: 0 }, disk: { used: 0, total: 0, percent: 0 } }
}

function getStatusColor(state: string): string {
  switch (state) {
    case 'running':
      return theme('colors-statusRunning')
    case 'stopped':
    case 'stopping':
      return theme('colors-statusStopped')
    case 'invalid':
      return theme('colors-error')
    default:
      return theme('colors-textMuted')
  }
}

// ============================================================================
// Layout
// ============================================================================

interface LayoutProps {
  title: string
  children: Child
}

function Layout({ title, children }: LayoutProps) {
  return (
    <html>
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>{title}</title>
        <link rel="stylesheet" href="/styles.css" />
      </head>
      <body>
        <ToolScript />
        <Container>
          {children}
        </Container>
      </body>
    </html>
  )
}

// ============================================================================
// Gauge Rendering
// ============================================================================

const G_SEGMENTS = 19
const G_START = -225
const G_SWEEP = 270
const G_CX = 60
const G_CY = 60
const G_R = 44
const G_GAP = 3
const G_SW = 8
const G_NL = 38

const gToRad = (deg: number) => (deg * Math.PI) / 180

const gSegColor = (i: number): string => {
  const t = i / (G_SEGMENTS - 1)
  if (t < 0.4) return '#4caf50'
  if (t < 0.6) return '#8bc34a'
  if (t < 0.75) return '#ffc107'
  if (t < 0.9) return '#ff9800'
  return '#f44336'
}

function renderGauge(value: number, id: string) {
  const segSweep = G_SWEEP / G_SEGMENTS
  const active = Math.round((value / 100) * G_SEGMENTS)
  const innerR = G_R - G_SW / 2
  const outerR = G_R + G_SW / 2

  const segments = []
  for (let i = 0; i < G_SEGMENTS; i++) {
    const s = G_START + i * segSweep + G_GAP / 2
    const e = G_START + (i + 1) * segSweep - G_GAP / 2
    const x1 = G_CX + outerR * Math.cos(gToRad(s))
    const y1 = G_CY + outerR * Math.sin(gToRad(s))
    const x2 = G_CX + outerR * Math.cos(gToRad(e))
    const y2 = G_CY + outerR * Math.sin(gToRad(e))
    const x3 = G_CX + innerR * Math.cos(gToRad(e))
    const y3 = G_CY + innerR * Math.sin(gToRad(e))
    const x4 = G_CX + innerR * Math.cos(gToRad(s))
    const y4 = G_CY + innerR * Math.sin(gToRad(s))
    segments.push(
      <path
        key={i}
        data-segment={i}
        d={`M ${x1} ${y1} A ${outerR} ${outerR} 0 0 1 ${x2} ${y2} L ${x3} ${y3} A ${innerR} ${innerR} 0 0 0 ${x4} ${y4} Z`}
        fill={i < active ? gSegColor(i) : 'var(--colors-border)'}
      />
    )
  }

  const angle = G_START + (value / 100) * G_SWEEP
  const nx = G_CX + G_NL * Math.cos(gToRad(angle))
  const ny = G_CY + G_NL * Math.sin(gToRad(angle))
  const pa = angle + 90
  const bw = 3
  const bx1 = G_CX + bw * Math.cos(gToRad(pa))
  const by1 = G_CY + bw * Math.sin(gToRad(pa))
  const bx2 = G_CX - bw * Math.cos(gToRad(pa))
  const by2 = G_CY - bw * Math.sin(gToRad(pa))

  return (
    <GaugesCard>
      <GaugeLabel>{id}</GaugeLabel>
      <svg id={`gauge-${id}`} viewBox="10 10 100 55" width="140" height="80" style="overflow: visible">
        {segments}
        <polygon data-needle points={`${nx},${ny} ${bx1},${by1} ${bx2},${by2}`} fill="var(--colors-text)" />
        <circle cx={G_CX} cy={G_CY} r="4" fill="var(--colors-textMuted)" />
      </svg>
      <GaugeValueText id={`value-${id}`}>{value}%</GaugeValueText>
    </GaugesCard>
  )
}

const gaugeScript = `
(function() {
  var S=19,ST=-225,SW=270,CX=60,CY=60,R=44,W=8,NL=38,GAP=3;
  var iR=R-W/2, oR=R+W/2;
  function rad(d){return d*Math.PI/180}
  function sc(i){var t=i/(S-1);return t<.4?'#4caf50':t<.6?'#8bc34a':t<.75?'#ffc107':t<.9?'#ff9800':'#f44336'}
  function upd(id,v){
    var svg=document.getElementById('gauge-'+id);if(!svg)return;
    var a=Math.round((v/100)*S);
    svg.querySelectorAll('[data-segment]').forEach(function(p,i){p.setAttribute('fill',i<a?sc(i):'var(--colors-border)')});
    var ang=ST+(v/100)*SW,nx=CX+NL*Math.cos(rad(ang)),ny=CY+NL*Math.sin(rad(ang));
    var pa=ang+90,bw=3;
    var bx1=CX+bw*Math.cos(rad(pa)),by1=CY+bw*Math.sin(rad(pa));
    var bx2=CX-bw*Math.cos(rad(pa)),by2=CY-bw*Math.sin(rad(pa));
    var n=svg.querySelector('[data-needle]');if(n)n.setAttribute('points',nx+','+ny+' '+bx1+','+by1+' '+bx2+','+by2);
    var el=document.getElementById('value-'+id);if(el)el.textContent=v+'%';
  }
  setInterval(function(){
    fetch('/api/system').then(function(r){return r.json()}).then(function(m){
      upd('cpu',m.cpu);upd('ram',m.ram.percent);upd('disk',m.disk.percent);
    }).catch(function(){});
  },2000);
})();
`

// ============================================================================
// App
// ============================================================================

const app = new Hype({ prettyHTML: false })

app.get('/ok', c => c.text('ok'))

app.get('/styles.css', c => c.text(baseStyles + stylesToCSS(), 200, {
  'Content-Type': 'text/css; charset=utf-8',
}))

// API endpoint for CLI
app.get('/api/metrics', async c => {
  const metrics = await getAppMetrics()
  return c.json(metrics)
})

app.get('/api/metrics/:name', async c => {
  const name = c.req.param('name')
  const metrics = await getAppMetricsByName(name)
  if (!metrics) {
    return c.json({ error: 'App not found' }, 404)
  }
  return c.json(metrics)
})

app.get('/api/data-history/:name', c => {
  const name = c.req.param('name')
  const history = getDataHistory(name)
  return c.json(history)
})

app.get('/api/system', async c => {
  const metrics = await fetchSystemMetrics()
  return c.json(metrics)
})

app.get('/api/history/:name', c => {
  const name = c.req.param('name')
  const history = getHistory(name)
  return c.json(history)
})

// Web UI
app.get('/', async c => {
  const appName = c.req.query('app')

  // Single app view
  if (appName) {
    const tab = c.req.query('tab') === 'global' ? 'global' : 'app'
    const appUrl = `/?app=${appName}`
    const globalUrl = `/?app=${appName}&tab=global`

    if (tab === 'global') {
      const metrics = await getAppMetrics()

      metrics.sort((a, b) => {
        if (a.state === 'running' && b.state !== 'running') return -1
        if (a.state !== 'running' && b.state === 'running') return 1
        return a.name.localeCompare(b.name)
      })

      if (metrics.length === 0) {
        return c.html(
          <Layout title="Metrics">
            <TabBar>
              <Tab href={appUrl}>App</Tab>
              <TabActive href={globalUrl}>Global</TabActive>
            </TabBar>
            <EmptyState>No apps found</EmptyState>
          </Layout>
        )
      }

      const running = metrics.filter(s => s.state === 'running')
      const totalCpu = running.reduce((sum, s) => sum + (s.cpu ?? 0), 0)
      const totalRss = running.reduce((sum, s) => sum + (s.rss ?? 0), 0)
      const totalData = metrics.reduce((sum, s) => sum + (s.dataSize ?? 0), 0)

      return c.html(
        <Layout title="Metrics - Global">
          <TabBar>
            <Tab href={appUrl}>App</Tab>
            <TabActive href={globalUrl}>Global</TabActive>
          </TabBar>
          <Table>
            <thead>
              <tr>
                <Th>Name</Th>
                <Th>State</Th>
                <ThRight>PID</ThRight>
                <ThRight>CPU</ThRight>
                <ThRight>MEM</ThRight>
                <ThRight>RSS</ThRight>
                <ThRight>Data</ThRight>
              </tr>
            </thead>
            <tbody>
              {metrics.map(s => (
                <Tr>
                  <Td>
                    {s.name}
                    {s.tool && <ToolBadge>[tool]</ToolBadge>}
                  </Td>
                  <Td>
                    <StatusBadge style={`color: ${getStatusColor(s.state)}`}>
                      {s.state}
                    </StatusBadge>
                  </Td>
                  <TdRight>{s.pid ?? '-'}</TdRight>
                  <TdRight>{formatPercent(s.cpu)}</TdRight>
                  <TdRight>{formatPercent(s.memory)}</TdRight>
                  <TdRight>{formatRss(s.rss)}</TdRight>
                  <TdRight>{formatBytes(s.dataSize)}</TdRight>
                </Tr>
              ))}
            </tbody>
          </Table>
          <Summary>
            {running.length} running &middot; {formatPercent(totalCpu)} CPU &middot; {formatRss(totalRss)} RSS &middot; {formatBytes(totalData)} data
          </Summary>
        </Layout>
      )
    }

    const metrics = await getAppMetricsByName(appName)

    if (!metrics) {
      return c.html(
        <Layout title="Metrics">
          <EmptyState>App not found: {appName}</EmptyState>
        </Layout>
      )
    }

    return c.html(
      <Layout title="Metrics">
        <TabBar>
          <TabActive href={appUrl}>App</TabActive>
          <Tab href={globalUrl}>Global</Tab>
        </TabBar>
        <Table>
          <thead>
            <tr>
              <Th>State</Th>
              <ThRight>PID</ThRight>
              <ThRight>CPU</ThRight>
              <ThRight>MEM</ThRight>
              <ThRight>RSS</ThRight>
              <ThRight>Data</ThRight>
            </tr>
          </thead>
          <tbody>
            <Tr>
              <Td>
                <StatusBadge style={`color: ${getStatusColor(metrics.state)}`}>
                  {metrics.state}
                </StatusBadge>
              </Td>
              <TdRight>{metrics.pid ?? '-'}</TdRight>
              <TdRight>{formatPercent(metrics.cpu)}</TdRight>
              <TdRight>{formatPercent(metrics.memory)}</TdRight>
              <TdRight>{formatRss(metrics.rss)}</TdRight>
              <TdRight>{formatBytes(metrics.dataSize)}</TdRight>
            </Tr>
          </tbody>
        </Table>

        <ChartsContainer>
          <ChartCard>
            <ChartTitle>CPU Usage</ChartTitle>
            <ChartWrapper>
              <canvas id="cpuChart"></canvas>
              <NoDataMessage id="cpuNoData" style="display: none">
                Collecting data...
              </NoDataMessage>
            </ChartWrapper>
          </ChartCard>
          <ChartCard>
            <ChartTitle>Memory %</ChartTitle>
            <ChartWrapper>
              <canvas id="memChart"></canvas>
              <NoDataMessage id="memNoData" style="display: none">
                Collecting data...
              </NoDataMessage>
            </ChartWrapper>
          </ChartCard>
          <ChartCard>
            <ChartTitle>RSS Memory</ChartTitle>
            <ChartWrapper>
              <canvas id="rssChart"></canvas>
              <NoDataMessage id="rssNoData" style="display: none">
                Collecting data...
              </NoDataMessage>
            </ChartWrapper>
          </ChartCard>
          <ChartCard>
            <ChartTitle>Data Size</ChartTitle>
            <ChartWrapper>
              <canvas id="dataChart"></canvas>
              <NoDataMessage id="dataNoData" style="display: none">
                Collecting data...
              </NoDataMessage>
            </ChartWrapper>
          </ChartCard>
        </ChartsContainer>

        <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
        <script dangerouslySetInnerHTML={{__html: `
          (function() {
            const appName = ${JSON.stringify(appName)};
            const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
            const textColor = isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)';
            const gridColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)';
            const cpuColor = 'rgb(59, 130, 246)';
            const dataColor = 'rgb(245, 158, 11)';
            const memColor = 'rgb(168, 85, 247)';
            const rssColor = 'rgb(34, 197, 94)';

            const commonOptions = {
              responsive: true,
              maintainAspectRatio: false,
              animation: false,
              plugins: {
                legend: { display: false },
                tooltip: {
                  mode: 'index',
                  intersect: false,
                }
              },
              scales: {
                x: {
                  display: true,
                  grid: { color: gridColor },
                  ticks: {
                    color: textColor,
                    maxTicksLimit: 6,
                    callback: function(val, idx) {
                      const label = this.getLabelForValue(val);
                      return label;
                    }
                  }
                },
                y: {
                  display: true,
                  grid: { color: gridColor },
                  ticks: { color: textColor },
                  beginAtZero: true
                }
              },
              elements: {
                point: { radius: 0 },
                line: { tension: 0.3, borderWidth: 2 }
              }
            };

            let cpuChart, dataChart, memChart, rssChart;

            function formatTime(ts) {
              const d = new Date(ts);
              return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
            }

            function formatBytes(bytes) {
              if (bytes < 1024) return bytes + ' B';
              if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
              if (bytes < 1024 * 1024 * 1024) return (bytes / 1024 / 1024).toFixed(1) + ' MB';
              return (bytes / 1024 / 1024 / 1024).toFixed(2) + ' GB';
            }

            function formatDate(dateStr) {
              const d = new Date(dateStr + 'T00:00:00');
              return d.toLocaleDateString([], { month: 'short', day: 'numeric' });
            }

            function formatRss(kb) {
              if (kb < 1024) return kb + ' KB';
              return (kb / 1024).toFixed(1) + ' MB';
            }

            function updateDataChart(history) {
              if (history.length === 0) {
                document.getElementById('dataChart').style.display = 'none';
                document.getElementById('dataNoData').style.display = 'flex';
                return;
              }

              document.getElementById('dataChart').style.display = 'block';
              document.getElementById('dataNoData').style.display = 'none';

              const labels = history.map(h => formatDate(h.date));
              const data = history.map(h => h.bytes / (1024 * 1024)); // Convert to MB

              if (!dataChart) {
                dataChart = new Chart(document.getElementById('dataChart'), {
                  type: 'line',
                  data: {
                    labels: labels,
                    datasets: [{
                      data: data,
                      borderColor: dataColor,
                      backgroundColor: dataColor.replace('rgb', 'rgba').replace(')', ', 0.1)'),
                      fill: true
                    }]
                  },
                  options: {
                    ...commonOptions,
                    scales: {
                      ...commonOptions.scales,
                      y: {
                        ...commonOptions.scales.y,
                        ticks: {
                          ...commonOptions.scales.y.ticks,
                          callback: v => v.toFixed(1) + ' MB'
                        }
                      }
                    }
                  }
                });
              } else {
                dataChart.data.labels = labels;
                dataChart.data.datasets[0].data = data;
                dataChart.update('none');
              }
            }

            function updateCharts(history) {
              const labels = history.map(h => formatTime(h.timestamp));
              const cpuData = history.map(h => h.cpu);
              const memData = history.map(h => h.memory);
              const rssData = history.map(h => h.rss / 1024); // Convert to MB

              if (history.length === 0) {
                document.getElementById('cpuChart').style.display = 'none';
                document.getElementById('memChart').style.display = 'none';
                document.getElementById('rssChart').style.display = 'none';
                document.getElementById('cpuNoData').style.display = 'flex';
                document.getElementById('memNoData').style.display = 'flex';
                document.getElementById('rssNoData').style.display = 'flex';
                return;
              }

              document.getElementById('cpuChart').style.display = 'block';
              document.getElementById('memChart').style.display = 'block';
              document.getElementById('rssChart').style.display = 'block';
              document.getElementById('cpuNoData').style.display = 'none';
              document.getElementById('memNoData').style.display = 'none';
              document.getElementById('rssNoData').style.display = 'none';

              if (!cpuChart) {
                cpuChart = new Chart(document.getElementById('cpuChart'), {
                  type: 'line',
                  data: {
                    labels: labels,
                    datasets: [{
                      data: cpuData,
                      borderColor: cpuColor,
                      backgroundColor: cpuColor.replace('rgb', 'rgba').replace(')', ', 0.1)'),
                      fill: true
                    }]
                  },
                  options: {
                    ...commonOptions,
                    scales: {
                      ...commonOptions.scales,
                      y: {
                        ...commonOptions.scales.y,
                        ticks: {
                          ...commonOptions.scales.y.ticks,
                          callback: v => v.toFixed(1) + '%'
                        }
                      }
                    }
                  }
                });
              } else {
                cpuChart.data.labels = labels;
                cpuChart.data.datasets[0].data = cpuData;
                cpuChart.update('none');
              }

              if (!memChart) {
                memChart = new Chart(document.getElementById('memChart'), {
                  type: 'line',
                  data: {
                    labels: labels,
                    datasets: [{
                      data: memData,
                      borderColor: memColor,
                      backgroundColor: memColor.replace('rgb', 'rgba').replace(')', ', 0.1)'),
                      fill: true
                    }]
                  },
                  options: {
                    ...commonOptions,
                    scales: {
                      ...commonOptions.scales,
                      y: {
                        ...commonOptions.scales.y,
                        ticks: {
                          ...commonOptions.scales.y.ticks,
                          callback: v => v.toFixed(1) + '%'
                        }
                      }
                    }
                  }
                });
              } else {
                memChart.data.labels = labels;
                memChart.data.datasets[0].data = memData;
                memChart.update('none');
              }

              if (!rssChart) {
                rssChart = new Chart(document.getElementById('rssChart'), {
                  type: 'line',
                  data: {
                    labels: labels,
                    datasets: [{
                      data: rssData,
                      borderColor: rssColor,
                      backgroundColor: rssColor.replace('rgb', 'rgba').replace(')', ', 0.1)'),
                      fill: true
                    }]
                  },
                  options: {
                    ...commonOptions,
                    scales: {
                      ...commonOptions.scales,
                      y: {
                        ...commonOptions.scales.y,
                        ticks: {
                          ...commonOptions.scales.y.ticks,
                          callback: v => v.toFixed(1) + ' MB'
                        }
                      }
                    }
                  }
                });
              } else {
                rssChart.data.labels = labels;
                rssChart.data.datasets[0].data = rssData;
                rssChart.update('none');
              }
            }

            async function fetchDataHistory() {
              try {
                const res = await fetch('/api/data-history/' + encodeURIComponent(appName));
                if (res.ok) {
                  const history = await res.json();
                  updateDataChart(history);
                }
              } catch (e) {
                console.error('Failed to fetch data history:', e);
              }
            }

            async function fetchHistory() {
              try {
                const res = await fetch('/api/history/' + encodeURIComponent(appName));
                if (res.ok) {
                  const history = await res.json();
                  updateCharts(history);
                }
              } catch (e) {
                console.error('Failed to fetch history:', e);
              }
            }

            // Initial fetch
            fetchHistory();
            fetchDataHistory();

            // Update every 10 seconds
            setInterval(fetchHistory, 10000);
            setInterval(fetchDataHistory, 60000); // Data size changes slowly
          })();
        `}} />
      </Layout>
    )
  }

  // Dashboard view: system metrics gauges
  const sys = await fetchSystemMetrics()

  return c.html(
    <Layout title="Metrics">
      <GaugesGrid>
        {renderGauge(sys.cpu, 'cpu')}
        {renderGauge(sys.ram.percent, 'ram')}
        {renderGauge(sys.disk.percent, 'disk')}
      </GaugesGrid>
      <script dangerouslySetInnerHTML={{ __html: gaugeScript }} />
    </Layout>
  )
})

export default app.defaults
