import { createDbService, type DrizzleDb } from '@/server/services/db.js';
/**
 * shipment-lifecycle.ts
 *
 * Workflows for ITEM-deal shipment creation and status advancement.
 *
 * Functions accept `env: { DATABASE_URL: string }` and derive the DB
 * connection via createDbService() — consistent with the env-driven factory pattern.
 *
 * Israeli business week: Sunday–Thursday (isBusinessDay in refund.ts).
 */

import { eq, and, isNull, inArray } from 'drizzle-orm';
import {
  deals,
  dealSkus,
  dealItemConfig,
  shipments,
  shipmentPurchases,
  shipmentEvents,
  stockReservations,
  vendorPayoutReleases,
  systemConfig,
  orderLine,
} from '@/server/db/schema.js';
import { recordStep, type OrdersSchema, order } from '@platform-modules/commerce-orders';
import type { Transaction } from '@platform-modules/db';
import { consumeReservation } from '@/server/stock/reserveStock.js';
import type { MultidealEnv } from '@/server/env.js';
import { writeNotification } from '@/server/notifications/send.js';
import { captureCaught } from '@/server/observability/capture.server.js';
import { addBusinessDays } from '@/server/calendar/il-business-days.js';
import { advanceOrderFulfillment } from '@/server/db/queries/purchases.js';
import { asOrderId, toModuleRef } from '@/server/platform-seams/ids.js';

// ---------------------------------------------------------------------------
// systemConfig helper
// ---------------------------------------------------------------------------

async function getSysConfigDays(db: DrizzleDb, key: string, fallback: number): Promise<number> {
  const [row] = await db.select().from(systemConfig).where(eq(systemConfig.key, key));
  if (!row) return fallback;
  const parsed = parseInt(row.value, 10);
  return Number.isFinite(parsed) ? parsed : fallback;
}

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

export interface CreateShipmentsOnPaidArgs {
  paymentIntentId: string;
}

export interface AdvanceShipmentStatusArgs {
  shipmentId: string;
  toStatus:
    | 'awaiting_ship'
    | 'shipped'
    | 'in_transit'
    | 'out_for_delivery'
    | 'delivered'
    | 'pickup_ready'
    | 'picked_up'
    | 'confirmed'
    | 'failed_delivery'
    | 'lost'
    | 'cancelled';
  /** The actor performing the transition: 'system' | 'vendor' | 'buyer' | 'admin' */
  source: string;
  actorId?: string;
  /** New carrier if being set at this transition. */
  carrier?: string;
  /** New tracking number if being set at this transition. */
  trackingNumber?: string;
  /** Optional carrier tracking URL — written atomically with status row. */
  trackingUrl?: string;
  /** Free-form note for the event. */
  note?: string;
}

// ---------------------------------------------------------------------------
// 1. createShipmentsOnPaid
// ---------------------------------------------------------------------------

/**
 * Idempotent: safe to call multiple times for the same paymentIntentId.
 *
 * 1. Load ITEM order lines by chargeRef, filter to dealType='ITEM'.
 * 2. Consume active stock reservations for each order line's SKU.
 * 3. Load dealItemConfig for each deal.
 * 4. Group order lines by vendor.
 * 5. INSERT one shipment row per vendor (skip vendor if shipment already exists
 *    for any order line in that group — uniqueIndex on shipment_purchases.order_line_id
 *    guarantees idempotency).
 * 6. INSERT shipmentPurchases linking rows.
 * 7. INSERT initial shipmentEvents row (toStatus='pending', source='system').
 */
