#!/usr/bin/env node

/**
 * CI Poll Decision Script
 *
 * Deterministic decision engine for CI monitoring.
 * Takes ci_information JSON + state args, outputs a single JSON action line.
 *
 * Architecture:
 *   classify()    — pure decision tree, returns { action, code, extra? }
 *   buildOutput() — maps classification to full output with messages, delays, counters
 *
 * Usage:
 *   node ci-poll-decide.mjs '<ci_info_json>' <poll_count> <verbosity> \
 *     [--wait-mode] [--prev-cipe-url <url>] [--expected-sha <sha>] \
 *     [--prev-status <status>] [--timeout <seconds>] [--new-cipe-timeout <seconds>] \
 *     [--env-rerun-count <n>] [--no-progress-count <n>] \
 *     [--prev-cipe-status <status>] [--prev-sh-status <status>] \
 *     [--prev-verification-status <status>] [--prev-failure-classification <status>]
 */

// --- Arg parsing ---

const args = process.argv.slice(2);
const ciInfoJson = args[0];
const pollCount = parseInt(args[1], 10) || 0;
const verbosity = args[2] || 'medium';

function getFlag(name) {
  return args.includes(name);
}

function getArg(name) {
  const idx = args.indexOf(name);
  return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : null;
}

const waitMode = getFlag('--wait-mode');
const prevCipeUrl = getArg('--prev-cipe-url');
const expectedSha = getArg('--expected-sha');
const prevStatus = getArg('--prev-status');
const timeoutSeconds = parseInt(getArg('--timeout') || '0', 10);
const newCipeTimeoutSeconds = parseInt(getArg('--new-cipe-timeout') || '0', 10);
const envRerunCount = parseInt(getArg('--env-rerun-count') || '0', 10);
const inputNoProgressCount = parseInt(getArg('--no-progress-count') || '0', 10);
const prevCipeStatus = getArg('--prev-cipe-status');
const prevShStatus = getArg('--prev-sh-status');
const prevVerificationStatus = getArg('--prev-verification-status');
const prevFailureClassification = getArg('--prev-failure-classification');

// --- Parse CI info ---

let ci;
try {
  ci = JSON.parse(ciInfoJson);
} catch {
  console.log(
    JSON.stringify({
      action: 'done',
      code: 'error',
      message: 'Failed to parse ci_information JSON',
      noProgressCount: inputNoProgressCount + 1,
      envRerunCount,
    }),
  );
  process.exit(0);
}

const {
  cipeStatus,
  selfHealingStatus,
  verificationStatus,
  selfHealingEnabled,
  selfHealingSkippedReason,
  failureClassification: rawFailureClassification,
  failedTaskIds = [],
  verifiedTaskIds = [],
  couldAutoApplyTasks,
  autoApplySkipped,
  autoApplySkipReason,
  userAction,
  cipeUrl,
  commitSha,
} = ci;

const failureClassification = rawFailureClassification?.toLowerCase() ?? null;

// --- Helpers ---

function categorizeTasks() {
  const verifiedSet = new Set(verifiedTaskIds);
  const unverified = failedTaskIds.filter((t) => !verifiedSet.has(t));
  if (unverified.length === 0) return { category: 'all_verified' };

  const e2e = unverified.filter((t) => {
    const parts = t.split(':');
    return parts.length >= 2 && parts[1].includes('e2e');
  });
  if (e2e.length === unverified.length) return { category: 'e2e_only' };

  const verifiable = unverified.filter((t) => {
    const parts = t.split(':');
    return !(parts.length >= 2 && parts[1].includes('e2e'));
  });
  return { category: 'needs_local_verify', verifiableTaskIds: verifiable };
}

function backoff(count) {
  const delays = [60, 90, 120, 180];
  return delays[Math.min(count, delays.length - 1)];
}

