import {
  DeckTooltipLayer,
  compareOptional,
  formatRelativeTime,
  KpiTile,
  PhaseBars,
  PlanTable,
  ProjectGroupRow,
  RunLanes,
  safeHttpUrl,
  SectionCard,
  SectionHeading,
  Select,
  StaleBadge,
  StatusChip,
  UndoToast,
  useDeckTooltip,
  type PlanTableItem,
  type ProjectGroupColumnKey,
  type ResolveAction,
  type UndoToastHandle,
} from '@overdeck/deck-ui'
import { DataTable, withColumnResizing, withSearch, withSorting, type DataTableColumn } from '@overdeck/deck-ui'
import { Skeleton } from '@astryxdesign/core'
import { useQueryClient } from '@tanstack/react-query'
import { useMemo, useRef, useState, type JSX } from 'react'
import { collectorQueryKeys } from '../../lib/collector-queries'
import { useCollectorState } from '../../lib/collector-queries'
import { postCollectorAction } from '../../lib/action-client'
import { forensicsTitleFor } from '../../lib/overview-mappers'
import type { StateResponse } from '../../lib/collector-types'
import { panelFreshness } from '../../lib/panel-freshness'
import type {
  ForensicsPanelData,
  HarnessPlansPanelData,
  HarnessQueueEntry,
} from '../../lib/panel-data'
import { CollectorQueryBoundary } from '../shared/CollectorQueryBoundary'
import { buildProjectGroups, planTableItemForRun, type ProjectRunGroup } from './plan-groups'

type ProjectSortKey = ProjectGroupColumnKey
type SortDirection = 'asc' | 'desc'

const PROJECT_SORT_OPTIONS = [
  { value: 'lastActivityMs:desc', label: 'Most recent activity' },
  { value: 'label:asc', label: 'Project name' },
  { value: 'plans:desc', label: 'Most plans' },
  { value: 'running:desc', label: 'Most running' },
  { value: 'needYou:desc', label: 'Most need you' },
  { value: 'failed:desc', label: 'Most failed' },
]

export const PLANS_VIEWPORT_LAYOUT_CLASS =
  'grid h-[calc(100dvh-4rem)] min-h-0 grid-cols-1 grid-rows-[auto_minmax(0,1fr)_minmax(0,1fr)] gap-4 overflow-hidden'

export interface QueueRow {
  id: string
  position: number
  repository: string
  repositoryPath: string
  slug: string
  status: string
  attempt: number
  window: string | null
  nextEligibleAt: string | null
  runHref: string | null
}

function queueWindowLabel(window: HarnessQueueEntry['window']): string | null {
  if (window === null) return null
  if (typeof window === 'string') return window
  if (typeof window !== 'object' || Array.isArray(window)) return JSON.stringify(window)
  const windowRecord = window as Record<string, unknown>
  const source = typeof windowRecord.source === 'string' ? windowRecord.source : null
  const start = typeof windowRecord.start === 'string' ? windowRecord.start : null
  const end = typeof windowRecord.end === 'string' ? windowRecord.end : null
  const timeZone = typeof windowRecord.time_zone === 'string' ? windowRecord.time_zone : null
  if (source && start && end && timeZone) return `${source} ${start}–${end} ${timeZone}`
  return JSON.stringify(window)
}

export function queueRowsForEntries(entries: HarnessQueueEntry[]): QueueRow[] {
  return entries.map((entry, index) => ({
    id: entry.id,
    position: index + 1,
    repository: entry.repo.split('/').filter(Boolean).at(-1) ?? entry.repo,
    repositoryPath: entry.repo,
    slug: entry.slug,
    status: entry.status,
    attempt: entry.attempt,
    window: queueWindowLabel(entry.window),
    nextEligibleAt: entry.nextEligibleAt,
    runHref: entry.runId === null ? null : `/plans/${entry.runId}`,
  }))
}

function QueueTimestamp({ value }: { value: string | null }) {
  const timestamp = value === null ? Number.NaN : Date.parse(value)
  const tooltip = useDeckTooltip(value ?? '')
  if (!Number.isFinite(timestamp)) return <>—</>
  return <span {...tooltip}>{formatRelativeTime(timestamp)}</span>
}

