import { compareOptional, formatRelativeTime, type KvRow, type SelectOption } from '@overdeck/deck-ui'
import { CollectorHttpError } from '../../lib/collector-client'
import type { Incident, IncidentCliOption, IncidentPriority, IncidentState, IncidentTypeOption } from '../../lib/incident-types'

export const EM_DASH = '—'

export const INCIDENT_STATE_LABEL: Record<IncidentState, string> = {
  filed: 'Filed',
  dispatching: 'Dispatching',
  running: 'Running',
  'needs-attention': 'Needs attention',
  resolved: 'Resolved',
}

// StatusChip categorises by an existing token vocabulary (packages/deck-ui/src/StatusChip.tsx);
// the incident's own wording is passed through as the visible label.
const INCIDENT_STATE_STATUS: Record<IncidentState, string> = {
  filed: 'queued',
  dispatching: 'queued',
  running: 'running',
  'needs-attention': 'attention',
  resolved: 'completed',
}

const PRIORITY_RANK: Record<IncidentPriority, number> = { P0: 0, P1: 1, P2: 2, P3: 3 }

export function incidentStatusToken(state: IncidentState): string {
  return INCIDENT_STATE_STATUS[state]
}

export function incidentEpoch(iso: string | null): number | null {
  if (iso === null) return null
  const epoch = Date.parse(iso)
  return Number.isNaN(epoch) ? null : epoch
}

export interface TimestampView {
  relative: string
  absolute: string | null
}

export function incidentTimestamp(iso: string | null, nowMs = Date.now()): TimestampView {
  const epoch = incidentEpoch(iso)
  if (epoch === null) return { relative: EM_DASH, absolute: null }
  return { relative: formatRelativeTime(epoch, nowMs), absolute: new Date(epoch).toISOString() }
}

export function priorityRank(priority: IncidentPriority | null): number | null {
  return priority === null ? null : PRIORITY_RANK[priority]
}

/**
 * Active incidents first, then P0→P3 with unknown priority last, then most recently
 * updated. Incidents whose update time is absent sort after those that have one.
 */
export function sortIncidents(incidents: readonly Incident[]): Incident[] {
  return [...incidents].sort((left, right) => {
    if (left.active !== right.active) return left.active ? -1 : 1

    const byPriority = compareOptional(priorityRank(left.priority), priorityRank(right.priority), 'asc')
    if (byPriority !== 0) return byPriority

    return compareOptional(incidentEpoch(left.updatedAt), incidentEpoch(right.updatedAt), 'desc')
  })
}

export function matchesIncidentFilter(incident: Incident, filter: string): boolean {
  const needle = filter.trim().toLowerCase()
  if (needle === '') return true
  const haystack = [
    incident.id,
    incident.title,
    incident.description,
    INCIDENT_STATE_LABEL[incident.state],
    incident.priority ?? '',
    incident.dispatch.cli ?? '',
    incident.dispatch.model ?? '',
    incident.dispatch.account ?? '',
  ]
  return haystack.some((field) => field.toLowerCase().includes(needle))
}

export function displayOrDash(value: string | null): string {
  const trimmed = value?.trim() ?? ''
  return trimmed === '' ? EM_DASH : trimmed
}

export function cliAndModel(incident: Incident): string {
  const cli = incident.dispatch.cli?.trim() ?? ''
  const model = incident.dispatch.model?.trim() ?? ''
  if (cli === '' && model === '') return EM_DASH
  if (model === '') return cli
  if (cli === '') return model
  return `${cli} · ${model}`
}

export function incidentKvRows(incident: Incident): KvRow[] {
  const { dispatch } = incident
  return [
    { label: 'Priority', value: incident.priority ?? EM_DASH },
    { label: 'Type', value: displayOrDash(incident.incidentType) },
    { label: 'Kanboard task', value: String(incident.kanboardTaskId) },
    { label: 'CLI', value: displayOrDash(dispatch.cli) },
    { label: 'Model', value: displayOrDash(dispatch.model) },
    { label: 'Reasoning effort', value: displayOrDash(dispatch.reasoningEffort) },
    { label: 'Account', value: displayOrDash(dispatch.account) },
    { label: 'Created', value: incidentTimestamp(incident.createdAt).absolute ?? EM_DASH },
    { label: 'Started', value: incidentTimestamp(dispatch.startedAt).absolute ?? EM_DASH },
    { label: 'Completed', value: incidentTimestamp(dispatch.completedAt).absolute ?? EM_DASH },
    { label: 'Resolved', value: incidentTimestamp(incident.resolvedAt).absolute ?? EM_DASH },
    {
      label: 'Exit code',
      value: dispatch.exitCode === null ? EM_DASH : String(dispatch.exitCode),
      ...(dispatch.exitCode !== null && dispatch.exitCode !== 0 ? { intent: 'err' as const } : {}),
    },
    {
      label: 'Failure class',
      value: displayOrDash(dispatch.failureClass),
      ...(dispatch.failureClass ? { intent: 'err' as const } : {}),
    },
  ]
}