export async function createShipmentsOnPaid(
  env: MultidealEnv,
  args: CreateShipmentsOnPaidArgs,
): Promise<void> {
  const db = createDbService({ DATABASE_URL: env.DATABASE_URL });
  const { paymentIntentId } = args;

  // 1. Load ITEM order lines for this payment intent.
  const rows = await db
    .select({
      orderLineId: orderLine.id,
      orderId: orderLine.orderId,
      buyerUserId: order.buyerUserId,
      vendorId: orderLine.vendorId,
      dealId: dealSkus.dealId,
      variantId: orderLine.variantId,
    })
    .from(orderLine)
    .innerJoin(order, eq(order.id, orderLine.orderId))
    .innerJoin(dealSkus, eq(dealSkus.id, orderLine.variantId))
    .innerJoin(deals, eq(deals.id, dealSkus.dealId))
    .where(
      and(
        eq(order.chargeRef, paymentIntentId),
        inArray(order.status, ['completed', 'paid', 'fulfilled']),
        eq(deals.dealType, 'ITEM'),
      ),
    );

  if (rows.length === 0) return;

  // 2. Consume active stock reservations for each SKU in this payment intent.
  //    consumeReservation is idempotent (noop if already consumed).
  const reservationRows = await db
    .select({ id: stockReservations.id })
    .from(stockReservations)
    .where(
      and(
        eq(stockReservations.paymentIntentId, paymentIntentId),
        isNull(stockReservations.consumedAt),
        isNull(stockReservations.releasedAt),
      ),
    );

  for (const res of reservationRows) {
    await consumeReservation(env, res.id);
  }

  // 3. Load dealItemConfig for all distinct deals.
  const dealIds = [...new Set(rows.map((r) => r.dealId))];
  const configs = await db
    .select()
    .from(dealItemConfig)
    .where(inArray(dealItemConfig.dealId, dealIds));
  const configByDealId = new Map(configs.map((c) => [c.dealId, c]));

  // 4. Group order lines by vendorId.
  type OrderLineRow = (typeof rows)[number];
  const byVendor = new Map<string, OrderLineRow[]>();
  for (const row of rows) {
    if (!row.vendorId) continue;
    const group = byVendor.get(row.vendorId) ?? [];
    group.push(row);
    byVendor.set(row.vendorId, group);
  }

  const now = new Date();
  const allOrderLineIds = rows.map((r) => r.orderLineId);
  const existingLinks =
    allOrderLineIds.length > 0
      ? await db
          .select({ orderLineId: shipmentPurchases.orderLineId })
          .from(shipmentPurchases)
          .where(inArray(shipmentPurchases.orderLineId, allOrderLineIds))
      : [];
  const linesWithShipment = new Set(existingLinks.map((l) => l.orderLineId));

  type PlannedShipment = {
    vendorId: string;
    group: OrderLineRow[];
    head: OrderLineRow;
    buyerId: string;
    handleByAt: Date;
    carrierValue: (typeof shipments.$inferInsert)['carrier'];
    pickupAddrId: string | null;
    shippingAddressId: string | null;
  };

  const planned: PlannedShipment[] = [];

  for (const [vendorId, group] of byVendor) {
    const head = group[0];
    if (!head) continue;
    if (group.some((r) => linesWithShipment.has(r.orderLineId))) continue;

    const cfg = configByDealId.get(head.dealId);
    const slaBusinessDays = cfg?.handleSlaBusinessDays ?? 2;
    const handleByAt = addBusinessDays(now, slaBusinessDays);

    const isPickup = cfg?.pickupEnabled === true;
    type CarrierType = (typeof shipments.$inferInsert)['carrier'];
    const carrierValue: CarrierType = isPickup ? 'pickup' : null;
    const pickupAddrId: string | null = isPickup ? (cfg?.pickupAddressId ?? null) : null;

    const buyerId = head.buyerUserId;
    if (!buyerId) {
      console.warn(
        JSON.stringify({
          level: 'warn',
          msg: 'shipment_skip_no_buyer',
          vendorId,
          paymentIntentId,
        }),
      );
      continue;
    }

    planned.push({
      vendorId,
      group,
      head,
      buyerId,
      handleByAt,
      carrierValue,
      pickupAddrId,
      shippingAddressId: null,
    });
  }

  const insertedShipments =
    planned.length > 0
      ? await db
          .insert(shipments)
          .values(
            planned.map((p) => ({
              vendorId: p.vendorId,
              buyerId: p.buyerId,
              shippingAddressId: p.shippingAddressId,
              pickupAddressId: p.pickupAddrId,
              status: 'pending' as const,
              carrier: p.carrierValue,
              handleByAt: p.handleByAt,
            })),
          )
          .returning({ id: shipments.id, vendorId: shipments.vendorId })
      : [];

  const shipmentIdByVendor = new Map(insertedShipments.map((s) => [s.vendorId, s.id]));

  const purchaseRows = planned.flatMap((p) => {
    const shipmentId = shipmentIdByVendor.get(p.vendorId);
    if (!shipmentId) return [];
    return p.group.map((r) => ({ shipmentId, orderLineId: r.orderLineId }));
  });
  if (purchaseRows.length > 0) {
    await db.insert(shipmentPurchases).values(purchaseRows);
  }

  const createdShipmentEvents: Array<typeof shipmentEvents.$inferInsert> = insertedShipments.map(
    (s) => ({
      shipmentId: s.id,
      fromStatus: null,
      toStatus: 'pending' as const,
      source: 'system' as const,
    }),
  );

  for (const p of planned) {
    const shipmentId = shipmentIdByVendor.get(p.vendorId);
    if (!shipmentId) continue;
    const { head } = p;

    // Emit buyer notification: order confirmed.
    try {
      await writeNotification({
        userId: p.buyerId,
        event: 'item.order_confirmed',
        data: { shipmentId },
        link: `/orders/${head.orderId}`,
        ctx: { db, env },
      });
    } catch (err) {
      captureCaught(err, {
        scope: 'shipment.notify',
        extra: { shipmentId, event: 'item.order_confirmed' },
      });
    }

    // T4: record shipment_created step on the order (best-effort).
    try {
      await recordStep(
        db as unknown as Transaction<OrdersSchema>,
        toModuleRef(asOrderId(head.orderId)),
        'shipment_created',
        { shipmentId, at: now.toISOString() },
      );
    } catch (err) {
      captureCaught(err, {
        scope: 'shipment.recordStep',
        severity: 'warning',
        extra: { shipmentId, step: 'shipment_created' },
      });
    }
  }

  if (createdShipmentEvents.length > 0) {
    await db.insert(shipmentEvents).values(createdShipmentEvents);
  }
}

