import { Types } from "mongoose";
import GasStationPaymentModel from "../models/GasStationPaymentModel";
import GasStationModel from "../models/GasStationModel";
import UserModel from "../models/UserModel";
import Trip from "../models/TripModel";
import { gasStationApprovedForAppFilter } from "../utils/gasStationVisibility";
import {
  applyWalletDebit,
  applyWalletCredit,
  getWalletBalanceCents,
} from "./walletService";
import WalletTransactionModel from "../models/WalletTransactionModel";
import { sendEmail } from "../utils/SendEmail";
import { gasStationPaymentReceiptEmail } from "../utils/SendEmail/templates";
import { PaginatedResult } from "./connectionService";
import { IGasStationPayment } from "../interfaces/models/gasStationPaymentInterface";
import { notify } from "./notificationService";

function dollarsToCents(dollars: number): number {
  return Math.round(dollars * 100);
}

function centsToDollars(cents: number): number {
  return Math.round(cents) / 100;
}

function formatUsd(cents: number): string {
  return `$${centsToDollars(cents).toFixed(2)}`;
}

function generateReceiptNumber(): string {
  const d = new Date();
  const y = d.getUTCFullYear();
  const m = String(d.getUTCMonth() + 1).padStart(2, "0");
  const day = String(d.getUTCDate()).padStart(2, "0");
  const rand = Math.random().toString(36).slice(2, 8).toUpperCase();
  return `TT-${y}${m}${day}-${rand}`;
}

function mapPayment(
  payment: IGasStationPayment | Record<string, unknown>,
  extras?: Record<string, unknown>,
): Record<string, unknown> {
  const p = payment as IGasStationPayment;
  return {
    _id: p._id,
    receiptNumber: p.receiptNumber,
    payerId: p.payerId,
    gasStationId: p.gasStationId,
    stationOwnerId: p.stationOwnerId,
    amount: centsToDollars(p.amountCents),
    amountCents: p.amountCents,
    currency: p.currency,
    status: p.status,
    note: p.note ?? null,
    tripId: p.tripId ?? null,
    paidAt: p.paidAt ?? null,
    createdAt: p.createdAt,
    updatedAt: p.updatedAt,
    ...extras,
  };
}

function isDuplicateKeyError(err: unknown): boolean {
  return (
    typeof err === "object" &&
    err !== null &&
    "code" in err &&
    (err as { code: number }).code === 11000
  );
}

async function sendReceiptEmails(paymentId: string): Promise<void> {
  const payment = await GasStationPaymentModel.findById(paymentId).lean();
  if (!payment || payment.status !== "completed") return;

  const [payer, owner, station] = await Promise.all([
    UserModel.findById(payment.payerId).select("fullName email").lean(),
    UserModel.findById(payment.stationOwnerId).select("fullName email").lean(),
    GasStationModel.findById(payment.gasStationId)
      .select("name address")
      .lean(),
  ]);

  const amountLabel = formatUsd(payment.amountCents);
  const paidAtLabel = payment.paidAt
    ? new Date(payment.paidAt).toISOString()
    : new Date().toISOString();
  const stationName = station?.name ?? "Gas station";
  const stationAddress = station?.address ?? "";
  const payerName = payer?.fullName || payer?.email || "Customer";

  const jobs: Promise<boolean>[] = [];

  if (payer?.email) {
    jobs.push(
      sendEmail(
        payer.email,
        `Receipt ${payment.receiptNumber} — ${amountLabel}`,
        gasStationPaymentReceiptEmail({
          role: "payer",
          receiptNumber: payment.receiptNumber,
          amountLabel,
          stationName,
          stationAddress,
          payerName,
          paidAtLabel,
          note: payment.note,
        }),
      ),
    );
  }

  if (owner?.email) {
    jobs.push(
      sendEmail(
        owner.email,
        `Payment received ${payment.receiptNumber} — ${amountLabel}`,
        gasStationPaymentReceiptEmail({
          role: "station",
          receiptNumber: payment.receiptNumber,
          amountLabel,
          stationName,
          stationAddress,
          payerName,
          paidAtLabel,
          note: payment.note,
        }),
      ),
    );
  }

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

  const results = await Promise.allSettled(jobs);
  const anySent = results.some(
    (r) => r.status === "fulfilled" && r.value === true,
  );
  if (anySent) {
    await GasStationPaymentModel.updateOne(
      { _id: payment._id },
      { $set: { receiptEmailedAt: new Date() } },
    );
  }
}