export interface CoverageGap {
  id: string
  label: string
  specRef: string
}

const SPEC_REF = '/docs/specs/2026-08-08-incidents-page-design.md'

export function incidentCoverageGaps(incident: Incident): CoverageGap[] {
  const gaps: CoverageGap[] = []
  if (incident.coverage.stale) {
    gaps.push({
      id: incident.id,
      label: incident.coverage.detail ?? 'incident metadata is incomplete',
      specRef: SPEC_REF,
    })
  }
  return gaps
}

export function listCoverageGaps(detail: string | undefined, stale: boolean): CoverageGap[] {
  if (!stale) return []
  return [{ id: 'incidents', label: detail ?? 'incident metadata is incomplete', specRef: SPEC_REF }]
}

// Canonical CLI identifiers (collector/src/adapters/agent-sessions.ts AgentRuntime); apps/web
// mirrors rather than imports, same rationale as ../../lib/incident-types.ts.
const KNOWN_CLIS: readonly string[] = ['claude', 'codex', 'cursor-agent']

export const REASONING_EFFORTS: readonly string[] = ['low', 'medium', 'high', 'xhigh', 'max']

function distinctSorted(values: readonly (string | null)[]): string[] {
  const set = new Set<string>()
  for (const value of values) {
    const trimmed = value?.trim() ?? ''
    if (trimmed !== '') set.add(trimmed)
  }
  return [...set].sort((left, right) => left.localeCompare(right))
}

function filingOptions(values: readonly string[]): SelectOption[] {
  return [{ value: '', label: 'None' }, ...values.map((value) => ({ value, label: value }))]
}

export function cliAuthorityOptions(clis: readonly IncidentCliOption[]): SelectOption[] {
  return clis.map((cli) => ({ value: cli.id, label: cli.label }))
}

export function modelAuthorityOptions(cli: IncidentCliOption | undefined): SelectOption[] {
  return filingOptions(cli?.models.map((model) => model.id) ?? [])
}

export function effortAuthorityOptions(cli: IncidentCliOption | undefined, modelId: string): SelectOption[] {
  const model = cli?.models.find((entry) => entry.id === modelId)
  return filingOptions(model?.efforts ?? [])
}

export function accountAuthorityOptions(cli: IncidentCliOption | undefined): SelectOption[] {
  return cli?.accounts
    .filter((account) => account.ready)
    .map((account) => ({ value: account.slug, label: account.label })) ?? [{ value: '', label: 'None' }]
}

export function cliFilingOptions(incidents: readonly Incident[]): SelectOption[] {
  return filingOptions(distinctSorted([...KNOWN_CLIS, ...incidents.map((incident) => incident.dispatch.cli)]))
}

export function modelFilingOptions(incidents: readonly Incident[]): SelectOption[] {
  return filingOptions(distinctSorted(incidents.map((incident) => incident.dispatch.model)))
}

export function reasoningEffortFilingOptions(): SelectOption[] {
  return filingOptions(REASONING_EFFORTS)
}

export function accountFilingOptions(incidents: readonly Incident[]): SelectOption[] {
  return filingOptions(distinctSorted(incidents.map((incident) => incident.dispatch.account)))
}

export function typeFilingOptions(types: readonly IncidentTypeOption[]): SelectOption[] {
  return [{ value: '', label: 'None' }, ...types.map((type) => ({ value: type.id, label: type.title }))]
}

export interface DispatchFailure {
  message: string
  briefBlocked: boolean
}

export function dispatchFailure(error: unknown): DispatchFailure {
  if (error instanceof CollectorHttpError) {
    const body = error.body as { error?: unknown; detail?: unknown } | null
    if (body !== null && typeof body === 'object' && body.error === 'brief-assembly-failed' && typeof body.detail === 'string') {
      return { message: body.detail, briefBlocked: true }
    }
    if (error.status === 503) {
      return { message: 'The incident store is unavailable, so the incident was not dispatched.', briefBlocked: false }
    }
  }
  return { message: 'The incident could not be dispatched.', briefBlocked: false }
}

/**
 * Deterministic keyword match: most distinct keyword hits in title+description wins;
 * ties keep taxonomy order. Zero hits suggest nothing.
 */
export function suggestIncidentType(title: string, description: string, types: readonly IncidentTypeOption[]): string | null {
  const haystack = `${title}\n${description}`.toLowerCase()
  let best: { id: string; hits: number } | null = null
  for (const type of types) {
    const hits = new Set(type.keywords.filter((keyword) => keyword.trim() !== '' && haystack.includes(keyword.toLowerCase()))).size
    if (hits > 0 && (best === null || hits > best.hits)) best = { id: type.id, hits }
  }
  return best?.id ?? null
}
