// @abtnode/cron shim for CF Cron Triggers
//
// The real @abtnode/cron exports { init } where init({ context, jobs, onError }) registers jobs.
// In CF Workers, we store the jobs and execute them via the scheduled() handler.
//
// The shim parses 6-field cron expressions (sec min hour day month weekday)
// and only runs jobs whose schedule matches the current 5-minute window.

type CronJob = {
  name: string;
  time: string;
  fn: () => Promise<any> | any;
  options?: { runOnInit?: boolean };
};

type InitOptions = {
  context?: any;
  jobs: CronJob[];
  onError?: (error: Error, name: string) => void;
};

// Singleton storage for registered cron jobs
const registeredJobs: CronJob[] = [];
let onErrorHandler: ((error: Error, name: string) => void) | undefined;

// --- Cron expression matcher ---
// Supports 6-field format: second minute hour dayOfMonth month dayOfWeek
// Supports: numbers, *, */N, ranges (1-5), lists (1,3,5)

function parseField(field: string, min: number, max: number): number[] | null {
  // null means "match all"
  if (field === '*') return null;

  const values = new Set<number>();

  for (const part of field.split(',')) {
    // */N — every N
    const stepMatch = part.match(/^\*\/(\d+)$/);
    if (stepMatch) {
      const step = parseInt(stepMatch[1], 10);
      for (let i = min; i <= max; i += step) {
        values.add(i);
      }
      continue;
    }

    // N-M — range
    const rangeMatch = part.match(/^(\d+)-(\d+)$/);
    if (rangeMatch) {
      const from = parseInt(rangeMatch[1], 10);
      const to = parseInt(rangeMatch[2], 10);
      for (let i = from; i <= to; i++) {
        values.add(i);
      }
      continue;
    }

    // N — single value
    const num = parseInt(part, 10);
    if (!isNaN(num)) {
      values.add(num);
    }
  }

  return values.size > 0 ? Array.from(values) : null;
}

/**
 * Check if a date matches a 6-field cron expression.
 * Returns true if the date's minute/hour/day/month/weekday match.
 * Seconds field is ignored (CF triggers are minute-level).
 */
function matchesCron(cronExpr: string, date: Date): boolean {
  const fields = cronExpr.trim().split(/\s+/);
  if (fields.length < 5) return true; // Can't parse — run it

  // 6-field: sec min hour dom month dow
  // 5-field: min hour dom month dow
  const offset = fields.length >= 6 ? 1 : 0;

  const minuteField = parseField(fields[offset], 0, 59);
  const hourField = parseField(fields[offset + 1], 0, 23);
  const domField = parseField(fields[offset + 2], 1, 31);
  const monthField = parseField(fields[offset + 3], 0, 11); // cron months are 1-12, JS is 0-11
  const dowField = parseField(fields[offset + 4], 0, 6);

  const m = date.getUTCMinutes();
  const h = date.getUTCHours();
  const dom = date.getUTCDate();
  const month = date.getUTCMonth() + 1; // JS 0-based → cron 1-based
  const dow = date.getUTCDay(); // 0=Sunday

  if (minuteField && !minuteField.includes(m)) return false;
  if (hourField && !hourField.includes(h)) return false;
  if (domField && !domField.includes(dom)) return false;
  if (monthField && !monthField.includes(month)) return false;
  if (dowField && !dowField.includes(dow)) return false;

  return true;
}

// Check if a cron expression should fire at the given date (minute-level match).
//
// History: this helper previously used a 5-minute look-ahead window, under the
// assumption that CF Cron Triggers fire every 5 minutes. Once the deploy config
// switched to every-minute cron, that window made every stepped expression fire
// 5x more often than intended, driving CF Queues past the free-tier daily cap
// (2026-04-17 incident). With CF Scheduled firing every minute, we only check
// the current minute — each cron expression triggers at its designed frequency.
// See docs/cf-queues-ops-alert-analysis.md § 改动 B.
function shouldRunInWindow(cronExpr: string, date: Date): boolean {
  return matchesCron(cronExpr, date);
}

// --- Cron shim API ---

function init(options: InitOptions) {
  registeredJobs.length = 0;
  onErrorHandler = options.onError;

  for (const job of options.jobs || []) {
    if (job.name && job.time && typeof job.fn === 'function') {
      registeredJobs.push(job);
    }
  }

  // Skip runOnInit jobs in CF Workers — they'll run on the next matching trigger
  // (running them at module init time would block the request and may exceed CPU limits)

  return {
    addJob(name: string, time: string, fn: Function, opts?: any) {
      registeredJobs.push({ name, time, fn: fn as any, options: opts });
    },
    start() { /* no-op */ },
  };
}

// Called by worker.ts scheduled handler — only runs jobs matching the trigger time.
// Accepts an optional `now` so the caller can pass `event.scheduledTime` (the
// intended trigger minute) instead of relying on wall-clock at execution time.
// CF may deliver scheduled events with a small delay that crosses a minute
// boundary; matching on `scheduledTime` keeps exact-minute cron reliable.
async function runAll(now: Date = new Date()) {
  const matched: string[] = [];
  const skipped: string[] = [];

  for (const job of registeredJobs) {
    if (shouldRunInWindow(job.time, now)) {
      matched.push(job.name);
      try {
        await job.fn();
      } catch (err: any) {
        console.error(`[Cron] ${job.name} failed:`, err?.message || err);
        onErrorHandler?.(err, job.name);
      }
    } else {
      skipped.push(job.name);
    }
  }

  console.log(`[Cron] Ran ${matched.length} jobs: [${matched.join(', ')}]. Skipped ${skipped.length}.`);
}

async function runJob(name: string) {
  const job = registeredJobs.find((j) => j.name === name);
  if (job) {
    try {
      await job.fn();
    } catch (err: any) {
      console.error(`[Cron] ${name} failed:`, err?.message || err);
      onErrorHandler?.(err, name);
    }
  } else {
    console.warn(`[Cron] Job ${name} not found`);
  }
}

function getJobNames(): string[] {
  return registeredJobs.map((j) => `${j.name} (${j.time})`);
}

// Export matching @abtnode/cron API: default export is { init }
export default { init };

// Export helper functions for the worker.ts scheduled handler
export const cronInstance = { runAll, runJob, getJobNames };

// Exported for unit tests; not part of the public cron API.
export const __test__ = { matchesCron, shouldRunInWindow };