function hasStateChanged() {
  if (prevCipeStatus && cipeStatus !== prevCipeStatus) return true;
  if (prevShStatus && selfHealingStatus !== prevShStatus) return true;
  if (prevVerificationStatus && verificationStatus !== prevVerificationStatus) return true;
  if (prevFailureClassification && failureClassification !== prevFailureClassification) return true;
  return false;
}

function isTimedOut() {
  if (timeoutSeconds <= 0) return false;
  const avgDelay = pollCount === 0 ? 0 : backoff(Math.floor(pollCount / 2));
  return pollCount * avgDelay >= timeoutSeconds;
}

function isWaitTimedOut() {
  if (newCipeTimeoutSeconds <= 0) return false;
  return pollCount * 30 >= newCipeTimeoutSeconds;
}

function isNewCipe() {
  return (
    (prevCipeUrl && cipeUrl && cipeUrl !== prevCipeUrl) ||
    (expectedSha && commitSha && commitSha === expectedSha)
  );
}

// ============================================================
// classify() — pure decision tree
//
// Returns: { action: 'poll'|'wait'|'done', code: string, extra? }
//
// Decision priority (top wins):
//   WAIT MODE:
//     1. new CI Attempt detected         → poll  (new_cipe_detected)
//     2. wait timed out                  → done  (no_new_cipe)
//     3. still waiting                   → wait  (waiting_for_cipe)
//   NORMAL MODE:
//     4. polling timeout                 → done  (polling_timeout)
//     5. circuit breaker (13 polls)      → done  (circuit_breaker)
//     6. CI succeeded                    → done  (ci_success)
//     7. CI canceled                     → done  (cipe_canceled)
//     8. CI timed out                    → done  (cipe_timed_out)
//     9. CI failed, no tasks recorded    → done  (cipe_no_tasks)
//    10. environment failure             → done  (environment_rerun_cap | environment_issue)
//    11. self-healing throttled          → done  (self_healing_throttled)
//    12. CI in progress / not started    → poll  (ci_running)
//    13. self-healing in progress        → poll  (sh_running)
//    14. flaky task auto-rerun           → poll  (flaky_rerun)
//    15. fix auto-applied                → poll  (fix_auto_applied)
//    16. auto-apply: skipped             → done  (fix_auto_apply_skipped)
//    17. auto-apply: verification pending→ poll  (verification_pending)
//    18. auto-apply: verified            → done  (fix_auto_applying)
//    19. fix: verification failed/none   → done  (fix_needs_review)
//    20. fix: all/e2e verified           → done  (fix_apply_ready)
//    21. fix: needs local verify         → done  (fix_needs_local_verify)
//    22. self-healing failed             → done  (fix_failed)
//    23. no fix available                → done  (no_fix)
//    24. fallback                        → poll  (fallback)
// ============================================================

