import { Response, Request } from "express";
import Stripe from "stripe";
import { Types } from "mongoose";
import { CustomRequest } from "../interfaces/auth";
import ResponseUtil from "../utils/Response/responseUtils";
import { STATUS_CODES } from "../constants/statusCodes";
import { stripeConfig, getStripe } from "../config/stripe";
import {
  walletTopUpSchema,
  walletReconcileTopUpSchema,
  walletWithdrawSchema,
} from "../validators/walletValidators";
import { connectStripeSchema } from "../validators/gasStationValidator";
import WalletTransactionModel from "../models/WalletTransactionModel";
import UserModel from "../models/UserModel";
import {
  creditWalletFromTopUpStripe,
  applyWalletDebit,
  getWalletBalanceCents,
  syncSucceededWalletTopUpsFromStripe,
  recordPendingWalletTopUp,
  reconcileWalletBalance,
  type RecentTopUp,
} from "../services/walletService";
import { getOrCreateStripeCustomer, isStripeMissingCustomerError } from "../services/stripeCustomerService";
import type { WalletTransactionType } from "../models/WalletTransactionModel";
import { renderStripeConnectLandingPage } from "../utils/stripeConnectLandingPage";
import {
  syncWalletUserStripeStatus,
} from "../services/walletStripeSync";
import {
  buildDefaultStripeConnectUrl,
  assertHttpsIfProduction,
} from "../utils/stripeConnectUrls";

const TX_TYPES: WalletTransactionType[] = [
  "topup",
  "payment_debit",
  "payment_credit",
  "withdrawal",
  "withdrawal_reversal",
  "refund",
  "adjustment",
];

/** Available balance + currency (ledger-backed balance on User). Syncs succeeded Stripe top-ups when webhooks may have missed. */
export const getWallet = async (req: CustomRequest, res: Response) => {
  try {
    const userId = req.userId!;
    const user = await UserModel.findById(userId)
      .select(
        "walletBalanceCents isDeleted isBanned stripeConnected stripeChargesEnabled stripeDetailsSubmitted stripeAccountId stripeCustomerId"
      )
      .lean();
    if (!user || user.isDeleted) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.NOT_FOUND,
        "User not found"
      );
    }
    let recentTopUps: RecentTopUp[] = [];
    if (stripeConfig.secretKey) {
      try {
        recentTopUps = await syncSucceededWalletTopUpsFromStripe(
          userId,
          user.stripeCustomerId ?? null
        );
      } catch (syncErr) {
        console.warn("[wallet] sync from Stripe failed:", syncErr);
      }
    }
    const fresh = await UserModel.findById(userId)
      .select(
        "walletBalanceCents isDeleted isBanned stripeConnected stripeChargesEnabled stripeDetailsSubmitted stripeAccountId"
      )
      .lean();
    if (!fresh || fresh.isDeleted) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.NOT_FOUND,
        "User not found"
      );
    }
    const balanceCents = fresh.walletBalanceCents ?? 0;
    const stripeAccountId = fresh.stripeAccountId ?? null;
    const stripeDetailsSubmitted = fresh.stripeDetailsSubmitted ?? false;
    const stripeChargesEnabled = fresh.stripeChargesEnabled ?? false;
    const stripeConnected = fresh.stripeConnected ?? false;
    const stripeStatus = stripeAccountId
      ? stripeChargesEnabled
        ? "active"
        : stripeDetailsSubmitted
          ? "pending"
          : "connect_required"
      : "connect_required";

    const trimmed = recentTopUps.slice(0, 5);
    const lastTopUp = trimmed[0];
    let topUpHint:
      | "no_top_up_attempted"
      | "awaiting_card"
      | "requires_action"
      | "processing"
      | "succeeded"
      | "canceled"
      | "failed"
      | null = null;

    if (!lastTopUp) {
      const hasAnyCompletedTopUp = await WalletTransactionModel.exists({
        userId: new Types.ObjectId(userId),
        type: "topup",
        status: "completed",
      });
      topUpHint = hasAnyCompletedTopUp ? "succeeded" : "no_top_up_attempted";
    } else if (
      lastTopUp.status === "requires_payment_method" ||
      lastTopUp.status === "requires_confirmation"
    ) {
      topUpHint = "awaiting_card";
    } else if (lastTopUp.status === "requires_action") {
      topUpHint = "requires_action";
    } else if (lastTopUp.status === "processing") {
      topUpHint = "processing";
    } else if (lastTopUp.status === "succeeded") {
      topUpHint = "succeeded";
    } else if (lastTopUp.status === "canceled") {
      topUpHint = "canceled";
    } else {
      topUpHint = "failed";
    }

    const centsToDollars = (cents: number) =>
      parseFloat((cents / 100).toFixed(2));

    return ResponseUtil.successResponse(
      res,
      STATUS_CODES.SUCCESS,
      {
        balance: centsToDollars(balanceCents),
        balanceCents,
        currency: "usd",
        stripeConnected,
        stripeChargesEnabled,
        stripeDetailsSubmitted,
        stripeStatus,
        recentTopUps: trimmed.map((t) => ({
          paymentIntentId: t.paymentIntentId,
          status: t.status,
          amount: centsToDollars(t.amountCents),
          amountReceived: centsToDollars(t.amountReceivedCents),
          currency: t.currency,
          createdAt: t.createdAt,
          credited: t.credited,
        })),
        topUpHint,
      },
      "Wallet balance"
    );
  } catch (e) {
    ResponseUtil.handleError(res, e);
  }
};

