/**
 * Group Deal workflows - FDS §7 (group buying).
 *
 * State machine:
 *   COLLECTING → THRESHOLD_MET (>= minGroupSize reservations)
 *   THRESHOLD_MET → SUCCEEDED (deadline reached, execute)
 *   COLLECTING → PARTIAL_PENDING (deadline reached, below threshold)
 *   PARTIAL_PENDING → VENDOR_HONORED → SUCCEEDED (vendor honors partial)
 *   PARTIAL_PENDING → FAILED (auto-fail after 24h or vendor declines)
 *   COLLECTING/THRESHOLD_MET → EXTENDED (vendor extends deadline, max 1x)
 *   COLLECTING/THRESHOLD_MET → FAILED (not enough participants)
 *
 * All DB access goes through src/server/db/queries/* helpers.
 * No raw SQL. All inputs are zod-validated at the API boundary.
 *
 * Capture path: Stripe manual-capture via deps.payments().captureHold.
 * providerAuthorizationId = Stripe PaymentIntent.id (status=requires_capture).
 */

import { and, eq, sql } from 'drizzle-orm';
import type { DrizzleClient } from '@/server/db/client.js';
import * as dealQueries from '@/server/db/queries/deals.js';
import * as groupDealQueries from '@/server/db/queries/group-deals.js';
import type { DoClient } from '@/server/services/types.js';
import * as groupReservationQueries from '@/server/db/queries/group-reservations.js';
import * as groupTierQueries from '@/server/db/queries/group-tiers.js';
import { deals, dealSkus, groupDeals, type groupTiers } from '@/server/db/schema.js';
import { getDefaultSku } from '@/server/domain/variants/read.js';
import { refreshDealCache } from '@/server/domain/variants/cache.js';
import { buildDefaultSkuRow } from '@/server/domain/variants/default-sku.js';
import { setInventory, toInventoryTx } from '@/server/stock/inventory-platform.js';
import type { CreateGroupDealBody } from '@/server/schemas/group-deal.js';
import { transition } from '../domain/group-deal/machine.js';
import type { GroupDealContext } from '../domain/group-deal/machine.js';
import { applyEffects } from '../domain/group-deal/apply-effects.js';
import { createGroupDeal as createGroupDealHandler } from '../domain/group-deal/create-handler.js';
import { executeGroupDeal as executeGroupDealHandler } from '../domain/group-deal/execute-handler.js';
import {
  handlePartialExecution as handlePartialExecutionHandler,
  honorPartialGroupDeal as honorPartialGroupDealHandler,
} from '../domain/group-deal/partial-handler.js';
import { extendGroupDeal as extendGroupDealHandler } from '../domain/group-deal/extend-handler.js';
import {
  buildCtx,
  enqueueEvent,
  GroupDealError,
  makeApplyCtx,
  safeDisarmGroupDealAlarm,
  type GroupDealDeps,
  withoutAlarmEffects,
} from '../domain/group-deal/runtime.js';
import { env } from '@/server/env.js';
import { captureCaught } from '@/server/observability/capture.server';

// ─── Deps ─────────────────────────────────────────────────────────────────────

export { GroupDealError } from '../domain/group-deal/runtime.js';
export type { GroupDealDeps } from '../domain/group-deal/runtime.js';

// ─── Count-driven transition helpers (called from group-reservation.ts) ───────

/**
 * Called by group-reservation after incrementing count, if count crossed threshold.
 * Routes through the state machine. No-op if group deal is not in COLLECTING or EXTENDED.
 */
export async function recordThresholdMet(
  db: DrizzleClient,
  doClient: DoClient,
  groupDealId: string,
  dealId: string,
  vendorId: string,
  participantCount: number,
): Promise<void> {
  const groupDeal = await groupDealQueries.findById(db, groupDealId);
  if (!groupDeal) return;

  const ctx: GroupDealContext = {
    state: groupDeal.groupState as GroupDealContext['state'],
    groupDealId,
    dealId,
    vendorId,
    minGroupSize: groupDeal.minGroupSize,
    currentReservationCount: groupDeal.currentReservationCount,
    extensionCount: groupDeal.extensionCount,
  };

  const result = transition(ctx, { kind: 'threshold_met', at: new Date() });
  if (!result.ok) return; // already THRESHOLD_MET or terminal — skip

  await groupDealQueries.updateGroupState(db, groupDealId, result.nextState);

  // Apply only outbox effects (alarm is not affected by threshold_met)
  const applyCtxObj = {
    db,
    env,
    doClient,
    executeGroupDeal: async () => undefined as unknown,
    releaseGroupDealReservations: async () => undefined,
  };
  // Override outbox payload with richer data
  const effects = result.effects.map((e) =>
    e.kind === 'enqueue-outbox' ? { ...e, payload: { ...e.payload, participantCount } } : e,
  );
  await applyEffects(applyCtxObj, effects);
}

