import { and, eq, inArray, isNull, lt, or, sql } from 'drizzle-orm';
import type { DrizzleClient, TxDrizzleClient } from '@/server/db/client.js';
import { cartCheckoutCaptures, cartCheckoutCompensations, outbox } from '@/server/db/schema.js';
import { settleCartCheckoutCompensatedOrder } from './fail-order.js';
import { getDb } from '@/server/db/client.js';
import { getPaymentProvider } from '@/server/payments/get-provider.js';
import type { DispatchEnv } from './outbox/types.js';
import { enqueueOutbox } from '@/server/queues/outbox-producer.js';
import { releasePromoReservationsForCheckout } from '@/server/promo/service.js';

export class CartCheckoutCompensationRetryableError extends Error {
  readonly retryable = true;
}

export async function enqueueCheckoutCleanup(
  tx: TxDrizzleClient,
  aggregateId: string,
  checkoutKey: string,
): Promise<void> {
  await tx
    .insert(outbox)
    .values({
      aggregateType: 'cart_checkout_compensation_cleanup',
      aggregateId,
      eventType: 'cart.checkout.compensation.cleanup',
      dedupeKey: `cart-compensation:cleanup:${aggregateId}`,
      payload: { checkoutKey },
    })
    .onConflictDoNothing({ target: outbox.dedupeKey });
}

async function enqueueOutboxIds(ids: string[]): Promise<void> {
  for (const id of ids) await enqueueOutbox(id);
}

export async function persistCartCaptureIntent(
  db: DrizzleClient,
  input: {
    checkoutKey: string;
    orderId: string;
    orderLineId: string;
    providerHoldId: string;
    amountAgorot: number;
    checkoutOrderIds: string[];
  },
): Promise<void> {
  await db
    .insert(cartCheckoutCaptures)
    .values({
      ...input,
      captureIdempotencyKey: `cap:hold:${input.providerHoldId}`,
    })
    .onConflictDoNothing({
      target: [cartCheckoutCaptures.checkoutKey, cartCheckoutCaptures.orderId],
    });
}

export async function recordCartCapture(
  db: DrizzleClient,
  input: { checkoutKey: string; orderId: string; providerPaymentId: string },
): Promise<void> {
  await db
    .update(cartCheckoutCaptures)
    .set({ status: 'captured', providerPaymentId: input.providerPaymentId })
    .where(
      and(
        eq(cartCheckoutCaptures.checkoutKey, input.checkoutKey),
        eq(cartCheckoutCaptures.orderId, input.orderId),
      ),
    );
}

export async function createCartFulfillmentWork(
  db: TxDrizzleClient,
  input: { checkoutKey: string; orderIds: string[] },
): Promise<string[]> {
  const aggregateId = input.orderIds[0];
  if (!aggregateId) throw new Error(`checkout ${input.checkoutKey} has no orders`);
  const eventIds = await db.transaction(async (tx) => {
    const captures = await tx
      .select({
        orderId: cartCheckoutCaptures.orderId,
        providerPaymentId: cartCheckoutCaptures.providerPaymentId,
        status: cartCheckoutCaptures.status,
      })
      .from(cartCheckoutCaptures)
      .where(eq(cartCheckoutCaptures.checkoutKey, input.checkoutKey));
    if (
      captures.length !== input.orderIds.length ||
      captures.some((capture) => capture.status !== 'captured' || !capture.providerPaymentId)
    ) {
      throw new Error(`checkout ${input.checkoutKey} has incomplete capture state`);
    }
    await tx
      .insert(outbox)
      .values({
        aggregateType: 'cart_checkout_fulfillment',
        aggregateId,
        eventType: 'cart.checkout.fulfillment',
        dedupeKey: `cart-fulfillment:${input.checkoutKey}`,
        payload: { checkoutKey: input.checkoutKey },
      })
      .onConflictDoNothing({ target: outbox.dedupeKey });
    return tx
      .select({ id: outbox.id })
      .from(outbox)
      .where(
        and(
          eq(outbox.aggregateType, 'cart_checkout_fulfillment'),
          eq(outbox.aggregateId, aggregateId),
          eq(outbox.eventType, 'cart.checkout.fulfillment'),
          isNull(outbox.processedAt),
          isNull(outbox.deadAt),
        ),
      );
  });
  return eventIds.map((event) => event.id);
}

