export type NormalizedAgentEvent =
  | { kind: 'assistant_delta'; content: string; externalMessageId?: string }
  | {
      kind: 'assistant_completed';
      content?: string;
      externalMessageId?: string;
    }
  | {
      kind: 'tool_started';
      name: string;
      input?: string;
      externalMessageId?: string;
    }
  | {
      kind: 'tool_completed';
      name: string;
      output?: string;
      externalMessageId?: string;
    }
  | { kind: 'file_edited'; path: string }
  | {
      kind: 'command_executed';
      command: string;
      exitCode?: number;
      output?: string;
    }
  | {
      kind: 'permission_requested';
      externalRequestId: string;
      title: string;
      body: string;
      metadata?: string;
    }
  | {
      kind: 'question_requested';
      externalRequestId: string;
      title: string;
      body: string;
      options?: string[];
      metadata?: string;
    }
  | { kind: 'session'; sessionId: string }
  | { kind: 'status'; status: string; metadata?: string }
  | { kind: 'error'; message: string; metadata?: string };

const stringify = (value: unknown) => {
  if (typeof value === 'string') return value;
  if (value === undefined || value === null) return '';
  if (
    typeof value === 'number' ||
    typeof value === 'boolean' ||
    typeof value === 'bigint'
  ) {
    return value.toString();
  }
  try {
    return JSON.stringify(value, null, 2);
  } catch {
    return '';
  }
};

const asRecord = (value: unknown): Record<string, unknown> | null =>
  value && typeof value === 'object'
    ? (value as Record<string, unknown>)
    : null;

const textFromPart = (part: Record<string, unknown>) => {
  const text = part.text ?? part.content ?? part.delta;
  return typeof text === 'string' ? text : '';
};

const commandString = (value: unknown) => {
  if (Array.isArray(value))
    return value.map((part) => stringify(part)).join(' ');
  return stringify(value);
};

const toolNameFromRecord = (record: Record<string, unknown> | null) =>
  stringify(
    record?.tool ??
      record?.tool_name ??
      record?.toolName ??
      record?.name ??
      record?.function ??
      (stringify(record?.type).toLowerCase().includes('exec') || record?.command
        ? 'Command'
        : record?.type) ??
      'tool',
  );

const toolInputFromRecord = (record: Record<string, unknown> | null) =>
  commandString(
    record?.input ??
      record?.arguments ??
      record?.args ??
      record?.params ??
      record?.command ??
      record?.cmd,
  );

const toolOutputFromRecord = (
  record: Record<string, unknown> | null,
  fallback?: unknown,
) =>
  stringify(
    record?.output ??
      record?.aggregated_output ??
      record?.stdout ??
      record?.stderr ??
      record?.result ??
      record?.content ??
      record?.text ??
      (record?.exit_code !== undefined
        ? `exit code: ${stringify(record.exit_code)}`
        : undefined) ??
      fallback,
  );

const recordLooksLikeTool = (
  type: string,
  record: Record<string, unknown> | null,
) => {
  const recordType = stringify(record?.type).toLowerCase();
  const lowerType = type.toLowerCase();
  return (
    lowerType.includes('tool') ||
    lowerType.includes('function_call') ||
    recordType.includes('tool') ||
    recordType.includes('function_call') ||
    recordType.includes('local_shell_call') ||
    recordType.includes('exec_command') ||
    recordType.includes('command') ||
    recordType.includes('mcp') ||
    Boolean(
      record?.tool ?? record?.tool_name ?? record?.name ?? record?.command,
    )
  );
};

const isCodexConfigWarning = (message: string) =>
  message.includes('`[features].codex_hooks` is deprecated') ||
  message.includes('Use `[features].hooks` instead');

