import type {
  FulfillmentDbSchema,
  FulfillmentNotification,
  FulfillmentPorts,
  FulfillmentSchema,
  StorageAdapter,
} from '@platform-modules/commerce-fulfillment';
import type { Querier, TransactionalDatabase } from '@platform-modules/db';
import { base64UrlDecodeBytes, base64UrlDecodeStr, bytesToHex } from '@/lib/encoding.js';
import type { DrizzleClient, DrizzleDb } from '@/server/db/client.js';
import { captureCaught } from '@/server/observability/capture.server';

export function toFulfillmentDb(db: DrizzleClient | DrizzleDb): Querier<FulfillmentSchema> {
  return db as unknown as TransactionalDatabase<FulfillmentSchema>;
}

export function makeFulfillmentPorts(
  db: DrizzleDb,
  _env: { QR_SECRET: string; R2: R2Bucket },
): FulfillmentPorts {
  const storage: StorageAdapter = {
    put: async () => {},
    signedUrl: async () => ({ url: '' }),
  };

  return {
    db: toFulfillmentDb(db) as unknown as TransactionalDatabase<FulfillmentDbSchema>,
    notify: async (_n: FulfillmentNotification) => {},
    storage,
    resolveBlobKey: (async () => null) as unknown as FulfillmentPorts['resolveBlobKey'],
    resolveShippingAddress: (async () =>
      null) as unknown as FulfillmentPorts['resolveShippingAddress'],
    carriers: undefined,
  };
}

interface VoucherQrPayload {
  voucherId: string;
  exp: number;
  nonce: string;
}

/** Pre-cutover payload shape (HMAC scheme unchanged; keyed by purchaseId, not voucherId). */
interface LegacyQrPayload {
  purchaseId: string;
  exp: number;
  nonce: string;
}

export async function generateVoucherQrToken(
  voucherId: string,
  qrSecret: string,
  ttlSeconds = 300,
): Promise<{ token: string; tokenHash: string }> {
  const nonce = crypto.randomUUID();
  const exp = Math.floor(Date.now() / 1000) + ttlSeconds;
  const payload = JSON.stringify({ voucherId, exp, nonce });

  const enc = new TextEncoder();
  const payloadBytes = enc.encode(payload);
  const payloadB64 = btoa(String.fromCharCode(...payloadBytes))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=/g, '');

  const key = await crypto.subtle.importKey(
    'raw',
    enc.encode(qrSecret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign'],
  );

  const signatureBuffer = await crypto.subtle.sign('HMAC', key, enc.encode(payloadB64));
  const signatureB64 = btoa(String.fromCharCode(...new Uint8Array(signatureBuffer)))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=/g, '');

  const token = `${payloadB64}.${signatureB64}`;
  const hashBuffer = await crypto.subtle.digest('SHA-256', enc.encode(token));
  const tokenHash = bytesToHex(hashBuffer);

  return { token, tokenHash };
}

export async function verifyVoucherQrToken(
  token: string,
  qrSecret: string,
): Promise<{ valid: true; voucherId: string } | { valid: false }> {
  try {
    const dotIdx = token.lastIndexOf('.');
    if (dotIdx === -1) return { valid: false };

    const payloadB64 = token.slice(0, dotIdx);
    const signatureB64 = token.slice(dotIdx + 1);

    const payloadJson = base64UrlDecodeStr(payloadB64);
    const payload = JSON.parse(payloadJson) as VoucherQrPayload;

    if (!payload.voucherId || !payload.exp || !payload.nonce) {
      return { valid: false };
    }

    const enc = new TextEncoder();
    const key = await crypto.subtle.importKey(
      'raw',
      enc.encode(qrSecret),
      { name: 'HMAC', hash: 'SHA-256' },
      false,
      ['verify'],
    );

    const sigBytes = base64UrlDecodeBytes(signatureB64);
    const valid = await crypto.subtle.verify(
      'HMAC',
      key,
      sigBytes as BufferSource,
      enc.encode(payloadB64),
    );
    if (!valid) return { valid: false };

    const nowSecs = Math.floor(Date.now() / 1000);
    if (payload.exp <= nowSecs) return { valid: false };

    return { valid: true, voucherId: payload.voucherId };
  } catch (err) {
    captureCaught(err, { scope: 'server.fulfillment.verifyVoucherQrToken', severity: 'warning' });
    return { valid: false };
  }
}

/**
 * Verify a pre-cutover (legacy) QR token. Same HMAC-SHA256 scheme as
 * {@link verifyVoucherQrToken}, but the payload is keyed by purchaseId
 * (order line id) instead of voucherId — there was one voucher per line
 * before the per-unit voucher table existed.
 */
export async function verifyLegacyQrToken(
  token: string,
  qrSecret: string,
): Promise<{ valid: true; purchaseId: string } | { valid: false }> {
  try {
    const dotIdx = token.lastIndexOf('.');
    if (dotIdx === -1) return { valid: false };

    const payloadB64 = token.slice(0, dotIdx);
    const signatureB64 = token.slice(dotIdx + 1);

    const payloadJson = base64UrlDecodeStr(payloadB64);
    const payload = JSON.parse(payloadJson) as LegacyQrPayload;

    if (!payload.purchaseId || !payload.exp || !payload.nonce) {
      return { valid: false };
    }

    const enc = new TextEncoder();
    const key = await crypto.subtle.importKey(
      'raw',
      enc.encode(qrSecret),
      { name: 'HMAC', hash: 'SHA-256' },
      false,
      ['verify'],
    );

    const sigBytes = base64UrlDecodeBytes(signatureB64);
    const valid = await crypto.subtle.verify(
      'HMAC',
      key,
      sigBytes as BufferSource,
      enc.encode(payloadB64),
    );
    if (!valid) return { valid: false };

    const nowSecs = Math.floor(Date.now() / 1000);
    if (payload.exp <= nowSecs) return { valid: false };

    return { valid: true, purchaseId: payload.purchaseId };
  } catch (err) {
    captureCaught(err, { scope: 'server.fulfillment.verifyLegacyQrToken', severity: 'warning' });
    return { valid: false };
  }
}