export async function processCartCheckoutFulfillment(
  ctx: DispatchEnv,
  checkoutKey: string,
): Promise<void> {
  const db = getDb({ DATABASE_URL: ctx.DATABASE_URL });
  const captures = await db
    .select({
      providerPaymentId: cartCheckoutCaptures.providerPaymentId,
      status: cartCheckoutCaptures.status,
    })
    .from(cartCheckoutCaptures)
    .where(eq(cartCheckoutCaptures.checkoutKey, checkoutKey));
  if (captures.length === 0)
    throw new Error(`checkout ${checkoutKey} has no durable capture state`);
  if (captures.some((capture) => capture.status !== 'captured' || !capture.providerPaymentId))
    throw new CartCheckoutCompensationRetryableError(
      `checkout ${checkoutKey} is not ready for fulfillment`,
    );
  const provider = await getPaymentProvider(ctx);
  for (const capture of captures) {
    const providerPaymentId = capture.providerPaymentId;
    if (!providerPaymentId)
      throw new CartCheckoutCompensationRetryableError(
        `checkout ${checkoutKey} is not ready for fulfillment`,
      );
    const result = await provider.finalize({ providerPaymentId });
    if (!result.ok) throw new Error(result.message);
  }
}

export type CartCompensationState = 'compensation_pending' | 'no_captured_work';
export type CartCompensationResult = {
  outboxIds: string[];
  state: CartCompensationState;
};

async function createCartCompensationInTx(
  tx: TxDrizzleClient,
  input: { checkoutKey: string; orderIds: string[] },
): Promise<{ state: CartCompensationState; aggregateIds: string[] }> {
  let state: CartCompensationState = 'no_captured_work';
  let aggregateIds: string[] = [];
  const intents = await tx
    .select()
    .from(cartCheckoutCaptures)
    .where(eq(cartCheckoutCaptures.checkoutKey, input.checkoutKey));
  for (const capture of intents) {
    if (capture.status !== 'captured') {
      if (capture.status !== 'cancelled') state = 'compensation_pending';
      await tx
        .insert(outbox)
        .values({
          aggregateType: 'cart_checkout_capture',
          aggregateId: capture.id,
          eventType: 'cart.checkout.compensation',
          dedupeKey: `cart-compensation:release:${capture.id}`,
          payload: { captureId: capture.id, action: 'release' },
        })
        .onConflictDoNothing({ target: outbox.dedupeKey });
      continue;
    }
    if (!capture.providerPaymentId) continue;
    state = 'compensation_pending';
    const [intent] = await tx
      .insert(cartCheckoutCompensations)
      .values({
        checkoutKey: capture.checkoutKey,
        orderId: capture.orderId,
        orderLineId: capture.orderLineId,
        providerPaymentId: capture.providerPaymentId,
        amountAgorot: capture.amountAgorot,
        checkoutOrderIds: input.orderIds,
        refundIdempotencyKey: `checkout-compensation:${input.checkoutKey}:${capture.orderId}`,
      })
      .onConflictDoNothing({
        target: [cartCheckoutCompensations.checkoutKey, cartCheckoutCompensations.orderId],
      })
      .returning({ id: cartCheckoutCompensations.id });
    const compensation =
      intent ??
      (
        await tx
          .select({ id: cartCheckoutCompensations.id })
          .from(cartCheckoutCompensations)
          .where(
            and(
              eq(cartCheckoutCompensations.checkoutKey, capture.checkoutKey),
              eq(cartCheckoutCompensations.orderId, capture.orderId),
            ),
          )
      )[0];
    if (compensation) aggregateIds.push(compensation.id);
    if (compensation)
      await tx
        .insert(outbox)
        .values({
          aggregateType: 'cart_checkout_compensation',
          aggregateId: compensation.id,
          eventType: 'cart.checkout.compensation',
          dedupeKey: `cart-compensation:refund:${compensation.id}`,
          payload: { compensationId: compensation.id },
        })
        .onConflictDoNothing({ target: outbox.dedupeKey });
  }
  const compensations = await tx
    .select({
      id: cartCheckoutCompensations.id,
      status: cartCheckoutCompensations.status,
    })
    .from(cartCheckoutCompensations)
    .where(eq(cartCheckoutCompensations.checkoutKey, input.checkoutKey));
  if (
    compensations.some(
      (compensation) => !['refunded', 'cleanup_completed'].includes(compensation.status),
    )
  )
    state = 'compensation_pending';
  aggregateIds = [...new Set([...aggregateIds, ...intents.map((capture) => capture.id)])];
  return { state, aggregateIds };
}

async function loadPendingCompensationOutboxIds(
  db: DrizzleClient,
  aggregateIds: string[],
): Promise<string[]> {
  const pendingEvents =
    aggregateIds.length === 0
      ? []
      : await db
          .select({ id: outbox.id })
          .from(outbox)
          .where(
            and(
              inArray(outbox.aggregateId, aggregateIds),
              eq(outbox.eventType, 'cart.checkout.compensation'),
              isNull(outbox.processedAt),
              isNull(outbox.deadAt),
            ),
          );
  return pendingEvents.map((event) => event.id);
}