/** Paginated transaction history (newest first). */
export const listWalletTransactions = async (
  req: CustomRequest,
  res: Response
) => {
  try {
    const userId = req.userId!;
    const page = Math.max(parseInt((req.query.page as string) || "1", 10), 1);
    const limit = Math.min(
      Math.max(parseInt((req.query.limit as string) || "20", 10), 1),
      100
    );
    const skip = (page - 1) * limit;
    const typeQ = (req.query.type as string | undefined)?.trim();

    const filter: Record<string, unknown> = {
      userId: new Types.ObjectId(userId),
    };
    if (typeQ && (TX_TYPES as string[]).includes(typeQ)) {
      filter.type = typeQ;
    }

    const [transactions, total] = await Promise.all([
      WalletTransactionModel.find(filter)
        .sort({ createdAt: -1 })
        .skip(skip)
        .limit(limit)
        .lean(),
      WalletTransactionModel.countDocuments(filter),
    ]);

    const formattedTransactions = transactions.map((tx) => ({
      ...tx,
      delta: parseFloat((tx.deltaCents / 100).toFixed(2)),
      balanceAfter: parseFloat((tx.balanceAfterCents / 100).toFixed(2)),
    }));

    return ResponseUtil.successResponse(
      res,
      STATUS_CODES.SUCCESS,
      {
        total,
        page,
        limit,
        totalPages: Math.ceil(total / limit),
        transactions: formattedTransactions,
      },
      "Wallet transactions"
    );
  } catch (e) {
    ResponseUtil.handleError(res, e);
  }
};

/**
 * Create Stripe PaymentIntent for mobile top-up.
 * Client confirms with Stripe SDK using clientSecret; webhook credits wallet.
 */
