import { useMemo, useState } from 'react'
import {
  DataTable,
  withSorting,
  type DataTableColumn,
  type DataTableSort,
} from '@platform-modules/ui-primitives'
import {
  Button,
  FilterInput,
  formatDurationMs,
  formatRelativeTime,
  KvPanel,
  LiveDuration,
  SectionCard,
  StaleBadge,
  StatusChip,
  useDeckTooltip,
} from '@overdeck/deck-ui'
import { adapterIdForPanel } from '../../lib/collector-client'
import { useCollectorState } from '../../lib/collector-queries'
import {
  groupSessionsByProject,
  idleTime,
  isRunning,
  runningTime,
  projectLabel,
  sessionStateChip,
  toAgentSessionRow,
  type AgentSessionRow,
  type Elapsed,
  type SessionsPanelData,
} from '../../lib/session-types'
import { CollectorQueryBoundary } from '../shared/CollectorQueryBoundary'
import { SessionHostsTable } from './SessionHostsTable'
import { openSessionTerminal } from './session-terminal'

/** Rows shown per project before the reader asks for the rest. */
const DEFAULT_VISIBLE_ROWS = 5

const UNKNOWN = <span className="text-fg-subtle">—</span>

function Unknown({ note }: { note: string }) {
  const tooltip = useDeckTooltip(note, 'Not recorded')
  return <span className="text-fg-subtle" {...tooltip}>—</span>
}

function TitleCell({ row, onOpen }: { row: AgentSessionRow; onOpen(): void }) {
  const idTooltip = useDeckTooltip(row.id, 'Ledger id')
  return (
    <span className="flex max-w-[22rem] min-w-0 flex-col gap-0.5">
      <button
        type="button"
        onClick={onOpen}
        className="block max-w-full truncate text-left font-semibold text-fg hover:text-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
      >
        {row.title ?? <span className="font-normal italic text-fg-subtle">no title recorded</span>}
      </button>
      <span className="block max-w-full truncate text-[11px] text-fg-subtle" {...idTooltip}>{row.id}</span>
      {row.activity ? (
        <span className="block max-w-full truncate text-[11px] text-fg-muted">{row.activity}</span>
      ) : (
        <span className="block text-[11px] italic text-fg-subtle">no current activity recorded</span>
      )}
    </span>
  )
}

function WhoCell({ row }: { row: AgentSessionRow }) {
  const parentTooltip = useDeckTooltip(row.parent ?? '', 'Launched by session')
  if (row.launchedBy === null) {
    return <Unknown note="The recorder does not stamp launchedBy yet, and this session has no parent session id." />
  }
  return (
    <span className="flex min-w-0 flex-col">
      <span className="text-fg-muted">{row.launchedBy}</span>
      {row.parent ? (
        <span className="block max-w-[12rem] truncate text-[11px] text-fg-subtle" {...parentTooltip}>{row.parent}</span>
      ) : null}
    </span>
  )
}

function HostCell({ row }: { row: AgentSessionRow }) {
  const note = row.hostFacts?.notes ?? ''
  const tooltip = useDeckTooltip(
    note || (row.hostFacts?.reachability === 'unknown' ? 'Not listed in the buildbox registry' : ''),
    'Host registry',
  )
  if (!row.host) return <Unknown note="The recorder stamped no host on this entry." />
  const disabled = row.hostFacts?.reachability === 'declared-disabled'
  return (
    <span className={disabled ? 'text-warning' : 'text-fg-muted'} {...tooltip}>
      {row.host}
      {disabled ? <span className="ml-1 text-[11px]">{row.hostFacts?.state ?? 'unusable'}</span> : null}
    </span>
  )
}

function StateCell({ row }: { row: AgentSessionRow }) {
  const chip = sessionStateChip(row.ledgerState)
  const tooltip = useDeckTooltip(row.evidence ?? '', 'Classifier evidence')
  return (
    <span className="flex flex-col items-start gap-0.5" {...(row.evidence ? tooltip : {})}>
      <StatusChip status={chip.status} label={chip.label} />
      <span className="text-[11px] text-fg-subtle">
        {row.attached === null ? 'window unknown' : row.attached ? 'window attached' : 'no window attached'}
      </span>
    </span>
  )
}

