import { dispatchCron } from '../../cron/index.js';
import { captureCaught } from '../../observability/capture.server.js';
import { BaseDO } from './_base.js';
import { getDb, warmDb } from '../lib/db.js';
import { dueKeys, nextTickMs } from '../lib/schedule.js';

export class SchedulerDO extends BaseDO {
  protected className = 'SchedulerDO';

  override async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url);

    if (request.method === 'POST' && url.pathname === '/ensure') {
      const nextAlarm = await this.ctx.storage.getAlarm();
      if (nextAlarm !== null) {
        return this.json({ ok: true, armed: false, nextAlarm });
      }

      const now = Date.now();
      const nextTick = nextTickMs(now);
      await this.ctx.storage.put('lastTick', now);
      await this.ctx.storage.setAlarm(nextTick);
      return this.json({ ok: true, armed: true, nextAlarm: nextTick });
    }

    if (request.method === 'GET' && url.pathname === '/status') {
      const [lastTick, nextAlarm, lastRun] = await Promise.all([
        this.ctx.storage.get<number>('lastTick'),
        this.ctx.storage.getAlarm(),
        this.ctx.storage.get<Record<string, number>>('lastRun'),
      ]);
      return this.json({
        lastTick: lastTick ?? null,
        nextAlarm,
        lastRun: lastRun ?? {},
      });
    }

    return super.fetch(request);
  }

  override async alarm(): Promise<void> {
    const startedAt = Date.now();
    let due: string[] = [];

    try {
      await this.ctx.storage.setAlarm(nextTickMs(startedAt));
      const lastTick = (await this.ctx.storage.get<number>('lastTick')) ?? startedAt;
      const db = getDb({ DATABASE_URL: this.env.DATABASE_URL });
      await warmDb(db);
      const cronEnv = { ...this.env, db } as unknown as Parameters<typeof dispatchCron>[1];
      due = dueKeys(lastTick, startedAt);

      for (const key of due) {
        try {
          await dispatchCron(key, cronEnv);
        } catch (err) {
          captureCaught(err, {
            scope: 'scheduler-do.alarm.dispatch',
            extra: { key },
          });
        }
      }

      try {
        const lastRun = (await this.ctx.storage.get<Record<string, number>>('lastRun')) ?? {};
        await this.ctx.storage.put('lastTick', startedAt);
        await this.ctx.storage.put('lastRun', {
          ...lastRun,
          ...Object.fromEntries(due.map((key) => [key, startedAt])),
        });
      } catch (err) {
        captureCaught(err, { scope: 'scheduler-do.alarm.persist' });
      }
    } catch (err) {
      captureCaught(err, { scope: 'scheduler-do.alarm.setup' });
    }

    console.warn(
      JSON.stringify({
        event: 'scheduler_tick',
        due,
        durationMs: Date.now() - startedAt,
      }),
    );
  }

  private json(body: unknown): Response {
    return new Response(JSON.stringify(body), {
      headers: { 'Content-Type': 'application/json' },
    });
  }
}
