import { and, count, eq, inArray } from 'drizzle-orm';
import type { DrizzleClient, DrizzleDb, TxDrizzleClient } from '@/server/db/client.js';
import { order, promoReservations } from '@/server/db/schema.js';
import * as promoQueries from '@/server/db/queries/promo-codes.js';
import type { PromoFunder } from '@/lib/enums/promo-funder';
import { floorAgorotPercent } from '@/lib/money';
import type {
  DiscountBreakdown,
  PreviewResult,
  PromoClaim,
  PromoCode,
  PromoRejectReason,
  ResolvedCart,
} from './types.js';
import type { ValidateCtx } from './validator.js';
import {
  applyPromo as applyPlatformPromo,
  buildPromoContext,
  discountResultToBreakdown,
  getPromoUsageCounts,
  mapHostPromoToModule,
  mapPlatformReason,
  resolvedCartToDiscountableCart,
  validatePromo as validatePlatformPromo,
} from './promotions-platform.js';
import { decide as decidePromo } from '@/server/domain/promo/machine.js';
import { applyEffects as applyPromoEffects } from '@/server/domain/promo/apply-effects.js';
import { asOrderLineId, asPromoId, asUserId, asVendorId } from '@/server/platform-seams/ids.js';

const DEFAULT_PLATFORM_FEE_PCT = 10;

/** Stripe minimum charge for ILS (500 agorot = ₪5). */
export const STRIPE_MIN_CHARGE_AGOROT = 500;

export type OrchestrationResult =
  | { ok: true; breakdown: DiscountBreakdown }
  | { ok: false; reason: PromoRejectReason };

/** Pure orchestration: validate → resolve → financial-feasibility gate. Same sequence on preview and reserve paths. */
export function runPromoOrchestration(code: PromoCode, ctx: ValidateCtx): OrchestrationResult {
  const rules = code.rulesJson as {
    eligibility?: { userAllowlist?: string[] };
  };
  const platformCode = mapHostPromoToModule(code);
  const platformCtx = buildPromoContext(
    ctx.cart,
    ctx.user,
    ctx.userPriorPurchaseCount,
    code.redemptionCount,
    ctx.userRedemptionCount,
    rules.eligibility?.userAllowlist ?? [],
  );
  const v = validatePlatformPromo(platformCode, platformCtx);
  if (!v.ok) return { ok: false, reason: mapPlatformReason(v.reason) };

  const breakdown = discountResultToBreakdown(
    applyPlatformPromo(platformCode, resolvedCartToDiscountableCart(ctx.cart)),
    platformCode,
    new Map(ctx.cart.lines.map((line) => [line.lineId, { vendorId: line.vendorId }])),
  );

  if (ctx.cart.totalAgorot > 0) {
    const chargeAgorot = ctx.cart.totalAgorot - breakdown.totalDiscountAgorot;
    if (breakdown.totalDiscountAgorot >= ctx.cart.totalAgorot || chargeAgorot <= 0) {
      return {
        ok: false,
        reason:
          code.funder === 'platform' ? 'PLATFORM_FUND_INSUFFICIENT' : 'VENDOR_FUND_INSUFFICIENT',
      };
    }
  }

  const feePct = ctx.platformFeePct ?? DEFAULT_PLATFORM_FEE_PCT;
  const F = floorAgorotPercent(ctx.cart.totalAgorot, feePct);

  if (code.funder === 'platform') {
    const discountsByLineId = new Map(
      breakdown.perLine.map((line) => [line.lineId, line.amountAgorot] as const),
    );
    for (const line of ctx.cart.lines) {
      const lineDiscountAgorot = discountsByLineId.get(line.lineId) ?? 0;
      if (lineDiscountAgorot === 0) continue;
      const { amountPaidAgorot, commissionAgorot, vendorAgorot } = computePromoAwareSplit(
        line.lineSubtotalAgorot,
        lineDiscountAgorot,
        code.funder,
        feePct,
      );
      if (!isPromoSplitValid(amountPaidAgorot, commissionAgorot, vendorAgorot)) {
        return { ok: false, reason: 'PLATFORM_FUND_INSUFFICIENT' };
      }
    }

    if (breakdown.totalDiscountAgorot > F) {
      return { ok: false, reason: 'PLATFORM_FUND_INSUFFICIENT' };
    }
  } else {
    const maxVendorDiscount = Math.max(0, ctx.cart.totalAgorot - F - STRIPE_MIN_CHARGE_AGOROT);
    if (breakdown.totalDiscountAgorot > maxVendorDiscount) {
      return { ok: false, reason: 'VENDOR_FUND_INSUFFICIENT' };
    }
  }

  return { ok: true, breakdown };
}