export async function createCartCompensation(
  db: TxDrizzleClient,
  input: { checkoutKey: string; orderIds: string[] },
): Promise<CartCompensationResult> {
  const created = await db.transaction((tx) => createCartCompensationInTx(tx, input));
  return {
    state: created.state,
    outboxIds: await loadPendingCompensationOutboxIds(db, created.aggregateIds),
  };
}

export async function recordCartCaptureAndCreateCompensationInTx(
  tx: TxDrizzleClient,
  input: {
    captureId: string;
    providerPaymentId: string;
    checkoutKey: string;
    orderIds: string[];
  },
): Promise<CartCompensationResult> {
  const [captured] = await tx
    .update(cartCheckoutCaptures)
    .set({ status: 'captured', providerPaymentId: input.providerPaymentId })
    .where(
      and(eq(cartCheckoutCaptures.id, input.captureId), eq(cartCheckoutCaptures.status, 'pending')),
    )
    .returning({ id: cartCheckoutCaptures.id });
  if (!captured) return { state: 'no_captured_work', outboxIds: [] };

  const created = await createCartCompensationInTx(tx, {
    checkoutKey: input.checkoutKey,
    orderIds: input.orderIds,
  });
  return {
    state: created.state,
    outboxIds: await loadPendingCompensationOutboxIds(tx, created.aggregateIds),
  };
}

export async function processCartHoldRelease(ctx: DispatchEnv, captureId: string): Promise<void> {
  const db = getDb({ DATABASE_URL: ctx.DATABASE_URL });
  const [intent] = await db
    .select()
    .from(cartCheckoutCaptures)
    .where(eq(cartCheckoutCaptures.id, captureId));
  if (!intent || intent.status !== 'pending') return;
  const provider = await getPaymentProvider(ctx);
  const reconciler = provider as typeof provider & {
    reconcileHold?: (input: { providerHoldId: string }) => Promise<{
      status: 'succeeded' | 'cancellable' | 'processing' | string;
      providerPaymentId?: string;
    }>;
  };
  if (reconciler.reconcileHold) {
    const state = await reconciler.reconcileHold({
      providerHoldId: intent.providerHoldId,
    });
    if (state.status === 'succeeded') {
      if (!state.providerPaymentId)
        throw new Error(`captured hold ${intent.providerHoldId} missing provider payment id`);
      const result = await db.transaction((tx) =>
        recordCartCaptureAndCreateCompensationInTx(tx, {
          captureId,
          providerPaymentId: state.providerPaymentId!,
          checkoutKey: intent.checkoutKey,
          orderIds: intent.checkoutOrderIds,
        }),
      );
      await enqueueOutboxIds(result.outboxIds);
      return;
    }
    if (state.status === 'processing')
      throw new CartCheckoutCompensationRetryableError(
        `hold ${intent.providerHoldId} remains indeterminate`,
      );
    if (state.status !== 'cancellable') throw new Error(`unexpected hold state ${state.status}`);
  }
  await provider.releaseHold({
    reservationId: intent.orderId,
    providerHoldId: intent.providerHoldId,
  });
  await db.transaction(async (tx) => {
    const [cancelled] = await tx
      .update(cartCheckoutCaptures)
      .set({ status: 'cancelled' })
      .where(
        and(eq(cartCheckoutCaptures.id, captureId), eq(cartCheckoutCaptures.status, 'pending')),
      )
      .returning({ checkoutKey: cartCheckoutCaptures.checkoutKey });
    if (cancelled) await enqueueCheckoutCleanup(tx, captureId, cancelled.checkoutKey);
  });
}