function classify() {
  // --- Wait mode ---
  if (waitMode) {
    if (isNewCipe()) return { action: 'poll', code: 'new_cipe_detected' };
    if (isWaitTimedOut()) return { action: 'done', code: 'no_new_cipe' };
    return { action: 'wait', code: 'waiting_for_cipe' };
  }

  // --- Guards ---
  if (isTimedOut()) return { action: 'done', code: 'polling_timeout' };
  if (noProgressCount >= 13) return { action: 'done', code: 'circuit_breaker' };

  // --- Terminal CI states ---
  if (cipeStatus === 'SUCCEEDED') return { action: 'done', code: 'ci_success' };
  if (cipeStatus === 'CANCELED') return { action: 'done', code: 'cipe_canceled' };
  if (cipeStatus === 'TIMED_OUT') return { action: 'done', code: 'cipe_timed_out' };

  // --- CI failed, no tasks ---
  if (cipeStatus === 'FAILED' && failedTaskIds.length === 0 && selfHealingStatus == null)
    return { action: 'done', code: 'cipe_no_tasks' };

  // --- Environment failure ---
  if (failureClassification === 'environment_state') {
    if (envRerunCount >= 2) return { action: 'done', code: 'environment_rerun_cap' };
    return { action: 'done', code: 'environment_issue' };
  }

  // --- Throttled ---
  if (selfHealingSkippedReason === 'THROTTLED')
    return { action: 'done', code: 'self_healing_throttled' };

  // --- Still running: CI ---
  if (cipeStatus === 'IN_PROGRESS' || cipeStatus === 'NOT_STARTED')
    return { action: 'poll', code: 'ci_running' };

  // --- Still running: self-healing ---
  if (
    (selfHealingStatus === 'IN_PROGRESS' || selfHealingStatus === 'NOT_STARTED') &&
    !selfHealingSkippedReason
  )
    return { action: 'poll', code: 'sh_running' };

  // --- Still running: flaky rerun ---
  if (failureClassification === 'flaky_task') return { action: 'poll', code: 'flaky_rerun' };

  // --- Fix auto-applied, waiting for new CI Attempt ---
  if (userAction === 'APPLIED_AUTOMATICALLY') return { action: 'poll', code: 'fix_auto_applied' };

  // --- Auto-apply path (couldAutoApplyTasks) ---
  if (couldAutoApplyTasks === true) {
    if (autoApplySkipped === true)
      return {
        action: 'done',
        code: 'fix_auto_apply_skipped',
        extra: { autoApplySkipReason },
      };
    if (verificationStatus === 'NOT_STARTED' || verificationStatus === 'IN_PROGRESS')
      return { action: 'poll', code: 'verification_pending' };
    if (verificationStatus === 'COMPLETED') return { action: 'done', code: 'fix_auto_applying' };
    // verification FAILED or NOT_EXECUTABLE → falls through to fix_needs_review
  }

  // --- Fix available ---
  if (selfHealingStatus === 'COMPLETED') {
    if (
      verificationStatus === 'FAILED' ||
      verificationStatus === 'NOT_EXECUTABLE' ||
      (couldAutoApplyTasks !== true && !verificationStatus)
    )
      return { action: 'done', code: 'fix_needs_review' };

    const tasks = categorizeTasks();
    if (tasks.category === 'all_verified' || tasks.category === 'e2e_only')
      return { action: 'done', code: 'fix_apply_ready' };
    return {
      action: 'done',
      code: 'fix_needs_local_verify',
      extra: { verifiableTaskIds: tasks.verifiableTaskIds },
    };
  }

  // --- Fix failed ---
  if (selfHealingStatus === 'FAILED') return { action: 'done', code: 'fix_failed' };

  // --- No fix available ---
  if (
    cipeStatus === 'FAILED' &&
    (selfHealingEnabled === false || selfHealingStatus === 'NOT_EXECUTABLE')
  )
    return { action: 'done', code: 'no_fix' };

  // --- Fallback ---
  return { action: 'poll', code: 'fallback' };
}

// ============================================================
// buildOutput() — maps classification to full JSON output
// ============================================================

