/**
 * bundle.ts — per-request service bundle factory.
 *
 * Spec 03 — request-scoped service bundle.
 *
 * Constructs the Services bundle from the Cloudflare Workers env.
 * Capability flags are computed eagerly (cheap secret presence check).
 * Service clients are constructed lazily (zero cost for unused services).
 *
 * No module-level state — every call returns a new independent bundle.
 */

import { lazy } from './lazy.js';
import type {
  Services,
  EmailClient,
  StorageClient,
  DoClient,
  PaymentService,
  AnalyticsService,
  CookiesService,
  CryptoService,
  CarriersService,
} from './types.js';
import type { PushClient } from '@/server/push/types.js';
import type { SendEmailOptions } from '@/server/email/resend.js';
import type { MultidealEnv } from '@/server/env.js';
import type { CronEnv } from '@/server/cron/deal-expiry.js';
import type { MockScenario } from '@/server/payments/mock/scenarios.js';
// createDbService and getPaymentProvider are dynamically imported inside buildServices() below.
// This defers drizzle-orm + @neondatabase/serverless + full schema evaluation
// from module cold-start to first actual request.
import * as doClient from '@/server/do-client.js';
import { blindIndex, encrypt, decryptExpr, safeDecryptExpr } from '@/server/db/crypto.js';
import { setUserPhone, setUserEmail } from '@/server/db/pii-write.js';
import {
  buildCsrfCookieHeader,
  signVendorRegCookie,
  verifyVendorRegCookie,
  buildMh,
  parseMh,
  extractPhHint,
  setMhCookie,
  clearMhCookie,
  isHttpsRequest,
} from '@/server/auth/cookies.js';
import { readMockScenario } from '@/server/payments/mock/read-scenario.js';
import { captureCaught } from '@/server/observability/capture.server';
import { getCarrier } from '@/server/carriers/registry.js';

// ─── No-op stubs ─────────────────────────────────────────────────────────────

/** No-op PushClient stub — used when VAPID keys are absent. */
function buildNoPushClient(): PushClient {
  return {
    sendToUser: async () => {
      captureCaught(new Error('push.sendToUser called but push is disabled (no VAPID keys)'), {
        scope: 'services.push',
        severity: 'info',
      });
    },
    sendToVendor: async () => {
      captureCaught(new Error('push.sendToVendor called but push is disabled (no VAPID keys)'), {
        scope: 'services.push',
        severity: 'info',
      });
    },
  };
}

/** No-op EmailClient stub — used when RESEND_API_KEY is absent. */
function buildNoEmailClient(): EmailClient {
  return {
    sendEmail: async () => {
      captureCaught(new Error('email.sendEmail called but email is disabled (no RESEND_API_KEY)'), {
        scope: 'services.email',
        severity: 'info',
      });
      return { success: false, error: 'Email service not configured' };
    },
  };
}

// ─── Real client builders ────────────────────────────────────────────────────

async function buildPushClient(env: MultidealEnv): Promise<PushClient> {
  const { getPushClient } = await import('@/server/push/client.js');
  return getPushClient({
    DATABASE_URL: env.DATABASE_URL,
    VAPID_PUBLIC_KEY: env.VAPID_PUBLIC_KEY,
    VAPID_PRIVATE_KEY: env.VAPID_PRIVATE_KEY,
    VAPID_SUBJECT: env.VAPID_SUBJECT,
  });
}

function buildEmailClient(env: MultidealEnv): EmailClient {
  return {
    sendEmail: async (opts: SendEmailOptions) => {
      const { sendEmail } = await import('@/server/email/resend.js');
      return sendEmail(
        { RESEND_API_KEY: env.RESEND_API_KEY, RESEND_FROM_EMAIL: env.RESEND_FROM_EMAIL ?? '' },
        opts,
      );
    },
  };
}

function buildStorageClient(env: MultidealEnv): StorageClient {
  return {
    bucket: env.R2_BUCKET,
    publicBase: env.PUBLIC_SITE_URL,
    qrSecret: env.QR_SECRET,
    async uploadQr(purchaseId: string, token: string): Promise<string> {
      const { uploadPurchaseQr } = await import('@/server/storage/qr_png.js');
      return uploadPurchaseQr(env.R2_BUCKET, purchaseId, token, env.PUBLIC_SITE_URL);
    },
  };
}

