import { Prisma } from "@/app/generated/prisma/client";
import { prisma } from "@/lib/prisma";
import {
  MIDTRANS,
  createSnapTransaction,
  fetchMidtransTransactionStatus,
  mapMidtransStatus,
  verifyMidtransSignature,
  type MidtransTransactionStatus,
  type MidtransWebhookBody,
} from "@/lib/midtrans";
import { isTripDepartureDayPast } from "@/lib/trip-dates";
import { payoutService } from "@/server/services/payout.service";
import { emailService } from "@/lib/email/send";

const SERIAL_TX_ATTEMPTS = 6;

function isSerializationConflict(err: unknown): boolean {
  return (
    typeof err === "object" &&
    err !== null &&
    "code" in err &&
    (err as { code: string }).code === "P2034"
  );
}

function midtransOrderId(bookingId: string, attempt: number): string {
  return `midtrans-${bookingId}-${attempt}`;
}

export interface StartMidtransResult {
  paymentId: string;
  snapToken: string;
  snapJsUrl: string;
  clientKey: string;
  expiresAt: Date;
  orderId: string;
}

export type ApplyOutcome =
  | { ok: true; status: "updated" | "skipped" | "ignored" | "booking_conflict" }
  | { ok: false; reason: "amount_mismatch" };

export type WebhookOutcome =
  | ApplyOutcome
  | { ok: false; reason: "signature_mismatch" };

/**
 * Bentuk minimum yang dibutuhkan oleh `applyGatewayStatus` — bisa berasal dari
 * webhook callback (Midtrans → kita) atau dari hasil GET /v2/{order_id}/status
 * (kita → Midtrans saat rekonsiliasi). Bedanya cuma asal dan apakah signature
 * perlu dicek.
 */
interface GatewayUpdatePayload {
  order_id: string;
  gross_amount: string;
  transaction_status: string;
  fraud_status?: string | null;
  transaction_id?: string;
  payment_type?: string;
  /** Snapshot mentah untuk audit trail di `Payment.rawCallback`. */
  rawSource: Prisma.InputJsonValue;
}

/**
 * State machine: terjemahkan status dari gateway ke perubahan Payment + Booking
 * di DB. Idempotent: kalau payment sudah final, hanya update rawCallback.
 * Tidak melakukan auth — caller wajib pastikan source-nya terpercaya
 * (signature webhook valid, atau response Midtrans Core API).
 */