function QueueRepository({ row }: { row: QueueRow }) {
  const tooltip = useDeckTooltip(row.repositoryPath)
  return <span {...tooltip}>{row.repository}</span>
}

const QUEUE_COLUMNS: DataTableColumn<QueueRow>[] = [
  { id: 'position', header: 'Position', sortable: true, sortValue: (row) => row.position, minWidth: 80, cell: (row) => <span className="block text-right tabular-nums">{row.position}</span> },
  { id: 'repository', header: 'Repository', sortable: true, sortValue: (row) => row.repository.toLowerCase(), searchValue: (row) => `${row.repository} ${row.repositoryPath}`, minWidth: 160, cell: (row) => <QueueRepository row={row} /> },
  { id: 'plan', header: 'Plan', sortable: true, sortValue: (row) => row.slug.toLowerCase(), searchValue: (row) => row.slug, minWidth: 160, cell: (row) => row.slug },
  { id: 'status', header: 'Status', sortable: true, sortValue: (row) => row.status.toLowerCase(), searchValue: (row) => row.status, minWidth: 110, cell: (row) => <StatusChip status={row.status} /> },
  { id: 'attempt', header: 'Attempt', sortable: true, sortValue: (row) => row.attempt, minWidth: 80, cell: (row) => <span className="block text-right tabular-nums">{row.attempt}</span> },
  { id: 'window', header: 'Window', sortable: true, sortValue: (row) => (row.window ?? '').toLowerCase(), searchValue: (row) => row.window ?? '', minWidth: 180, cell: (row) => row.window ?? '—' },
  { id: 'nextEligible', header: 'Next eligible', sortable: true, sortValue: (row) => row.nextEligibleAt === null ? -1 : Date.parse(row.nextEligibleAt), searchValue: (row) => row.nextEligibleAt ?? '', minWidth: 130, cell: (row) => <QueueTimestamp value={row.nextEligibleAt} /> },
  { id: 'run', header: 'Run', minWidth: 70, cell: (row) => row.runHref === null ? '—' : <a href={safeHttpUrl(row.runHref)} className="font-semibold text-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent">Open</a> },
]

const QUEUE_CAPABILITIES = [withSorting({ defaultSort: { columnId: 'position', direction: 'asc' } }), withSearch({ label: 'Search plan queue' }), withColumnResizing()]

export function QueueSection({ entries }: { entries: HarnessQueueEntry[] }) {
  const rows = queueRowsForEntries(entries)
  return (
    <SectionCard title="Queue" className="min-h-0 overflow-hidden">
      <div className="max-h-52 overflow-y-auto overflow-x-hidden rounded-lg border border-border bg-surface">
        <DataTable caption="Harness plans queue" columns={QUEUE_COLUMNS} rows={rows} getRowId={(row) => row.id} capabilities={QUEUE_CAPABILITIES} />
      </div>
    </SectionCard>
  )
}

function PlansPageSkeleton(): JSX.Element {
  return (
    <div className={PLANS_VIEWPORT_LAYOUT_CLASS} data-plans-skeleton>
      <SectionCard title="Plan runs" className="flex min-h-0 flex-col overflow-hidden">
        <div className="min-h-0 flex-1 overflow-hidden rounded-lg border border-border bg-surface p-3">
          <Skeleton width={112} height={14} className="mb-4" />
          {Array.from({ length: 4 }, (_, index) => (
            <div key={index} className="flex items-center gap-3 border-t border-border py-3">
              <Skeleton width="40%" height={14} />
              <Skeleton width={64} height={14} />
              <Skeleton width={48} height={14} className="hidden @min-[28rem]:block" />
              <Skeleton width={80} height={14} className="ml-auto" />
            </div>
          ))}
        </div>
      </SectionCard>
      <SectionCard title="Run forensics">
        <div className="grid grid-cols-3 gap-3 @min-[48rem]:grid-cols-6">
          {Array.from({ length: 6 }, (_, index) => (
            <Skeleton key={index} height={64} />
          ))}
        </div>
        <div className="mt-3 grid grid-cols-1 gap-3 @min-[48rem]:grid-cols-2">
          <Skeleton height={160} />
          <Skeleton height={160} />
        </div>
      </SectionCard>
    </div>
  )
}