export const createWalletTopUpIntent = async (
  req: CustomRequest,
  res: Response
) => {
  try {
    if (!stripeConfig.secretKey) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.BAD_REQUEST,
        "Stripe is not configured"
      );
    }

    const body = await walletTopUpSchema.parseAsync(req.body);
    const userId = req.userId!;

    const user = await UserModel.findById(userId)
      .select("isDeleted isBanned")
      .lean();
    if (!user || user.isDeleted) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.NOT_FOUND,
        "User not found"
      );
    }
    if (user.isBanned) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.FORBIDDEN,
        "Account suspended"
      );
    }

    if (body.currency !== "usd") {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.BAD_REQUEST,
        "Only USD wallet top-up is supported"
      );
    }

    const stripe = getStripe();
    const customerId = await getOrCreateStripeCustomer(userId);

    let piParams: Stripe.PaymentIntentCreateParams = {
      amount: body.amountCents,
      currency: body.currency,
      customer: customerId,
      metadata: {
        userId: String(userId),
        purpose: "wallet_topup",
      },
    };

    if (body.paymentMethodId) {
      const pm = await stripe.paymentMethods.retrieve(body.paymentMethodId);
      const pmCustomer =
        typeof pm.customer === "string"
          ? pm.customer
          : pm.customer?.id ?? null;
      if (pmCustomer !== customerId) {
        return ResponseUtil.errorResponse(
          res,
          STATUS_CODES.BAD_REQUEST,
          "Payment method does not belong to this wallet"
        );
      }
      piParams = {
        ...piParams,
        payment_method: body.paymentMethodId,
        payment_method_types: ["card"],
        off_session: false,
      };
    } else {
      piParams = {
        ...piParams,
        automatic_payment_methods: { enabled: true },
        setup_future_usage: "on_session",
      };
    }

    // ── Update-or-create ────────────────────────────────────────────────────
    // If the user already has an unconfirmed top-up in flight, update its amount
    // in place instead of creating yet another PaymentIntent. This is what most
    // people mean when they say "I changed the amount" — the same intent should
    // reflect the new amount, not pile up a new orphan PI every time the user
    // taps a different denomination.
    const REUSE_WINDOW_MS = 24 * 60 * 60 * 1000;
    let pi: Stripe.PaymentIntent | null = null;
    let reused = false;

    if (!body.paymentMethodId) {
      // Only reuse for the dynamic-card flow. If the new request specifies a
      // saved payment method we always create fresh, since the existing PI may
      // have been built for a different flow.
      const recentPending = await WalletTransactionModel.findOne({
        userId: new Types.ObjectId(userId),
        type: "topup",
        status: "pending",
        stripePaymentIntentId: { $ne: null },
      })
        .sort({ createdAt: -1 })
        .select("_id stripePaymentIntentId createdAt")
        .lean();

      const ageMs = recentPending?.createdAt
        ? Date.now() - new Date(recentPending.createdAt).getTime()
        : Infinity;

      if (
        recentPending?.stripePaymentIntentId &&
        ageMs < REUSE_WINDOW_MS
      ) {
        try {
          const existing = await stripe.paymentIntents.retrieve(
            recentPending.stripePaymentIntentId
          );
          const existingCustomer =
            typeof existing.customer === "string"
              ? existing.customer
              : existing.customer?.id ?? null;
          const reusable =
            existing.status === "requires_payment_method" &&
            (existing.currency ?? "").toLowerCase() === body.currency &&
            existingCustomer === customerId &&
            existing.metadata?.purpose === "wallet_topup" &&
            String(existing.metadata?.userId ?? "") === String(userId);

          if (reusable) {
            pi = await stripe.paymentIntents.update(existing.id, {
              amount: body.amountCents,
              metadata: {
                userId: String(userId),
                purpose: "wallet_topup",
              },
            });
            // Keep the ledger row's deltaCents in sync — the credit path uses
            // tx.deltaCents from this row when finalizing the top-up.
            await WalletTransactionModel.updateOne(
              { _id: recentPending._id },
              {
                $set: {
                  deltaCents: body.amountCents,
                  currency: body.currency,
                  description: "Wallet top-up — awaiting Stripe confirmation",
                },
              }
            );
            reused = true;
            console.log(
              `[wallet:topup] Reused pending PI ${pi.id} for userId=${userId} ` +
                `new amount=${body.amountCents}`
            );
          }
        } catch (e) {
          console.warn(
            "[wallet:topup] Could not reuse pending PI; will create fresh:",
            e instanceof Error ? e.message : e
          );
        }
      }
    }

    if (!pi) {
      pi = await stripe.paymentIntents.create(piParams);
      // Record a pending ledger row immediately. This lets GET /wallet recover
      // the credit by retrieving the PI by id even if `stripeCustomerId` is
      // later cleared (e.g. STRIPE_SECRET_KEY rotated to a different account).
      await recordPendingWalletTopUp({
        userId: String(userId),
        amountCents: body.amountCents,
        currency: body.currency,
        stripePaymentIntentId: pi.id,
      });
    }

    return ResponseUtil.successResponse(
      res,
      STATUS_CODES.SUCCESS,
      {
        paymentIntentId: pi.id,
        clientSecret: pi.client_secret,
        publishableKey: stripeConfig.publishableKey ?? null,
        amount: parseFloat((pi.amount / 100).toFixed(2)),
        amountCents: pi.amount,
        currency: pi.currency,
        stripeCustomerId: customerId,
        reused,
      },
      reused
        ? "Top-up amount updated on the existing intent — confirm payment to credit the wallet."
        : body.paymentMethodId
          ? "Top-up created — confirm payment with your saved card to credit the wallet."
          : "Top-up created — confirm payment with your card (stripe.confirmPayment) to credit the wallet."
    );
  } catch (e) {
    ResponseUtil.handleError(res, e);
  }
};