// Handles the legacy `codex-rs` `{ id, msg: { type, ... } }` envelope.
const normalizeCodexMsgEvent = (
  msg: Record<string, unknown>,
  envelope: Record<string, unknown>,
): NormalizedAgentEvent[] => {
  const msgType = stringify(msg.type).toLowerCase();
  const events: NormalizedAgentEvent[] = [];
  if (msgType === 'session_configured' || msgType.includes('session')) {
    const sessionId = stringify(
      msg.session_id ?? envelope.session_id ?? envelope.id,
    );
    if (sessionId) events.push({ kind: 'session', sessionId });
  }
  if (
    msgType === 'agent_message_delta' ||
    msgType === 'agent_reasoning_delta'
  ) {
    const delta = stringify(msg.delta ?? msg.text);
    if (delta) events.push({ kind: 'assistant_delta', content: delta });
  }
  if (msgType === 'agent_message') {
    const text = stringify(msg.message ?? msg.text);
    if (text) {
      events.push({ kind: 'assistant_delta', content: `${text.trim()}\n\n` });
    }
  }
  if (msgType === 'exec_command_begin') {
    events.push({
      kind: 'tool_started',
      name: 'Command',
      input: commandString(msg.command),
    });
  }
  if (msgType === 'exec_command_end') {
    events.push({
      kind: 'tool_completed',
      name: 'Command',
      output: toolOutputFromRecord(msg),
    });
  }
  if (
    msgType === 'error' ||
    msgType === 'turn_failed' ||
    msgType === 'task_error'
  ) {
    const message = stringify(msg.message ?? msg.error ?? msg);
    if (isCodexConfigWarning(message)) {
      events.push({ kind: 'status', status: message });
    } else {
      events.push({ kind: 'error', message });
    }
  }
  if (msgType === 'task_complete' || msgType === 'turn_complete') {
    events.push({ kind: 'assistant_completed' });
  }
  return events;
};

export const normalizeCodexJsonLine = (
  line: string,
): NormalizedAgentEvent[] => {
  if (!line.trim()) return [];
  let parsed: unknown;
  try {
    parsed = JSON.parse(line) as unknown;
  } catch {
    return [{ kind: 'status', status: line }];
  }
  const event = asRecord(parsed);
  if (!event) return [];
  // Older Codex (`codex-rs`) protocol wraps events as `{ id, msg: { type, ... } }`
  // instead of the newer `{ type, item: { ... } }` shape. Unwrap it so version
  // skew between the pinned image and an upstream build degrades gracefully
  // instead of silently producing an empty assistant response.
  const msg = asRecord(event.msg);
  if (msg) {
    const msgEvents = normalizeCodexMsgEvent(msg, event);
    if (msgEvents.length > 0) return msgEvents;
  }
  const type = stringify(event.type ?? event.event);
  const id =
    event.id ??
    event.session_id ??
    event.sessionId ??
    event.thread_id ??
    event.threadId;
  const sessionId =
    typeof id === 'string' &&
    (type.toLowerCase().includes('session') ||
      type.toLowerCase().includes('thread.started'))
      ? id
      : undefined;
  const events: NormalizedAgentEvent[] = sessionId
    ? [{ kind: 'session', sessionId }]
    : [];
  const message = asRecord(event.message);
  const item = asRecord(event.item);
  const data = asRecord(event.data);
  const part = asRecord(event.part);
  const itemType = item ? stringify(item.type) : '';
  const lowerType = type.toLowerCase();
  const lowerItemType = itemType.toLowerCase();
  if (
    item &&
    recordLooksLikeTool(type, item) &&
    (lowerType.includes('started') ||
      lowerType.includes('in_progress') ||
      lowerType.includes('created'))
  ) {
    events.push({
      kind: 'tool_started',
      name: toolNameFromRecord(item),
      input: toolInputFromRecord(item),
      externalMessageId: stringify(item.id ?? event.id),
    });
  }
  if (
    item &&
    recordLooksLikeTool(type, item) &&
    (lowerType.includes('completed') ||
      lowerType.includes('done') ||
      lowerType.includes('finished'))
  ) {
    events.push({
      kind: 'tool_completed',
      name: toolNameFromRecord(item),
      output: toolOutputFromRecord(item, event.output ?? data?.output),
      externalMessageId: stringify(item.id ?? event.id),
    });
  }
  const delta = event.delta ?? data?.delta;
  if (typeof delta === 'string') {
    events.push({ kind: 'assistant_delta', content: delta });
  }
  const text =
    (part ? textFromPart(part) : '') ||
    (message ? stringify(message.content ?? message.text) : '') ||
    (item ? stringify(item.content ?? item.text) : '');
  if (
    text &&
    (type.includes('message') ||
      type.includes('response.output_text') ||
      type.includes('agent_message') ||
      itemType.includes('message') ||
      itemType.includes('agent_message'))
  ) {
    events.push({
      kind: 'assistant_delta',
      content: itemType.includes('agent_message') ? `${text.trim()}\n\n` : text,
      externalMessageId: stringify(item?.id ?? event.id),
    });
  }
  const error = event.error ?? item?.error;
  if (error || itemType === 'error') {
    const message = stringify(error ?? item?.message ?? event.message);
    if (isCodexConfigWarning(message)) {
      events.push({ kind: 'status', status: message });
      return events;
    }
    events.push({
      kind: 'error',
      message,
    });
  }
  const command =
    event.command ??
    data?.command ??
    (lowerItemType.includes('shell') ? item?.command : undefined);
  if (typeof command === 'string') {
    events.push({
      kind: 'command_executed',
      command,
      output: stringify(event.output ?? data?.output),
    });
  } else if (Array.isArray(command)) {
    events.push({
      kind: 'command_executed',
      command: command.map((part) => stringify(part)).join(' '),
      output: stringify(event.output ?? data?.output ?? item?.output),
    });
  }
  const file =
    event.file ??
    event.path ??
    data?.file ??
    data?.path ??
    item?.file ??
    item?.path;
  if (typeof file === 'string' && type.includes('file')) {
    events.push({ kind: 'file_edited', path: file });
  }
  if (type.includes('error')) {
    events.push({
      kind: 'error',
      message: stringify(event.message ?? event.error ?? data),
    });
  }
  if (
    type.includes('completed') &&
    itemType !== 'error' &&
    !itemType.includes('message') &&
    !itemType.includes('agent_message') &&
    !recordLooksLikeTool(type, item)
  ) {
    events.push({ kind: 'assistant_completed' });
  }
  if (type.includes('turn.done')) {
    events.push({ kind: 'assistant_completed' });
  }
  if (events.length === 0) {
    events.push({ kind: 'status', status: type || 'codex_event' });
  }
  return events;
};

