'use strict';

const RECOVERY_LANES = Object.freeze([
  {
    id: 'ctm-restart-active-run',
    title: 'Restart CTM during an active agent run',
    invariants: ['sessionKey', 'statusNotRegressed', 'promptCountNotReduced', 'latestOutputPreserved'],
  },
  {
    id: 'tab-switch-idempotency',
    title: 'Switch tabs without terminal refresh churn',
    invariants: ['sessionKey', 'contentHashStable', 'statusNotInvented'],
  },
  {
    id: 'provider-auth-failure',
    title: 'Provider auth failure keeps run trace and explanation',
    invariants: ['sessionKey', 'terminalErrorPresent', 'statusFailedOrWaiting'],
  },
  {
    id: 'compaction-growth',
    title: 'Context growth triggers compaction without losing recent turns',
    invariants: ['sessionKey', 'compactionRecorded', 'promptCountNotReduced'],
  },
  {
    id: 'scheduler-restart-skills',
    title: 'Restart Wall-E with queued skills and missed jobs',
    invariants: ['jobStatePreserved', 'lastRunTraceLinked'],
  },
  {
    id: 'model-catalog-availability',
    title: 'Model picker uses available coding-capable provider models',
    invariants: ['availableModelsStable', 'unavailableReasonsPresent'],
  },
]);

function createRecoveryLanePlan({ lanes = RECOVERY_LANES, enabled = null, overrides = {} } = {}) {
  const enabledSet = enabled ? new Set(enabled) : null;
  return lanes
    .filter(lane => !enabledSet || enabledSet.has(lane.id))
    .map(lane => ({
      ...lane,
      ...(overrides[lane.id] || {}),
      invariants: [...new Set([...(lane.invariants || []), ...((overrides[lane.id] || {}).invariants || [])])],
    }));
}

function evaluateRecoveryLane(lane, before = {}, after = {}) {
  const failures = [];
  for (const invariant of lane.invariants || []) {
    const result = evaluateInvariant(invariant, before, after);
    if (!result.ok) failures.push({ invariant, reason: result.reason });
  }
  return {
    ok: failures.length === 0,
    laneId: lane.id,
    failures,
  };
}

function evaluateInvariant(invariant, before = {}, after = {}) {
  if (invariant === 'sessionKey') {
    return sameValue(before.sessionKey, after.sessionKey, 'session key changed');
  }
  if (invariant === 'statusNotRegressed') {
    const beforeRank = statusRank(before.status);
    const afterRank = statusRank(after.status);
    return afterRank >= beforeRank || before.status === after.status
      ? ok()
      : fail(`status regressed from ${before.status} to ${after.status}`);
  }
  if (invariant === 'statusNotInvented') {
    if (before.status !== 'running' && after.status === 'running' && !after.hasRunningEvidence) {
      return fail('running status appeared without running evidence');
    }
    return ok();
  }
  if (invariant === 'promptCountNotReduced') {
    return Number(after.promptCount || 0) >= Number(before.promptCount || 0)
      ? ok()
      : fail('prompt count decreased');
  }
  if (invariant === 'latestOutputPreserved') {
    if (!before.latestOutputHash) return ok();
    return sameValue(before.latestOutputHash, after.latestOutputHash, 'latest output hash changed');
  }
  if (invariant === 'contentHashStable') {
    return sameValue(before.contentHash, after.contentHash, 'content hash changed');
  }
  if (invariant === 'terminalErrorPresent') {
    return after.terminalError ? ok() : fail('terminal error missing');
  }
  if (invariant === 'statusFailedOrWaiting') {
    return ['failed', 'waiting'].includes(after.status) ? ok() : fail(`unexpected status ${after.status}`);
  }
  if (invariant === 'compactionRecorded') {
    return after.compaction?.compacted || Number(after.compaction?.count || 0) > Number(before.compaction?.count || 0)
      ? ok()
      : fail('compaction was not recorded');
  }
  if (invariant === 'jobStatePreserved') {
    return sameValue(before.jobId, after.jobId, 'job id changed');
  }
  if (invariant === 'lastRunTraceLinked') {
    return after.lastRunTraceId ? ok() : fail('last run trace id missing');
  }
  if (invariant === 'availableModelsStable') {
    return Array.isArray(after.availableModels) && after.availableModels.length >= Number(before.availableModels?.length || 0)
      ? ok()
      : fail('available model list shrank');
  }
  if (invariant === 'unavailableReasonsPresent') {
    const unavailable = after.models?.filter(model => model.available === false) || [];
    return unavailable.every(model => model.unavailableReason)
      ? ok()
      : fail('unavailable model reason missing');
  }
  return fail(`unknown invariant ${invariant}`);
}

function statusRank(status) {
  const ranks = {
    queued: 0,
    idle: 1,
    waiting: 2,
    running: 3,
    completed: 4,
    failed: 4,
    canceled: 4,
  };
  return ranks[status] ?? 0;
}

function sameValue(before, after, reason) {
  if (before === undefined || before === null || before === '') return ok();
  return before === after ? ok() : fail(reason);
}

function ok() {
  return { ok: true };
}

function fail(reason) {
  return { ok: false, reason };
}

module.exports = {
  RECOVERY_LANES,
  createRecoveryLanePlan,
  evaluateRecoveryLane,
  evaluateInvariant,
};