// ---------------------------------------------------------------------------
// 2. advanceShipmentStatus
// ---------------------------------------------------------------------------

/**
 * Advance a shipment to a new status, insert an audit event, and — on
 * confirmed | picked_up — recompute vendorPayoutReleases.releaseAt for the
 * associated order lines using the IL business-day calendar.
 */
export async function advanceShipmentStatus(
  env: MultidealEnv,
  args: AdvanceShipmentStatusArgs,
): Promise<void> {
  const db = createDbService({ DATABASE_URL: env.DATABASE_URL });
  const { shipmentId, toStatus, source, actorId, carrier, trackingNumber, trackingUrl, note } =
    args;

  // Load current shipment.
  const [current] = await db
    .select({ id: shipments.id, status: shipments.status, buyerId: shipments.buyerId })
    .from(shipments)
    .where(eq(shipments.id, shipmentId));

  if (!current) {
    throw new Error(`shipment_not_found:${shipmentId}`);
  }

  const fromStatus = current.status;
  const now = new Date();

  // Build update payload.
  type ShipmentInsert = typeof shipments.$inferInsert;
  type ShipmentStatus = NonNullable<ShipmentInsert['status']>;
  type CarrierInsert = ShipmentInsert['carrier'];

  const updatePayload: Partial<ShipmentInsert> = {
    status: toStatus as ShipmentStatus,
    updatedAt: now,
  };
  if (carrier !== undefined) {
    updatePayload.carrier = carrier as CarrierInsert;
  }
  if (trackingNumber !== undefined) {
    updatePayload.trackingNumber = trackingNumber;
  }
  if (trackingUrl !== undefined) {
    updatePayload.trackingUrl = trackingUrl;
  }
  if (toStatus === 'shipped') {
    updatePayload.shippedAt = now;
  }
  if (toStatus === 'delivered' || toStatus === 'picked_up') {
    updatePayload.deliveredAt = now;
  }
  if (toStatus === 'confirmed' || toStatus === 'picked_up') {
    updatePayload.confirmedAt = now;
  }

  // Apply status transition.
  await db.update(shipments).set(updatePayload).where(eq(shipments.id, shipmentId));

  // Insert audit event.
  type EventInsert = typeof shipmentEvents.$inferInsert;
  type EventStatus = EventInsert['fromStatus'];
  await db.insert(shipmentEvents).values({
    shipmentId,
    fromStatus: fromStatus as EventStatus,
    toStatus: toStatus as NonNullable<EventInsert['toStatus']>,
    source,
    actorId: actorId ?? null,
    note: note ?? null,
  });

  // On confirmed / picked_up: recompute vendorPayoutReleases.releaseAt.
  if (toStatus === 'confirmed' || toStatus === 'picked_up') {
    const holdDays = await getSysConfigDays(db, 'settlement.hold_business_days', 14);
    const releaseAt = addBusinessDays(now, holdDays);

    // Find all orderLineIds linked to this shipment.
    const links = await db
      .select({ orderLineId: shipmentPurchases.orderLineId })
      .from(shipmentPurchases)
      .where(eq(shipmentPurchases.shipmentId, shipmentId));

    if (links.length > 0) {
      const orderLineIds = links.map((l) => l.orderLineId);
      await db
        .update(vendorPayoutReleases)
        .set({ releaseAt })
        .where(
          and(
            inArray(vendorPayoutReleases.orderLineId, orderLineIds),
            eq(vendorPayoutReleases.status, 'held'),
          ),
        );
    }
  }

  // Resolve one orderLineId for notification deep-link (indexed lookup).
  const [orderLineLink] = await db
    .select({ orderLineId: shipmentPurchases.orderLineId })
    .from(shipmentPurchases)
    .where(eq(shipmentPurchases.shipmentId, shipmentId))
    .limit(1);
  const notifOrderLineId = orderLineLink?.orderLineId;
  let notifOrderId: string | null = null;
  if (notifOrderLineId) {
    const [ol] = await db
      .select({ orderId: orderLine.orderId })
      .from(orderLine)
      .where(eq(orderLine.id, notifOrderLineId))
      .limit(1);
    notifOrderId = ol?.orderId ?? null;
  }

  // Emit buyer-facing notifications for key status transitions.
  // Failures are captured and never fail or roll back the transition.
  const buyerId = current.buyerId;
  if (buyerId) {
    if (toStatus === 'shipped') {
      try {
        await writeNotification({
          userId: buyerId,
          event: 'item.shipped',
          data: { shipmentId, trackingNumber, trackingUrl, carrier },
          link: notifOrderLineId ? `/orders/${notifOrderLineId}/tracking` : undefined,
          ctx: { db, env },
        });
      } catch (err) {
        captureCaught(err, {
          scope: 'shipment.notify',
          extra: { shipmentId, event: 'item.shipped' },
        });
      }
    } else if (toStatus === 'out_for_delivery') {
      try {
        await writeNotification({
          userId: buyerId,
          event: 'item.out_for_delivery',
          data: { shipmentId },
          link: notifOrderLineId ? `/orders/${notifOrderLineId}/tracking` : undefined,
          ctx: { db, env },
        });
      } catch (err) {
        captureCaught(err, {
          scope: 'shipment.notify',
          extra: { shipmentId, event: 'item.out_for_delivery' },
        });
      }
    } else if (toStatus === 'delivered') {
      try {
        await writeNotification({
          userId: buyerId,
          event: 'item.delivered',
          data: { shipmentId },
          link: notifOrderLineId ? `/orders/${notifOrderLineId}/tracking` : undefined,
          ctx: { db, env },
        });
      } catch (err) {
        captureCaught(err, {
          scope: 'shipment.notify',
          extra: { shipmentId, event: 'item.delivered' },
        });
      }
    } else if (toStatus === 'pickup_ready') {
      try {
        await writeNotification({
          userId: buyerId,
          event: 'item.pickup_ready',
          data: { shipmentId },
          link: notifOrderLineId ? `/orders/${notifOrderLineId}/tracking` : undefined,
          ctx: { db, env },
        });
      } catch (err) {
        captureCaught(err, {
          scope: 'shipment.notify',
          extra: { shipmentId, event: 'item.pickup_ready' },
        });
      }
    }

    // T4: record shipping milestone step on the order (best-effort).
    const shipmentStepIds: Record<string, string> = {
      shipped: 'shipped',
      out_for_delivery: 'out_for_delivery',
      delivered: 'delivered',
      pickup_ready: 'pickup_ready',
      confirmed: 'confirmed',
      picked_up: 'picked_up',
    };
    const stepId = shipmentStepIds[toStatus];
    if (stepId && notifOrderId) {
      try {
        await recordStep(
          db as unknown as Transaction<OrdersSchema>,
          toModuleRef(asOrderId(notifOrderId)),
          stepId,
          {
            shipmentId,
            toStatus,
            at: now.toISOString(),
          },
        );
      } catch (err) {
        captureCaught(err, {
          scope: 'shipment.recordStep',
          severity: 'warning',
          extra: { shipmentId, stepId },
        });
      }
    }
  }

  if (toStatus === 'delivered' && notifOrderId) {
    try {
      await advanceOrderFulfillment(db, notifOrderId);
    } catch (err) {
      captureCaught(err, {
        scope: 'shipment.fulfillment',
        severity: 'warning',
        extra: { shipmentId, orderId: notifOrderId },
      });
    }
  }
}

// ---------------------------------------------------------------------------
// 3. cancelShipmentBeforeShipment
// ---------------------------------------------------------------------------

/**
 * Cancel a shipment that has not yet shipped.
 * Caller is responsible for releasing stock reservations and issuing refunds.
 */
export async function cancelShipmentBeforeShipment(
  env: { DATABASE_URL: string },
  shipmentId: string,
  reason: string,
): Promise<void> {
  const db = createDbService({ DATABASE_URL: env.DATABASE_URL });

  const [current] = await db
    .select({ id: shipments.id, status: shipments.status })
    .from(shipments)
    .where(eq(shipments.id, shipmentId));

  if (!current) {
    throw new Error(`shipment_not_found:${shipmentId}`);
  }

  const now = new Date();

  await db
    .update(shipments)
    .set({ status: 'cancelled', updatedAt: now })
    .where(eq(shipments.id, shipmentId));

  type CancelEventInsert = typeof shipmentEvents.$inferInsert;
  await db.insert(shipmentEvents).values({
    shipmentId,
    fromStatus: current.status as CancelEventInsert['fromStatus'],
    toStatus: 'cancelled',
    source: 'system',
    note: reason,
  });
}
