import { and, eq, sql } from 'drizzle-orm';
import * as dealQueries from '@/server/db/queries/deals.js';
import * as groupDealQueries from '@/server/db/queries/group-deals.js';
import * as groupReservationQueries from '@/server/db/queries/group-reservations.js';
import * as groupTierQueries from '@/server/db/queries/group-tiers.js';
import { getDefaultSku } from '@/server/domain/variants/read.js';
import { refreshDealCache } from '@/server/domain/variants/cache.js';
import { splitCommission } from '@/lib/money.js';
import { dealSkus, order, orderLine, orderLineVoucherExt } from '@/server/db/schema.js';
import { fulfillOrder, voucher } from '@platform-modules/commerce-fulfillment';
import { getOrderById, type OrdersSchema } from '@platform-modules/commerce-orders';
import type { Querier } from '@platform-modules/db';
import type { DrizzleDb } from '@/server/db/client.js';
import { makeFulfillmentPorts } from '@/server/fulfillment/fulfillment-platform.js';
import { generateQrToken } from '@/server/workflows/purchase.js';
import { captureCaught } from '@/server/observability/capture.server.js';
import { applyEffects } from './apply-effects.js';
import { transition } from './machine.js';
import {
  buildCtx,
  enqueueEvent,
  GroupDealError,
  makeApplyCtx,
  safeDisarmGroupDealAlarm,
  type GroupDealDeps,
  withoutAlarmEffects,
} from './runtime.js';
import {
  createGroupDealExecutionStateStore,
  type GroupDealExecutionProgress,
  type GroupDealExecutionProgressEntry,
  type GroupDealExecutionStateStore,
} from './execution-state.js';

interface ExecutionReservation {
  id: string;
  status: string;
  quantity: number;
  userId: string | null;
  guestEmail: string | null;
  providerAuthorizationId: string | null;
  providerTransactionId: string | null;
  orderLineId: string | null;
}

interface LoadedExecutionContext {
  groupDeal: NonNullable<Awaited<ReturnType<typeof groupDealQueries.findById>>>;
  deal: NonNullable<Awaited<ReturnType<typeof dealQueries.findById>>>;
  reservations: ExecutionReservation[];
  defaultSku: Awaited<ReturnType<typeof getDefaultSku>>;
  finalUnitPrice: string;
}

export interface ExecuteGroupDealResult {
  captureResults: { reservationId: string; success: boolean; error?: string }[];
  finalUnitPrice: string;
}

export interface ExecuteGroupDealHandlerDeps {
  executionState: GroupDealExecutionStateStore;
  loadExecutionContext(groupDealId: string): Promise<LoadedExecutionContext>;
  captureHold(args: {
    reservationId: string;
    providerHoldId: string;
    prePurchaseId: string;
    totalAgorot: number;
  }): Promise<{ ok: true; providerPaymentId: string } | { ok: false; code: string; message: string }>;
  completeCapturedReservation(args: {
    reservation: ExecutionReservation;
    entry: GroupDealExecutionProgressEntry;
    context: LoadedExecutionContext;
  }): Promise<{ qrPngUrl: string; amountPaid: string }>;
  releaseReservation(reservationId: string): Promise<void>;
  recordCaptureFailure(args: {
    reservation: ExecutionReservation;
    groupDealId: string;
    error: string;
  }): Promise<void>;
  finalizeExecution(args: {
    context: LoadedExecutionContext;
    captureResults: { reservationId: string; success: boolean; error?: string }[];
  }): Promise<void>;
}

function emptyProgress(): GroupDealExecutionProgress {
  return { version: 1, reservations: {} };
}

function normalizeProgress(progress: GroupDealExecutionProgress | null | undefined) {
  if (!progress || typeof progress !== 'object') return emptyProgress();
  if (!progress.reservations || typeof progress.reservations !== 'object') {
    return emptyProgress();
  }
  return progress;
}