async function applyGatewayStatus(
  update: GatewayUpdatePayload
): Promise<ApplyOutcome> {
  const payment = await prisma.payment.findUnique({
    where: { externalOrderId: update.order_id },
    include: { booking: true },
  });

  if (!payment) {
    return { ok: true, status: "ignored" };
  }

  const amountFromGateway = Math.round(Number(update.gross_amount));
  if (
    Number.isNaN(amountFromGateway) ||
    amountFromGateway !== payment.amount
  ) {
    await prisma.payment.update({
      where: { id: payment.id },
      data: {
        rawCallback: update.rawSource,
        rejectionReason: `Amount mismatch: gateway=${update.gross_amount}, expected=${payment.amount}`,
      },
    });
    return { ok: false, reason: "amount_mismatch" };
  }

  const finalStatuses = new Set([
    "PAID",
    "FAILED",
    "EXPIRED",
    "CANCELLED",
    "REFUNDED",
  ]);
  if (finalStatuses.has(payment.status)) {
    await prisma.payment.update({
      where: { id: payment.id },
      data: { rawCallback: update.rawSource },
    });
    return { ok: true, status: "skipped" };
  }

  const newStatus = mapMidtransStatus(
    update.transaction_status,
    update.fraud_status ?? null
  );

  let lastErr: unknown;
  for (let attempt = 0; attempt < SERIAL_TX_ATTEMPTS; attempt++) {
    try {
      await prisma.$transaction(
        async (tx) => {
          const now = new Date();

          const currentBooking = await tx.booking.findUnique({
            where: { id: payment.bookingId },
            select: { status: true, participantId: true },
          });

          const bookingInConflictState =
            currentBooking?.status === "CANCELLED" ||
            currentBooking?.status === "REFUNDED" ||
            currentBooking?.status === "EXPIRED";

          const conflictNote =
            bookingInConflictState && newStatus === "PAID"
              ? `Gateway PAID but Booking is ${currentBooking.status}. Manual review required (potential refund).`
              : null;

          await tx.payment.update({
            where: { id: payment.id },
            data: {
              status: newStatus,
              externalTxId: update.transaction_id ?? null,
              method: update.payment_type ?? null,
              rawCallback: update.rawSource,
              paidAt: newStatus === "PAID" ? now : null,
              failedAt:
                newStatus === "FAILED" || newStatus === "EXPIRED" ? now : null,
              rejectionReason: conflictNote,
            },
          });

          if (newStatus === "PAID" && !bookingInConflictState) {
            await tx.booking.update({
              where: { id: payment.bookingId },
              data: { status: "PAID" },
            });
            await tx.tripParticipant.update({
              where: { id: payment.booking.participantId },
              data: { paymentConfirmedAt: now, markedPaidAt: now },
            });
            await payoutService.createForPaidBooking(tx, {
              bookingId: payment.bookingId,
            });
          }
        },
        {
          isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
          maxWait: 5000,
          timeout: 15000,
        }
      );

      const finalBooking = await prisma.booking.findUnique({
        where: { id: payment.bookingId },
        select: { status: true },
      });
      const isConflict =
        newStatus === "PAID" && finalBooking?.status !== "PAID";

      // Notif email user kalau payment benar-benar berhasil di-apply ke booking.
      if (newStatus === "PAID" && !isConflict) {
        void notifyPaymentPaid(payment.id);
      }
      // E3.3 — pembayaran kadaluarsa/gagal: kabari user supaya bisa retry.
      if (newStatus === "EXPIRED" || newStatus === "FAILED") {
        void notifyPaymentFailed(payment.id);
      }

      return {
        ok: true,
        status: isConflict ? "booking_conflict" : "updated",
      };
    } catch (e) {
      lastErr = e;
      if (isSerializationConflict(e) && attempt < SERIAL_TX_ATTEMPTS - 1) {
        continue;
      }
      throw e;
    }
  }
  throw lastErr instanceof Error
    ? lastErr
    : new Error("Gagal apply status gateway karena konflik transaksi");
}