/**
 * Apply wallet credit from a succeeded top-up PaymentIntent if the Stripe webhook did not run
 * (common when STRIPE_WEBHOOK_SECRET/URL is wrong or the server is unreachable from Stripe).
 * Idempotent: same intent only credits once (unique stripePaymentIntentId on transactions).
 */
export const reconcileWalletTopUp = async (
  req: CustomRequest,
  res: Response
) => {
  try {
    if (!stripeConfig.secretKey) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.BAD_REQUEST,
        "Stripe is not configured"
      );
    }

    const body = await walletReconcileTopUpSchema.parseAsync(req.body);
    const userId = req.userId!;

    const user = await UserModel.findById(userId)
      .select("isDeleted isBanned")
      .lean();
    if (!user || user.isDeleted) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.NOT_FOUND,
        "User not found"
      );
    }
    if (user.isBanned) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.FORBIDDEN,
        "Account suspended"
      );
    }

    const stripe = getStripe();
    const pi = await stripe.paymentIntents.retrieve(body.paymentIntentId);

    if (pi.metadata?.purpose !== "wallet_topup") {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.BAD_REQUEST,
        "PaymentIntent is not a wallet top-up"
      );
    }
    if (String(pi.metadata?.userId ?? "") !== String(userId)) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.FORBIDDEN,
        "PaymentIntent does not belong to this user"
      );
    }
    if (pi.currency?.toLowerCase() !== "usd") {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.BAD_REQUEST,
        "Only USD wallet top-ups are supported"
      );
    }

    if (pi.status !== "succeeded") {
      const balanceCents = await getWalletBalanceCents(userId);
      return ResponseUtil.successResponse(
        res,
        STATUS_CODES.SUCCESS,
        {
          applied: false,
          paymentIntentStatus: pi.status,
          balance: parseFloat((balanceCents / 100).toFixed(2)),
          balanceCents,
        },
        "Payment not succeeded yet; retry after confirmation completes"
      );
    }

    const amount = pi.amount_received ?? pi.amount;
    await creditWalletFromTopUpStripe({
      userId,
      amountCents: amount,
      currency: pi.currency,
      stripePaymentIntentId: pi.id,
    });

    const balanceCents = await getWalletBalanceCents(userId);
    return ResponseUtil.successResponse(
      res,
      STATUS_CODES.SUCCESS,
      {
        applied: true,
        paymentIntentStatus: pi.status,
        balance: parseFloat((balanceCents / 100).toFixed(2)),
        balanceCents,
      },
      "Wallet balance synced from Stripe"
    );
  } catch (e) {
    ResponseUtil.handleError(res, e);
  }
};

/**
 * Force a balance reconciliation for the current user.
 *
 * - Pulls succeeded PaymentIntents from Stripe (catches any missed webhooks/orphan PIs).
 * - Recovers stale "processing" rows by marking them completed.
 * - Sets walletBalanceCents = max(0, sum of completed deltaCents) when under-credited.
 *
 * Use when the wallet shows a stale balance after a successful top-up. Idempotent.
 */
export const reconcileWalletBalanceEndpoint = async (
  req: CustomRequest,
  res: Response
) => {
  try {
    const userId = req.userId!;

    const user = await UserModel.findById(userId)
      .select("isDeleted isBanned stripeCustomerId")
      .lean();
    if (!user || user.isDeleted) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.NOT_FOUND,
        "User not found"
      );
    }
    if (user.isBanned) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.FORBIDDEN,
        "Account suspended"
      );
    }

    if (stripeConfig.secretKey) {
      try {
        await syncSucceededWalletTopUpsFromStripe(
          userId,
          user.stripeCustomerId ?? null,
          { force: true },
        );
      } catch (syncErr) {
        console.warn("[wallet:reconcile-balance] sync failed:", syncErr);
      }
    }

    const result = await reconcileWalletBalance(userId);

    return ResponseUtil.successResponse(
      res,
      STATUS_CODES.SUCCESS,
      {
        balance: parseFloat((result.actualAfter / 100).toFixed(2)),
        balanceCents: result.actualAfter,
        previousBalance: parseFloat((result.actualBefore / 100).toFixed(2)),
        previousBalanceCents: result.actualBefore,
        expectedBalance: parseFloat((result.expectedCents / 100).toFixed(2)),
        expectedBalanceCents: result.expectedCents,
        correctedCents: result.correctedCents,
        staleProcessingRowsRecovered: result.staleRecovered,
        skippedDueToInFlight: result.skippedDueToInFlight,
      },
      result.correctedCents > 0
        ? `Wallet balance reconciled — corrected by ${(result.correctedCents / 100).toFixed(2)} USD.`
        : result.skippedDueToInFlight
          ? "Reconciliation deferred — top-up still in flight, retry shortly."
          : "Wallet balance already correct."
    );
  } catch (e) {
    ResponseUtil.handleError(res, e);
  }
};