/** Funder-aware split for a single checkout line (mirrors cart-checkout.ts). */
export function computePromoAwareSplit(
  lineSubtotalAgorot: number,
  lineDiscountAgorot: number,
  funder: PromoFunder,
  platformFeePct: number = DEFAULT_PLATFORM_FEE_PCT,
): { amountPaidAgorot: number; commissionAgorot: number; vendorAgorot: number } {
  const feeOnOriginal = floorAgorotPercent(lineSubtotalAgorot, platformFeePct);

  if (lineDiscountAgorot === 0) {
    return {
      amountPaidAgorot: lineSubtotalAgorot,
      commissionAgorot: feeOnOriginal,
      vendorAgorot: lineSubtotalAgorot - feeOnOriginal,
    };
  }

  if (funder === 'vendor') {
    const amountPaidAgorot = lineSubtotalAgorot - lineDiscountAgorot;
    const commissionAgorot = feeOnOriginal;
    const vendorAgorot = amountPaidAgorot - commissionAgorot;
    return { amountPaidAgorot, commissionAgorot, vendorAgorot };
  }

  const amountPaidAgorot = lineSubtotalAgorot - lineDiscountAgorot;
  const commissionAgorot = feeOnOriginal - lineDiscountAgorot;
  const vendorAgorot = lineSubtotalAgorot - feeOnOriginal;
  return { amountPaidAgorot, commissionAgorot, vendorAgorot };
}

/** Guard: vendor payout ≥ 0 and platform fee ≤ customer charge. */
export function isPromoSplitValid(
  amountPaidAgorot: number,
  commissionAgorot: number,
  vendorAgorot: number,
): boolean {
  return vendorAgorot >= 0 && commissionAgorot >= 0 && commissionAgorot <= amountPaidAgorot;
}

/** First-purchase predicate: user has zero orders in completed or any refund* status. */
export async function countQualifyingPriorPurchases(
  db: DrizzleClient,
  userId: string,
): Promise<number> {
  const [row] = await db
    .select({ c: count() })
    .from(order)
    .where(
      and(
        eq(order.buyerUserId, userId),
        inArray(order.status, ['completed', 'refunded', 'partially_refunded', 'charging']),
      ),
    );
  return row?.c ?? 0;
}

export async function buildValidateCtx(
  db: DrizzleClient,
  user: { id: string; isClubMember: boolean },
  cart: ResolvedCart,
  userRedemptionCount: number,
): Promise<ValidateCtx> {
  const userPriorPurchaseCount = await countQualifyingPriorPurchases(db, user.id);

  return {
    now: new Date(),
    user,
    cart,
    userPriorPurchaseCount,
    userRedemptionCount,
  };
}