/**
 * Renders a duration, or the reason it is unknown — never a negative one, and never a
 * ticking clock for a session that is no longer running.
 */
function ElapsedCell({
  elapsed,
  label,
  missingNote,
}: {
  elapsed: Elapsed
  label: string
  missingNote: string
}) {
  const staticTooltip = useDeckTooltip(
    elapsed.kind === 'static' ? 'measured to the recorded end of the session, not to now' : '',
    label,
  )
  const seenTooltip = useDeckTooltip(
    elapsed.kind === 'lastSeen' ? new Date(elapsed.atMs).toISOString() : '',
    'Last observed activity',
  )
  const problemTooltip = useDeckTooltip(
    elapsed.kind === 'unknown' ? elapsed.problem ?? missingNote : '',
    'Data coverage gap',
  )

  if (elapsed.kind === 'unknown') {
    return <span className="text-fg-subtle" {...problemTooltip}>—</span>
  }
  if (elapsed.kind === 'static') {
    return (
      <span className="tabular-nums text-fg-muted" {...staticTooltip}>{formatDurationMs(elapsed.ms)}</span>
    )
  }
  if (elapsed.kind === 'lastSeen') {
    return (
      <time
        dateTime={new Date(elapsed.atMs).toISOString()}
        className="tabular-nums text-fg-subtle"
        {...seenTooltip}
      >
        {formatRelativeTime(elapsed.atMs)}
      </time>
    )
  }
  return (
    <span className="tabular-nums text-fg-muted">
      <LiveDuration startAtMs={elapsed.startAtMs} tooltipLabel={label} missingNote={missingNote} />
    </span>
  )
}

function ReconnectCell({ row, onOpen }: { row: AgentSessionRow; onOpen(): void }) {
  const reasonTooltip = useDeckTooltip(row.attach.reason ?? '', 'Cannot reconnect')
  if (row.attach.kind === 'none') {
    return (
      <span className="block max-w-[14rem] truncate text-[11px] text-fg-subtle" {...reasonTooltip}>
        {row.attach.reason ?? 'no reconnect path'}
      </span>
    )
  }
  const label = row.screenAttachable
    ? 'Attach'
    : row.attach.kind === 'resume'
      ? 'Resume'
      : 'Reopen'
  return (
    <Button size="sm" variant="outline" tone="accent" onClick={onOpen}>{label}</Button>
  )
}

/** Sort rank of the ledger state, most-alive first, so a state sort reads as urgency. */
const STATE_SORT: Record<AgentSessionRow['ledgerState'], number> = {
  'ALIVE-WORKING': 5,
  'ALIVE-IDLE': 4,
  'DETACHED-ALIVE': 3,
  ORPHANED: 2,
  FINISHED: 1,
}

/** Sortable magnitude of an elapsed value; unknown sorts last rather than as zero. */
function elapsedSortValue(elapsed: Elapsed, nowMs: number): number | null {
  switch (elapsed.kind) {
    case 'ticking': return nowMs - elapsed.startAtMs
    case 'static': return elapsed.ms
    case 'lastSeen': return nowMs - elapsed.atMs
    default: return null
  }
}

