import type { AgentActivityItem, AgentEditItem, AgentProgressItem, AgentWarningItem } from '@overdeck/deck-ui'
import type { HarnessEvent } from '../../lib/harness-types'
import type { FactoryPhaseDiffView } from '../../lib/factory-types'

export type ActivityCategory = 'read' | 'search' | 'command' | 'edit' | 'test' | 'decision' | 'warning' | 'control' | 'outcome' | 'other'
export interface VocabularyEntry { kind: string; required: string[]; category: ActivityCategory; severity: 'info' | 'warning' | 'error'; group: boolean; title(event: HarnessEvent): string }
const title = (value: string) => () => value
/** The sole translation table between transport vocabulary and story vocabulary. */
export const ACTIVITY_VOCABULARY: Record<string, VocabularyEntry> = {
  'attempt.started': { kind: 'attempt.started', required: [], category: 'control', severity: 'info', group: false, title: title('Attempt started') },
  'attempt.completed': { kind: 'attempt.completed', required: [], category: 'outcome', severity: 'info', group: false, title: title('Attempt completed') },
  'attempt.prompt': { kind: 'attempt.prompt', required: [], category: 'other', severity: 'info', group: false, title: title('Prompt recorded') },
  'attempt.reply': { kind: 'attempt.reply', required: [], category: 'other', severity: 'info', group: false, title: title('Response recorded') },
  'tool.read': { kind: 'tool.read', required: [], category: 'read', severity: 'info', group: true, title: title('Read file') },
  'tool.search': { kind: 'tool.search', required: [], category: 'search', severity: 'info', group: true, title: title('Searched files') },
  'tool.command': { kind: 'tool.command', required: [], category: 'command', severity: 'info', group: true, title: title('Ran command') },
  'tool.edit': { kind: 'tool.edit', required: ['path'], category: 'edit', severity: 'info', group: false, title: title('Edited file') },
  'gate.result': { kind: 'gate.result', required: [], category: 'test', severity: 'info', group: false, title: title('Checked changes') },
  decision: { kind: 'decision', required: [], category: 'decision', severity: 'info', group: false, title: title('Decision recorded') },
  warning: { kind: 'warning', required: [], category: 'warning', severity: 'warning', group: false, title: title('Warning recorded') },
  error: { kind: 'error', required: [], category: 'warning', severity: 'error', group: false, title: title('Failure recorded') },
}
function text(event: HarnessEvent): string | undefined { const value = event.payload.summary ?? event.payload.text; return typeof value === 'string' ? value : undefined }
function timestamp(event: HarnessEvent, index: number): number { const value = Date.parse(event.ts); return Number.isFinite(value) ? value : Number.MAX_SAFE_INTEGER - index }
function identity(event: HarnessEvent, category: ActivityCategory) { return `${event.source}:${event.taskId ?? ''}:${event.attemptId ?? ''}:${category}` }
type AgentGroupWithIdentity = Extract<AgentActivityItem, { kind: 'group' }> & { __identity: string }
export interface ProjectionResult { items: AgentActivityItem[]; warnings: AgentWarningItem[] }
export function projectAgentActivity(events: HarnessEvent[], options: { cap?: number; hasOlder?: boolean } = {}): ProjectionResult {
  const seen = new Map<string, HarnessEvent>(); const warnings: AgentWarningItem[] = []
  events.forEach(event => { const prior = seen.get(event.id); if (!prior) seen.set(event.id, event); else if (JSON.stringify(prior) !== JSON.stringify(event)) warnings.push({ id: `duplicate:${event.id}`, at: timestamp(event, 0), kind: 'warning', title: 'Conflicting activity record', summary: 'The first received record was kept.', diagnosticEventIds: [event.id], severity: 'warning' }) })
  const ordered = [...seen.values()].map((event, index) => ({ event, index })).sort((a, b) => timestamp(a.event, a.index) - timestamp(b.event, b.index) || a.index - b.index)
  const cap = options.cap ?? 1024; const omitted = ordered.length > cap; const kept = omitted ? ordered.slice(-cap) : ordered
  const rows: AgentActivityItem[] = []
  for (const { event, index } of kept) {
    const rule = ACTIVITY_VOCABULARY[event.kind]; const category = rule?.category ?? 'other'; const summary = text(event)
    const base = { id: event.id, at: timestamp(event, index), title: rule?.title(event) ?? 'Recorded activity', summary, diagnosticEventIds: [event.id] as string[], severity: rule?.severity ?? 'info' }
    if (category === 'edit') { const path = typeof event.payload.path === 'string' ? event.payload.path : 'file not recorded'; rows.push({ ...base, kind: 'edit', path, insertions: null, deletions: null, diffText: null, truncated: false, evidenceLevel: 'event' }); continue }
    const kind = category === 'test' ? 'test' : category === 'decision' ? 'decision' : category === 'warning' ? 'warning' : category === 'outcome' ? 'outcome' : 'progress'
    const item = { ...base, kind } as AgentActivityItem
    const previous = rows.at(-1)
    if (rule?.group && previous?.kind === 'group' && previous.category === category && (previous as AgentGroupWithIdentity).__identity === identity(event, category)) { previous.children.push(item as AgentProgressItem); previous.diagnosticEventIds.push(event.id); continue }
    if (rule?.group) rows.push({ ...base, id: `${event.attemptId ?? ''}:${category}:${event.id}`, kind: 'group', category, children: [item as AgentProgressItem], expanded: false, __identity: identity(event, category) } as AgentActivityItem)
    else rows.push(item)
  }
  if (omitted) warnings.unshift({ id: 'activity-cap', at: rows[0]?.at ?? 0, kind: 'warning', title: 'Earlier activity is not loaded', summary: options.hasOlder ? 'More recorded activity can be loaded.' : 'Older records are unavailable.', diagnosticEventIds: [], severity: 'warning' })
  return { items: [...warnings, ...rows], warnings }
}
export function selectUniquePhaseDiff(event: HarnessEvent, runId: string, path: string, diffs: FactoryPhaseDiffView[]): FactoryPhaseDiffView | undefined {
  const candidates = diffs.filter((diff) => diff.adwId === runId && diff.taskId === event.taskId && diff.attemptId === event.attemptId && diff.phaseId === event.phaseId && diff.files.some((file) => file.path === path))
  return candidates.length === 1 ? candidates[0] : undefined
}

/** Attaches only phase-level evidence whose complete external identity chain is explicit. */
export function attachPhaseDiff(edit: AgentEditItem, event: HarnessEvent, runId: string, diff: FactoryPhaseDiffView | undefined): AgentEditItem {
  if (!diff || event.taskId == null || event.attemptId == null || event.phaseId == null || diff.phaseId !== event.phaseId || diff.adwId !== runId || diff.taskId !== event.taskId || diff.attemptId !== event.attemptId || diff.linkage !== 'linked' || !diff.files.some(file => file.path === edit.path)) return edit
  return { ...edit, diffText: diff.diffText, truncated: diff.truncated, insertions: diff.files.find(file => file.path === edit.path)?.insertions ?? null, deletions: diff.files.find(file => file.path === edit.path)?.deletions ?? null, evidenceLevel: 'phase', summary: 'Phase changes' }
}