export const paymentService = {
  /**
   * Mulai pembayaran Midtrans untuk Booking. Idempotent — kalau ada Payment
   * MIDTRANS aktif (status PENDING/AWAITING), reuse token yang sudah ada
   * selama belum expired.
   */
  async startMidtransPayment(
    bookingId: string,
    userId: string,
    options?: { finishUrl?: string }
  ): Promise<StartMidtransResult> {
    const clientKey = process.env.NEXT_PUBLIC_MIDTRANS_CLIENT_KEY;
    if (!clientKey) {
      throw new Error("NEXT_PUBLIC_MIDTRANS_CLIENT_KEY belum di-set");
    }

    const booking = await prisma.booking.findUnique({
      where: { id: bookingId },
      include: {
        user: { select: { id: true, name: true, email: true } },
        trip: { select: { title: true, date: true } },
        payments: {
          where: { provider: "MIDTRANS" },
          orderBy: { createdAt: "desc" },
        },
      },
    });

    if (!booking) {
      throw new Error("Booking tidak ditemukan");
    }
    if (booking.userId !== userId) {
      throw new Error("Booking ini bukan milikmu");
    }
    if (booking.amount <= 0) {
      throw new Error("Booking gratis tidak butuh pembayaran online");
    }
    if (booking.status === "PAID") {
      throw new Error("Booking sudah lunas");
    }
    if (booking.status !== "AWAITING_PAY") {
      throw new Error(
        "Booking belum siap menerima pembayaran (tunggu approve organizer)"
      );
    }
    if (isTripDepartureDayPast(booking.trip.date)) {
      throw new Error(
        "Trip sudah lewat tanggal berangkat — pembayaran ditutup"
      );
    }

    // Reuse Payment AWAITING/PENDING yang belum kadaluarsa, kalau ada.
    const now = new Date();
    const reusable = booking.payments.find(
      (p) =>
        (p.status === "PENDING" || p.status === "AWAITING") &&
        p.snapToken &&
        (!p.expiresAt || p.expiresAt.getTime() > now.getTime())
    );

    const snapJsUrl = MIDTRANS.snapJsUrl();

    if (reusable && reusable.snapToken) {
      return {
        paymentId: reusable.id,
        snapToken: reusable.snapToken,
        snapJsUrl,
        clientKey,
        expiresAt: reusable.expiresAt ?? new Date(now.getTime() + 24 * 3600 * 1000),
        orderId: reusable.externalOrderId,
      };
    }

    // Tutup attempt lama yang sudah expired (housekeeping ringan)
    const expiredIds = booking.payments
      .filter(
        (p) =>
          (p.status === "PENDING" || p.status === "AWAITING") &&
          p.expiresAt &&
          p.expiresAt.getTime() <= now.getTime()
      )
      .map((p) => p.id);
    if (expiredIds.length > 0) {
      await prisma.payment.updateMany({
        where: { id: { in: expiredIds } },
        data: { status: "EXPIRED" },
      });
    }

    const attemptNumber = booking.payments.length + 1;
    const orderId = midtransOrderId(booking.id, attemptNumber);
    const expirySeconds = 24 * 3600; // 24 jam, default Midtrans
    const expiresAt = new Date(now.getTime() + expirySeconds * 1000);

    // Create Payment row dulu (PENDING) supaya kalau call gagal, kita tetap punya audit trail.
    const payment = await prisma.payment.create({
      data: {
        bookingId: booking.id,
        provider: "MIDTRANS",
        externalOrderId: orderId,
        amount: booking.amount,
        status: "PENDING",
        expiresAt,
      },
    });

    let snapResult;
    try {
      snapResult = await createSnapTransaction({
        orderId,
        grossAmount: booking.amount,
        customer: {
          name: booking.user.name,
          email: booking.user.email,
        },
        itemName: booking.trip.title,
        expirySeconds,
        finishUrl: options?.finishUrl,
      });
    } catch (err) {
      // Roll back Payment ke FAILED supaya orderId tidak nyangkut PENDING selamanya.
      await prisma.payment.update({
        where: { id: payment.id },
        data: {
          status: "FAILED",
          failedAt: new Date(),
          rejectionReason: err instanceof Error ? err.message : "Snap API error",
        },
      });
      throw err;
    }

    const updated = await prisma.payment.update({
      where: { id: payment.id },
      data: {
        snapToken: snapResult.token,
        status: "AWAITING",
      },
    });

    return {
      paymentId: updated.id,
      snapToken: snapResult.token,
      snapJsUrl,
      clientKey,
      expiresAt,
      orderId,
    };
  },

  /**
   * Handle webhook callback dari Midtrans. Idempotent — boleh dipanggil berulang.
   * Verifikasi signature dulu, lalu delegate ke `applyGatewayStatus`.
   */
  async handleMidtransWebhook(
    payload: MidtransWebhookBody
  ): Promise<WebhookOutcome> {
    const signatureValid = verifyMidtransSignature(
      payload.order_id,
      payload.status_code,
      payload.gross_amount,
      payload.signature_key
    );
    if (!signatureValid) {
      return { ok: false, reason: "signature_mismatch" };
    }

    return applyGatewayStatus({
      order_id: payload.order_id,
      gross_amount: payload.gross_amount,
      transaction_status: payload.transaction_status,
      fraud_status: payload.fraud_status ?? null,
      transaction_id: payload.transaction_id,
      payment_type: payload.payment_type,
      rawSource: payload as unknown as Prisma.InputJsonValue,
    });
  },

  /**
   * Rekonsiliasi server-side: tarik status terkini dari Midtrans Core API,
   * lalu apply ke DB. Dipakai saat user kembali dari Snap (redirect flow) atau
   * saat webhook belum sampai (mis. dev di localhost). Aman dipanggil
   * berulang — idempotent via `applyGatewayStatus`.
   *
   * Auth: caller harus pastikan `userId` adalah owner booking; kita verifikasi
   * di sini lewat lookup payment → booking.userId.
   */
  /**
   * Admin variant `reconcileFromGateway` — skip ownership check (admin bypass).
   * Dipakai dari `/admin/bookings/[id]` saat user lapor "sudah bayar tapi
   * status belum update". Idempotent: aman dipanggil berulang.
   */
  async adminReconcile(
    orderId: string
  ): Promise<
    | {
        ok: true;
        status:
          | "updated"
          | "skipped"
          | "ignored"
          | "booking_conflict"
          | "not_found";
      }
    | { ok: false; reason: "amount_mismatch" | "not_found" }
  > {
    const payment = await prisma.payment.findUnique({
      where: { externalOrderId: orderId },
      select: { id: true },
    });
    if (!payment) {
      return { ok: false, reason: "not_found" };
    }

    const status = await fetchMidtransTransactionStatus(orderId);
    if (!status) {
      return { ok: true, status: "not_found" };
    }

    return applyGatewayStatus({
      order_id: status.order_id,
      gross_amount: status.gross_amount,
      transaction_status: status.transaction_status,
      fraud_status: status.fraud_status ?? null,
      transaction_id: status.transaction_id,
      payment_type: status.payment_type,
      rawSource: status as unknown as Prisma.InputJsonValue,
    });
  },

  async reconcileFromGateway(
    orderId: string,
    userId: string
  ): Promise<
    | {
        ok: true;
        status:
          | "updated"
          | "skipped"
          | "ignored"
          | "booking_conflict"
          | "not_found";
      }
    | { ok: false; reason: "amount_mismatch" | "forbidden" | "not_found" }
  > {
    const payment = await prisma.payment.findUnique({
      where: { externalOrderId: orderId },
      include: { booking: { select: { userId: true } } },
    });
    if (!payment) {
      return { ok: false, reason: "not_found" };
    }
    if (payment.booking.userId !== userId) {
      return { ok: false, reason: "forbidden" };
    }

    const status = await fetchMidtransTransactionStatus(orderId);
    if (!status) {
      return { ok: true, status: "not_found" };
    }

    const result = await applyGatewayStatus({
      order_id: status.order_id,
      gross_amount: status.gross_amount,
      transaction_status: status.transaction_status,
      fraud_status: status.fraud_status ?? null,
      transaction_id: status.transaction_id,
      payment_type: status.payment_type,
      rawSource: status as unknown as Prisma.InputJsonValue,
    });

    return result;
  },
};