/**
 * Called by group-reservation after decrementing count, if count dropped below minGroupSize.
 * Routes through the state machine. No-op if group deal is not in THRESHOLD_MET.
 */
export async function recordThresholdLost(
  db: DrizzleClient,
  doClient: DoClient,
  groupDealId: string,
  dealId: string,
  vendorId: string,
  currentCount: number,
  minGroupSize: number,
): Promise<void> {
  const groupDeal = await groupDealQueries.findById(db, groupDealId);
  if (!groupDeal) return;

  const ctx: GroupDealContext = {
    state: groupDeal.groupState as GroupDealContext['state'],
    groupDealId,
    dealId,
    vendorId,
    minGroupSize: groupDeal.minGroupSize,
    currentReservationCount: groupDeal.currentReservationCount,
    extensionCount: groupDeal.extensionCount,
  };

  const result = transition(ctx, { kind: 'threshold_lost', at: new Date() });
  if (!result.ok) return; // not THRESHOLD_MET or terminal — skip

  await groupDealQueries.updateGroupState(db, groupDealId, result.nextState);

  const applyCtxObj = {
    db,
    env,
    doClient,
    executeGroupDeal: async () => undefined as unknown,
    releaseGroupDealReservations: async () => undefined,
  };
  const effects = result.effects.map((e) =>
    e.kind === 'enqueue-outbox'
      ? { ...e, payload: { ...e.payload, currentCount, minGroupSize } }
      : e,
  );
  await applyEffects(applyCtxObj, effects);
}

// ─── createGroupDeal ──────────────────────────────────────────────────────────

/**
 * Creates a GROUP-type deal with all group-specific fields.
 * - VETERAN vendors: deal goes ACTIVE immediately (groupState: COLLECTING)
 * - Others: PENDING_APPROVAL
 */
export async function createGroupDeal(
  deps: GroupDealDeps,
  vendorId: string,
  input: CreateGroupDealBody,
) {
  return createGroupDealHandler(deps, vendorId, input);
}

export async function incrementDealSkuQuantitySold(
  db: DrizzleClient,
  skuId: string,
  quantity: number,
): Promise<void> {
  const updated = await db
    .update(dealSkus)
    .set({ quantitySold: sql`${dealSkus.quantitySold} + ${quantity}` })
    .where(
      and(
        eq(dealSkus.id, skuId),
        sql`${dealSkus.quantitySold} + ${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 ${skuId}`,
    );
  }
}

// ─── executeGroupDeal ─────────────────────────────────────────────────────────

/**
 * Executes a group deal: captures all HELD reservations, creates purchase rows,
 * generates QR codes, transitions state to SUCCEEDED.
 * Handles partial capture failures gracefully.
 */
export async function executeGroupDeal(deps: GroupDealDeps, groupDealId: string) {
  return executeGroupDealHandler(deps, groupDealId);
}

// ─── failGroupDeal ────────────────────────────────────────────────────────────

/**
 * Fails a group deal: releases all HELD reservations and transitions state to FAILED.
 */