/**
 * Withdraw from wallet (balance decreases immediately).
 * Sending funds to a bank requires Stripe Connect or manual ops; ledger + balance enforced here.
 */
export const requestWalletWithdrawal = async (
  req: CustomRequest,
  res: Response
) => {
  try {
    const body = await walletWithdrawSchema.parseAsync(req.body);
    const userId = req.userId!;

    const user = await UserModel.findById(userId)
      .select("isDeleted isBanned")
      .lean();
    if (!user || user.isDeleted) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.NOT_FOUND,
        "User not found"
      );
    }
    if (user.isBanned) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.FORBIDDEN,
        "Account suspended"
      );
    }

    const desc = body.note?.trim()
      ? `Withdrawal: ${body.note.trim()}`
      : "Withdrawal";

    const result = await applyWalletDebit({
      userId,
      amountCents: body.amountCents,
      type: "withdrawal",
      description: desc,
      metadata: body.note ? { note: body.note } : undefined,
    });

    if (!result.ok) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.BAD_REQUEST,
        "Insufficient wallet balance"
      );
    }

    return ResponseUtil.successResponse(
      res,
      STATUS_CODES.SUCCESS,
      {
        balance: parseFloat((result.balanceAfterCents / 100).toFixed(2)),
        balanceCents: result.balanceAfterCents,
        transactionId: result.transactionId,
      },
      "Withdrawal recorded from wallet. External payout is separate if applicable."
    );
  } catch (e) {
    ResponseUtil.handleError(res, e);
  }
};

/** Saved cards for the authenticated user (empty until first top-up or SetupIntent). */
export const listWalletPaymentMethods = async (
  req: CustomRequest,
  res: Response
) => {
  try {
    if (!stripeConfig.secretKey) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.BAD_REQUEST,
        "Stripe is not configured"
      );
    }

    const userId = req.userId!;
    const user = await UserModel.findById(userId)
      .select("stripeCustomerId isDeleted isBanned")
      .lean();
    if (!user || user.isDeleted) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.NOT_FOUND,
        "User not found"
      );
    }
    if (user.isBanned) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.FORBIDDEN,
        "Account suspended"
      );
    }

    if (!user.stripeCustomerId) {
      return ResponseUtil.successResponse(
        res,
        STATUS_CODES.SUCCESS,
        { paymentMethods: [] },
        "No saved cards yet"
      );
    }

    const stripe = getStripe();
    let list;
    try {
      list = await stripe.paymentMethods.list({
        customer: user.stripeCustomerId,
        type: "card",
      });
    } catch (e) {
      if (isStripeMissingCustomerError(e)) {
        await UserModel.findByIdAndUpdate(userId, {
          $set: { stripeCustomerId: null },
        });
        return ResponseUtil.successResponse(
          res,
          STATUS_CODES.SUCCESS,
          { paymentMethods: [] },
          "Stripe customer was reset; no saved cards for this account"
        );
      }
      throw e;
    }

    const paymentMethods = list.data.map((pm) => ({
      id: pm.id,
      brand: pm.card?.brand ?? null,
      last4: pm.card?.last4 ?? null,
      expMonth: pm.card?.exp_month ?? null,
      expYear: pm.card?.exp_year ?? null,
    }));

    return ResponseUtil.successResponse(
      res,
      STATUS_CODES.SUCCESS,
      { paymentMethods },
      "Saved payment methods"
    );
  } catch (e) {
    ResponseUtil.handleError(res, e);
  }
};