export async function processCartCompensation(
  ctx: DispatchEnv,
  compensationId: string,
): Promise<void> {
  const db = getDb({ DATABASE_URL: ctx.DATABASE_URL });
  const staleAt = new Date(Date.now() - 10 * 60 * 1000);
  const [claimed] = await db
    .update(cartCheckoutCompensations)
    .set({ status: 'processing', updatedAt: new Date() })
    .where(
      and(
        eq(cartCheckoutCompensations.id, compensationId),
        or(
          inArray(cartCheckoutCompensations.status, ['pending', 'failed']),
          and(
            eq(cartCheckoutCompensations.status, 'processing'),
            lt(cartCheckoutCompensations.updatedAt, staleAt),
          ),
        ),
      ),
    )
    .returning();
  if (!claimed) return;
  try {
    if (!claimed.providerPaymentId)
      throw new Error('captured compensation missing provider payment id');
    const result = await (
      await getPaymentProvider(ctx)
    ).refund({
      purchaseId: claimed.orderLineId,
      providerPaymentId: claimed.providerPaymentId,
      amountAgorot: claimed.amountAgorot,
      idempotencyKey: claimed.refundIdempotencyKey,
    });
    if (!result.ok) throw new Error(result.message);
    await db.transaction(async (tx) => {
      const [refunded] = await tx
        .update(cartCheckoutCompensations)
        .set({
          status: 'refunded',
          providerRefundId: result.providerRefundId,
          lastError: null,
          updatedAt: new Date(),
        })
        .where(
          and(
            eq(cartCheckoutCompensations.id, compensationId),
            eq(cartCheckoutCompensations.status, 'processing'),
          ),
        )
        .returning({ id: cartCheckoutCompensations.id });
      if (refunded) await enqueueCheckoutCleanup(tx, refunded.id, claimed.checkoutKey);
    });
    return;
  } catch (error) {
    await db
      .update(cartCheckoutCompensations)
      .set({
        status: 'failed',
        lastError: error instanceof Error ? error.message : String(error),
        updatedAt: new Date(),
      })
      .where(
        and(
          eq(cartCheckoutCompensations.id, compensationId),
          eq(cartCheckoutCompensations.status, 'processing'),
        ),
      );
    throw error;
  }
}

export async function processCartCompensationCleanup(
  ctx: DispatchEnv,
  checkoutKey: string,
): Promise<void> {
  const db = getDb({ DATABASE_URL: ctx.DATABASE_URL });
  const [claimed] = await db
    .update(cartCheckoutCompensations)
    .set({ status: 'cleaning', updatedAt: new Date() })
    .where(
      and(
        eq(cartCheckoutCompensations.checkoutKey, checkoutKey),
        eq(cartCheckoutCompensations.status, 'refunded'),
      ),
    )
    .returning();
  if (!claimed) return;
  try {
    await db.transaction(async (tx) => {
      const incomplete = await tx
        .select({ id: cartCheckoutCompensations.id })
        .from(cartCheckoutCompensations)
        .where(
          and(
            eq(cartCheckoutCompensations.checkoutKey, checkoutKey),
            sql`${cartCheckoutCompensations.status} NOT IN ('refunded', 'cleaning', 'cleanup_completed')`,
          ),
        );
      const pendingHolds = await tx
        .select({ id: cartCheckoutCaptures.id })
        .from(cartCheckoutCaptures)
        .where(
          and(
            eq(cartCheckoutCaptures.checkoutKey, checkoutKey),
            sql`${cartCheckoutCaptures.status} NOT IN ('captured', 'cancelled')`,
          ),
        );
      if (incomplete.length || pendingHolds.length) {
        await tx
          .update(cartCheckoutCompensations)
          .set({ status: 'refunded', updatedAt: new Date() })
          .where(
            and(
              eq(cartCheckoutCompensations.checkoutKey, checkoutKey),
              eq(cartCheckoutCompensations.status, 'cleaning'),
            ),
          );
        throw new CartCheckoutCompensationRetryableError(
          `checkout ${claimed.checkoutKey} still has pending compensation work`,
        );
      }
      for (const orderId of claimed.checkoutOrderIds) {
        await tx.execute(
          sql`UPDATE voucher SET state = 'CANCELLED' WHERE order_id = ${orderId} AND state = 'UNREDEEMED'`,
        );
        await settleCartCheckoutCompensatedOrder(tx, orderId);
      }
      await releasePromoReservationsForCheckout(tx, claimed.checkoutKey);
      await tx
        .update(cartCheckoutCompensations)
        .set({ status: 'cleanup_completed', updatedAt: new Date() })
        .where(
          and(
            eq(cartCheckoutCompensations.checkoutKey, checkoutKey),
            inArray(cartCheckoutCompensations.status, ['refunded', 'cleaning']),
          ),
        );
    });
  } catch (error) {
    if (error instanceof CartCheckoutCompensationRetryableError) {
      await db
        .update(cartCheckoutCompensations)
        .set({ status: 'refunded', updatedAt: new Date() })
        .where(
          and(
            eq(cartCheckoutCompensations.checkoutKey, checkoutKey),
            eq(cartCheckoutCompensations.status, 'cleaning'),
          ),
        );
      throw error;
    }
    await db
      .update(cartCheckoutCompensations)
      .set({
        status: 'refunded',
        lastError: error instanceof Error ? error.message : String(error),
        updatedAt: new Date(),
      })
      .where(
        and(
          eq(cartCheckoutCompensations.checkoutKey, checkoutKey),
          eq(cartCheckoutCompensations.status, 'cleaning'),
        ),
      );
    throw error;
  }
}