export async function executeGroupDealHandler(
  deps: ExecuteGroupDealHandlerDeps,
  groupDealId: string,
): Promise<ExecuteGroupDealResult> {
  const state = await deps.executionState.begin(groupDealId);
  const progress = normalizeProgress(state.progress);

  if (state.status === 'COMPLETED' && state.result) {
    return state.result as unknown as ExecuteGroupDealResult;
  }

  const context = await deps.loadExecutionContext(groupDealId);
  const captureResults: { reservationId: string; success: boolean; error?: string }[] = [];

  for (const reservation of context.reservations) {
    const current = progress.reservations[reservation.id];
    if (current?.status === 'completed' || reservation.status === 'CAPTURED') {
      if (!progress.reservations[reservation.id]) {
        progress.reservations[reservation.id] = {
          reservationId: reservation.id,
          idempotencyKey: `group-purchase-${reservation.id}`,
          prePurchaseId: reservation.orderLineId ?? crypto.randomUUID(),
          providerTransactionId: reservation.providerTransactionId ?? '',
          captureAmount: (parseFloat(context.finalUnitPrice) * reservation.quantity).toFixed(2),
          totalAgorot: Math.round(parseFloat(context.finalUnitPrice) * reservation.quantity * 100),
          status: 'completed',
        };
        await deps.executionState.saveProgress(state.commandId, progress);
      }
      captureResults.push({ reservationId: reservation.id, success: true });
      continue;
    }

    let entry = current;
    if (!entry || entry.status === 'pending_capture') {
      const captureAmount = (parseFloat(context.finalUnitPrice) * reservation.quantity).toFixed(2);
      const totalAgorot = Math.round(parseFloat(captureAmount) * 100);
      const prePurchaseId = entry?.prePurchaseId ?? crypto.randomUUID();
      const idempotencyKey = `group-purchase-${reservation.id}`;

      if (!reservation.providerAuthorizationId) {
        const failedEntry: GroupDealExecutionProgressEntry = {
          reservationId: reservation.id,
          idempotencyKey,
          prePurchaseId,
          providerTransactionId: '',
          captureAmount,
          totalAgorot,
          status: 'failed',
          error: 'No providerAuthorizationId on reservation — capture skipped',
        };
        progress.reservations[reservation.id] = failedEntry;
        await deps.executionState.saveProgress(state.commandId, progress);
        await deps.releaseReservation(reservation.id);
        const error = failedEntry.error ?? 'No providerAuthorizationId on reservation — capture skipped';
        await deps.recordCaptureFailure({
          reservation,
          groupDealId,
          error,
        });
        captureResults.push({
          reservationId: reservation.id,
          success: false,
          error,
        });
        continue;
      }

      if (!entry) {
        entry = {
          reservationId: reservation.id,
          idempotencyKey,
          prePurchaseId,
          providerTransactionId: '',
          captureAmount,
          totalAgorot,
          status: 'pending_capture',
        };
        progress.reservations[reservation.id] = entry;
        await deps.executionState.saveProgress(state.commandId, progress);
      }

      const captureResult = await deps.captureHold({
        reservationId: reservation.id,
        providerHoldId: reservation.providerAuthorizationId,
        prePurchaseId,
        totalAgorot,
      });

      if (!captureResult.ok) {
        const error = `Capture failed: ${captureResult.code} — ${captureResult.message}`;
        const failedEntry: GroupDealExecutionProgressEntry = {
          reservationId: reservation.id,
          idempotencyKey,
          prePurchaseId,
          providerTransactionId: '',
          captureAmount,
          totalAgorot,
          status: 'failed',
          error,
        };
        progress.reservations[reservation.id] = failedEntry;
        await deps.executionState.saveProgress(state.commandId, progress);
        await deps.releaseReservation(reservation.id);
        await deps.recordCaptureFailure({ reservation, groupDealId, error });
        captureResults.push({ reservationId: reservation.id, success: false, error });
        continue;
      }

      entry = {
        reservationId: reservation.id,
        idempotencyKey,
        prePurchaseId,
        providerTransactionId: captureResult.providerPaymentId,
        captureAmount,
        totalAgorot,
        status: 'captured',
      };
      progress.reservations[reservation.id] = entry;
      await deps.executionState.saveProgress(state.commandId, progress);
    }

    await deps.completeCapturedReservation({ reservation, entry, context });
    progress.reservations[reservation.id] = { ...entry, status: 'completed' };
    await deps.executionState.saveProgress(state.commandId, progress);
    captureResults.push({ reservationId: reservation.id, success: true });
  }

  await deps.finalizeExecution({ context, captureResults });
  const result = {
    captureResults,
    finalUnitPrice: context.finalUnitPrice,
  } satisfies ExecuteGroupDealResult;
  await deps.executionState.complete(state.commandId, result as unknown as Record<string, unknown>);
  return result;
}