async function loadStationSummary(gasStationId: Types.ObjectId | string) {
  return GasStationModel.findById(gasStationId)
    .select("name address userId")
    .lean();
}

async function buildPayResponse(
  payment: IGasStationPayment | Record<string, unknown>,
  extras?: {
    idempotentReplay?: boolean;
    walletBalanceCents?: number;
  },
): Promise<Record<string, unknown>> {
  const p = payment as IGasStationPayment;
  const station = await loadStationSummary(p.gasStationId);
  const walletBalanceCents =
    extras?.walletBalanceCents ??
    (await getWalletBalanceCents(String(p.payerId)));

  return mapPayment(p, {
    gasStation: station
      ? {
          _id: station._id,
          name: station.name,
          address: station.address,
        }
      : null,
    walletBalanceCents,
    walletBalance: centsToDollars(walletBalanceCents),
    ...(extras?.idempotentReplay ? { idempotentReplay: true } : {}),
  });
}

/**
 * Finish (or resume) debit → credit for a payment document.
 * Wallet ops are idempotent via externalRef — safe after crashes/retries.
 */
async function settlePayment(
  paymentDoc: InstanceType<typeof GasStationPaymentModel>,
): Promise<{
  payment: InstanceType<typeof GasStationPaymentModel>;
  walletBalanceCents: number;
}> {
  const paymentId = String(paymentDoc._id);
  const payerId = String(paymentDoc.payerId);
  const stationOwnerId = String(paymentDoc.stationOwnerId);
  const gasStationId = String(paymentDoc.gasStationId);
  const amountCents = paymentDoc.amountCents;

  const station = await loadStationSummary(gasStationId);
  const stationName = station?.name ?? "Gas station";

  const debit = await applyWalletDebit({
    userId: payerId,
    amountCents,
    type: "payment_debit",
    description: `Gas station payment — ${stationName} (${paymentDoc.receiptNumber})`,
    externalRef: `gsp_debit_${paymentId}`,
    metadata: {
      gasStationPaymentId: paymentId,
      gasStationId,
      stationOwnerId,
    },
  });

  if (!debit.ok) {
    paymentDoc.status = "failed";
    paymentDoc.failureReason = "insufficient_funds";
    await paymentDoc.save();
    throw new Error("INSUFFICIENT_FUNDS");
  }

  try {
    const credit = await applyWalletCredit({
      userId: stationOwnerId,
      amountCents,
      type: "payment_credit",
      description: `Gas station payment received — ${paymentDoc.receiptNumber}`,
      externalRef: `gsp_credit_${paymentId}`,
      metadata: {
        gasStationPaymentId: paymentId,
        gasStationId,
        payerId,
      },
    });

    paymentDoc.status = "completed";
    paymentDoc.paidAt = paymentDoc.paidAt ?? new Date();
    paymentDoc.payerWalletTxId = new Types.ObjectId(debit.transactionId);
    paymentDoc.payeeWalletTxId = new Types.ObjectId(credit.transactionId);
    paymentDoc.failureReason = null;
    await paymentDoc.save();

    const payer = await UserModel.findById(payerId)
      .select("fullName email")
      .lean();
    const payerName = (payer?.fullName || payer?.email || "A customer").trim();
    void notify({
      userId: stationOwnerId,
      type: "payment",
      eventType: "received",
      title: "Payment received",
      body: `${payerName} paid ${formatUsd(amountCents)} at ${stationName}`,
      data: {
        paymentId,
        gasStationId,
        payerId,
        receiptNumber: paymentDoc.receiptNumber,
        amountCents,
      },
      dedupeKey: `payment:received:${paymentId}`,
    });

    return {
      payment: paymentDoc,
      walletBalanceCents: debit.balanceAfterCents,
    };
  } catch (creditErr) {
    try {
      await applyWalletCredit({
        userId: payerId,
        amountCents,
        type: "refund",
        description: `Refund — failed gas station payment ${paymentDoc.receiptNumber}`,
        externalRef: `gsp_refund_${paymentId}`,
        metadata: { gasStationPaymentId: paymentId },
      });
    } catch (refundErr) {
      console.error(
        "[gasStationPayment] debit reverse failed after credit error",
        refundErr,
      );
    }
    paymentDoc.status = "failed";
    paymentDoc.failureReason =
      creditErr instanceof Error ? creditErr.message : "credit_failed";
    await paymentDoc.save();
    throw new Error("PAYMENT_FAILED");
  }
}