export async function failGroupDeal(deps: GroupDealDeps, groupDealId: string) {
  const groupDeal = await groupDealQueries.findById(deps.db, groupDealId);
  if (!groupDeal) throw new GroupDealError('GROUP_DEAL_NOT_FOUND', 'Group deal not found');

  // Validate via machine — fail_requested is valid from all non-terminal states
  const failCtx = buildCtx(groupDeal, null);
  const failResult = transition(failCtx, {
    kind: 'fail_requested',
    reason: 'manual_fail',
    at: new Date(),
  });
  if (!failResult.ok) {
    throw new GroupDealError(
      'INVALID_STATE',
      `Cannot fail 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 heldReservations = await groupReservationQueries.findHeldByGroupDeal(deps.db, groupDealId);

  const releaseResults: { reservationId: string; success: boolean; error?: string }[] = [];

  // Release all holds via provider (Stripe PI cancel is idempotent)
  for (const reservation of heldReservations) {
    let releaseOk = true;
    if (reservation.providerAuthorizationId) {
      try {
        await (
          await deps.payments()
        ).releaseHold({
          reservationId: reservation.id,
          providerHoldId: reservation.providerAuthorizationId,
        });
      } catch (err) {
        captureCaught(err, {
          scope: 'group-deal.releaseHold',
          extra: { reservationId: reservation.id },
        });
        releaseOk = false;
      }
    }
    await groupReservationQueries.updateStatus(deps.db, reservation.id, 'RELEASED');
    releaseResults.push({ reservationId: reservation.id, success: releaseOk });
    if (reservation.userId) {
      await enqueueEvent(deps.db, groupDealId, 'group_deal.reservation_released', {
        reservationId: reservation.id,
        userId: reservation.userId,
        dealTitle: deal.title,
      });
    }
  }

  // Transition group state to FAILED via machine
  await groupDealQueries.updateGroupState(deps.db, groupDealId, failResult.nextState, {
    failedAt: new Date(),
  });

  // Transition parent deal to EXPIRED — use query layer so alarm is disarmed
  await dealQueries.markExpired(deps.db, { doClient: deps.doClient }, deal.id);
  // Also disarm deal alarm (belt-and-suspenders; alarm may have already fired)
  try {
    await deps.doClient.disarmDealAlarm(deal.id);
  } catch (err) {
    captureCaught(err, { scope: 'disarmDealAlarm', extra: { dealId: deal.id } });
  }

  // Apply effects: disarm group-deal alarm + enqueue outbox with richer payload
  const failEffects = withoutAlarmEffects(
    failResult.effects
      .filter((e) => e.kind !== 'release-reservations') // already released above
      .map((e) =>
        e.kind === 'enqueue-outbox' ? { ...e, payload: { ...e.payload, releaseResults } } : e,
      ),
  );
  await safeDisarmGroupDealAlarm(deps.doClient, groupDealId);
  await applyEffects(
    makeApplyCtx(deps, (id) => executeGroupDeal(deps, id)),
    failEffects,
  );

  try {
    await deps.push.sendToVendor(deal.vendorId, {
      title: 'עסקת קבוצה לא הצליחה',
      body: `${deal.title} - לא הגיע למינימום משתתפים`,
      url: `/vendor/deals/${deal.id}`,
      tag: 'group_deal_failed',
      data: { groupDealId, dealTitle: deal.title },
    });
  } catch (err) {
    captureCaught(err, { scope: 'server.workflows.group-deal', severity: 'warning' });
    // Push failure must never roll back
  }

  return { releaseResults };
}

// ─── handlePartialExecution ───────────────────────────────────────────────────

/**
 * Called when deadline passes and participant count is between 0 and minGroupSize.
 * Sets groupState to PARTIAL_PENDING and notifies vendor to decide within 24h.
 */
export async function handlePartialExecution(deps: GroupDealDeps, groupDealId: string) {
  return handlePartialExecutionHandler(deps, groupDealId);
}

// ─── honorPartialGroupDeal ────────────────────────────────────────────────────

/**
 * Vendor honors a partial group deal (below minGroupSize).
 * Transitions to VENDOR_HONORED then immediately calls executeGroupDeal.
 */
export async function honorPartialGroupDeal(deps: GroupDealDeps, groupDealId: string) {
  return honorPartialGroupDealHandler(deps, groupDealId);
}

// ─── extendGroupDeal ──────────────────────────────────────────────────────────

/**
 * Extends the deadline of a group deal by 24-48h.
 * Maximum 1 extension allowed.
 */
export async function extendGroupDeal(deps: GroupDealDeps, groupDealId: string, newDeadline: Date) {
  return extendGroupDealHandler(deps, groupDealId, newDeadline);
}

// ─── relaunchGroupDeal ────────────────────────────────────────────────────────

/**
 * Relaunches a failed/expired group deal as a new DRAFT.
 * Duplicates deal + group_deals + group_tiers with reset counters.
 */
export async function relaunchGroupDeal(deps: GroupDealDeps, groupDealId: string) {
  const sourceGroupDeal = await groupDealQueries.findById(deps.db, groupDealId);
  if (!sourceGroupDeal) {
    throw new GroupDealError('GROUP_DEAL_NOT_FOUND', 'Group deal not found');
  }

  if (sourceGroupDeal.groupState !== 'FAILED' && sourceGroupDeal.groupState !== 'SUCCEEDED') {
    throw new GroupDealError(
      'INVALID_STATE',
      `Can only relaunch FAILED or SUCCEEDED group deals. Current: ${sourceGroupDeal.groupState}`,
    );
  }

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

  const sourceSku = await getDefaultSku(deps.db, sourceGroupDeal.dealId);
  if (!sourceSku) throw new GroupDealError('DEAL_NOT_FOUND', 'Parent deal has no default SKU');

  // 1. Duplicate parent deal row as DRAFT with no deadline set + default SKU atomically
  const newDeal = await deps.db.transaction(async (tx) => {
    const [d] = await tx
      .insert(deals)
      .values({
        vendorId: sourceDeal.vendorId,
        dealType: 'GROUP',
        title: sourceDeal.title,
        description: sourceDeal.description,
        categoryId: sourceDeal.categoryId ?? undefined,
        pickupAddress: sourceDeal.pickupAddress,
        specialInstructions: sourceDeal.specialInstructions ?? undefined,
        commissionRate: sourceDeal.commissionRate,
        dealState: 'DRAFT',
        // windowEnd intentionally not copied - vendor sets fresh deadline
      })
      .returning();
    if (!d) throw new GroupDealError('DB_ERROR', 'Failed to create duplicate deal row');

    const [groupSku3] = await tx
      .insert(dealSkus)
      .values(
        buildDefaultSkuRow({
          dealId: d.id,
          originalPrice: sourceSku.originalPrice,
          discountPercent: sourceSku.discountPercent,
          discountedPrice: sourceSku.discountedPrice,
          quantityTotal: sourceGroupDeal.maxGroupSize,
        }),
      )
      .returning({ id: dealSkus.id });
    if (groupSku3) {
      await setInventory(toInventoryTx(tx), {
        skuId: groupSku3.id,
        vendorId: d.vendorId,
        quantityTotal: sourceGroupDeal.maxGroupSize,
      });
    }
    await refreshDealCache(tx, d.id);
    return d;
  });

  // 2. Duplicate group_deals row with reset counters, linking sourceGroupDealId
  const [newGroupDeal] = await deps.db
    .insert(groupDeals)
    .values({
      dealId: newDeal.id,
      fillRule: sourceGroupDeal.fillRule,
      groupState: 'COLLECTING',
      minGroupSize: sourceGroupDeal.minGroupSize,
      maxGroupSize: sourceGroupDeal.maxGroupSize,
      perCustomerLimit: sourceGroupDeal.perCustomerLimit,
      cancellationPolicy: sourceGroupDeal.cancellationPolicy,
      tieredPricingEnabled: sourceGroupDeal.tieredPricingEnabled,
      earlyBirdEnabled: sourceGroupDeal.earlyBirdEnabled,
      earlyBirdSlots: sourceGroupDeal.earlyBirdSlots,
      earlyBirdDiscountPercent: sourceGroupDeal.earlyBirdDiscountPercent,
      currentReservationCount: 0,
      extensionCount: 0,
      sourceGroupDealId: groupDealId,
    })
    .returning();

  if (!newGroupDeal)
    throw new GroupDealError('DB_ERROR', 'Failed to create duplicate group_deal row');

  // 3. Duplicate group_tiers if tiered pricing enabled
  let newTiers: (typeof groupTiers.$inferSelect)[] = [];
  if (sourceGroupDeal.tieredPricingEnabled) {
    const sourceTiers = await groupTierQueries.findByGroupDealId(deps.db, groupDealId);
    if (sourceTiers.length > 0) {
      newTiers = await groupTierQueries.createMany(
        deps.db,
        newGroupDeal.id,
        sourceTiers.map((t) => ({
          minParticipants: t.minParticipants,
          pricePerUnit: t.pricePerUnit,
          discountPercent: t.discountPercent,
          sortOrder: t.sortOrder,
        })),
      );
    }
  }

  return { deal: newDeal, groupDeal: newGroupDeal, tiers: newTiers };
}