async function notifyPaymentPaid(paymentId: string) {
  const payment = await prisma.payment.findUnique({
    where: { id: paymentId },
    include: {
      booking: {
        include: {
          user: { select: { email: true, name: true } },
          trip: { select: { id: true, title: true } },
        },
      },
    },
  });
  if (!payment) return;
  await emailService.send({
    to: payment.booking.user.email,
    idempotencyKey: `payment_paid-${payment.id}`,
    template: {
      template: "payment_paid",
      data: {
        userName: payment.booking.user.name,
        tripTitle: payment.booking.trip.title,
        tripId: payment.booking.trip.id,
        amount: payment.amount,
      },
    },
  });
}

/** E3.3 — kabari user kalau pembayaran expired/gagal supaya bisa retry. */
async function notifyPaymentFailed(paymentId: string) {
  const payment = await prisma.payment.findUnique({
    where: { id: paymentId },
    include: {
      booking: {
        include: {
          user: { select: { email: true, name: true } },
          trip: { select: { id: true, title: true } },
        },
      },
    },
  });
  if (!payment) return;
  await emailService.send({
    to: payment.booking.user.email,
    idempotencyKey: `payment_expired-${payment.id}`,
    template: {
      template: "payment_expired",
      data: {
        userName: payment.booking.user.name,
        tripTitle: payment.booking.trip.title,
        tripId: payment.booking.trip.id,
        amount: payment.amount,
      },
    },
  });
}

// Re-export untuk testing langsung kalau perlu (tetap private dari modul lain).
export const _internal = { applyGatewayStatus };
export type { MidtransTransactionStatus };
