import { useEffect, useId, useMemo, useState, type JSX } from 'react'
import {
  Button,
  DeckTooltipLayer,
  DetailDrawer,
  FilterInput,
  KpiTile,
  KvPanel,
  LinkButton,
  SectionCard,
  formatDurationMs,
  formatRelativeTime,
  TimeSeriesChart,
  StatusChip,
  planStatusCategory,
  useDeckTooltip,
} from '@overdeck/deck-ui'
import {
  DataTable,
  withColumnResizing,
  withSearch,
  withSorting,
  type DataTableColumn,
} from '@platform-modules/ui-primitives'
import {
  type ActivityCategory,
  type ActivityEvent,
  type ActivityReadResponse,
  type ActivitySeries,
  type ActivityQuery,
  type ActivitySourceCoverage,
  type ActivitySeverity,
  type SuppressedNotificationRow,
  ACTIVITY_SEVERITY_ORDER,
} from '../../lib/activity-types'
import { useActivity } from '../../lib/collector-queries'
import { activityDetailRows } from './activity-detail-rows'
import { CollectorQueryBoundary } from '../shared/CollectorQueryBoundary'

type RangeRange = '1h' | '1d' | '3d' | '7d' | '30d' | 'all'
type Grouping = 'on' | 'off'

interface LogsContentBodyProps {
  data: ActivityReadResponse
  errorTotal: number | undefined
  range: RangeRange
  category: ActivityCategory | undefined
  project: string | undefined
  severityFloor: ActivitySeverity | undefined
  grouping: Grouping
  searchText: string
  onRange(next: RangeRange): void
  onCategory(next: ActivityCategory | undefined): void
  onProject(next: string | undefined): void
  onSeverity(next: ActivitySeverity | undefined): void
  onGrouping(next: Grouping): void
  onSearch(next: string): void
  selectedEventId: string | null
  onSelectEvent(id: string | null): void
}

interface EventRow {
  kind: 'single' | 'group'
  key: string
  count: number
  events: ActivityEvent[]
  newest: ActivityEvent
  spanMs: number | undefined
}

const LOG_LIMIT = 500

const RANGE_MS: Record<Exclude<RangeRange, 'all'>, number> = {
  '1h': 60 * 60 * 1000,
  '1d': 24 * 60 * 60 * 1000,
  '3d': 3 * 24 * 60 * 60 * 1000,
  '7d': 7 * 24 * 60 * 60 * 1000,
  '30d': 30 * 24 * 60 * 60 * 1000,
}

const SOURCE_CAPABILITIES = [withSorting(), withSearch({ label: 'Search activity sources' }), withColumnResizing()]
const EVENT_CAPABILITIES = [withSorting({ defaultSort: { columnId: 'time', direction: 'desc' } }), withSearch({ label: 'Search activity events' }), withColumnResizing()]
const SUPPRESSED_CAPABILITIES = [withSorting(), withSearch({ label: 'Search suppressed notifications' }), withColumnResizing()]

function UnknownValue({ text }: { text: string }) {
  return <span className="text-fg-subtle">{text}</span>
}

function TruncatedCell({ value, className = 'text-fg-muted' }: { value: string; className?: string }): JSX.Element {
  const tooltipProps = useDeckTooltip(value)
  return <span {...tooltipProps} className={`block max-w-full truncate ${className}`}>{value}</span>
}

function RelativeTimeCell({ value, tooltip }: { value?: string | null; tooltip?: string }): JSX.Element {
  const timestamp = value === undefined || value === null ? Number.NaN : Date.parse(value)
  const tooltipProps = useDeckTooltip(value ?? '', tooltip)
  if (!Number.isFinite(timestamp)) return <UnknownValue text="—" />
  return (
    <time dateTime={value ?? undefined} className="tabular-nums" {...tooltipProps}>
      {formatRelativeTime(timestamp)}
    </time>
  )
}

function parseTime(value: string): number {
  return Date.parse(value)
}

function dedupeKeyOf(value: string | undefined): string | undefined {
  const text = value?.trim()
  if (text === undefined || text === '') return undefined
  return text
}