async function resumeOrReturnExisting(
  payment: InstanceType<typeof GasStationPaymentModel>,
): Promise<Record<string, unknown>> {
  if (payment.status === "completed") {
    return buildPayResponse(payment, { idempotentReplay: true });
  }

  if (payment.status === "pending") {
    const settled = await settlePayment(payment);
    void sendReceiptEmails(String(settled.payment._id)).catch((e) =>
      console.error("[gasStationPayment] receipt email failed", e),
    );
    return buildPayResponse(settled.payment, {
      idempotentReplay: true,
      walletBalanceCents: settled.walletBalanceCents,
    });
  }

  // failed: only resume if money was never refunded (crash after debit, before
  // successful compensate). If a refund ledger row exists, settle would credit
  // the station without re-debiting the payer — refuse and return snapshot.
  if (
    payment.status === "failed" &&
    payment.failureReason !== "insufficient_funds"
  ) {
    const refunded = await WalletTransactionModel.exists({
      userId: payment.payerId,
      externalRef: `gsp_refund_${payment._id}`,
      status: "completed",
    });
    if (!refunded) {
      const settled = await settlePayment(payment);
      void sendReceiptEmails(String(settled.payment._id)).catch((e) =>
        console.error("[gasStationPayment] receipt email failed", e),
      );
      return buildPayResponse(settled.payment, {
        idempotentReplay: true,
        walletBalanceCents: settled.walletBalanceCents,
      });
    }
  }

  // Terminal failed (insufficient funds, or already refunded): do not return 200 success
  if (payment.status === "failed") {
    if (payment.failureReason === "insufficient_funds") {
      throw new Error("INSUFFICIENT_FUNDS");
    }
    throw new Error("PAYMENT_PREVIOUSLY_FAILED");
  }

  return buildPayResponse(payment, { idempotentReplay: true });
}

export async function payGasStation(opts: {
  payerId: string;
  gasStationId: string;
  amountDollars: number;
  idempotencyKey: string;
  note?: string;
  tripId?: string;
}): Promise<Record<string, unknown>> {
  const { payerId, gasStationId, amountDollars, idempotencyKey, note, tripId } =
    opts;

  const existing = await GasStationPaymentModel.findOne({
    payerId: new Types.ObjectId(payerId),
    idempotencyKey,
  });

  if (existing) {
    return resumeOrReturnExisting(existing);
  }

  const station = await GasStationModel.findOne({
    _id: new Types.ObjectId(gasStationId),
    ...gasStationApprovedForAppFilter(),
  }).lean();

  if (!station) {
    const any = await GasStationModel.findById(gasStationId).select("_id").lean();
    throw new Error(any ? "STATION_UNAVAILABLE" : "STATION_NOT_FOUND");
  }

  const stationOwnerId = String(station.userId);
  if (stationOwnerId === payerId) {
    throw new Error("CANNOT_PAY_OWN");
  }

  if (tripId) {
    const trip = await Trip.findOne({
      _id: new Types.ObjectId(tripId),
      userId: new Types.ObjectId(payerId),
      isDeleted: false,
    })
      .select("_id")
      .lean();
    if (!trip) throw new Error("TRIP_NOT_FOUND");
  }

  const amountCents = dollarsToCents(amountDollars);
  if (amountCents < 1) throw new Error("INVALID_AMOUNT");

  const createPayload = {
    receiptNumber: generateReceiptNumber(),
    payerId: new Types.ObjectId(payerId),
    gasStationId: new Types.ObjectId(gasStationId),
    stationOwnerId: new Types.ObjectId(stationOwnerId),
    amountCents,
    currency: "usd",
    status: "pending" as const,
    note: note?.trim() || null,
    tripId: tripId ? new Types.ObjectId(tripId) : null,
    idempotencyKey,
  };

  let payment: InstanceType<typeof GasStationPaymentModel>;
  try {
    payment = await GasStationPaymentModel.create(createPayload);
  } catch (err: unknown) {
    if (!isDuplicateKeyError(err)) throw err;

    const raced = await GasStationPaymentModel.findOne({
      payerId: new Types.ObjectId(payerId),
      idempotencyKey,
    });
    if (raced) {
      return resumeOrReturnExisting(raced);
    }

    // Rare receiptNumber collision — retry create once with a new number
    payment = await GasStationPaymentModel.create({
      ...createPayload,
      receiptNumber: generateReceiptNumber(),
    });
  }

  const settled = await settlePayment(payment);
  void sendReceiptEmails(String(settled.payment._id)).catch((e) =>
    console.error("[gasStationPayment] receipt email failed", e),
  );

  return buildPayResponse(settled.payment, {
    walletBalanceCents: settled.walletBalanceCents,
  });
}