function buildColumns(
  onOpen: (row: AgentSessionRow) => void,
): Array<DataTableColumn<AgentSessionRow>> {
  return [
    {
      id: 'session',
      header: 'Session',
      sortable: true,
      sortValue: (row) => (row.title ?? row.id).toLowerCase(),
      cell: (row) => <TitleCell row={row} onOpen={() => onOpen(row)} />,
    },
    {
      id: 'cli',
      header: 'CLI',
      sortable: true,
      sortValue: (row) => row.cli,
      cell: (row) => <span className="text-fg-muted">{row.cli}</span>,
    },
    {
      id: 'who',
      header: 'Started by',
      sortable: true,
      sortValue: (row) => row.launchedBy,
      cell: (row) => <WhoCell row={row} />,
    },
    {
      id: 'host',
      header: 'Host',
      sortable: true,
      sortValue: (row) => row.host,
      cell: (row) => <HostCell row={row} />,
    },
    {
      id: 'branch',
      header: 'Branch',
      sortable: true,
      sortValue: (row) => row.branch,
      cell: (row) => (row.branch ? <span className="text-fg-muted">{row.branch}</span> : UNKNOWN),
    },
    {
      id: 'state',
      header: 'State',
      sortable: true,
      sortValue: (row) => STATE_SORT[row.ledgerState],
      cell: (row) => <StateCell row={row} />,
    },
    {
      id: 'running',
      header: 'Running',
      sortable: true,
      sortValue: (row) => elapsedSortValue(runningTime(row, Date.now()), Date.now()),
      cell: (row) => (
        <ElapsedCell
          elapsed={runningTime(row, Date.now())}
          label="Session started"
          missingNote="No startedAt recorded for this session"
        />
      ),
    },
    {
      id: 'idle',
      header: 'Idle',
      sortable: true,
      sortValue: (row) => elapsedSortValue(idleTime(row, Date.now()), Date.now()),
      cell: (row) => (
        <ElapsedCell
          elapsed={idleTime(row, Date.now())}
          label="Last observed activity"
          missingNote="No lastActiveAt recorded — the classifier saw no transcript or heartbeat for this session"
        />
      ),
    },
    {
      id: 'reconnect',
      header: 'Reconnect',
      cell: (row) => <ReconnectCell row={row} onOpen={() => onOpen(row)} />,
    },
  ]
}

function compareKeys(left: string | number, right: string | number): number {
  if (typeof left === 'number' && typeof right === 'number') return left - right
  return String(left).localeCompare(String(right))
}

/**
 * Running sessions stay above stopped ones under every sort — the chosen column only ever
 * orders rows within the live half and within the dead half, so re-sorting can never push
 * a live session behind the truncation control.
 */
export function orderSessions(
  rows: AgentSessionRow[],
  columns: Array<DataTableColumn<AgentSessionRow>>,
  sort: DataTableSort | null,
): AgentSessionRow[] {
  const column = sort ? columns.find((entry) => entry.id === sort.columnId) : undefined
  const sortValue = column?.sortValue
  const direction = sort?.direction === 'desc' ? -1 : 1
  return rows
    .map((row, index) => ({ row, index }))
    .sort((left, right) => {
      const liveRank = Number(!isRunning(left.row.state)) - Number(!isRunning(right.row.state))
      if (liveRank !== 0) return liveRank
      if (sortValue) {
        const keyLeft = sortValue(left.row)
        const keyRight = sortValue(right.row)
        if (keyLeft === null || keyRight === null) {
          if (keyLeft !== keyRight) return keyLeft === null ? 1 : -1
        } else {
          const order = compareKeys(keyLeft, keyRight)
          if (order !== 0) return order * direction
        }
      }
      return left.index - right.index
    })
    .map((entry) => entry.row)
}