/** Preview path: zero DB writes; returns shape for UI. */
export async function previewPromo(
  db: DrizzleClient,
  codeStr: string,
  user: { id: string; isClubMember: boolean },
  cart: ResolvedCart,
  platformFeePct?: number,
): Promise<PreviewResult> {
  const code = await promoQueries.getPromoCodeByCode(db, codeStr);
  if (!code) return { ok: false, reason: 'NOT_FOUND' };

  const usage = await getPromoUsageCounts(db, code.id, user.id);

  const ctx = await buildValidateCtx(db, user, cart, usage.userUses);
  if (platformFeePct != null) ctx.platformFeePct = platformFeePct;
  const result = runPromoOrchestration({ ...code, redemptionCount: usage.globalUses }, ctx);

  if (!result.ok) return result;

  const perVendorMap = new Map<
    string,
    {
      vendorId: string;
      dealIds: Set<string>;
      discountAgorot: number;
      funder: PromoFunder;
    }
  >();
  for (const line of result.breakdown.perLine) {
    const existing = perVendorMap.get(line.vendorId);
    if (existing) {
      existing.discountAgorot += line.amountAgorot;
    } else {
      perVendorMap.set(line.vendorId, {
        vendorId: line.vendorId,
        dealIds: new Set(),
        discountAgorot: line.amountAgorot,
        funder: result.breakdown.funder,
      });
    }
  }

  // Populate dealIds per vendor from cart lines
  for (const cartLine of cart.lines) {
    const entry = perVendorMap.get(cartLine.vendorId);
    if (entry) entry.dealIds.add(cartLine.dealId);
  }

  return {
    ok: true,
    totalDiscountAgorot: result.breakdown.totalDiscountAgorot,
    kind: result.breakdown.kind,
    perVendor: Array.from(perVendorMap.values()).map((v) => ({
      vendorId: v.vendorId,
      dealIds: Array.from(v.dealIds),
      discountAgorot: v.discountAgorot,
      funder: v.funder,
    })),
  };
}

/**
 * Reserve path: validate + gate under row lock, atomically increment redemptionCount.
 * Must run inside the checkout transaction so quota rolls back with the tx on failure.
 */
export async function reservePromoOrchestration(
  db: TxDrizzleClient,
  codeStr: string,
  user: { id: string; isClubMember: boolean },
  cart: ResolvedCart,
  platformFeePct?: number,
): Promise<
  | { ok: true; code: PromoCode; breakdown: DiscountBreakdown }
  | { ok: false; reason: PromoRejectReason }
> {
  const { reservePromo } = await import('@/server/workflows/promo-reserve.js');
  return reservePromo(db, codeStr, user, cart, platformFeePct);
}

/** Build per-purchase claim from a vendor-grouped breakdown slice. */
export function buildPromoClaim(
  code: PromoCode,
  vendorDiscountAgorot: number,
  userId: string,
): PromoClaim {
  return {
    promoCodeId: code.id,
    codeSnapshot: code.code,
    discountAgorot: vendorDiscountAgorot,
    funder: code.funder,
    userId,
  };
}

/** Release checkout-time quota when payment never completes. Idempotent via PENDING→FAILED guard. */
export async function releasePromoReservation(
  db: DrizzleDb | TxDrizzleClient,
  args: {
    reservationId: string;
    purchaseId: string;
    userId: string;
    vendorId: string;
  },
): Promise<void> {
  await db.transaction(async (tx) => {
    const [r] = await tx
      .select()
      .from(promoReservations)
      .where(eq(promoReservations.id, args.reservationId))
      .for('update');
    if (
      !r ||
      r.id !== args.reservationId ||
      r.purchaseId !== args.purchaseId ||
      r.orderLineId !== args.purchaseId ||
      r.userId !== args.userId ||
      r.vendorId !== args.vendorId
    )
      return;
    if (r.status !== 'pending' || !r.quotaOwner) return;
    const [failed] = await tx
      .update(promoReservations)
      .set({ status: 'failed', updatedAt: new Date() })
      .where(and(eq(promoReservations.id, r.id), eq(promoReservations.status, 'pending')))
      .returning({ id: promoReservations.id });
    if (!failed) return;
    const decision = decidePromo(
      { usageReserved: true, redemptionFinalized: false },
      {
        kind: 'promo_exhausted',
        promoCodeId: asPromoId(r.promoCodeId),
        userId: asUserId(r.userId),
      },
    );
    if (decision.ok) await applyPromoEffects({ db: tx }, decision.effects);
  });
}

export async function releasePromoReservationsForCheckout(
  db: DrizzleDb | TxDrizzleClient,
  checkoutScope: string,
): Promise<void> {
  await db.transaction(async (tx) => {
    const reservations = await tx
      .select()
      .from(promoReservations)
      .where(eq(promoReservations.checkoutScope, checkoutScope))
      .for('update');
    for (const reservation of reservations) {
      if (reservation.status !== 'pending') continue;
      const [failed] = await tx
        .update(promoReservations)
        .set({ status: 'failed', updatedAt: new Date() })
        .where(
          and(eq(promoReservations.id, reservation.id), eq(promoReservations.status, 'pending')),
        )
        .returning({
          id: promoReservations.id,
          quotaOwner: promoReservations.quotaOwner,
        });
      if (!failed || !failed.quotaOwner) continue;
      const decision = decidePromo(
        { usageReserved: true, redemptionFinalized: false },
        {
          kind: 'promo_exhausted',
          promoCodeId: asPromoId(reservation.promoCodeId),
          userId: asUserId(reservation.userId),
        },
      );
      if (decision.ok) await applyPromoEffects({ db: tx }, decision.effects);
    }
  });
}