export const normalizeOpenCodeEvent = (
  input: unknown,
): NormalizedAgentEvent[] => {
  const event = asRecord(input);
  if (!event) return [];
  const type = stringify(event.type);
  const properties =
    asRecord(event.properties) ?? asRecord(event.data) ?? event;
  const events: NormalizedAgentEvent[] = [];
  const sessionId = properties.sessionID ?? properties.sessionId;
  if (typeof sessionId === 'string' && type.includes('session')) {
    events.push({ kind: 'session', sessionId });
  }
  if (type === 'message.part.delta') {
    const part = asRecord(properties.part) ?? properties;
    const text = textFromPart(part);
    if (text) {
      events.push({
        kind: 'assistant_delta',
        content: text,
        externalMessageId: stringify(properties.messageID),
      });
    }
  }
  if (type === 'message.updated' || type === 'message.part.updated') {
    const part = asRecord(properties.part);
    const text = part ? textFromPart(part) : stringify(properties.message);
    if (text) {
      events.push({
        kind: 'assistant_delta',
        content: text,
        externalMessageId: stringify(properties.messageID),
      });
    }
  }
  if (type.includes('tool.started')) {
    events.push({
      kind: 'tool_started',
      name: stringify(properties.tool ?? properties.name ?? 'tool'),
      input: stringify(properties.input),
      externalMessageId: stringify(properties.messageID),
    });
  }
  if (type.includes('tool.finished') || type.includes('tool.completed')) {
    events.push({
      kind: 'tool_completed',
      name: stringify(properties.tool ?? properties.name ?? 'tool'),
      output: stringify(properties.output ?? properties.result),
      externalMessageId: stringify(properties.messageID),
    });
  }
  if (type.includes('tool.updated') || type.includes('tool.output')) {
    events.push({
      kind: 'tool_completed',
      name: stringify(properties.tool ?? properties.name ?? 'tool'),
      output: stringify(properties.output ?? properties.result ?? properties),
      externalMessageId: stringify(properties.messageID),
    });
  }
  if (type === 'file.edited') {
    const file = properties.file;
    if (typeof file === 'string')
      events.push({ kind: 'file_edited', path: file });
  }
  if (type === 'command.executed') {
    events.push({
      kind: 'command_executed',
      command: stringify(properties.command),
      output: stringify(properties.output),
    });
  }
  if (type.includes('permission') && type.includes('asked')) {
    events.push({
      kind: 'permission_requested',
      externalRequestId: stringify(properties.permissionID ?? properties.id),
      title: 'Permission requested',
      body: stringify(
        properties.permission ?? properties.message ?? properties,
      ),
      metadata: stringify(properties),
    });
  }
  if (type.includes('question') && type.includes('asked')) {
    events.push({
      kind: 'question_requested',
      externalRequestId: stringify(properties.requestID ?? properties.id),
      title: 'Agent question',
      body: stringify(properties.question ?? properties.message ?? properties),
      metadata: stringify(properties),
    });
  }
  if (type === 'session.idle') events.push({ kind: 'assistant_completed' });
  if (type === 'session.error') {
    events.push({
      kind: 'error',
      message: stringify(properties.error ?? properties.message ?? properties),
    });
  }
  if (events.length === 0 && type) {
    events.push({
      kind: 'status',
      status: type,
      metadata: stringify(properties),
    });
  }
  return events;
};