export async function getPaymentById(
  paymentId: string,
  viewerId: string,
): Promise<Record<string, unknown>> {
  if (!Types.ObjectId.isValid(paymentId)) throw new Error("NOT_FOUND");

  const payment = await GasStationPaymentModel.findById(paymentId).lean();
  if (!payment) throw new Error("NOT_FOUND");

  const isPayer = String(payment.payerId) === viewerId;
  const isOwner = String(payment.stationOwnerId) === viewerId;
  if (!isPayer && !isOwner) throw new Error("FORBIDDEN");

  const [station, payer] = await Promise.all([
    GasStationModel.findById(payment.gasStationId)
      .select("name address city state")
      .lean(),
    UserModel.findById(payment.payerId).select("fullName email image").lean(),
  ]);

  return mapPayment(payment as IGasStationPayment, {
    gasStation: station
      ? {
          _id: station._id,
          name: station.name,
          address: station.address,
          city: station.city ?? "",
          state: station.state ?? "",
        }
      : null,
    payer: payer
      ? {
          _id: payer._id,
          fullName: payer.fullName ?? "",
          email: payer.email ?? "",
        }
      : null,
    role: isPayer ? "payer" : "station",
  });
}

export async function listPaymentsByPayer(
  payerId: string,
  page: number,
  limit: number,
): Promise<PaginatedResult<Record<string, unknown>>> {
  const skip = (page - 1) * limit;
  const filter = {
    payerId: new Types.ObjectId(payerId),
    status: "completed" as const,
  };

  const [total, rows] = await Promise.all([
    GasStationPaymentModel.countDocuments(filter),
    GasStationPaymentModel.find(filter)
      .sort({ paidAt: -1, createdAt: -1 })
      .skip(skip)
      .limit(limit)
      .populate("gasStationId", "name address")
      .lean(),
  ]);

  const data = rows.map((row) => {
    const station = row.gasStationId as unknown as {
      _id: Types.ObjectId;
      name?: string;
      address?: string;
    } | null;
    return mapPayment(row as IGasStationPayment, {
      gasStation: station
        ? {
            _id: station._id,
            name: station.name ?? "",
            address: station.address ?? "",
          }
        : null,
    });
  });

  return {
    data,
    total,
    page,
    totalPages: Math.ceil(total / limit) || 0,
  };
}

export async function listPaymentsByStationOwner(
  ownerId: string,
  page: number,
  limit: number,
): Promise<PaginatedResult<Record<string, unknown>>> {
  const skip = (page - 1) * limit;
  const filter = {
    stationOwnerId: new Types.ObjectId(ownerId),
    status: "completed" as const,
  };

  const [total, rows] = await Promise.all([
    GasStationPaymentModel.countDocuments(filter),
    GasStationPaymentModel.find(filter)
      .sort({ paidAt: -1, createdAt: -1 })
      .skip(skip)
      .limit(limit)
      .populate("payerId", "fullName email")
      .populate("gasStationId", "name address")
      .lean(),
  ]);

  const data = rows.map((row) => {
    const payer = row.payerId as unknown as {
      _id: Types.ObjectId;
      fullName?: string;
      email?: string;
    } | null;
    const station = row.gasStationId as unknown as {
      _id: Types.ObjectId;
      name?: string;
      address?: string;
    } | null;
    return mapPayment(row as IGasStationPayment, {
      payer: payer
        ? {
            _id: payer._id,
            fullName: payer.fullName ?? "",
            email: payer.email ?? "",
          }
        : null,
      gasStation: station
        ? {
            _id: station._id,
            name: station.name ?? "",
            address: station.address ?? "",
          }
        : null,
    });
  });

  return {
    data,
    total,
    page,
    totalPages: Math.ceil(total / limit) || 0,
  };
}