async function loadExecutionContextFromWorkflow(
  deps: GroupDealDeps,
  groupDealId: string,
): Promise<LoadedExecutionContext> {
  const groupDeal = await groupDealQueries.findById(deps.db, groupDealId);
  if (!groupDeal) throw new GroupDealError('GROUP_DEAL_NOT_FOUND', 'Group deal not found');

  const validation = transition(buildCtx(groupDeal, null), {
    kind: 'window_expired_above_threshold',
    at: new Date(),
  });
  if (!validation.ok) {
    throw new GroupDealError('INVALID_STATE', `Cannot execute group deal in state ${groupDeal.groupState}`);
  }

  const deal = await dealQueries.findById(deps.db, groupDeal.dealId);
  if (!deal) throw new GroupDealError('DEAL_NOT_FOUND', 'Parent deal not found');

  const reservations = (await groupReservationQueries.findByGroupDealId(deps.db, groupDealId)) as ExecutionReservation[];
  if (reservations.length === 0) {
    throw new GroupDealError('NO_HELD_RESERVATIONS', 'No held reservations to execute');
  }

  const defaultSku = await getDefaultSku(deps.db, deal.id);
  let finalUnitPrice = defaultSku.discountedPrice;
  if (groupDeal.tieredPricingEnabled) {
    const applicableTier = await groupTierQueries.getApplicableTier(
      deps.db,
      groupDealId,
      groupDeal.currentReservationCount,
    );
    if (applicableTier) finalUnitPrice = applicableTier.pricePerUnit;
  }

  return { groupDeal, deal, reservations, defaultSku, finalUnitPrice };
}

async function completeCapturedReservationFromWorkflow(
  deps: GroupDealDeps,
  context: LoadedExecutionContext,
  reservation: ExecutionReservation,
  entry: GroupDealExecutionProgressEntry,
) {
  const { deal, defaultSku, finalUnitPrice } = context;
  splitCommission(entry.captureAmount, deal.commissionRate);

  const expiresAt = deal.windowEnd ?? new Date(Date.now() + 180 * 24 * 60 * 60 * 1000);
  const [orderRow] = await deps.db
    .insert(order)
    .values({
      id: crypto.randomUUID(),
      buyerUserId: reservation.userId ?? null,
      status: 'paid',
      chargeRef: entry.providerTransactionId,
      currency: 'ILS',
      priceMode: 'inclusive',
      subtotal: BigInt(entry.totalAgorot),
      tax: BigInt(0),
      discount: BigInt(0),
      total: BigInt(entry.totalAgorot),
      idempotencyKey: entry.idempotencyKey,
      requestHash: entry.idempotencyKey,
    })
    .onConflictDoNothing()
    .returning();

  const existingOrder =
    orderRow ??
    (
      await deps.db
        .select({ id: order.id })
        .from(order)
        .where(eq(order.idempotencyKey, entry.idempotencyKey))
        .limit(1)
    )[0];
  if (!existingOrder) {
    throw new GroupDealError('DB_ERROR', `Failed to create order row for ${reservation.id}`);
  }

  await deps.db
    .insert(orderLine)
    .values({
      id: entry.prePurchaseId,
      orderId: existingOrder.id,
      variantId: defaultSku.id,
      kind: 'voucher',
      qty: reservation.quantity,
      unitPrice: BigInt(Math.round(parseFloat(finalUnitPrice) * 100)),
      lineTotal: BigInt(entry.totalAgorot),
      vendorId: deal.vendorId,
    })
    .onConflictDoNothing();

  const fulfillmentPorts = makeFulfillmentPorts(deps.db as unknown as DrizzleDb, {
    QR_SECRET: deps.qrSecret,
    R2: deps.storage.bucket,
  });
  const platformOrder = await getOrderById(
    deps.db as unknown as Querier<OrdersSchema>,
    existingOrder.id,
    { userId: reservation.userId ?? undefined },
  );
  if (!platformOrder) {
    throw new GroupDealError('DB_ERROR', `Failed to load order for ${reservation.id}`);
  }

  const fulfillResult = await fulfillOrder(fulfillmentPorts, platformOrder);
  if (fulfillResult.overall !== 'fulfilled') {
    throw new GroupDealError('FULFILLMENT_FAILED', `Fulfillment failed: ${fulfillResult.overall}`);
  }

  const { token, tokenHash } = await generateQrToken(entry.prePurchaseId, deps.qrSecret);
  const qrPngUrl = await deps.storage.uploadQr(entry.prePurchaseId, token);

  for (const line of fulfillResult.lines) {
    if (line.kind !== 'voucher') continue;
    for (const voucherId of line.voucherIds) {
      await deps.db
        .insert(orderLineVoucherExt)
        .values({
          voucherId,
          lineId: entry.prePurchaseId,
          qrTokenHash: tokenHash,
          qrPngUrl,
        })
        .onConflictDoUpdate({
          target: orderLineVoucherExt.voucherId,
          set: { qrTokenHash: tokenHash, qrPngUrl },
        });
    }
  }

  await deps.db.update(voucher).set({ expiresAt }).where(eq(voucher.lineId, entry.prePurchaseId));
  await groupReservationQueries.setCaptured(deps.db, reservation.id, {
    providerTransactionId: entry.providerTransactionId,
    orderLineId: entry.prePurchaseId,
  });

  try {
    const updated = await deps.db
      .update(dealSkus)
      .set({
        quantitySold: sql`${dealSkus.quantitySold} + ${reservation.quantity}`,
      })
      .where(
        and(
          eq(dealSkus.id, defaultSku.id),
          sql`${dealSkus.quantitySold} + ${reservation.quantity} <= ${dealSkus.quantityTotal}`,
        ),
      )
      .returning({ id: dealSkus.id });
    if (updated.length === 0) {
      throw new GroupDealError(
        'SKU_QUANTITY_SOLD_CAS_REJECTED',
        `Failed to increment quantity_sold for sku ${defaultSku.id}`,
      );
    }
  } catch (err) {
    captureCaught(err, {
      scope: 'server.workflows.group-deal.quantity-sold-cas',
      severity: 'error',
      extra: {
        groupDealId: context.groupDeal.id,
        reservationId: reservation.id,
        skuId: defaultSku.id,
        quantity: reservation.quantity,
      },
    });
  }

  await refreshDealCache(deps.db, deal.id);
  await enqueueEvent(deps.db, context.groupDeal.id, 'group_deal.purchase_created', {
    purchaseId: entry.prePurchaseId,
    userId: reservation.userId,
    guestEmail: reservation.guestEmail,
    dealTitle: deal.title,
    amountPaid: entry.captureAmount,
    qrPngUrl,
  });

  return { qrPngUrl, amountPaid: entry.captureAmount };
}