// Message templates keyed by status or key
const messages = {
  // wait mode
  new_cipe_detected: () => `New CI Attempt detected! CI: ${cipeStatus || 'N/A'}`,
  no_new_cipe: () => 'New CI Attempt timeout exceeded. No new CI Attempt detected.',
  waiting_for_cipe: () => 'Waiting for new CI Attempt...',

  // guards
  polling_timeout: () => 'Polling timeout exceeded.',
  circuit_breaker: () => 'No progress after 13 consecutive polls. Stopping.',

  // terminal
  ci_success: () => 'CI passed successfully!',
  cipe_canceled: () => 'CI Attempt was canceled.',
  cipe_timed_out: () => 'CI Attempt timed out.',
  cipe_no_tasks: () => 'CI failed but no Nx tasks were recorded.',

  // environment
  environment_rerun_cap: () => 'Environment rerun cap (2) exceeded. Bailing.',
  environment_issue: () => 'CI: FAILED | Classification: ENVIRONMENT_STATE',

  // throttled
  self_healing_throttled: () => 'Self-healing throttled \u2014 too many unapplied fixes.',

  // polling
  ci_running: () => `CI: ${cipeStatus}`,
  sh_running: () => `CI: ${cipeStatus} | Self-healing: ${selfHealingStatus}`,
  flaky_rerun: () => 'CI: FAILED | Classification: FLAKY_TASK (auto-rerun in progress)',
  fix_auto_applied: () => 'CI: FAILED | Fix auto-applied, new CI Attempt spawning',
  verification_pending: () =>
    `CI: FAILED | Self-healing: COMPLETED | Verification: ${verificationStatus}`,

  // actionable
  fix_auto_applying: () => 'Fix verified! Auto-applying...',
  fix_auto_apply_skipped: (extra) =>
    `Fix verified but auto-apply was skipped. ${
      extra?.autoApplySkipReason
        ? `Reason: ${extra.autoApplySkipReason}`
        : 'Offer to apply manually.'
    }`,
  fix_needs_review: () =>
    `Fix available but needs review. Verification: ${verificationStatus || 'N/A'}`,
  fix_apply_ready: () => 'Fix available and verified. Ready to apply.',
  fix_needs_local_verify: (extra) =>
    `Fix available. ${extra.verifiableTaskIds.length} task(s) need local verification.`,
  fix_failed: () => 'Self-healing failed to generate a fix.',
  no_fix: () => 'CI failed, no fix available.',

  // fallback
  fallback: () =>
    `CI: ${cipeStatus || 'N/A'} | Self-healing: ${
      selfHealingStatus || 'N/A'
    } | Verification: ${verificationStatus || 'N/A'}`,
};

// Codes where noProgressCount resets to 0 (genuine progress occurred)
const resetProgressCodes = new Set([
  'ci_success',
  'fix_auto_applying',
  'fix_auto_apply_skipped',
  'fix_needs_review',
  'fix_apply_ready',
  'fix_needs_local_verify',
]);

function formatMessage(msg) {
  if (verbosity === 'minimal') {
    const currentStatus = `${cipeStatus}|${selfHealingStatus}|${verificationStatus}`;
    if (currentStatus === (prevStatus || '')) return null;
    return msg;
  }
  if (verbosity === 'verbose') {
    return [
      `Poll #${pollCount + 1} | CI: ${cipeStatus || 'N/A'} | Self-healing: ${
        selfHealingStatus || 'N/A'
      } | Verification: ${verificationStatus || 'N/A'}`,
      msg,
    ].join('\n');
  }
  return `Poll #${pollCount + 1} | ${msg}`;
}

function buildOutput(decision) {
  const { action, code, extra } = decision;

  // noProgressCount is already computed before classify() was called.
  // Here we only handle the reset for "genuine progress" done-codes.

  const msgFn = messages[code];
  const rawMsg = msgFn ? msgFn(extra) : `Unknown: ${code}`;
  const message = formatMessage(rawMsg);

  const result = {
    action,
    code,
    message,
    noProgressCount: resetProgressCodes.has(code) ? 0 : noProgressCount,
    envRerunCount,
  };

  // Add delay
  if (action === 'wait') {
    result.delay = 30;
  } else if (action === 'poll') {
    result.delay = code === 'new_cipe_detected' ? 60 : backoff(noProgressCount);
    result.fields = 'light';
  }

  // Add extras
  if (code === 'new_cipe_detected') result.newCipeDetected = true;
  if (extra?.verifiableTaskIds) result.verifiableTaskIds = extra.verifiableTaskIds;
  if (extra?.autoApplySkipReason) result.autoApplySkipReason = extra.autoApplySkipReason;

  console.log(JSON.stringify(result));
}

// --- Run ---

// Compute noProgressCount from input. Single assignment, no mutation.
// Wait mode: reset on new cipe, otherwise unchanged (wait doesn't count as no-progress).
// Normal mode: reset on any state change, otherwise increment.
const noProgressCount = (() => {
  if (waitMode) return isNewCipe() ? 0 : inputNoProgressCount;
  if (isNewCipe() || hasStateChanged()) return 0;
  return inputNoProgressCount + 1;
})();

buildOutput(classify());