function eventContext(event: ActivityEvent): string {
  return event.project ?? event.host ?? event.runtime ?? event.session ?? '—'
}

function spanLabel(spanMs: number | undefined): string {
  if (spanMs === undefined) return '—'
  return formatDurationMs(spanMs)
}

function buildEventRows(events: readonly ActivityEvent[], grouping: Grouping): EventRow[] {
  if (grouping === 'off') {
    return events.map((event) => ({
      kind: 'single',
      key: `event:${event.id}`,
      count: 1,
      events: [event],
      newest: event,
      spanMs: undefined,
    }))
  }

  const grouped = new Map<string, ActivityEvent[]>()
  const singles: EventRow[] = []

  for (const event of events) {
    const dedupe = dedupeKeyOf(event.dedupeKey)
    if (dedupe === undefined) {
      singles.push({
        kind: 'single',
        key: `event:${event.id}`,
        count: 1,
        events: [event],
        newest: event,
        spanMs: undefined,
      })
      continue
    }

    const list = grouped.get(dedupe)
    if (list === undefined) grouped.set(dedupe, [event])
    else list.push(event)
  }

  const groupedRows: EventRow[] = [...grouped.entries()].map(([dedupe, groupedEvents]) => {
    const sorted = [...groupedEvents].sort((left, right) => {
      const leftTime = parseTime(left.ts)
      const rightTime = parseTime(right.ts)
      if (Number.isFinite(leftTime) && Number.isFinite(rightTime)) return rightTime - leftTime
      if (Number.isFinite(leftTime)) return -1
      if (Number.isFinite(rightTime)) return 1
      return 0
    })

    const times = sorted.map((event) => parseTime(event.ts)).filter((entry): entry is number => Number.isFinite(entry))
    const newest = sorted[0]
    return {
      kind: 'group',
      key: `group:${dedupe}`,
      count: sorted.length,
      events: sorted,
      newest: newest ?? groupedEvents[0]!,
      spanMs: times.length === 0 ? undefined : times.at(0)! - times.at(-1)!,
    }
  })

  return [...singles, ...groupedRows]
}

function detailRowsForRow(row: EventRow): Array<{ label: string; value: string }> {
  const rows = activityDetailRows(row.newest)
  if (row.kind === 'group') {
    rows.unshift({ label: 'count', value: `×${row.count}` })
    rows.unshift({ label: 'span', value: spanLabel(row.spanMs) })
  }
  return rows
}

function EventDeepLinks({ event }: { event: ActivityEvent }) {
  const workload = typeof event.detail?.workloadUid === 'string' ? event.detail.workloadUid : null
  const build = typeof event.detail?.buildKey === 'string' ? event.detail.buildKey : null
  return <div className="mb-2 flex flex-wrap gap-2">{event.evidence?.map((evidence) => <LinkButton key={`${evidence.sourceId}:${evidence.recordId}`} href={`/logs/${encodeURIComponent(evidence.sourceId)}?event=${encodeURIComponent(evidence.recordId)}`}>Open evidence</LinkButton>)}{event.host ? <LinkButton href={`/cluster?node=${encodeURIComponent(event.host)}`}>Open cluster node</LinkButton> : null}{workload ? <LinkButton href={`/cluster?workload=${encodeURIComponent(workload)}`}>Open workload</LinkButton> : null}{build ? <LinkButton href={`/cluster?build=${encodeURIComponent(build)}`}>Open build placement</LinkButton> : null}{event.session ? <LinkButton href={`/sessions?session=${encodeURIComponent(event.session)}`}>Open session record</LinkButton> : null}</div>
}

function sortedCoverageRows(coverage: ActivitySourceCoverage[]) {
  return [...coverage].sort((left, right) => left.label.localeCompare(right.label))
}

function SuppressedSample({ row }: { row: SuppressedNotificationRow }) {
  const sample = [row.summarySample, row.bodySample].filter((part) => part !== undefined && part !== '').join(' — ')
  if (sample === '') return <UnknownValue text="not recorded" />
  return <span className="block max-w-full truncate text-fg-muted">{sample}</span>
}

