import { connectDB } from "@/lib/db";
import {
  AuthorizationError,
  NotFoundError,
  ValidationError,
} from "@/lib/api/errors";
import { notFoundResponse, successResponse } from "@/lib/api/response";
import { isValidObjectId, validateBody } from "@/lib/api/validate";
import { VENDOR_PERMISSIONS } from "@/config/permissions.config";
import { PAYMENT_STATUS } from "@/config/app.config";
import { AdminUpdateReturnRequestSchema } from "@/lib/validations";
import { hasVendorPermission, isAdmin } from "@/lib/rbac";
import { requireApprovedVendorByUserId } from "@/lib/vendor-guard";
import { rateLimitByUser } from "@/lib/api/rate-limit-middleware";
import { getSettings } from "@/models/settings.model";
import { Order, PaymentTransaction, ReturnRequest } from "@/models";
import type { ReturnRequestItem } from "@/models/return-request.model";
import { RETURN_REFUND_STATUS, RETURN_STATUS } from "@/lib/returns";
import { refundOrderPayment } from "@/lib/order-refund";
import {
  createRefundTransaction,
  ensureChargeTransaction,
} from "@/lib/payment-transactions";
import { restoreSubOrderInventory } from "@/lib/order-inventory";
import type { IUser } from "@/types";
import { notifyReturnRequestCustomer } from "@/lib/notifications";
import { withApi } from "@/lib/api/handler";

function getTimestampUpdate(status?: string) {
  const now = new Date();
  if (status === RETURN_STATUS.APPROVED) return { approvedAt: now };
  if (status === RETURN_STATUS.REJECTED) return { rejectedAt: now };
  if (status === RETURN_STATUS.RECEIVED) return { receivedAt: now };
  if (status === RETURN_STATUS.INSPECTED) return { inspectedAt: now };
  if (status === RETURN_STATUS.REFUNDED) return { refundedAt: now, closedAt: now };
  if (status === RETURN_STATUS.CLOSED) return { closedAt: now };
  if (status === RETURN_STATUS.CANCELLED) return { closedAt: now };
  return {};
}

async function getVendorAccess(sessionUser: { id: string; role?: string }) {
  const user = sessionUser as unknown as IUser;
  const canView = await hasVendorPermission(user, VENDOR_PERMISSIONS.VIEW_ORDERS);
  if (!canView && !isAdmin(user)) {
    throw new AuthorizationError("You do not have permission to view returns");
  }
  const settings = await getSettings();
  if (!settings.multiVendorMode?.enabled) throw new NotFoundError("Vendor");
  return requireApprovedVendorByUserId(sessionUser.id);
}

export const GET = withApi<{ id: string }>(
  {
    auth: "user",
    rateLimit: { action: "vendor:returns:read", preset: "lenient" },
  },
  async ({ params, session }) => {
    const vendor = await getVendorAccess(session.user);
    const { id } = params;
    if (!isValidObjectId(id)) return notFoundResponse("Return request");

    const returnRequest = await ReturnRequest.findOne({
      _id: id,
      $or: [
        { ownerType: "vendor", ownerVendorId: vendor._id },
        { ownerType: { $exists: false }, vendorIds: vendor._id },
      ],
    })
      .populate("customerId", "name email phone")
      .lean();

    if (!returnRequest) return notFoundResponse("Return request");
    return successResponse(returnRequest);
  },
);