/** Remove a saved card from the user’s Stripe customer. */
export const detachWalletPaymentMethod = async (
  req: CustomRequest,
  res: Response
) => {
  try {
    if (!stripeConfig.secretKey) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.BAD_REQUEST,
        "Stripe is not configured"
      );
    }

    const userId = req.userId!;
    const paymentMethodId = String(req.params.paymentMethodId || "").trim();

    if (!/^pm_[a-zA-Z0-9]+$/.test(paymentMethodId)) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.BAD_REQUEST,
        "Invalid payment method id"
      );
    }

    const user = await UserModel.findById(userId)
      .select("stripeCustomerId isDeleted isBanned")
      .lean();
    if (!user || user.isDeleted) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.NOT_FOUND,
        "User not found"
      );
    }
    if (user.isBanned) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.FORBIDDEN,
        "Account suspended"
      );
    }

    if (!user.stripeCustomerId) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.NOT_FOUND,
        "No saved cards"
      );
    }

    const stripe = getStripe();
    const pm = await stripe.paymentMethods.retrieve(paymentMethodId);
    const pmCustomer =
      typeof pm.customer === "string" ? pm.customer : pm.customer?.id ?? null;
    if (pmCustomer !== user.stripeCustomerId) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.FORBIDDEN,
        "Payment method does not belong to this wallet"
      );
    }

    await stripe.paymentMethods.detach(paymentMethodId);

    return ResponseUtil.successResponse(
      res,
      STATUS_CODES.SUCCESS,
      { paymentMethodId },
      "Payment method removed"
    );
  } catch (e) {
    ResponseUtil.handleError(res, e);
  }
};

/**
 * Create Stripe Connect account link for app user onboarding (Express), same flow as gas station.
 */
export const connectWalletStripe = async (req: CustomRequest, res: Response) => {
  try {
    const userId = req.userId;
    if (!userId) {
      return ResponseUtil.errorResponse(res, STATUS_CODES.BAD_REQUEST, "Unauthorized");
    }

    if (!stripeConfig.secretKey) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.INTERNAL_SERVER_ERROR,
        "Stripe is not configured"
      );
    }

    const parsed = await connectStripeSchema.parseAsync(req.body ?? {});

    const defaultReturnUrl = buildDefaultStripeConnectUrl("/user/stripe/return");
    const defaultRefreshUrl = buildDefaultStripeConnectUrl("/user/stripe/refresh");
    const returnUrl = parsed.returnUrl ?? defaultReturnUrl;
    const refreshUrl = parsed.refreshUrl ?? defaultRefreshUrl;

    if (!returnUrl || !refreshUrl) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.BAD_REQUEST,
        "Stripe return URL is not configured. Set PUBLIC_API_URL on the server, or pass returnUrl/refreshUrl in the request body (must be a public, no-JWT page)."
      );
    }

    try {
      assertHttpsIfProduction(returnUrl, "returnUrl");
      assertHttpsIfProduction(refreshUrl, "refreshUrl");
    } catch (e) {
      const message = e instanceof Error ? e.message : "Invalid callback URL";
      return ResponseUtil.errorResponse(res, STATUS_CODES.BAD_REQUEST, message);
    }

    const user = await UserModel.findById(userId);
    if (!user || user.isDeleted) {
      return ResponseUtil.errorResponse(res, STATUS_CODES.NOT_FOUND, "User not found");
    }
    if (user.isBanned) {
      return ResponseUtil.errorResponse(res, STATUS_CODES.FORBIDDEN, "Account suspended");
    }

    const appendUid = (rawUrl: string): string => {
      const url = new URL(rawUrl);
      url.searchParams.set("uid", String(userId));
      return url.toString();
    };
    const finalReturnUrl = appendUid(returnUrl);
    const finalRefreshUrl = appendUid(refreshUrl);
    const stripe = getStripe();

    const createAccountAndLink = async (): Promise<{ url: string; accountId: string }> => {
      const account = await stripe.accounts.create({
        type: "express",
        country: "US",
        capabilities: {
          card_payments: { requested: true },
          transfers: { requested: true },
        },
      });
      const newAccountId = account.id;
      user.stripeAccountId = newAccountId;
      await user.save();

      const accountLink = await stripe.accountLinks.create({
        account: newAccountId,
        refresh_url: finalRefreshUrl,
        return_url: finalReturnUrl,
        type: "account_onboarding",
      });
      return { url: accountLink.url, accountId: newAccountId };
    };

    let accountId = user.stripeAccountId ?? undefined;

    if (!accountId) {
      const result = await createAccountAndLink();
      return ResponseUtil.successResponse(
        res,
        STATUS_CODES.SUCCESS,
        {
          url: result.url,
          stripeAccountId: result.accountId,
          message: "Redirect user to this URL to connect Stripe",
        },
        "Stripe Connect link created"
      );
    }

    try {
      const accountLink = await stripe.accountLinks.create({
        account: accountId,
        refresh_url: finalRefreshUrl,
        return_url: finalReturnUrl,
        type: "account_onboarding",
      });
      return ResponseUtil.successResponse(
        res,
        STATUS_CODES.SUCCESS,
        {
          url: accountLink.url,
          stripeAccountId: accountId,
          message: "Redirect user to this URL to connect Stripe",
        },
        "Stripe Connect link created"
      );
    } catch (linkError: unknown) {
      const err = linkError as { message?: string; code?: string };
      const isInvalidAccount =
        err?.message?.includes("not connected to your platform") ||
        err?.message?.includes("does not exist") ||
        err?.code === "resource_missing";

      if (isInvalidAccount) {
        user.stripeAccountId = undefined;
        await user.save();
        const result = await createAccountAndLink();
        return ResponseUtil.successResponse(
          res,
          STATUS_CODES.SUCCESS,
          {
            url: result.url,
            stripeAccountId: result.accountId,
            message: "Redirect user to this URL to connect Stripe",
          },
          "Stripe Connect link created"
        );
      }
      throw linkError;
    }
  } catch (error: unknown) {
    ResponseUtil.handleError(res, error);
  }
};