function ProjectSection({
  project,
  rows,
  liveCount,
  homeDir,
  columns,
  sort,
  onSortChange,
}: {
  project: string
  rows: AgentSessionRow[]
  liveCount: number
  homeDir: string | null
  columns: Array<DataTableColumn<AgentSessionRow>>
  sort: DataTableSort | null
  onSortChange(next: DataTableSort | null): void
}) {
  // A project with a running session opens itself; the rest stay one click away.
  const [open, setOpen] = useState(liveCount > 0)
  const [showAll, setShowAll] = useState(false)
  const label = projectLabel(project, homeDir)
  const summary = `${label} · ${rows.length} session${rows.length === 1 ? '' : 's'}${liveCount > 0 ? ` · ${liveCount} live` : ''}`
  const ordered = useMemo(() => orderSessions(rows, columns, sort), [rows, columns, sort])
  // Never fewer than the live rows: a project with more live sessions than the default
  // shows all of them rather than hiding a running session behind the control.
  const collapsedCount = Math.max(DEFAULT_VISIBLE_ROWS, liveCount)
  const shown = showAll ? ordered : ordered.slice(0, collapsedCount)
  const hidden = ordered.length - shown.length

  return (
    <SectionCard title={summary} collapsible={{ expanded: open, onToggle: () => setOpen(!open) }}>
      <p className="mb-2 truncate text-[11px] text-fg-subtle"><code>{project}</code></p>
      <DataTable
        caption={`Sessions in ${label}`}
        columns={columns}
        rows={shown}
        getRowId={(row) => row.id}
        capabilities={[withSorting({ sort, onSortChange, manual: true })]}
        emptyState={<span className="text-fg-muted">No sessions in this project.</span>}
      />
      {hidden > 0 || showAll ? (
        <div className="mt-2">
          <Button size="sm" variant="outline" tone="neutral" onClick={() => setShowAll(!showAll)}>
            {showAll ? `Show ${collapsedCount} of ${ordered.length}` : `${hidden} more`}
          </Button>
        </div>
      ) : null}
    </SectionCard>
  )
}

function matches(row: AgentSessionRow, needle: string): boolean {
  if (needle === '') return true
  const hay = [
    row.title,
    row.id,
    row.cli,
    row.host,
    row.project,
    row.branch,
    row.activity,
    row.launchedBy,
    sessionStateChip(row.ledgerState).label,
  ]
    .filter((value): value is string => typeof value === 'string')
    .join(' ')
    .toLowerCase()
  return hay.includes(needle.toLowerCase())
}

/** Names every field the panel wants but the ledger does not carry yet, so an empty
 * column reads as a known gap rather than as "nothing is happening". */
function coverageGaps(rows: AgentSessionRow[]): string[] {
  const gaps: string[] = []
  const missing = (predicate: (row: AgentSessionRow) => boolean): number => rows.filter(predicate).length
  const noTitle = missing((row) => row.title === null)
  const noActivity = missing((row) => row.activity === null)
  const noLauncher = missing((row) => row.launchedBy === null)
  const noHost = missing((row) => row.host === null)
  if (noTitle > 0) gaps.push(`${noTitle} without a title — the recorder does not derive one from the transcript yet`)
  if (noActivity > 0) gaps.push(`${noActivity} without a current activity — the recorder does not record one yet`)
  if (noLauncher > 0) gaps.push(`${noLauncher} without a launcher — user / factory / headless cannot be told apart until the recorder stamps launchedBy`)
  if (noHost > 0) gaps.push(`${noHost} without a host`)
  return gaps
}

export function SessionsContent() {
  const stateQuery = useCollectorState()
  const [filter, setFilter] = useState('')

  return (
    <CollectorQueryBoundary query={stateQuery}>
      {(state) => {
        const panel = state.panels.find((entry) => entry.id === 'sessions')
        const data = panel?.data as SessionsPanelData | undefined
        const status = state.adapters.find((adapter) => adapter.id === adapterIdForPanel('sessions'))

        if (!panel || !data) {
          return <div className="text-fg-muted">Sessions unavailable — the sessions adapter has not reported yet.</div>
        }

        return (
          <SessionsPanel
            data={data}
            staleBadge={status ? <StaleBadge status={status} /> : undefined}
            filter={filter}
            onFilter={setFilter}
            onOpen={openSessionTerminal}
          />
        )
      }}
    </CollectorQueryBoundary>
  )
}