export const PUT = withApi<{ id: string }>(
  { auth: "user" },
  async ({ request, params, session }) => {
    const user = session.user as unknown as IUser;
    const canEdit = await hasVendorPermission(user, VENDOR_PERMISSIONS.EDIT_ORDERS);
    const canManage = canEdit
      ? true
      : await hasVendorPermission(user, VENDOR_PERMISSIONS.MANAGE_ORDERS);
    if (!canEdit && !canManage && !isAdmin(user)) {
      throw new AuthorizationError("You do not have permission to update returns");
    }

    await rateLimitByUser(
      request,
      session.user.id,
      "vendor:returns:update",
      "moderate",
      session.user.role,
    );

    const { id } = params;
    if (!isValidObjectId(id)) return notFoundResponse("Return request");
    const body = await validateBody(request, AdminUpdateReturnRequestSchema);

    await connectDB();
    const settings = await getSettings();
    if (!settings.multiVendorMode?.enabled) throw new NotFoundError("Vendor");
    const vendor = await requireApprovedVendorByUserId(session.user.id);

    const before = await ReturnRequest.findOne({
      _id: id,
      $or: [
        { ownerType: "vendor", ownerVendorId: vendor._id },
        { ownerType: { $exists: false }, vendorIds: vendor._id },
      ],
    }).lean();
    if (!before) return notFoundResponse("Return request");

    const updates: Record<string, unknown> = {
      updatedBy: session.user.id,
      ...getTimestampUpdate(body.status),
    };
    if (body.status) updates.status = body.status;
    if (body.adminNote !== undefined) updates.adminNote = body.adminNote;
    if (body.rejectionReason !== undefined) {
      updates.rejectionReason = body.rejectionReason;
    }
    if (body.carrier !== undefined) updates["shipment.carrier"] = body.carrier;
    if (body.trackingNumber !== undefined) {
      updates["shipment.trackingNumber"] = body.trackingNumber;
      updates.status = RETURN_STATUS.IN_TRANSIT;
    }

    if (body.receivedItems) {
      const items = (before.items as ReturnRequestItem[]).map((item) => {
        const received = body.receivedItems?.find(
          (entry) => entry.orderItemIndex === item.orderItemIndex,
        );
        if (!received) return item;
        return {
          ...item,
          quantityReceived: Math.min(
            Number(item.quantityApproved || item.quantityRequested || 0),
            Number(received.quantityReceived || 0),
          ),
          condition: received.condition || item.condition,
          restockable: received.restockable ?? item.restockable,
        };
      });
      updates.items = items;
      updates.receivedAt = new Date();
      updates.status = body.status || RETURN_STATUS.RECEIVED;
    }

    if (body.status === RETURN_STATUS.REJECTED) {
      updates.refundStatus = RETURN_REFUND_STATUS.NOT_REQUIRED;
    }

    let refundAmount = 0;
    let refundTxnId: string | undefined;
    let gatewayResult:
      | Awaited<ReturnType<typeof refundOrderPayment>>
      | null = null;

    if (body.refundAmount !== undefined) {
      refundAmount = Number(body.refundAmount || 0);
      if (!Number.isFinite(refundAmount) || refundAmount <= 0) {
        throw new ValidationError("Refund amount must be greater than 0");
      }
      if (
        before.status === RETURN_STATUS.REJECTED ||
        before.status === RETURN_STATUS.CANCELLED
      ) {
        throw new ValidationError("Rejected or cancelled returns cannot be refunded");
      }
      // The cap is CUMULATIVE across repeated partial refunds of this return
      // (actualRefund.amount holds the running total) — checking only the
      // current call would let several individually-valid partials together
      // exceed the estimate. Mirrors the admin return route.
      const estimatedTotal = Number(before.estimatedRefund?.total || 0);
      const previouslyRefunded = Number(before.actualRefund?.amount || 0);
      const cumulativeRefunded = previouslyRefunded + refundAmount;
      if (estimatedTotal > 0 && cumulativeRefunded > estimatedTotal + 0.01) {
        throw new ValidationError(
          `Refund exceeds this vendor return's estimated value (${estimatedTotal.toFixed(2)}${
            previouslyRefunded > 0
              ? `; ${previouslyRefunded.toFixed(2)} already refunded`
              : ""
          }).`,
        );
      }

      const order = await Order.findById(before.orderId).lean();
      if (!order) throw new ValidationError("Order not found for this return");
      if (
        order.paymentStatus !== PAYMENT_STATUS.PAID &&
        order.paymentStatus !== PAYMENT_STATUS.PARTIALLY_REFUNDED
      ) {
        throw new ValidationError("Refunds can only be issued for paid orders");
      }

      const total = Number(order.total || 0);
      const [refundSummary] = await PaymentTransaction.aggregate([
        {
          $match: {
            orderId: order._id,
            type: "refund",
            status: "succeeded",
          },
        },
        { $group: { _id: null, totalRefunded: { $sum: "$grossAmount" } } },
      ]);
      const alreadyRefunded = Number(refundSummary?.totalRefunded || 0);

      // Atomically reserve this refund against the order's running refund total
      // so a vendor return refund and an admin order/return refund (or two
      // vendor refunds) cannot both pass the cap concurrently. Mirrors the
      // admin refund endpoints.
      const refundClaim = await Order.findOneAndUpdate(
        {
          _id: order._id,
          $expr: {
            $lte: [
              {
                $add: [
                  { $ifNull: ["$refundedTotal", alreadyRefunded] },
                  refundAmount,
                ],
              },
              total + 0.01,
            ],
          },
        },
        [
          {
            $set: {
              refundedTotal: {
                $add: [
                  { $ifNull: ["$refundedTotal", alreadyRefunded] },
                  refundAmount,
                ],
              },
            },
          },
        ],
        { new: true },
      ).lean();
      if (!refundClaim) {
        throw new ValidationError("Refund amount exceeds order total");
      }
      const nextRefunded = Number(
        (refundClaim as { refundedTotal?: number }).refundedTotal ??
          alreadyRefunded + refundAmount,
      );

      try {
        gatewayResult = await refundOrderPayment({
          order: {
            paymentMethod: order.paymentMethod,
            channel: order.channel,
            paymentId: order.paymentId,
            stripePaymentIntentId: order.stripePaymentIntentId,
            paypalCaptureId: order.paypalCaptureId,
            razorpayPaymentId: order.razorpayPaymentId,
            paystackTransactionId: order.paystackTransactionId,
            pesapalConfirmationCode: order.pesapalConfirmationCode,
            currency:
              (order as { currency?: string }).currency ||
              settings.general?.defaultCurrency,
          },
          amount: refundAmount,
          reason: body.refundReason || `Return ${before.returnNumber}`,
          manual: Boolean(body.manualRefund),
          actor: session.user.email || session.user.id,
        });
      } catch (gatewayError) {
        // Release the reservation on gateway failure.
        await Order.updateOne(
          { _id: order._id },
          { $inc: { refundedTotal: -refundAmount } },
        ).catch((rollbackErr) =>
          console.error("Failed to roll back refund reservation:", rollbackErr),
        );
        throw gatewayError;
      }

      const paymentStatus =
        nextRefunded >= total - 0.01
          ? PAYMENT_STATUS.REFUNDED
          : PAYMENT_STATUS.PARTIALLY_REFUNDED;
      const orderAfterRefund = await Order.findByIdAndUpdate(
        order._id,
        { $set: { paymentStatus } },
        { new: true },
      ).lean();
      if (!orderAfterRefund) throw new ValidationError("Order refund update failed");

      await ensureChargeTransaction({
        _id: String(orderAfterRefund._id),
        orderNumber: orderAfterRefund.orderNumber,
        paymentMethod: orderAfterRefund.paymentMethod,
        paymentStatus: orderAfterRefund.paymentStatus,
        paymentId: orderAfterRefund.paymentId,
        stripePaymentIntentId: orderAfterRefund.stripePaymentIntentId,
        paypalCaptureId: orderAfterRefund.paypalCaptureId,
        razorpayPaymentId: orderAfterRefund.razorpayPaymentId,
        paystackTransactionId: orderAfterRefund.paystackTransactionId,
        pesapalConfirmationCode: orderAfterRefund.pesapalConfirmationCode,
        subtotal: orderAfterRefund.subtotal,
        shippingCost: orderAfterRefund.shippingCost,
        tax: orderAfterRefund.tax,
        discount: orderAfterRefund.discount,
        total: orderAfterRefund.total,
        currency: settings.general?.defaultCurrency,
        channel: orderAfterRefund.channel || "online",
        posLocationId: orderAfterRefund.posLocationId
          ? String(orderAfterRefund.posLocationId)
          : undefined,
        createdAt: orderAfterRefund.createdAt,
      });

      const txn = await createRefundTransaction({
        order: {
          _id: String(orderAfterRefund._id),
          orderNumber: orderAfterRefund.orderNumber,
          paymentMethod: orderAfterRefund.paymentMethod,
          paymentStatus: orderAfterRefund.paymentStatus,
          paymentId: orderAfterRefund.paymentId,
          stripePaymentIntentId: orderAfterRefund.stripePaymentIntentId,
          paypalCaptureId: orderAfterRefund.paypalCaptureId,
          razorpayPaymentId: orderAfterRefund.razorpayPaymentId,
          paystackTransactionId: orderAfterRefund.paystackTransactionId,
          pesapalConfirmationCode: orderAfterRefund.pesapalConfirmationCode,
          subtotal: orderAfterRefund.subtotal,
          shippingCost: orderAfterRefund.shippingCost,
          tax: orderAfterRefund.tax,
          discount: orderAfterRefund.discount,
          total: orderAfterRefund.total,
          currency: settings.general?.defaultCurrency,
          channel: orderAfterRefund.channel || "online",
          posLocationId: orderAfterRefund.posLocationId
            ? String(orderAfterRefund.posLocationId)
            : undefined,
          createdAt: orderAfterRefund.createdAt,
        },
        amount: refundAmount,
        reason: body.refundReason || `Return ${before.returnNumber}`,
        createdBy: session.user.id,
        externalRefundId: gatewayResult.externalRefundId,
        gatewayCalled: gatewayResult.gatewayCalled,
      });
      refundTxnId = txn ? String(txn._id) : undefined;

      if (body.restoreInventoryOnRefund) {
        await restoreSubOrderInventory({
          orderId: String(order._id),
          vendorId: String(vendor._id),
        }).catch((err) =>
          console.error("Failed to restore vendor inventory on return refund:", err),
        );
      }

      updates.status =
        estimatedTotal > 0 && cumulativeRefunded >= estimatedTotal - 0.01
          ? RETURN_STATUS.REFUNDED
          : RETURN_STATUS.PARTIALLY_REFUNDED;
      updates.refundStatus =
        gatewayResult.gatewayCalled === false
          ? RETURN_REFUND_STATUS.MANUAL_REQUIRED
          : RETURN_REFUND_STATUS.SUCCEEDED;
      updates.refundedAt = new Date();
      updates.closedAt =
        updates.status === RETURN_STATUS.REFUNDED ? new Date() : undefined;
      // actualRefund.amount holds the RUNNING total for this return (the
      // cumulative-cap check reads it); per-refund amounts live in the
      // PaymentTransaction rows. $inc (not $set of a precomputed sum) so a
      // concurrent refund of the same return can't lose an increment.
      updates["actualRefund.paymentTransactionId"] = refundTxnId;
      updates["actualRefund.provider"] = gatewayResult.provider;
      updates["actualRefund.externalRefundId"] = gatewayResult.externalRefundId;
    }

    const returnRequest = await ReturnRequest.findByIdAndUpdate(
      id,
      refundAmount > 0
        ? { $set: updates, $inc: { "actualRefund.amount": refundAmount } }
        : { $set: updates },
      { new: true, runValidators: true },
    )
      .populate("customerId", "name email phone")
      .lean();

    if (!returnRequest) return notFoundResponse("Return request");
    const statusChanged =
      returnRequest.status && String(returnRequest.status) !== String(before.status);
    const refundStatusChanged =
      returnRequest.refundStatus &&
      String(returnRequest.refundStatus) !== String(before.refundStatus);
    if (statusChanged || refundStatusChanged || body.refundAmount !== undefined) {
      await notifyReturnRequestCustomer(
        returnRequest,
        String(returnRequest.status),
        settings,
      ).catch((err) =>
        console.error("Failed to create return customer notification:", err),
      );
    }
    return successResponse(returnRequest);
  },
);
