/**
 * D1 auto-retry wrapper.
 *
 * Cloudflare D1 occasionally returns transient errors like:
 *   - "Network connection lost"
 *   - "503 Service Unavailable"
 *   - connection timeouts
 *
 * These are usually recoverable within 50-100ms. This Proxy wraps the D1 binding
 * and automatically retries such errors once with a small backoff.
 *
 * Non-transient errors (SQL syntax, constraint violations, etc.) are re-thrown
 * immediately to avoid masking real bugs.
 *
 * Usage in worker.ts:
 *   setDB(withD1Retry(c.env.DB.withSession('first-primary')));
 */

const TRANSIENT_ERROR_PATTERNS = [
  /network connection lost/i,
  /503/,
  /timeout/i,
  /ECONN/,
  /socket hang up/i,
  /internal error/i,
];

function isTransientError(err: any): boolean {
  const msg = String(err?.message || err || '');
  return TRANSIENT_ERROR_PATTERNS.some((p) => p.test(msg));
}

async function retryAsync<T>(fn: () => Promise<T>, maxRetries = 1): Promise<T> {
  let lastErr: any;
  for (let i = 0; i <= maxRetries; i++) {
    try {
      return await fn();
    } catch (err: any) {
      lastErr = err;
      if (!isTransientError(err)) throw err;
      if (i < maxRetries) {
        console.warn(`[D1 retry] transient error, retrying (${i + 1}/${maxRetries}):`, err?.message || err);
        // Small jittered backoff: 50-100ms on first retry
        const delay = 50 + Math.random() * 50;
        await new Promise((r) => setTimeout(r, delay));
      }
    }
  }
  throw lastErr;
}

function wrapStatement(stmt: any): any {
  return new Proxy(stmt, {
    get(target, prop, receiver) {
      if (prop === 'bind') {
        return (...args: any[]) => wrapStatement(target.bind(...args));
      }
      if (prop === 'all' || prop === 'run' || prop === 'first' || prop === 'raw') {
        return (...args: any[]) => retryAsync(() => (target as any)[prop](...args));
      }
      return Reflect.get(target, prop, receiver);
    },
  });
}

/** Wrap a D1Database binding so all queries auto-retry on transient errors. */
export function withD1Retry(db: any): any {
  return new Proxy(db, {
    get(target, prop, receiver) {
      if (prop === 'prepare') {
        return (sql: string) => wrapStatement(target.prepare(sql));
      }
      if (prop === 'batch') {
        return (stmts: any[]) => retryAsync(() => target.batch(stmts));
      }
      if (prop === 'exec') {
        return (sql: string) => retryAsync(() => target.exec(sql));
      }
      if (prop === 'withSession') {
        return (...args: any[]) => withD1Retry(target.withSession(...args));
      }
      return Reflect.get(target, prop, receiver);
    },
  });
}