/** Refresh Connect status after user returns from Stripe onboarding */
export const refreshWalletStripeStatus = async (req: CustomRequest, res: Response) => {
  try {
    const userId = req.userId;
    if (!userId) {
      return ResponseUtil.errorResponse(res, STATUS_CODES.BAD_REQUEST, "Unauthorized");
    }

    if (!stripeConfig.secretKey) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.INTERNAL_SERVER_ERROR,
        "Stripe is not configured"
      );
    }

    const data = await syncWalletUserStripeStatus(String(userId));
    return ResponseUtil.successResponse(res, STATUS_CODES.SUCCESS, data, "Stripe status refreshed");
  } catch (error: unknown) {
    ResponseUtil.handleError(res, error);
  }
};

/** Public Stripe return_url — syncs app user Connect status (no JWT; `uid` added by connect-stripe). */
export const walletStripeConnectReturnPage = async (req: Request, res: Response) => {
  const renderReturnPage = (
    opts: Omit<Parameters<typeof renderStripeConnectLandingPage>[0], "kind">,
  ) => renderStripeConnectLandingPage({ ...opts, kind: "return" });

  try {
    const uid = typeof req.query.uid === "string" ? req.query.uid : "";
    if (!uid) {
      return res.status(400).type("html").send(
        renderReturnPage({
          title: "Invalid request",
          subtitle: "Missing user identifier in callback URL.",
          ok: false,
        }),
      );
    }
    if (!stripeConfig.secretKey) {
      return res.status(503).type("html").send(
        renderReturnPage({
          title: "Configuration error",
          subtitle: "Stripe is not configured on this server.",
          ok: false,
        }),
      );
    }
    const status = await syncWalletUserStripeStatus(uid);
    return res.status(200).type("html").send(
      renderReturnPage({
        title: "Wallet Stripe connected",
        subtitle:
          "Your payout account is linked. You can close this page and return to the app.",
        statusText: `status: ${status.stripeStatus}`,
        ok: true,
      }),
    );
  } catch (error: unknown) {
    const msg =
      error instanceof Error ? error.message : "Unable to complete Stripe callback.";
    return res.status(500).type("html").send(
      renderReturnPage({
        title: "Connection failed",
        subtitle: msg,
        ok: false,
      }),
    );
  }
};

/** Public Stripe refresh_url — same as gas-station flow for expired account links. */
export const walletStripeConnectRefreshPage = async (req: Request, res: Response) => {
  const renderRefreshPage = (
    opts: Omit<Parameters<typeof renderStripeConnectLandingPage>[0], "kind">,
  ) => renderStripeConnectLandingPage({ ...opts, kind: "refresh" });

  try {
    const uid = typeof req.query.uid === "string" ? req.query.uid : "";
    if (!uid) {
      return res.status(400).type("html").send(
        renderRefreshPage({
          title: "Invalid request",
          subtitle: "Missing user identifier in refresh URL.",
          ok: false,
        }),
      );
    }
    if (!stripeConfig.secretKey) {
      return res.status(503).type("html").send(
        renderRefreshPage({
          title: "Configuration error",
          subtitle: "Stripe is not configured on this server.",
          ok: false,
        }),
      );
    }
    await syncWalletUserStripeStatus(uid);
    return res.status(200).type("html").send(
      renderRefreshPage({
        title: "Session refreshed",
        subtitle: "Please go back and continue Stripe onboarding.",
        ok: true,
      }),
    );
  } catch (error: unknown) {
    const msg =
      error instanceof Error ? error.message : "Could not refresh Stripe session.";
    return res.status(500).type("html").send(
      renderRefreshPage({
        title: "Refresh failed",
        subtitle: msg,
        ok: false,
      }),
    );
  }
};