function buildDoClient(env: MultidealEnv): DoClient {
  return {
    armDealAlarm: (dealId, at) => doClient.armDealAlarm(env, dealId, at),
    disarmDealAlarm: (dealId) => doClient.disarmDealAlarm(env, dealId),
    armGoldWindowAlarm: (dealId, at) => doClient.armGoldWindowAlarm(env, dealId, at),
    disarmGoldWindowAlarm: (dealId) => doClient.disarmGoldWindowAlarm(env, dealId),
    armGroupDealAlarm: (groupDealId, at, kind) =>
      doClient.armGroupDealAlarm(env, groupDealId, at, kind),
    disarmGroupDealAlarm: (groupDealId) => doClient.disarmGroupDealAlarm(env, groupDealId),
    armPersonalOfferAlarm: (requestId, at) => doClient.armPersonalOfferAlarm(env, requestId, at),
    disarmPersonalOfferAlarm: (requestId) => doClient.disarmPersonalOfferAlarm(env, requestId),
    armScheduledPublishAlarm: (page, at) => doClient.armScheduledPublishAlarm(env, page, at),
    disarmScheduledPublishAlarm: (page) => doClient.disarmScheduledPublishAlarm(env, page),
    armSupportTicketAlarm: (ticketId, at) => doClient.armSupportTicketAlarm(env, ticketId, at),
    disarmSupportTicketAlarm: (ticketId) => doClient.disarmSupportTicketAlarm(env, ticketId),
    armSupportCaseAlarm: (caseId, kind, at) => doClient.armSupportCaseAlarm(env, caseId, kind, at),
    disarmSupportCaseAlarms: (caseId) => doClient.disarmSupportCaseAlarms(env, caseId),
  };
}

function cookiesFromRequest(request: Request): {
  get(name: string): { value: string } | undefined;
} {
  const jar = new Map<string, string>();
  const header = request.headers.get('cookie');
  if (!header) {
    return { get: () => undefined };
  }

  for (const part of header.split(';')) {
    const trimmed = part.trim();
    if (!trimmed) continue;
    const index = trimmed.indexOf('=');
    if (index <= 0) continue;
    const name = trimmed.slice(0, index).trim();
    const value = trimmed.slice(index + 1).trim();
    try {
      jar.set(name, decodeURIComponent(value));
    } catch (err) {
      captureCaught(err, { scope: 'services.bundle.cookiesFromRequest' });
      jar.set(name, value);
    }
  }

  return {
    get(name: string) {
      const value = jar.get(name);
      return value === undefined ? undefined : { value };
    },
  };
}

// ─── Bundle factory ───────────────────────────────────────────────────────────

/**
 * Build a per-request Services bundle from the Cloudflare Workers env.
 *
 * Async to allow dynamic imports of heavy modules (drizzle+neon+schema,
 * payment provider) at first request rather than at module cold-start.
 * Dynamic imports are cached by the JS module system — subsequent calls
 * return the cached module synchronously.
 *
 * Capability flags are set eagerly. Client construction is deferred via lazy()
 * so cold-start cost is the bundle struct allocation only.
 */