function SourceStatus({ source }: { source: ActivitySourceCoverage }) {
  return (
    <span className="flex items-center">
      <StatusChip status={planStatusCategory(source.status === 'ok' ? 'done' : source.status === 'error' ? 'error' : 'not-started', false)} label={source.status === 'absent' ? 'not recorded' : source.status} />
    </span>
  )
}

function LogsSkeleton(): JSX.Element {
  return <SectionCard title="Logs">Loading logs activity…</SectionCard>
}

function RangeFilters({ value, onChange }: { value: RangeRange; onChange(next: RangeRange): void }) {
  const options: RangeRange[] = ['1h', '1d', '3d', '7d', '30d', 'all']
  return (
    <div className="flex flex-wrap items-center gap-2">
      <span className="text-fg-muted text-sm">Range</span>
      {options.map((option) => {
        const active = option === value
        return (
          <Button
            key={option}
            variant={active ? 'solid' : 'outline'}
            tone={active ? 'accent' : 'neutral'}
            size="sm"
            onClick={() => onChange(option)}
          >
            {option}
          </Button>
        )
      })}
    </div>
  )
}

function SeverityFilters({ value, onChange }: { value: ActivitySeverity | undefined; onChange(next: ActivitySeverity | undefined): void }) {
  const options: Array<{ label: string; value: ActivitySeverity | undefined }> = [
    { label: 'All', value: undefined },
    { label: 'info', value: 'info' },
    { label: 'notice', value: 'notice' },
    { label: 'warn', value: 'warn' },
    { label: 'error', value: 'error' },
  ]
  return (
    <div className="flex flex-wrap items-center gap-2">
      <span className="text-fg-muted text-sm">Severity</span>
      {options.map((option) => {
        const active = option.value === value
        return (
          <Button
            key={`${option.label}-${option.value ?? 'all'}`}
            variant={active ? 'solid' : 'outline'}
            tone={active ? 'accent' : 'neutral'}
            size="sm"
            onClick={() => onChange(option.value)}
          >
            {option.label}
          </Button>
        )
      })}
    </div>
  )
}

function CategoryFacets({ category, counts, onChange }: { category: ActivityCategory | undefined; counts: Array<{ category: ActivityCategory; count: number }>; onChange(next: ActivityCategory | undefined): void }) {
  return <div className="flex flex-wrap items-center gap-2"><span className="text-fg-muted text-sm">Category</span><Button variant={category === undefined ? 'solid' : 'outline'} tone={category === undefined ? 'accent' : 'neutral'} size="sm" onClick={() => onChange(undefined)}>All</Button>{counts.map((entry) => <Button key={entry.category} variant={category === entry.category ? 'solid' : 'outline'} tone={category === entry.category ? 'accent' : 'neutral'} size="sm" onClick={() => onChange(category === entry.category ? undefined : entry.category)}>{entry.category}<span className="ml-1 tabular-nums text-fg-subtle">{entry.count}</span></Button>)}</div>
}

function ProjectFacets({ project, counts, onChange }: { project: string | undefined; counts: Array<{ project: string; count: number }>; onChange(next: string | undefined): void }) {
  const [search, setSearch] = useState('')
  const visible = counts.filter((entry) => entry.project.toLowerCase().includes(search.trim().toLowerCase())).slice(0, 20)
  return <div className="flex flex-col gap-2"><FilterInput label="Search projects" value={search} onChange={setSearch} /><div className="flex flex-wrap items-center gap-2"><span className="text-fg-muted text-sm">Project</span><Button variant={project === undefined ? 'solid' : 'outline'} tone={project === undefined ? 'accent' : 'neutral'} size="sm" onClick={() => onChange(undefined)}>All</Button>{visible.map((entry) => <Button key={entry.project} variant={project === entry.project ? 'solid' : 'outline'} tone={project === entry.project ? 'accent' : 'neutral'} size="sm" onClick={() => onChange(project === entry.project ? undefined : entry.project)}>{entry.project}<span className="ml-1 tabular-nums text-fg-subtle">{entry.count}</span></Button>)}</div></div>
}