function sortProjectGroups(
  groups: ProjectRunGroup[],
  sort: { key: ProjectSortKey; direction: SortDirection } | null,
): ProjectRunGroup[] {
  if (!sort) return groups
  return [...groups].sort((left, right) => {
    const leftValue = left.summary[sort.key]
    const rightValue = right.summary[sort.key]
    if (sort.key === 'label') {
      const comparison = String(leftValue).localeCompare(String(rightValue))
      return sort.direction === 'asc' ? comparison : -comparison
    }
    return compareOptional(leftValue as number | null, rightValue as number | null, sort.direction)
  })
}

export function PlansContent() {
  const stateQuery = useCollectorState()
  return (
    <CollectorQueryBoundary query={stateQuery} skeleton={<PlansPageSkeleton />}>
      {(state) => <PlansContentBody state={state} />}
    </CollectorQueryBoundary>
  )
}

function PlansContentBody({ state }: { state: StateResponse }) {
  const queryClient = useQueryClient()
  const [expandedGroups, setExpandedGroups] = useState<Set<string>>(() => new Set())
  const [projectSort, setProjectSort] = useState<{
    key: ProjectSortKey
    direction: SortDirection
  }>({ key: 'lastActivityMs', direction: 'desc' })
  const [pendingAbandonedRunIds, setPendingAbandonedRunIds] = useState<Set<string>>(() => new Set())
  const undoRef = useRef<UndoToastHandle>(null)

  const plansPanel = state.panels.find((panel) => panel.id === 'plans')
  const plansData = plansPanel?.data as HarnessPlansPanelData | undefined
  const runs = plansData?.runs ?? []
  const queue = plansData?.queue ?? []
  const groups = useMemo(
    () => buildProjectGroups(runs.filter((run) => !pendingAbandonedRunIds.has(run.runId))),
    [pendingAbandonedRunIds, runs],
  )
  const orderedGroups = useMemo(() => sortProjectGroups(groups, projectSort), [groups, projectSort])
  const visibleRuns = orderedGroups.flatMap((group) => group.runs)
  const activeRunId = visibleRuns[0]?.runId
  const abandonedCount = runs.filter((run) => run.abandoned).length
  const hasRunningCoverageGap = visibleRuns.some((run) => run.status === 'running' && !run.currentActivityStartedAt)

  const forensicsPanel = useMemo(() => {
    if (!activeRunId) return undefined
    return state.panels.find((panel) => panel.id === `forensics:${activeRunId}`)
  }, [activeRunId, state.panels])

  if (!plansData || (runs.length === 0 && queue.length === 0)) {
    return <SectionCard title="Plan runs">No plan runs in collector state.</SectionCard>
  }

  const forensicsData = forensicsPanel?.data as ForensicsPanelData | undefined
  const harnessStatus = panelFreshness(
    plansPanel,
    state.adapters.find((adapter) => adapter.id === 'harness'),
  )

  const toggleGroup = (key: string) => {
    setExpandedGroups((current) => {
      const next = new Set(current)
      if (next.has(key)) next.delete(key)
      else next.add(key)
      return next
    })
  }

  const abandon = (item: PlanTableItem) => {
    setPendingAbandonedRunIds((current) => new Set(current).add(item.runId))
    undoRef.current?.toastUndo(`Abandoned ${item.title || item.slug} — nothing deleted.`, {
      seconds: 5,
      onUndo: () => {
        setPendingAbandonedRunIds((current) => {
          const next = new Set(current)
          next.delete(item.runId)
          return next
        })
      },
      onCommit: () => {
        void postCollectorAction('abandon', {
          args: { runId: item.runId },
          requestedBy: 'overdeck-web',
        }).then(
          () => queryClient.invalidateQueries({ queryKey: collectorQueryKeys.state }),
          (error: unknown) => {
            setPendingAbandonedRunIds((current) => {
              const next = new Set(current)
              next.delete(item.runId)
              return next
            })
            undoRef.current?.toast(error instanceof Error ? error.message : String(error))
          },
        ).catch((error: unknown) => {
          undoRef.current?.toast(error instanceof Error ? error.message : String(error))
        })
      },
    })
  }

  const openRun = (item: PlanTableItem) => {
    const target = safeHttpUrl(item.href)
    if (target) window.location.assign(target)
  }

  const resolve = (item: PlanTableItem, action: ResolveAction) => {
    if (action.kind === 'retry' || action.kind === 'reroute' || action.kind === 'open-run') return
    const target = safeHttpUrl(item.href)
    if (target) window.location.assign(target)
  }

  return (
    <div className={PLANS_VIEWPORT_LAYOUT_CLASS}>
      {queue.length > 0 && <QueueSection entries={queue} />}
      <SectionCard
        title="Plan runs"
        titleBadge={harnessStatus ? <StaleBadge status={harnessStatus} /> : undefined}
        className="flex min-h-0 flex-col overflow-hidden"
      >
        <div data-plans-list-pane className="flex min-h-0 flex-1 flex-col overflow-hidden">
          <div className="mb-2 flex justify-end">
            <Select
              label="Sort projects"
              labelMode="inline"
              value={`${projectSort.key}:${projectSort.direction}`}
              options={PROJECT_SORT_OPTIONS}
              onValueChange={(value) => {
                const [key, direction] = value.split(':') as [ProjectSortKey, SortDirection]
                setProjectSort({ key, direction })
              }}
            />
          </div>
          <div className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden rounded-lg border border-border bg-surface">
            <div className="divide-y divide-border">
              {orderedGroups.map((group) => (
                  <ProjectGroupRow
                    key={group.summary.key}
                    group={group.summary}
                    expanded={expandedGroups.has(group.summary.key)}
                    onToggle={() => toggleGroup(group.summary.key)}
                  >
                    <PlanTable
                      items={group.runs.map(planTableItemForRun)}
                      selectedRunId={activeRunId ?? null}
                      onOpen={openRun}
                      onAbandon={abandon}
                      onResolve={resolve}
                    />
                  </ProjectGroupRow>
              ))}
            </div>
            {orderedGroups.length === 0 && (
              <p className="px-4 py-8 text-center text-sm text-fg-muted">
                No active plans. Restore an abandoned plan to return it here.
              </p>
            )}
          </div>
          <p className="mt-2 text-[10px] leading-relaxed text-fg-subtle">
            Fail-class actions activate only when harness forwards failClass; failed runs open from the row or Open link.
          </p>
          {hasRunningCoverageGap && (
            <p className="mt-1 text-[10px] leading-relaxed text-fg-subtle" data-running-duration-gap>
              Running for counts from run start: harness /runs does not forward currentActivityStartedAt.
            </p>
          )}
          {abandonedCount > 0 && (
            <footer className="mt-2 flex items-center justify-end gap-1 border-t border-border pt-2 text-xs text-fg-muted">
              <span>{abandonedCount} abandoned {abandonedCount === 1 ? 'plan' : 'plans'} →</span>
              <a href="/plans/abandoned" className="font-semibold text-accent">View</a>
            </footer>
          )}
        </div>
      </SectionCard>

      <div data-plans-detail-pane className="min-h-0 overflow-y-auto pr-1">
        {forensicsData && activeRunId ? (
          <div className="flex flex-col gap-3">
            <SectionHeading
              title={`Run forensics — ${forensicsTitleFor(activeRunId, plansPanel)}`}
              subtitle="· per-run drill-down"
              titleBadge={harnessStatus ? <StaleBadge status={harnessStatus} /> : undefined}
            />
            <div className="grid grid-cols-6 gap-3">
              {forensicsData.tiles.map((tile) => (
                <KpiTile key={tile.key} tile={tile} />
              ))}
            </div>
            <div className="grid grid-cols-2 gap-3">
              <SectionCard title="Where the time went">
                <PhaseBars attribution={forensicsData.attribution} />
              </SectionCard>
              <SectionCard title="Runs">
                <RunLanes runs={forensicsData.runs} segments={forensicsData.segments} />
              </SectionCard>
            </div>
          </div>
        ) : (
          <SectionCard title="Run forensics">
            No forensics panel for {activeRunId ?? 'selected run'}.
          </SectionCard>
        )}
      </div>
      <UndoToast handleRef={undoRef} />
      <DeckTooltipLayer />
    </div>
  )
}
