import { createDbService, type DrizzleDb } from '@/server/services/db.js';
/**
 * settlement-reconcile.ts — daily cron: cross-check vendor payout releases
 * against Stripe BalanceTransaction records and log any drift to payoutAuditLog.
 */

import type { MultidealEnv } from '@/server/env.js';
import { vendors, vendorPayoutReleases, payoutAuditLog } from '@/server/db/schema.js';
import { eq, and, gte, lte } from 'drizzle-orm';
import { getStripe } from '@/server/payments/stripe/client.js';
import { captureCaught } from '@/server/observability/capture.server';

/** Reconcile vendor balances for the last 24 hours. */
export async function reconcileVendorBalances(env: MultidealEnv): Promise<void> {
  const db = createDbService({ DATABASE_URL: env.DATABASE_URL });
  const stripe = getStripe(env);

  // Window: last 24 hours
  const windowEnd = new Date();
  const windowStart = new Date(windowEnd.getTime() - 24 * 60 * 60 * 1000);

  // Fetch all vendors with an active Stripe Connect account and payouts enabled
  const activeVendors = await db
    .select({
      id: vendors.id,
      stripeAccountId: vendors.stripeAccountId,
    })
    .from(vendors)
    .where(and(eq(vendors.stripePayoutsEnabled, true)));

  for (const vendor of activeVendors) {
    const stripeAccountId = vendor.stripeAccountId;
    if (!stripeAccountId) continue;

    try {
      await _reconcileOneVendor(db, stripe, vendor.id, stripeAccountId, windowStart, windowEnd);
    } catch (e) {
      captureCaught(e, {
        scope: 'settlement.reconcile.vendor',
        severity: 'error',
        extra: { vendorId: vendor.id, stripeAccountId },
      });
    }
  }
}

type StripeClient = ReturnType<typeof getStripe>;

async function _reconcileOneVendor(
  db: DrizzleDb,
  stripe: StripeClient,
  vendorId: string,
  stripeAccountId: string,
  windowStart: Date,
  windowEnd: Date,
): Promise<void> {
  // --- Pull Stripe BalanceTransactions for this connected account (last 24h) ---
  // Use auto-pagination iterator; collect all bt.id values.
  const stripeReleasedIds = new Set<string>();
  let stripeTotalAgorot = 0;

  // Do NOT await the list call — autoPagingEach lives on the ApiListPromise,
  // not on the resolved Response. Awaiting would strip the pagination methods.
  const btListPromise = stripe.balanceTransactions.list(
    {
      created: {
        gte: Math.floor(windowStart.getTime() / 1000),
        lte: Math.floor(windowEnd.getTime() / 1000),
      },
      limit: 100,
    },
    { stripeAccount: stripeAccountId },
  );

  // Stripe SDK auto-pagination via autoPagingEach
  await btListPromise.autoPagingEach((bt) => {
    // Only count payouts credited to the vendor (positive net amounts)
    if (bt.type === 'payout' || bt.type === 'transfer') {
      stripeReleasedIds.add(bt.id);
      stripeTotalAgorot += bt.amount; // amount in smallest currency unit (agorot for ILS)
    }
  });

  // --- Pull our vendor_payout_releases rows for this vendor in the window ---
  const ourReleases = await db
    .select({
      id: vendorPayoutReleases.id,
      status: vendorPayoutReleases.status,
      payoutId: vendorPayoutReleases.payoutId,
      netAmountAgorot: vendorPayoutReleases.netAmountAgorot,
      releasedAt: vendorPayoutReleases.releasedAt,
      heldAt: vendorPayoutReleases.heldAt,
    })
    .from(vendorPayoutReleases)
    .where(
      and(
        eq(vendorPayoutReleases.vendorId, vendorId),
        gte(vendorPayoutReleases.heldAt, windowStart),
        lte(vendorPayoutReleases.heldAt, windowEnd),
      ),
    );

  // Sum what we released in the window
  const ourReleasedAgorot = ourReleases
    .filter((r) => r.status === 'released')
    .reduce((sum, r) => sum + r.netAmountAgorot, 0);

  const ourHeldAgorot = ourReleases
    .filter((r) => r.status === 'held' || r.status === 'enqueued' || r.status === 'releasing')
    .reduce((sum, r) => sum + r.netAmountAgorot, 0);

  // --- Drift detection ---

  // Case 1: released by us but Stripe shows pending (no matching bt for our payoutId)
  const releasedByUs = ourReleases.filter((r) => r.status === 'released' && r.payoutId != null);
  const driftAuditRows: Array<typeof payoutAuditLog.$inferInsert> = [];
  for (const release of releasedByUs) {
    const stripePayoutId = release.payoutId;
    if (!stripePayoutId) continue;
    // Check if Stripe has this payout id in the balance transactions
    if (!stripeReleasedIds.has(stripePayoutId)) {
      captureCaught(
        new Error(
          `settlement_drift: release=${release.id} payoutId=${stripePayoutId} released by us but not found in Stripe BT window`,
        ),
        {
          scope: 'settlement.reconcile.drift.released_not_in_stripe',
          severity: 'warning',
          extra: { vendorId, stripeAccountId, releaseId: release.id, payoutId: stripePayoutId },
        },
      );

      driftAuditRows.push({
        kind: 'reconcile_drift_released_not_in_stripe',
        vendorId,
        stripeBalanceAgorot: stripeTotalAgorot,
        heldLiabilityAgorot: ourHeldAgorot,
        driftAgorot: release.netAmountAgorot,
        payoutId: stripePayoutId,
        note: `release_id=${release.id} released_by_us_stripe_pending`,
      });
    }
  }

  // Case 2: held by us but Stripe already paid out (stripe shows credit but we haven't released)
  const heldByUs = ourReleases.filter(
    (r) => r.status === 'held' || r.status === 'enqueued' || r.status === 'releasing',
  );
  for (const release of heldByUs) {
    const stripePayoutId = release.payoutId;
    if (!stripePayoutId) continue;
    if (stripeReleasedIds.has(stripePayoutId)) {
      captureCaught(
        new Error(
          `settlement_drift: release=${release.id} payoutId=${stripePayoutId} held by us but Stripe already paid out`,
        ),
        {
          scope: 'settlement.reconcile.drift.held_but_stripe_paid',
          severity: 'warning',
          extra: { vendorId, stripeAccountId, releaseId: release.id, payoutId: stripePayoutId },
        },
      );

      driftAuditRows.push({
        kind: 'reconcile_drift_held_but_stripe_paid',
        vendorId,
        stripeBalanceAgorot: stripeTotalAgorot,
        heldLiabilityAgorot: ourHeldAgorot,
        driftAgorot: release.netAmountAgorot,
        payoutId: stripePayoutId,
        note: `release_id=${release.id} held_by_us_stripe_already_paid`,
      });
    }
  }

  if (driftAuditRows.length > 0) {
    await db.insert(payoutAuditLog).values(driftAuditRows);
  }

  // Log a clean summary row if everything balanced (no drift found above)
  const driftAgorot = stripeTotalAgorot - ourReleasedAgorot;
  if (Math.abs(driftAgorot) > 0) {
    await db.insert(payoutAuditLog).values({
      kind: 'reconcile_balance_mismatch',
      vendorId,
      stripeBalanceAgorot: stripeTotalAgorot,
      heldLiabilityAgorot: ourHeldAgorot,
      driftAgorot,
      note: `window=${windowStart.toISOString()}/${windowEnd.toISOString()}`,
    });
  }
}