async function finalizeExecutionFromWorkflow(
  deps: GroupDealDeps,
  context: LoadedExecutionContext,
  captureResults: { reservationId: string; success: boolean; error?: string }[],
) {
  const executeResult = transition(buildCtx(context.groupDeal, context.deal), {
    kind: 'window_expired_above_threshold',
    at: new Date(),
  });
  if (!executeResult.ok) {
    throw new GroupDealError('INVALID_STATE', `Cannot transition to SUCCEEDED from ${context.groupDeal.groupState}`);
  }

  await groupDealQueries.updateGroupState(deps.db, context.groupDeal.id, executeResult.nextState, {
    executedAt: new Date(),
  });
  await dealQueries.markSoldOut(deps.db, { doClient: deps.doClient }, context.deal.id);

  const executeEffects = executeResult.effects.map((effect) =>
    effect.kind === 'enqueue-outbox'
      ? {
          ...effect,
          payload: {
            ...effect.payload,
            captureResults,
            finalUnitPrice: context.finalUnitPrice,
          },
        }
      : effect,
  );

  await safeDisarmGroupDealAlarm(deps.doClient, context.groupDeal.id);
  await applyEffects(
    makeApplyCtx(deps, (groupDealId) => executeGroupDeal(deps, groupDealId)),
    withoutAlarmEffects(executeEffects.filter((effect) => effect.kind !== 'execute-deal')),
  );

  try {
    await deps.push.sendToVendor(context.deal.vendorId, {
      title: 'עסקת קבוצה הצליחה!',
      body: `${context.deal.title} - ${captureResults.filter((row) => row.success).length} משתתפים`,
      url: `/vendor/deals/${context.deal.id}`,
      tag: 'group_deal_succeeded',
      data: { groupDealId: context.groupDeal.id, dealTitle: context.deal.title },
    });
  } catch (err) {
    captureCaught(err, { scope: 'server.workflows.group-deal', severity: 'warning' });
  }
}

export async function executeGroupDeal(deps: GroupDealDeps, groupDealId: string) {
  return executeGroupDealHandler(
    {
      executionState: createGroupDealExecutionStateStore(deps.db),
      loadExecutionContext: (id) => loadExecutionContextFromWorkflow(deps, id),
      captureHold: async (args) =>
        (await deps.payments()).captureHold({
          reservationId: args.reservationId,
          purchaseId: args.prePurchaseId,
          providerHoldId: args.providerHoldId,
          totalAgorot: args.totalAgorot,
        }),
      completeCapturedReservation: ({ reservation, entry, context }) =>
        completeCapturedReservationFromWorkflow(deps, context, reservation, entry),
      releaseReservation: (reservationId) =>
        groupReservationQueries.updateStatus(deps.db, reservationId, 'RELEASED').then(() => undefined),
      recordCaptureFailure: async ({ reservation, groupDealId: aggregateId, error }) => {
        await enqueueEvent(deps.db, reservation.id, 'admin_alert.hold_capture_failed', {
          reservationId: reservation.id,
          groupDealId: aggregateId,
          error,
        });
      },
      finalizeExecution: ({ context, captureResults }) =>
        finalizeExecutionFromWorkflow(deps, context, captureResults),
    },
    groupDealId,
  );
}

export type { GroupDealExecutionStateStore } from './execution-state.js';
