'use strict';

const crypto = require('node:crypto');

const STATUS_ORDER = Object.freeze([
  'queued',
  'running',
  'waiting',
  'idle',
  'completed',
  'failed',
  'canceled',
]);

const TERMINAL_STATUSES = new Set(['completed', 'failed', 'canceled']);

function nowIso() {
  return new Date().toISOString();
}

function createRunId(prefix = 'run') {
  return `${prefix}-${crypto.randomUUID()}`;
}

function normalizeRunStatus(status, fallback = 'queued') {
  const value = String(status || '').toLowerCase();
  if (value === 'complete' || value === 'success' || value === 'done') return 'completed';
  if (value === 'error') return 'failed';
  if (value === 'cancelled') return 'canceled';
  if (STATUS_ORDER.includes(value)) return value;
  return fallback;
}

function createRunEnvelope(input = {}, { clock = nowIso } = {}) {
  const timestamp = input.createdAt || clock();
  const status = normalizeRunStatus(input.status, 'queued');
  return {
    version: 1,
    runId: input.runId || input.id || createRunId(),
    parentRunId: input.parentRunId || null,
    sessionId: input.sessionId || null,
    sessionKey: input.sessionKey || input.sessionId || null,
    agentKind: input.agentKind || input.kind || 'walle-coding',
    source: input.source || 'ctm',
    sourceAccount: input.sourceAccount || null,
    routeId: input.routeId || null,
    cwd: input.cwd || '',
    branch: input.branch || '',
    providerId: input.providerId || input.provider || '',
    modelId: input.modelId || input.model || '',
    harnessId: input.harnessId || '',
    status,
    statusReason: input.statusReason || '',
    promptEnvelopeId: input.promptEnvelopeId || null,
    compaction: normalizeCompaction(input.compaction),
    retry: normalizeRetry(input.retry),
    toolSummary: normalizeToolSummary(input.toolSummary),
    usage: normalizeUsage(input.usage),
    terminal: normalizeTerminal(input.terminal, status),
    createdAt: timestamp,
    updatedAt: input.updatedAt || timestamp,
    metadata: { ...(input.metadata || {}) },
  };
}

function updateRunEnvelope(envelope, patch = {}, { clock = nowIso } = {}) {
  if (!envelope) return createRunEnvelope(patch, { clock });
  const nextStatus = patch.status !== undefined
    ? normalizeRunStatus(patch.status, envelope.status)
    : envelope.status;
  return {
    ...envelope,
    ...withoutUndefined(patch),
    status: nextStatus,
    compaction: normalizeCompaction({ ...(envelope.compaction || {}), ...(patch.compaction || {}) }),
    retry: normalizeRetry({ ...(envelope.retry || {}), ...(patch.retry || {}) }),
    toolSummary: normalizeToolSummary({ ...(envelope.toolSummary || {}), ...(patch.toolSummary || {}) }),
    usage: normalizeUsage({ ...(envelope.usage || {}), ...(patch.usage || {}) }),
    terminal: normalizeTerminal({ ...(envelope.terminal || {}), ...(patch.terminal || {}) }, nextStatus),
    metadata: {
      ...(envelope.metadata || {}),
      ...(patch.metadata || {}),
    },
    updatedAt: patch.updatedAt || clock(),
  };
}

function envelopeFromExecutionTrace(trace = {}, extras = {}) {
  const winner = trace.selection?.winner || {};
  const winnerId = typeof winner === 'string' ? winner : winner.id;
  const latestAttempt = Array.isArray(trace.attempts) ? trace.attempts[trace.attempts.length - 1] : null;
  return createRunEnvelope({
    runId: trace.runId,
    parentRunId: trace.parentRunId,
    sessionId: trace.sessionId,
    sessionKey: extras.sessionKey || trace.sessionKey || trace.sessionId,
    agentKind: extras.agentKind || trace.request?.taskType || 'walle-coding',
    source: extras.source || trace.source || 'ctm',
    cwd: trace.request?.cwd || extras.cwd || '',
    providerId: latestAttempt?.provider || trace.request?.providerPreference || extras.providerId || '',
    modelId: latestAttempt?.model || trace.request?.modelPreference || extras.modelId || '',
    harnessId: latestAttempt?.harnessId || trace.request?.harnessId || winnerId || extras.harnessId || '',
    status: trace.status,
    statusReason: trace.errors?.[trace.errors.length - 1]?.message || trace.liveness || '',
    compaction: trace.context || {},
    toolSummary: summarizeTraceTools(trace),
    usage: latestAttempt?.usage || trace.usage || {},
    terminal: {
      liveness: trace.liveness,
      stopReason: latestAttempt?.stopReason || trace.stopReason || '',
      error: trace.errors?.[trace.errors.length - 1]?.message || null,
    },
    createdAt: trace.startedAt,
    updatedAt: trace.endedAt || trace.updatedAt,
    metadata: {
      fallbackUsed: Boolean(trace.selection?.fallbackUsed),
      selectionReason: trace.selection?.reason || '',
      ...(extras.metadata || {}),
    },
  });
}