/**
 * Webhook path: insert ledger row only (quota already reserved at checkout).
 * Idempotent via UNIQUE(purchase_id, promo_code_id).
 *
 * Economic and identity fields are bound to the persisted purchase row and promo
 * code record — caller-supplied promo metadata is validated, not trusted.
 */
export async function finalizePromoRedemption(
  db: DrizzleDb,
  args: {
    reservationId: string;
    purchaseId: string;
    userId: string;
    vendorId: string;
  },
): Promise<{ ok: true; insertedRedemption: boolean } | { ok: false; reason: PromoRejectReason }> {
  return db.transaction(async (tx) => {
    const purchase = await promoQueries.getPurchasePromoAnchor(tx, args.purchaseId);
    if (!purchase || purchase.userId !== args.userId || purchase.vendorId !== args.vendorId)
      return { ok: false, reason: 'NOT_FOUND' };
    const [reservation] = await tx
      .select()
      .from(promoReservations)
      .where(eq(promoReservations.id, args.reservationId))
      .for('update');
    if (
      !reservation ||
      reservation.id !== args.reservationId ||
      reservation.purchaseId !== args.purchaseId ||
      reservation.userId !== args.userId ||
      reservation.vendorId !== args.vendorId
    )
      return { ok: false, reason: 'NOT_FOUND' };
    if (reservation.status === 'finalized') return { ok: true, insertedRedemption: false };
    if (reservation.status !== 'pending') return { ok: false, reason: 'NOT_FOUND' };
    const snapshot = reservation.snapshot as {
      code?: { funder?: PromoFunder };
      currency?: string;
    };
    if (
      !snapshot.code ||
      snapshot.code.funder !== reservation.funder ||
      snapshot.currency !== reservation.currency ||
      reservation.discountAgorot <= 0
    )
      return { ok: false, reason: 'NOT_FOUND' };
    if (reservation.discountAgorot > purchase.lineTotalAgorot)
      return { ok: false, reason: 'OUT_OF_QUOTA' };
    const boundUserId = purchase.userId;
    const boundVendorId = purchase.vendorId;
    const boundFunder = reservation.funder;
    const boundDiscountAgorot = reservation.discountAgorot;
    const decision = decidePromo(
      { usageReserved: true, redemptionFinalized: false },
      {
        kind: 'promo_usage_recorded',
        promoCodeId: asPromoId(reservation.promoCodeId),
        purchaseId: asOrderLineId(args.purchaseId),
        userId: asUserId(boundUserId),
        vendorId: asVendorId(boundVendorId),
        discountAgorot: boundDiscountAgorot,
        funder: boundFunder,
        appliedToLines: [asOrderLineId(args.purchaseId)],
      },
    );
    if (!decision.ok) return { ok: false, reason: 'OUT_OF_QUOTA' };
    const result = await applyPromoEffects(
      {
        db: tx,
        finalize: {
          ids: {
            promoCodeId: asPromoId(reservation.promoCodeId),
            purchaseId: asOrderLineId(args.purchaseId),
            userId: asUserId(boundUserId),
            vendorId: asVendorId(boundVendorId),
          },
        },
      },
      decision.effects,
    );
    if (!result.ok) return result;
    const [finalized] = await tx
      .update(promoReservations)
      .set({ status: 'finalized', updatedAt: new Date() })
      .where(
        and(eq(promoReservations.id, args.reservationId), eq(promoReservations.status, 'pending')),
      )
      .returning({ id: promoReservations.id });
    if (!finalized) return { ok: true, insertedRedemption: false };
    return { ok: true, insertedRedemption: result.insertedRedemption };
  });
}
