'use strict';

const { createAgentRunId } = require('../agent-runners/contract');

const WRITE_TOOL_NAMES = new Set([
  'edit_file',
  'write_file',
  'multi_edit',
  'apply_patch',
  'run_shell',
  'task',
]);

const EXTERNAL_WRITE_TOOL_NAMES = new Set([
  'calendar_create',
  'mail_reply',
  'mail_send',
  'notification',
  'reminder_create',
  'slack_send_message',
  'sms_send',
]);

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

function createExecutionTrace({
  runId = createAgentRunId('run'),
  parentRunId = null,
  mode = 'build',
  provider = '',
  model = '',
  runnerId = '',
  harnessId = '',
  sessionId = '',
  cwd = '',
  sandbox = null,
  taskType = 'coding',
  now = nowIso,
} = {}) {
  const startedAt = now();
  return {
    version: 1,
    runId,
    parentRunId,
    startedAt,
    endedAt: null,
    status: 'running',
    liveness: 'running',
    request: {
      taskType,
      mode,
      providerPreference: provider || '',
      modelPreference: model || '',
      runnerId,
      harnessId,
      sandbox,
      cwd,
    },
    selection: {
      candidates: [],
      winner: null,
      fallbackUsed: false,
      reason: '',
    },
    attempts: [],
    tools: [],
    context: {
      compacted: false,
      compactionCount: 0,
      inputTokensBefore: null,
      inputTokensAfter: null,
      truncations: [],
    },
    review: null,
    errors: [],
    sessionId,
  };
}

function recordSelection(trace, selection = {}) {
  if (!trace) return trace;
  trace.selection = {
    candidates: selection.candidates || [],
    winner: selection.selected?.id || selection.winner || null,
    fallbackUsed: Boolean(selection.fallbackUsed),
    reason: selection.reason || '',
  };
  return trace;
}

function recordAttemptStart(trace, attempt = {}) {
  if (!trace) return null;
  const id = attempt.id || `attempt-${trace.attempts.length + 1}`;
  const entry = {
    id,
    provider: attempt.provider || attempt.providerType || '',
    model: attempt.model || '',
    runnerId: attempt.runnerId || '',
    harnessId: attempt.harnessId || '',
    startedAt: attempt.startedAt || nowIso(),
    endedAt: null,
    status: 'running',
    stopReason: '',
    error: null,
    usage: null,
  };
  trace.attempts.push(entry);
  return entry;
}

function recordAttemptFinish(trace, attemptId, patch = {}) {
  if (!trace) return null;
  const entry = trace.attempts.find(attempt => attempt.id === attemptId) || trace.attempts[trace.attempts.length - 1];
  if (!entry) return null;
  Object.assign(entry, {
    endedAt: patch.endedAt || nowIso(),
    status: patch.status || (patch.success === false ? 'failed' : 'completed'),
    stopReason: patch.stopReason || entry.stopReason || '',
    error: patch.error || null,
    usage: patch.usage || entry.usage || null,
  });
  if (entry.status === 'failed' && entry.error) trace.errors.push({ attemptId: entry.id, message: entry.error });
  return entry;
}

function recordToolEvent(trace, event = {}) {
  if (!trace) return null;
  const toolName = event.toolName || event.name || 'unknown';
  const sideEffect = inferToolSideEffect(toolName, event);
  const entry = {
    id: event.id || `tool-${trace.tools.length + 1}`,
    toolName,
    status: event.status || 'completed',
    sideEffect,
    startedAt: event.startedAt || null,
    endedAt: event.endedAt || nowIso(),
    changedFiles: Array.isArray(event.changedFiles) ? event.changedFiles : [],
    error: event.error || null,
  };
  trace.tools.push(entry);
  return entry;
}

function inferToolSideEffect(toolName, event = {}) {
  if (event.sideEffect) return event.sideEffect;
  if (event.readOnly === true) return 'read';
  if (event.changedFiles?.length > 0) return 'write';
  if (WRITE_TOOL_NAMES.has(toolName)) return toolName === 'run_shell' ? 'unknown' : 'write';
  if (EXTERNAL_WRITE_TOOL_NAMES.has(toolName)) return 'external_write';
  return 'read';
}

function recordContextCompaction(trace, compaction = {}) {
  if (!trace) return trace;
  trace.context.compacted = Boolean(compaction.compacted || trace.context.compacted);
  if (compaction.compacted) trace.context.compactionCount += 1;
  if (Number.isFinite(compaction.tokensBeforeCompaction)) {
    trace.context.inputTokensBefore = compaction.tokensBeforeCompaction;
  }
  if (Number.isFinite(compaction.tokensAfterCompaction)) {
    trace.context.inputTokensAfter = compaction.tokensAfterCompaction;
  }
  if (compaction.truncation) trace.context.truncations.push(compaction.truncation);
  return trace;
}

function recordReview(trace, quorum = {}) {
  if (!trace) return trace;
  trace.review = {
    quorumId: quorum.quorumId || null,
    verdict: quorum.verdict || quorum.aggregate?.verdict || '',
    blockers: Array.isArray(quorum.issues) ? quorum.issues.filter(issue => /block/i.test(String(issue))).length : 0,
    scores: quorum.aggregate?.scores || quorum.scores || {},
    confidence: quorum.confidence ?? quorum.aggregate?.confidence ?? null,
    agreement: quorum.agreement ?? quorum.aggregate?.agreement ?? null,
  };
  return trace;
}

function finalizeTrace(trace, { status = 'completed', liveness = null, error = null } = {}) {
  if (!trace) return trace;
  trace.status = status;
  trace.liveness = liveness || (status === 'completed' ? 'finished' : status);
  trace.endedAt = nowIso();
  if (error) trace.errors.push({ message: String(error) });
  return trace;
}

function hasWriteSideEffects(trace) {
  return (trace?.tools || []).some(tool => isUnsafeRetrySideEffect(tool.sideEffect));
}

function hasCompletedWriteSideEffects(trace) {
  return (trace?.tools || []).some(tool => isUnsafeRetrySideEffect(tool.sideEffect) && tool.status === 'completed');
}

function isUnsafeRetrySideEffect(sideEffect) {
  return sideEffect === 'write' || sideEffect === 'unknown' || sideEffect === 'external_write';
}

function decideRetry(trace, failure = {}) {
  if (!trace) return { retry: false, reason: 'missing-trace' };
  if ((trace.tools || []).length === 0) return { retry: true, reason: 'no-tool-side-effects' };
  if (!hasWriteSideEffects(trace)) return { retry: true, reason: 'read-only-tools' };
  if (hasCompletedWriteSideEffects(trace) && failure.toolResultPersisted === true && failure.fileChangesRecorded === true) {
    return { retry: true, reason: 'write-side-effects-recorded' };
  }
  return { retry: false, reason: 'write-side-effects-not-safe-to-retry' };
}

function attachTraceToResult(result = {}, trace = null) {
  if (!trace) return result;
  return {
    ...result,
    trace,
  };
}

module.exports = {
  attachTraceToResult,
  createExecutionTrace,
  decideRetry,
  finalizeTrace,
  hasCompletedWriteSideEffects,
  hasWriteSideEffects,
  inferToolSideEffect,
  recordAttemptFinish,
  recordAttemptStart,
  recordContextCompaction,
  recordReview,
  recordSelection,
  recordToolEvent,
};