/**
 * Stripe webhook — raw body required. Registered in app.ts before express.json().
 *
 * Logs every event with a tag so operators can confirm Stripe is reaching the server,
 * see signature verification failures, and trace per-event handling outcomes.
 */
export const stripeWalletWebhook = async (req: Request, res: Response) => {
  const tag = "[wallet:webhook]";
  try {
    if (!stripeConfig.webhookSecret) {
      console.warn(`${tag} STRIPE_WEBHOOK_SECRET missing; webhook disabled`);
      return res.status(503).send("Webhook not configured");
    }

    const sig = req.headers["stripe-signature"];
    const signature = typeof sig === "string" ? sig : undefined;
    if (!signature) {
      console.warn(`${tag} Rejected — missing stripe-signature header`);
      return res.status(400).send("Missing stripe-signature");
    }

    const buf = req.body as Buffer;
    if (!Buffer.isBuffer(buf)) {
      console.warn(
        `${tag} Rejected — req.body is not a Buffer (raw-body parser missing). Check app.ts ordering.`
      );
      return res.status(400).send("Expected raw body");
    }

    let event: Stripe.Event;
    try {
      const stripe = getStripe();
      event = stripe.webhooks.constructEvent(
        buf,
        signature,
        stripeConfig.webhookSecret
      );
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      // Most common cause: STRIPE_WEBHOOK_SECRET belongs to a different Stripe
      // account than the one signing this request, or it was rotated.
      console.error(
        `${tag} Signature verification FAILED — ${msg}. ` +
          `Verify STRIPE_WEBHOOK_SECRET in .env matches the signing secret on this exact destination in the same Stripe account as STRIPE_SECRET_KEY.`
      );
      return res.status(400).send(`Webhook Error: ${msg}`);
    }

    console.log(`${tag} Received event id=${event.id} type=${event.type}`);

    if (
      event.type === "payment_intent.succeeded" ||
      event.type === "payment_intent.payment_failed"
    ) {
      const pi = event.data.object as Stripe.PaymentIntent;
      const purpose = pi.metadata?.purpose;
      const userId = pi.metadata?.userId;
      if (purpose !== "wallet_topup") {
        console.log(
          `${tag} Skipped pi=${pi.id} — purpose="${purpose ?? "(none)"}" (not a wallet top-up)`
        );
        return res.json({ received: true });
      }
      if (!userId) {
        console.warn(
          `${tag} Skipped pi=${pi.id} — wallet_topup intent without userId metadata`
        );
        return res.json({ received: true });
      }

      if (event.type === "payment_intent.succeeded") {
        const amount = pi.amount_received ?? pi.amount;
        try {
          await creditWalletFromTopUpStripe({
            userId,
            amountCents: amount,
            currency: pi.currency,
            stripePaymentIntentId: pi.id,
          });
          console.log(
            `${tag} Credited userId=${userId} amount=${amount} pi=${pi.id}`
          );
        } catch (creditErr) {
          console.error(
            `${tag} Credit FAILED for pi=${pi.id} userId=${userId}:`,
            creditErr
          );
          return res.status(500).send("Credit failed; will retry");
        }
      } else {
        const failMsg =
          pi.last_payment_error?.message ?? "Payment failed";
        await WalletTransactionModel.updateOne(
          {
            stripePaymentIntentId: pi.id,
            status: { $in: ["pending", "processing"] },
          },
          {
            $set: {
              status: "failed",
              description: `Top-up failed: ${failMsg}`,
            },
          },
        );
        console.log(
          `${tag} Marked failed userId=${userId} pi=${pi.id} reason="${failMsg}"`
        );
      }
    }

    return res.json({ received: true });
  } catch (e) {
    console.error(`${tag} Unhandled error:`, e);
    return res.status(500).send("Webhook handler error");
  }
};