export function SessionsPanel({
  data,
  staleBadge,
  filter,
  onFilter,
  onOpen,
}: {
  data: SessionsPanelData
  staleBadge?: React.ReactNode
  filter: string
  onFilter(value: string): void
  onOpen(sessionId: string): void
}) {
  const [sort, setSort] = useState<DataTableSort | null>(null)
  const rows = useMemo(() => data.sessions.map(toAgentSessionRow), [data.sessions])
  const visible = useMemo(() => rows.filter((row) => matches(row, filter)), [rows, filter])
  const groups = useMemo(() => groupSessionsByProject(visible), [visible])
  const gaps = coverageGaps(rows)
  const columns = useMemo(
    () => buildColumns((row) => onOpen(row.id)),
    [onOpen],
  )

  const live = rows.filter((row) => row.state === 'live').length
  const idle = rows.filter((row) => row.state === 'idle').length
  const reconnectable = rows.filter((row) => row.attach.kind !== 'none').length

  return (
    <div className="flex flex-col gap-3.5">
      <SectionCard title="Agent sessions" {...(staleBadge ? { titleBadge: staleBadge } : {})}>
        <KvPanel
          rows={[
            { label: 'Live', value: String(live), ...(live > 0 ? { intent: 'ok' as const } : {}) },
            { label: 'Idle', value: String(idle), ...(idle > 0 ? { intent: 'warn' as const } : {}) },
            { label: 'Reconnectable', value: String(reconnectable) },
            { label: 'Projects', value: String(groupSessionsByProject(rows).length) },
            { label: 'Collector host', value: data.localHost },
          ]}
        />
        <div className="mt-2">
          <FilterInput
            value={filter}
            onChange={onFilter}
            placeholder="Search by project, CLI, session id, host, state"
            label="Search sessions"
          />
        </div>
        <p className="mt-2 text-[11px] text-fg-muted">
          Only sessions enrolled in the ledger at <code>{data.ledgerDir}</code> appear here; a
          session started outside the recorder is invisible to this page. The recorder runs on
          {' '}<code>{data.localHost}</code> only; another host appears here just for the jobs the
          workstation launched on it, so the per-host table below marks those counts unobserved
          rather than zero.
          codex and cursor-agent sessions are closed when their process disappears, so a crash and
          a clean exit are indistinguishable for those two — only claude reports orphaned. Idle
          time for a claude session is measured from its transcript, so a session running one tool
          call for longer than ten minutes writes nothing and reads idle while it is still working.
          A project with a running session opens itself; every other project starts collapsed to
          its header, which still states how many sessions it holds. An open project shows its
          {' '}{DEFAULT_VISIBLE_ROWS} most recent runs, every running session among them; the rest
          stay one click away behind the per-project control.
          {data.ledgerMissing
            ? ' The directory does not exist yet — no session recorder has written to it.'
            : ''}
          {!data.attachEnabled ? ` Browser attach is off: ${data.attachBlockedReason ?? 'disabled'}.` : ''}
          {!data.tmuxAvailable ? ' tmux is not installed here, so no screen can be rendered in the browser.' : ''}
          {data.hostsRegistryMissing
            ? ` Host registry ${data.hostsRegistryPath} is unreadable, so hosts are shown unqualified.`
            : ''}
          {data.unreadableFiles.length > 0
            ? ` ${data.unreadableFiles.length} ledger file(s) could not be read: ${data.unreadableFiles.join(', ')}.`
            : ''}
        </p>
        {gaps.length > 0 ? (
          <ul className="mt-2 flex flex-col gap-0.5 text-[11px] text-fg-subtle">
            {gaps.map((gap) => <li key={gap}>Not recorded: {gap}.</li>)}
          </ul>
        ) : null}
      </SectionCard>

      <SessionHostsTable rows={rows} panel={data} />

      {groups.length === 0 ? (
        <SectionCard title={rows.length === 0 ? 'No sessions recorded' : 'No sessions match the search'}>
          <p className="text-[12px] text-fg-muted">
            {rows.length === 0
              ? <>Nothing to show. Sessions appear here once the recorder writes entries to <code>{data.ledgerDir}</code>.</>
              : 'Clear the search to see every recorded session.'}
          </p>
        </SectionCard>
      ) : (
        groups.map((group) => (
          <ProjectSection
            key={group.project}
            project={group.project}
            rows={group.rows}
            liveCount={group.liveCount}
            homeDir={data.homeDir}
            columns={columns}
            sort={sort}
            onSortChange={setSort}
          />
        ))
      )}
    </div>
  )
}
