// CF Workers lock shim — replaces libs/lock.ts
//
// In Blocklet Server (single-process Node.js), the original in-memory lock works fine.
// In CF Workers, each HTTP request runs in a separate isolate, so in-memory locks
// are useless for cross-request mutual exclusion.
//
// This shim uses D1 (SQLite) as the coordination layer:
// - INSERT OR IGNORE with PRIMARY KEY constraint provides atomic lock acquisition
// - owner token prevents accidental release by other isolates
// - TTL-based expiry prevents deadlocks from crashed isolates
// - Exponential backoff with jitter avoids D1 write hotspots

import { getDB } from './sequelize-d1/model';

let _nanoidCounter = 0;
function simpleId(): string {
  // Lightweight unique ID — no need for crypto-strength randomness for lock owners
  return `${Date.now().toString(36)}-${(++_nanoidCounter).toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
}

export class Lock {
  name: string;
  owner: string;
  locked: boolean;
  ttl: number;

  // EventEmitter stub for API compatibility — original Lock extends EventEmitter
  events: { on: () => void; removeListener: () => void; emit: () => void };

  constructor(name: string, options?: { ttl?: number }) {
    this.name = name;
    this.owner = simpleId();
    this.locked = false;
    this.ttl = options?.ttl || 5000; // default 5s TTL for short critical sections
    this.events = { on: () => {}, removeListener: () => {}, emit: () => {} };
  }

  async acquire(maxWaitMs = 10000): Promise<true> {
    const db = getDB();
    const deadline = Date.now() + maxWaitMs;
    let delay = 30; // start at 30ms

    while (Date.now() < deadline) {
      const now = Date.now();

      try {
        // Atomically: clean expired locks + try to insert ours + verify ownership
        // All 3 statements in a single D1 batch (1 round-trip instead of 2)
        const batchResult = await db.batch([
          db.prepare('DELETE FROM _locks WHERE name = ? AND expires_at < ?').bind(this.name, now),
          db.prepare('INSERT OR IGNORE INTO _locks (name, owner, expires_at) VALUES (?, ?, ?)')
            .bind(this.name, this.owner, now + this.ttl),
          db.prepare('SELECT owner FROM _locks WHERE name = ?').bind(this.name),
        ]);

        const row = batchResult[2]?.results?.[0] as { owner: string } | undefined;
        if (row?.owner === this.owner) {
          this.locked = true;
          return true;
        }
      } catch (err: any) {
        // D1 errors (e.g. table not found on first deploy) — log and retry
        console.error(`[D1Lock] acquire error for "${this.name}":`, err?.message || err);
      }

      // Exponential backoff with jitter to avoid D1 write hotspots
      const jitter = Math.random() * delay * 0.3;
      await new Promise(r => setTimeout(r, delay + jitter));
      delay = Math.min(delay * 2, 500); // cap at 500ms
    }

    // Timeout — throw to match original behavior where acquire() always resolves
    // All callers use try/catch or don't check the return value
    console.warn(`[D1Lock] acquire timeout: "${this.name}" after ${maxWaitMs}ms`);
    throw new Error(`[D1Lock] Failed to acquire lock "${this.name}" within ${maxWaitMs}ms`);
  }

  release(): void {
    if (!this.locked) return;
    this.locked = false;

    // Fire-and-forget: 8 out of 10 call sites do NOT await release().
    // We register the promise with __cfPendingJobs__ so flushPendingJobs()
    // in worker.ts ensures it completes before the response is sent.
    try {
      const db = getDB();
      const promise = db.prepare('DELETE FROM _locks WHERE name = ? AND owner = ?')
        .bind(this.name, this.owner)
        .run()
        .catch((err: any) => console.error(`[D1Lock] release error for "${this.name}":`, err?.message || err));

      // For HTTP context, use waitUntil (non-blocking) instead of pendingJobs (blocking)
      const isHttp = (globalThis as any).__cfHttpContext__;
      if (isHttp) {
        const waitUntil = (globalThis as any).__cfWaitUntil__;
        if (typeof waitUntil === 'function') {
          waitUntil(promise);
        }
      } else {
        const pending = (globalThis as any).__cfPendingJobs__;
        if (Array.isArray(pending)) {
          pending.push(promise);
        }
      }
    } catch (err: any) {
      console.error(`[D1Lock] release setup error for "${this.name}":`, err?.message || err);
    }
  }
}

// In CF Workers, don't cache Lock instances across requests — each request is isolated.
// The original code caches by name, but that's only useful within a single process.
export function getLock(name: string, options?: { ttl?: number }): Lock {
  return new Lock(name, options);
}
