import type { AddOptions, Job } from "./index.ts";
import type { DurableQueue } from "./durable.ts";

export interface ScheduledJob<T = unknown> {
  name: string;
  data: T;
  everyMs: number;
  options?: AddOptions;
}

export interface QueueScheduler {
  start(): void;
  stop(): void;
  tick(now?: number): Promise<number>;
  snapshot(): { running: boolean; schedules: number; nextRuns: Record<string, number> };
}

/** Restart-safe scheduler when used with a durable queue and stable idempotency buckets. */
export function createQueueScheduler(
  queue: DurableQueue,
  schedules: ScheduledJob[],
  options: { pollMs?: number; now?: () => number } = {},
): QueueScheduler {
  const now = options.now ?? Date.now;
  const pollMs = options.pollMs ?? 1000;
  const nextRuns = new Map(schedules.map((schedule) => [schedule.name, now()]));
  let timer: ReturnType<typeof setInterval> | null = null;
  const tick = async (at = now()) => {
    let added = 0;
    for (const schedule of schedules) {
      if (!Number.isFinite(schedule.everyMs) || schedule.everyMs < 1)
        throw new RangeError(`Schedule '${schedule.name}' everyMs must be positive`);
      const next = nextRuns.get(schedule.name) ?? at;
      if (next > at) continue;
      const bucket = Math.floor(at / schedule.everyMs);
      await queue.add(schedule.name, schedule.data, {
        ...schedule.options,
        idempotencyKey: schedule.options?.idempotencyKey ?? `schedule:${schedule.name}:${bucket}`,
      });
      nextRuns.set(schedule.name, (bucket + 1) * schedule.everyMs);
      added++;
    }
    return added;
  };
  return {
    start() {
      if (!timer) timer = setInterval(() => void tick(), pollMs);
    },
    stop() {
      if (timer) clearInterval(timer);
      timer = null;
    },
    tick,
    snapshot: () => ({
      running: timer !== null,
      schedules: schedules.length,
      nextRuns: Object.fromEntries(nextRuns),
    }),
  };
}

export async function addBatch<T>(
  queue: DurableQueue,
  name: string,
  values: T[],
  options?: AddOptions,
): Promise<Job<T>[]> {
  return Promise.all(
    values.map((value, index) =>
      queue.add(name, value, {
        ...options,
        idempotencyKey: options?.idempotencyKey ? `${options.idempotencyKey}:${index}` : undefined,
      }),
    ),
  );
}

export interface QueueDashboardSnapshot {
  generatedAt: number;
  pending: number;
  failed: number;
  byName: Record<string, number>;
  oldestRunAt?: number;
}

export async function queueDashboardSnapshot(queue: DurableQueue): Promise<QueueDashboardSnapshot> {
  const jobs = await queue.list();
  return {
    generatedAt: Date.now(),
    pending: jobs.length,
    failed: queue.failed().length,
    byName: jobs.reduce<Record<string, number>>((counts, job) => {
      counts[job.name] = (counts[job.name] ?? 0) + 1;
      return counts;
    }, {}),
    oldestRunAt: jobs.length ? Math.min(...jobs.map((job) => job.runAt)) : undefined,
  };
}

export function renderQueueDashboard(snapshot: QueueDashboardSnapshot): string {
  const rows = Object.entries(snapshot.byName)
    .sort(([left], [right]) => left.localeCompare(right))
    .map(
      ([name, count]) =>
        `<tr><td>${name.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]!)}</td><td>${count}</td></tr>`,
    )
    .join("");
  return `<!doctype html><html><head><meta charset="utf-8"><title>WRNexus Queue Dashboard</title></head><body><main><h1>Queue dashboard</h1><p>Pending: ${snapshot.pending} · Failed: ${snapshot.failed}</p><table><thead><tr><th>Queue</th><th>Pending</th></tr></thead><tbody>${rows}</tbody></table></main></body></html>`;
}

/** Long-running scheduler/worker loop suitable for a dedicated process or container. */
export async function runQueueDaemon(
  queue: DurableQueue,
  scheduler: QueueScheduler,
  options: { signal?: AbortSignal; pollMs?: number; onError?: (error: unknown) => void } = {},
): Promise<void> {
  const pollMs = options.pollMs ?? 250;
  if (!Number.isInteger(pollMs) || pollMs < 10)
    throw new RangeError("Daemon pollMs must be at least 10ms");
  scheduler.start();
  try {
    while (!options.signal?.aborted) {
      try {
        await scheduler.tick();
        await queue.drain();
      } catch (error) {
        options.onError?.(error);
      }
      await new Promise<void>((resolve) => {
        const timer = setTimeout(resolve, pollMs);
        options.signal?.addEventListener(
          "abort",
          () => {
            clearTimeout(timer);
            resolve();
          },
          { once: true },
        );
      });
    }
  } finally {
    scheduler.stop();
    await queue.shutdown();
  }
}