export async function buildServices(env: MultidealEnv, request?: Request): Promise<Services> {
  // ── Defer heavy module evaluation from cold-start to first request ────────
  // These dynamic imports are cached per-isolate by the runtime module system.
  const [{ createDbService }, { getPaymentProvider }] = await Promise.all([
    import('@/server/services/db.js'),
    import('@/server/payments/get-provider.js'),
  ]);

  // ── Capability flags (eager) ──────────────────────────────────────────────
  const pushEnabled = Boolean(env.VAPID_PUBLIC_KEY && env.VAPID_PRIVATE_KEY && env.VAPID_SUBJECT);
  const emailEnabled = Boolean(env.RESEND_API_KEY);

  // ── Lazy client factories ─────────────────────────────────────────────────
  const getDbClient = lazy(() => createDbService({ DATABASE_URL: env.DATABASE_URL }));
  const getPushClientAsync = lazy(() =>
    pushEnabled ? buildPushClient(env) : Promise.resolve(buildNoPushClient()),
  );
  const getEmailClient = lazy(() => (emailEnabled ? buildEmailClient(env) : buildNoEmailClient()));
  const getStorage = lazy(() => buildStorageClient(env));
  const getDoClient = lazy(() => buildDoClient(env));

  const getPaymentsProvider = lazy(() => Promise.resolve(getPaymentProvider(env)));
  const getStripeClient = lazy(async () => {
    const { getStripe } = await import('@/server/payments/stripe/client.js');
    return getStripe(env);
  });

  const payments = Object.assign(() => getPaymentsProvider(), {
    provider: () => getPaymentsProvider(),
    stripe: () => getStripeClient(),
    readMockScenario: (): MockScenario | null =>
      request ? readMockScenario(cookiesFromRequest(request), env) : null,
  }) as PaymentService;

  const crypto: CryptoService = {
    key: env.PII_KEY,
    blindIndex: (value) => blindIndex(value, env.PII_KEY),
    encrypt: (value) => encrypt(value, env.PII_KEY),
    decryptExpr: (colExpr) => decryptExpr(colExpr, env.PII_KEY),
    safeDecryptExpr: (colExpr) => safeDecryptExpr(colExpr, env.PII_KEY),
    setUserPhone: (value) => setUserPhone(value, env.PII_KEY),
    setUserEmail: (value) => setUserEmail(value, env.PII_KEY),
  };

  const secure = request ? isHttpsRequest(request) : false;
  const cookies: CookiesService = {
    isHttps: secure,
    buildCsrfCookieHeader: (token) => buildCsrfCookieHeader(token, secure),
    signVendorRegCookie: (payload) => signVendorRegCookie(payload, env.SESSION_SECRET),
    verifyVendorRegCookie: (raw) => verifyVendorRegCookie(raw, env.SESSION_SECRET),
    buildMh,
    parseMh,
    extractPhHint,
    setMhCookie,
    clearMhCookie,
  };

  const analytics: AnalyticsService = {
    vendor: {
      getFunnel: async (vendorId) => {
        const { getVendorFunnel } = await import('@/server/db/queries/vendor-analytics.js');
        return getVendorFunnel(getDbClient(), vendorId);
      },
    },
    admin: {
      getOverview: async (opts) => {
        const { getAnalytics } = await import('@/server/admin/resources/analytics/queries.js');
        return getAnalytics(getDbClient(), opts);
      },
    },
  };

  const carriers: CarriersService = {
    get: (kind) =>
      kind === 'wolt_drive'
        ? getCarrier('wolt_drive', {
            WOLT_DRIVE_MERCHANT_KEY: env.WOLT_DRIVE_MERCHANT_KEY,
            WOLT_DRIVE_MERCHANT_ID: env.WOLT_DRIVE_MERCHANT_ID,
            WOLT_DRIVE_MOCK_URL: env.WOLT_DRIVE_MOCK_URL,
            WOLT_DRIVE_ENV: env.WOLT_DRIVE_ENV,
            WOLT_DRIVE_WEBHOOK_SECRET: env.WOLT_DRIVE_WEBHOOK_SECRET,
            SUPPORT_EMAIL: env.SUPPORT_EMAIL,
            SUPPORT_PHONE: env.SUPPORT_PHONE,
            E2E_SECRET: env.E2E_SECRET,
          })
        : getCarrier(kind),
  };

  return {
    get db() {
      return getDbClient();
    },
    crypto,
    payments,
    analytics,
    carriers,
    returns: {
      buildReturnsDeps: (db, payments) => ({ db, env, payments }),
    },
    supportCases: {
      buildWorkflowDeps: (db) => ({ db, env, messageEncryptionKey: env.PII_KEY }),
    },
    sharing: {
      shareBaseUrl: env.SHARE_BASE_URL ?? 'https://multi.deal',
    },
    dev: {
      emailMockDb: env.EMAIL_MOCK_DB,
    },
    referrals: {
      referralTestBypassSecret: env.REFERRAL_TEST_BYPASS_SECRET,
      referralClicksDataset: env.MULTIDEAL_DEV,
      cronSecret: env.CRON_SECRET,
      analyticsEnv: {
        CF_ACCOUNT_ID: env.CF_ACCOUNT_ID,
        CF_AE_API_TOKEN: env.CF_AE_API_TOKEN,
      },
    },
    vendorDeals: {
      buildWorkflowEnv: () => ({ DATABASE_URL: env.DATABASE_URL }),
      enqueueOutbox: async (outboxId) => {
        await env.OUTBOX_QUEUE?.send({ outboxId });
      },
    },
    invoicing: { credentialsKey: env.INVOICE_KEK },
    queues: {
      cronSecret: env.CRON_SECRET,
      runLlmJobs: async () => {
        const { handleLlmJobsBatch } =
          await import('@/server/platform/queues/llm-jobs-consumer.js');
        await handleLlmJobsBatch(
          {
            messages: [],
            queue: 'multideal-llm-jobs-preview',
            ackAll() {},
            retryAll() {},
          } as never,
          env,
        );
      },
      runOutbox: async () => {
        // The queue consumer only processes delivered messages; draining on
        // demand must scan pending outbox rows like the cron backstop does.
        const { runProcessOutbox } = await import('@/server/cron/process-outbox.js');
        await runProcessOutbox(env as unknown as CronEnv);
      },
    },
    cookies,
    get alarms() {
      return getDoClient();
    },
    push: {
      enabled: pushEnabled,
      get client() {
        return getPushClientAsync();
      },
    },
    email: {
      enabled: emailEnabled,
      get client() {
        return getEmailClient();
      },
    },
    get storage() {
      return getStorage();
    },
    /** @deprecated Use `doClient`; removed in W6. */
    get do() {
      return getDoClient();
    },
    get doClient() {
      return getDoClient();
    },
  };
}