function SeriesTable({ series }: { series: ActivitySeries[] }) {
  const rows = series.map((entry) => ({ ...entry, summary: entry.id === 'activeAgents' ? Math.max(0, ...entry.points.map((point) => point.value)) : entry.points.reduce((sum, point) => sum + point.value, 0), latest: entry.points.at(-1)?.ts }))
  const columns: DataTableColumn<(typeof rows)[number]>[] = [
    { id: 'series', header: 'Series', sortable: true, sortValue: (row) => row.label, cell: (row) => <span className="font-medium text-fg">{row.label}</span> },
    { id: 'coverage', header: 'Coverage', sortable: true, sortValue: (row) => row.status, cell: (row) => <span className="flex flex-col gap-1"><StatusChip status={planStatusCategory(row.status === 'available' ? 'done' : 'blocked', false)} label={row.status} />{row.coverageFrom ? <span className="text-xs text-fg-subtle">from {new Date(row.coverageFrom).toISOString()}</span> : null}{row.gaps?.map((gap) => <span key={`${gap.from}:${gap.to}`} className="text-xs text-fg-subtle">Gap: {gap.reason}</span>)}{row.reason ? <span className="text-xs text-fg-subtle">{row.reason}</span> : null}</span> },
    { id: 'summary', header: 'Summary', sortable: true, sortValue: (row) => row.summary, cell: (row) => <span className="block text-right tabular-nums">{row.status === 'available' ? `${row.id === 'activeAgents' ? 'peak ' : ''}${row.summary}` : '—'}</span> },
    { id: 'latest', header: 'Latest bucket', sortable: true, sortValue: (row) => row.latest ? Date.parse(row.latest) : null, cell: (row) => <RelativeTimeCell value={row.latest} /> },
  ]
  return <DataTable caption="Requested activity series" columns={columns} rows={rows} getRowId={(row) => row.id} capabilities={[withSorting()]} />
}

function GroupingToggle({ value, onChange }: { value: Grouping; onChange(next: Grouping): void }) {
  return (
    <div className="flex flex-wrap items-center gap-2">
      <span className="text-fg-muted text-sm">Grouping</span>
      {(['on', 'off'] as const).map((option) => {
        const active = option === value
        return (
          <Button
            key={option}
            variant={active ? 'solid' : 'outline'}
            tone={active ? 'accent' : 'neutral'}
            size="sm"
            onClick={() => onChange(option)}
          >
            {option.toUpperCase()}
          </Button>
        )
      })}
    </div>
  )
}