function toCtmSessionPatch(envelope = {}) {
  const status = normalizeRunStatus(envelope.status, 'idle');
  return {
    id: envelope.sessionId || envelope.sessionKey || envelope.runId,
    runId: envelope.runId,
    status,
    statusReason: envelope.statusReason || envelope.terminal?.stopReason || '',
    provider: envelope.providerId || '',
    model: envelope.modelId || '',
    harnessId: envelope.harnessId || '',
    cwd: envelope.cwd || '',
    branch: envelope.branch || '',
    lastActiveAt: envelope.updatedAt || envelope.createdAt || nowIso(),
    running: status === 'running',
    waiting: status === 'waiting',
    terminal: {
      liveness: envelope.terminal?.liveness || status,
      stopReason: envelope.terminal?.stopReason || '',
      error: envelope.terminal?.error || null,
    },
    runEnvelopeVersion: envelope.version || 1,
  };
}

function toBrainMemorySummary(envelope = {}) {
  const status = normalizeRunStatus(envelope.status, 'idle');
  const parts = [
    `${envelope.agentKind || 'agent'} run ${envelope.runId || '(unknown)'} is ${status}`,
    envelope.cwd ? `cwd=${envelope.cwd}` : '',
    envelope.branch ? `branch=${envelope.branch}` : '',
    envelope.providerId || envelope.modelId ? `model=${[envelope.providerId, envelope.modelId].filter(Boolean).join('/')}` : '',
    envelope.toolSummary?.total ? `tools=${envelope.toolSummary.total}` : '',
    envelope.compaction?.count ? `compactions=${envelope.compaction.count}` : '',
  ].filter(Boolean);
  return parts.join('; ');
}

function normalizeCompaction(compaction = {}) {
  return {
    compacted: Boolean(compaction.compacted),
    count: Number(compaction.count ?? compaction.compactionCount ?? 0) || 0,
    tokensBefore: numberOrNull(compaction.tokensBefore ?? compaction.inputTokensBefore ?? compaction.tokensBeforeCompaction),
    tokensAfter: numberOrNull(compaction.tokensAfter ?? compaction.inputTokensAfter ?? compaction.tokensAfterCompaction),
    lastCompactionId: compaction.lastCompactionId || compaction.compactionId || null,
  };
}

function normalizeRetry(retry = {}) {
  return {
    count: Number(retry.count ?? retry.retryCount ?? 0) || 0,
    lastReason: retry.lastReason || retry.reason || '',
    safeToRetry: retry.safeToRetry ?? null,
  };
}

function normalizeToolSummary(toolSummary = {}) {
  return {
    total: Number(toolSummary.total ?? 0) || 0,
    read: Number(toolSummary.read ?? 0) || 0,
    write: Number(toolSummary.write ?? 0) || 0,
    unknown: Number(toolSummary.unknown ?? 0) || 0,
    failed: Number(toolSummary.failed ?? 0) || 0,
    changedFiles: Array.isArray(toolSummary.changedFiles) ? [...toolSummary.changedFiles] : [],
  };
}

function normalizeUsage(usage = {}) {
  return {
    inputTokens: numberOrNull(usage.inputTokens ?? usage.input_tokens ?? usage.input),
    outputTokens: numberOrNull(usage.outputTokens ?? usage.output_tokens ?? usage.output),
    totalTokens: numberOrNull(usage.totalTokens ?? usage.total_tokens ?? usage.total),
    costUsd: numberOrNull(usage.costUsd ?? usage.cost_usd ?? usage.cost),
  };
}

function normalizeTerminal(terminal = {}, status = 'queued') {
  return {
    liveness: terminal.liveness || (TERMINAL_STATUSES.has(status) ? 'finished' : status),
    stopReason: terminal.stopReason || terminal.stop_reason || '',
    error: terminal.error || null,
    details: { ...(terminal.details || {}) },
  };
}

function summarizeTraceTools(trace = {}) {
  const summary = normalizeToolSummary();
  for (const tool of trace.tools || []) {
    summary.total += 1;
    if (tool.status === 'failed') summary.failed += 1;
    const sideEffect = tool.sideEffect || 'read';
    if (sideEffect === 'write') summary.write += 1;
    else if (sideEffect === 'unknown') summary.unknown += 1;
    else summary.read += 1;
    for (const file of tool.changedFiles || []) {
      if (!summary.changedFiles.includes(file)) summary.changedFiles.push(file);
    }
  }
  return summary;
}

function numberOrNull(value) {
  const number = Number(value);
  return Number.isFinite(number) ? number : null;
}

function withoutUndefined(value = {}) {
  return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
}

module.exports = {
  STATUS_ORDER,
  TERMINAL_STATUSES,
  createRunEnvelope,
  createRunId,
  envelopeFromExecutionTrace,
  normalizeRunStatus,
  toBrainMemorySummary,
  toCtmSessionPatch,
  updateRunEnvelope,
};