export function LogsContentBody(props: LogsContentBodyProps) {
  const { data, errorTotal } = props
  const [selectedEventId, setSelectedEventId] = useState(props.selectedEventId)
  const eventTitleId = useId()
  const sourceRowsSorted = useMemo(() => sortedCoverageRows(data.coverage), [data.coverage])
  const eventRows = useMemo(() => buildEventRows(data.events, props.grouping), [data.events, props.grouping])
  const selectedEvent = eventRows.find((row) => row.key === selectedEventId) ?? null

  const sourceColumns: DataTableColumn<ActivitySourceCoverage>[] = [
    { id: 'source', header: 'Source', minWidth: 150, resizable: true, sortable: true, sortValue: (row) => row.label.toLowerCase(), searchValue: (row) => row.label, cell: (row) => <a href={`/logs/${encodeURIComponent(row.id)}`} data-source-link={row.id} className="block max-w-full truncate font-semibold text-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent">{row.label}</a> },
    { id: 'status', header: 'Status', minWidth: 130, resizable: true, sortable: true, sortValue: (row) => row.status.toLowerCase(), searchValue: (row) => row.status, cell: (row) => <span className="flex flex-col gap-1"><SourceStatus source={row} />{row.status === 'error' ? <TruncatedCell value={row.error ?? 'not recorded'} className="text-xs text-danger" /> : null}</span> },
    { id: 'eventsInRange', header: 'Events in range', minWidth: 130, resizable: true, sortable: true, sortValue: (row) => row.records, cell: (row) => <span className="block text-right tabular-nums">{row.records}</span> },
    { id: 'sourceRecords', header: 'Source records', minWidth: 120, resizable: true, sortable: true, sortValue: (row) => row.totalRecords, cell: (row) => <span className="block text-right tabular-nums">{row.totalRecords}</span> },
    { id: 'skipped', header: 'Skipped', minWidth: 90, resizable: true, sortable: true, sortValue: (row) => row.skipped, cell: (row) => <span className="block text-right tabular-nums">{row.skipped}</span> },
    { id: 'earliest', header: 'Earliest', minWidth: 110, resizable: true, sortable: true, sortValue: (row) => row.earliest ? Date.parse(row.earliest) : null, cell: (row) => <RelativeTimeCell value={row.earliest} tooltip="Earliest record" /> },
    { id: 'latest', header: 'Latest', minWidth: 110, resizable: true, sortable: true, sortValue: (row) => row.latest ? Date.parse(row.latest) : null, cell: (row) => <RelativeTimeCell value={row.latest} tooltip="Latest record" /> },
    { id: 'authority', header: 'Authority', minWidth: 120, resizable: true, sortable: true, sortValue: (row) => row.authority ?? '', cell: (row) => row.authority ?? <UnknownValue text="—" /> },
    { id: 'storage', header: 'Storage', minWidth: 180, resizable: true, sortable: true, sortValue: (row) => row.storage, searchValue: (row) => row.storage, cell: (row) => <TruncatedCell value={row.storage} /> },
    { id: 'retention', header: 'Retention', minWidth: 220, resizable: true, sortable: true, sortValue: (row) => row.retention ?? '', searchValue: (row) => row.retention ?? '', cell: (row) => row.retention ? <TruncatedCell value={row.retention} /> : <UnknownValue text="not recorded" /> },
    { id: 'queryBounds', header: 'Query bounds', minWidth: 240, resizable: true, sortable: true, sortValue: (row) => row.queryBounds, searchValue: (row) => row.queryBounds, cell: (row) => <TruncatedCell value={row.queryBounds} /> },
    { id: 'path', header: 'Path', minWidth: 240, resizable: true, sortable: true, sortValue: (row) => row.path.toLowerCase(), searchValue: (row) => row.path, cell: (row) => <TruncatedCell value={row.path} className="text-fg" /> },
  ]

  const eventColumns: DataTableColumn<EventRow>[] = [
    { id: 'time', header: 'Time', minWidth: 120, resizable: true, sortable: true, sortValue: (row) => Date.parse(row.newest.ts), cell: (row) => <span className="flex flex-col" data-event-row={row.key}><RelativeTimeCell value={row.newest.ts} tooltip="Latest event" />{row.kind === 'group' && row.count > 1 ? <span className="text-[11px] text-fg-subtle">span {spanLabel(row.spanMs)}</span> : null}</span> },
    { id: 'source', header: 'Source', minWidth: 110, resizable: true, sortable: true, sortValue: (row) => row.newest.source.toLowerCase(), searchValue: (row) => row.newest.source, cell: (row) => <span className="flex items-center gap-1"><TruncatedCell value={row.newest.source} className="font-medium text-fg" />{row.count > 1 ? <span className="shrink-0 tabular-nums text-[10px] text-fg-muted">×{row.count}</span> : null}</span> },
    { id: 'category', header: 'Category', minWidth: 100, resizable: true, sortable: true, sortValue: (row) => row.newest.category.toLowerCase(), searchValue: (row) => row.newest.category, cell: (row) => <TruncatedCell value={row.newest.category} className="text-fg" /> },
    { id: 'severity', header: 'Severity', minWidth: 100, resizable: true, sortable: true, sortValue: (row) => ACTIVITY_SEVERITY_ORDER[row.newest.severity], searchValue: (row) => row.newest.severity, cell: (row) => <StatusChip status={row.newest.severity} /> },
    { id: 'actor', header: 'Actor', minWidth: 100, resizable: true, sortable: true, sortValue: (row) => row.newest.actor.toLowerCase(), searchValue: (row) => row.newest.actor, cell: (row) => <TruncatedCell value={row.newest.actor} className="text-fg" /> },
    { id: 'account', header: 'Account', minWidth: 100, resizable: true, sortable: true, sortValue: (row) => row.newest.account?.toLowerCase() ?? '', searchValue: (row) => row.newest.account ?? '', cell: (row) => row.newest.account === undefined ? <UnknownValue text="—" /> : <TruncatedCell value={row.newest.account} className="text-fg" /> },
    { id: 'context', header: 'Context', minWidth: 120, resizable: true, sortable: true, sortValue: (row) => eventContext(row.newest).toLowerCase(), searchValue: (row) => eventContext(row.newest), cell: (row) => <TruncatedCell value={eventContext(row.newest)} className="text-fg" /> },
    { id: 'duration', header: 'Duration', minWidth: 100, resizable: true, sortable: true, sortValue: (row) => row.newest.durationMs ?? null, cell: (row) => <span className="block text-right tabular-nums">{row.newest.durationMs === undefined ? '—' : formatDurationMs(row.newest.durationMs)}</span> },
    { id: 'title', header: 'Title', minWidth: 220, resizable: true, sortable: true, sortValue: (row) => row.newest.title.toLowerCase(), searchValue: (row) => row.newest.title, cell: (row) => <TruncatedCell value={row.newest.title} /> },
    { id: 'details', header: '', minWidth: 90, resizable: true, cell: (row) => <Button size="sm" variant="ghost" tone="neutral" onClick={() => { setSelectedEventId(row.key); props.onSelectEvent(row.key) }} aria-expanded={selectedEventId === row.key}>Details</Button> },
  ]

  const suppressedColumns: DataTableColumn<SuppressedNotificationRow>[] = [
    { id: 'source', header: 'Source', minWidth: 140, sortable: true, sortValue: (row) => row.program.toLowerCase(), searchValue: (row) => row.program, cell: (row) => <span className="font-medium text-fg">{row.program}</span> },
    { id: 'attempts', header: 'Attempts', minWidth: 90, sortable: true, sortValue: (row) => row.count, cell: (row) => <span className="block text-right tabular-nums">{row.count}</span> },
    { id: 'lastAttempt', header: 'Last attempt', minWidth: 120, sortable: true, sortValue: (row) => row.last ? Date.parse(row.last) : null, cell: (row) => <RelativeTimeCell value={row.last} tooltip="Last attempt" /> },
    { id: 'sample', header: 'Sample message', minWidth: 240, sortable: true, sortValue: (row) => `${row.summarySample ?? ''} ${row.bodySample ?? ''}`.toLowerCase(), searchValue: (row) => `${row.summarySample ?? ''} ${row.bodySample ?? ''}`, cell: (row) => <SuppressedSample row={row} /> },
  ]

  const kpiTiles = useMemo(() => {
    const tiles: { key: string; label: string; value: number }[] = []
    if (Number.isFinite(data.total)) {
      tiles.push({ key: 'events-in-range', label: 'Events in range', value: data.total })
    }
    if (errorTotal !== undefined && Number.isFinite(errorTotal)) {
      tiles.push({ key: 'errors-in-range', label: 'Errors in range', value: errorTotal })
    }
    const sourcesReporting = data.coverage.filter((source) => source.status === 'ok').length
    if (Number.isFinite(sourcesReporting)) {
      tiles.push({ key: 'sources-reporting', label: 'Sources reporting', value: sourcesReporting })
    }
    if (Number.isFinite(data.suppressedNotifications.attemptCount)) {
      tiles.push({
        key: 'suppressed-attempts',
        label: 'Suppressed notification attempts',
        value: data.suppressedNotifications.attemptCount,
      })
    }
    return tiles
  }, [data, errorTotal])

  return (
    <div className="flex flex-col gap-3.5" data-testid="logs-page">
      <SectionCard title="Controls">
        <div className="flex flex-col gap-2">
          <RangeFilters value={props.range} onChange={props.onRange} />
          <SeverityFilters value={props.severityFloor} onChange={props.onSeverity} />
          <CategoryFacets category={props.category} counts={data.categoryCounts.filter((entry) => entry.count > 0)} onChange={props.onCategory} />
          <ProjectFacets project={props.project} counts={(data.projectCounts ?? []).filter((entry) => entry.count > 0)} onChange={props.onProject} />
          <GroupingToggle value={props.grouping} onChange={props.onGrouping} />
          <FilterInput label="Filter" value={props.searchText} onChange={props.onSearch} />
        </div>
      </SectionCard>

      {kpiTiles.length > 0 && (
        <div className="grid gap-2 @min-[40rem]:grid-cols-2 @min-[64rem]:grid-cols-4">
          {kpiTiles.map((tile) => (
            <KpiTile key={tile.key} tile={{ key: tile.key, label: tile.label, value: tile.value }} />
          ))}
        </div>
      )}

      <SectionCard title="Activity series">
        <p className="mb-2 text-xs text-fg-subtle">Server-produced counts for the selected range and filters. Unavailable series name the missing authoritative coverage.</p>
        <div className="space-y-5">
          {(data.series ?? []).map((entry) => <TimeSeriesChart key={entry.id} label={entry.label} points={entry.points} status={entry.status} reason={entry.reason} coverageFrom={entry.coverageFrom} gaps={entry.gaps} valueLabel={entry.id === 'activeAgents' ? 'agents' : 'events'} />)}
          <SeriesTable series={data.series ?? []} />
        </div>
      </SectionCard>

      <SectionCard title="Sources">
        <p className="mb-2 text-xs text-fg-subtle">
          Select a source to read its individual entries, including the records it had to skip.
        </p>
        <DataTable caption="Activity source coverage" columns={sourceColumns} rows={sourceRowsSorted} getRowId={(row) => row.id} capabilities={SOURCE_CAPABILITIES} />
      </SectionCard>

      <SectionCard title="Events">
        {data.truncated ? (
          <p className="text-sm text-fg-muted">
            Showing the newest {data.events.length} of {data.total} events in this range — narrow the range or pick a category to see the rest.
          </p>
        ) : null}

        {data.events.length === 0 ? (
          <p className="text-sm text-fg-muted">No activity matched these filters in this range.</p>
        ) : (
          <DataTable caption="Activity events" columns={eventColumns} rows={eventRows} getRowId={(row) => row.key} capabilities={EVENT_CAPABILITIES} />
        )}
      </SectionCard>

      <SectionCard title="Suppressed notifications">
        <p className="mb-2 text-xs text-fg-subtle">
          How often each source tried to reach the screen, with a sample of what it said. Every individual attempt, with its
          full text, is in the{' '}
          <a
            href="/logs/notif-gate"
            className="font-semibold text-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
          >
            notification gate source
          </a>
          .
        </p>
        {data.suppressedNotifications.status === 'error' ? (
          <p className="mb-2 text-sm text-fg-subtle">{data.suppressedNotifications.error ?? 'Suppressed notification coverage failed.'}</p>
        ) : data.suppressedNotifications.status === 'absent' ? (
          <p className="mb-2 text-sm text-fg-subtle">not recorded</p>
        ) : (
          data.suppressedNotifications.rows.length === 0 ? (
            <p className="text-sm text-fg-subtle">No suppressed notifications matched these filters in this range.</p>
          ) : (
            <DataTable caption="Suppressed notifications" columns={suppressedColumns} rows={data.suppressedNotifications.rows} getRowId={(row) => row.id} capabilities={SUPPRESSED_CAPABILITIES} />
          )
        )}
      </SectionCard>

      {selectedEvent ? <DetailDrawer eyebrow={`${selectedEvent.newest.source} · ${selectedEvent.newest.category}`} title={selectedEvent.newest.title} titleId={eventTitleId} onClose={() => { setSelectedEventId(null); props.onSelectEvent(null) }}><div className="border-t border-border bg-surface px-2 py-2"><div className="max-w-2xl"><EventDeepLinks event={selectedEvent.newest} /><KvPanel rows={detailRowsForRow(selectedEvent)} /></div></div></DetailDrawer> : null}

      <DeckTooltipLayer />
    </div>
  )
}

const ACTIVITY_CATEGORIES = new Set<ActivityCategory>(['land', 'gate', 'deploy', 'run', 'agent', 'buildbox', 'guard', 'notification', 'git', 'service', 'ci'])

export function logsInitialState(search: string): { range: RangeRange; category?: ActivityCategory; project?: string; severity?: ActivitySeverity; grouping: Grouping; q: string; event: string | null } {
  const params = new URLSearchParams(search)
  const range = params.get('range')
  const category = params.get('category') as ActivityCategory | null
  const severity = params.get('severity') as ActivitySeverity | null
  return {
    range: range === 'all' || (range !== null && range in RANGE_MS) ? range as RangeRange : '1d',
    ...(category && ACTIVITY_CATEGORIES.has(category) ? { category } : {}),
    ...(params.get('project') ? { project: params.get('project')! } : {}),
    ...(severity && severity in ACTIVITY_SEVERITY_ORDER ? { severity } : {}),
    grouping: params.get('grouping') === 'off' ? 'off' : 'on',
    q: params.get('q') ?? '',
    event: params.get('event'),
  }
}

export function LogsContent() {
  const initial = useMemo(() => logsInitialState(typeof window === 'undefined' ? '' : window.location.search), [])
  const [range, setRange] = useState<RangeRange>(initial.range)
  const [category, setCategory] = useState<ActivityCategory | undefined>(initial.category)
  const [project, setProject] = useState<string | undefined>(initial.project)
  const [severityFloor, setSeverityFloor] = useState<ActivitySeverity | undefined>(initial.severity)
  const [grouping, setGrouping] = useState<Grouping>(initial.grouping)
  const [searchText, setSearchText] = useState(initial.q)
  const [debouncedSearch, setDebouncedSearch] = useState(initial.q)
  const [selectedEventId, setSelectedEventId] = useState<string | null>(initial.event)
  const [now, setNow] = useState(() => Date.now())
  const params = useMemo(() => new URLSearchParams(typeof window === 'undefined' ? '' : window.location.search), [])
  const node = params.get('node') ?? undefined
  const workload = params.get('workload') ?? undefined
  const build = params.get('build') ?? undefined

  useEffect(() => {
    const timer = window.setTimeout(() => {
      setDebouncedSearch(searchText.trim())
    }, 250)
    return () => window.clearTimeout(timer)
  }, [searchText])

  useEffect(() => {
    const timer = window.setInterval(() => setNow(Date.now()), 15_000)
    return () => window.clearInterval(timer)
  }, [])

  useEffect(() => {
    const params = new URLSearchParams(window.location.search)
    const values: Array<[string, string | undefined | null]> = [['range', range], ['category', category], ['project', project], ['severity', severityFloor], ['grouping', grouping === 'off' ? 'off' : undefined], ['q', searchText || undefined], ['event', selectedEventId]]
    for (const [key, value] of values) value ? params.set(key, value) : params.delete(key)
    window.history.replaceState(null, '', `${window.location.pathname}?${params.toString()}`)
  }, [category, grouping, project, range, searchText, selectedEventId, severityFloor])

  const from = useMemo(() => range === 'all' ? undefined : new Date(now - RANGE_MS[range]).toISOString(), [now, range])
  const to = useMemo(() => range === 'all' ? undefined : new Date(now).toISOString(), [now, range])
  const query = useMemo<ActivityQuery>(() => {
    return {
      limit: LOG_LIMIT,
      ...(from === undefined ? {} : { from, to }),
      ...(category === undefined ? {} : { category }),
      ...(project === undefined ? {} : { project }),
      ...(severityFloor === undefined ? {} : { severity: severityFloor }),
      ...(debouncedSearch === '' ? {} : { q: debouncedSearch }),
      ...(node ? { node } : {}), ...(workload ? { workload } : {}), ...(build ? { build } : {}),
    }
  }, [category, project, from, to, severityFloor, debouncedSearch, node, workload, build])

  const logsQuery = useActivity(query)
  return (
    <CollectorQueryBoundary query={logsQuery} skeleton={<LogsSkeleton />}>
      {(data) => <LogsContentBody {...{
        data,
        errorTotal: data.errorCount,
        range,
        category,
        project,
        severityFloor,
        grouping,
        searchText,
        onRange: setRange,
        onCategory: setCategory,
        onProject: setProject,
        onSeverity: setSeverityFloor,
        onGrouping: setGrouping,
        onSearch: setSearchText,
        selectedEventId,
        onSelectEvent: setSelectedEventId,
      }} />}
    </CollectorQueryBoundary>
  )
}
