import { multiplyIrr, parseIrr, sleep } from '@/utils';
import { ApiError } from '@/lib/api/errors';
import { ROUTES } from '@/constants';
import {
  bookingRequestsMockApi,
  mockMarkBookingRequestConverted,
} from '@/services/bookingRequests/apis/mockApi';
import { mockInsertConvertedBooking } from '@/services/bookings/apis/mockApi';
import type { BookingRequestDto } from '@/services/bookingRequests/types';
import {
  CHECKOUT_QUERY_REQUEST_ID,
  CHECKOUT_QUERY_TRANSACTION_ID,
  MOCK_PLATFORM_FEE_RATE,
  MOCK_VAT_RATE,
} from '../constants';
import type {
  CheckoutSummaryDto,
  ConfirmGatewayReturnInput,
  InitiatePaymentInput,
  InitiatePaymentResult,
  InvoiceDto,
  PaymentApi,
  PaymentOutcomeDto,
  PaymentTransactionStatus,
} from '../types';

const MOCK_LATENCY_MS = 350;

/** A request is a single visit until multi-session requests exist (b8 carries one date/time window). */
const SESSION_COUNT = 1;

// Integer-only rate math: rates as parts-per-10000 so the money path never touches a float.
// (BigInt via the constructor — the tsconfig target predates ES2020 literals, matching utils/money.ts.)
const RATE_SCALE = BigInt(10_000);
const FEE_RATE_PPM = BigInt(Math.round(MOCK_PLATFORM_FEE_RATE * Number(RATE_SCALE)));
const VAT_RATE_PPM = BigInt(Math.round(MOCK_VAT_RATE * Number(RATE_SCALE)));

/**
 * The C6 money decomposition, all BigInt (mirrors what REQ-016 asks the server to serve):
 * commission (net) = gross × feeRate; VAT = commission × vatRate (b11 rule: VAT is computed ON the
 * commission, the platform's taxable supply); service cost = the remainder (= the nurse payout leg).
 * Rows reconcile by construction: service + commission + vat = gross, and the b10 three-amount
 * invariant holds with balinyaarCommission = commission + vat.
 */
function splitAmounts(grossIrr: string) {
  const gross = parseIrr(grossIrr);
  const commissionNet = (gross * FEE_RATE_PPM) / RATE_SCALE;
  const vat = (commissionNet * VAT_RATE_PPM) / RATE_SCALE;
  const serviceCost = gross - commissionNet - vat;
  return {
    grossPriceIrr: gross.toString(),
    commissionIrr: commissionNet.toString(),
    vatIrr: vat.toString(),
    serviceCostIrr: serviceCost.toString(),
    balinyaarCommissionIrr: (commissionNet + vat).toString(),
    nursePayoutAmount: serviceCost.toString(),
  };
}

interface MockTransaction {
  transactionId: number;
  bookingRequestId: number;
  idempotencyKey: string;
  status: PaymentTransactionStatus;
  gatewayReferenceCode: string;
  grossPriceIrr: string;
  bookingId: number | null;
}

// Module-level stores (one browser session) — the same singleton pattern as the f7/f8 mocks, so the
// checkout, the C5 poll, and the bookings list all observe the same capture.
let transactions: MockTransaction[] = [];
const invoices: Record<number, InvoiceDto> = {};
let nextTransactionId = 42_001;
let nextInvoiceId = 101;

function latestTransactionFor(bookingRequestId: number): MockTransaction | undefined {
  return transactions.find((t) => t.bookingRequestId === bookingRequestId);
}

function requestGross(request: BookingRequestDto): string {
  // The frozen gross the server would charge: variant price × session count, never client-supplied.
  // A request without a price (REQ-013 unmet) must fail loudly — never a reconciling 0-rial checkout.
  if (request.variantPrice == null) {
    throw new ApiError(409, 'Request has no priced variant', 'unpriced_request');
  }
  return multiplyIrr(request.variantPrice, SESSION_COUNT);
}

function toOutcome(request: BookingRequestDto, transaction: MockTransaction | undefined): PaymentOutcomeDto {
  return {
    bookingRequestId: request.id,
    requestStatus: request.status,
    transactionStatus: transaction?.status ?? null,
    bookingId: request.bookingId ?? transaction?.bookingId ?? null,
  };
}

/** The webhook-confirm stand-in: flip the transaction, convert the request, insert the booking, invoice it. */
function capture(request: BookingRequestDto, transaction: MockTransaction): PaymentOutcomeDto {
  const amounts = splitAmounts(transaction.grossPriceIrr);
  const booking = mockInsertConvertedBooking({
    bookingRequestId: request.id,
    nurseId: request.nurseId,
    nurseName: request.nurseName,
    patientId: request.patientId,
    patientName: request.patientName,
    variantId: request.variantId,
    variantLabel: request.variantLabel,
    variantPriceUnit: request.variantPriceUnit,
    customerAddressId: request.customerAddressId,
    addressSnapshotJson: JSON.stringify({
      title: request.addressTitle,
      city: request.cityNameFa,
      district: request.districtNameFa,
      line: request.addressLine,
      postalCode: request.postalCode,
    }),
    grossPriceIrr: amounts.grossPriceIrr,
    balinyaarCommissionIrr: amounts.balinyaarCommissionIrr,
    nursePayoutAmount: amounts.nursePayoutAmount,
    // The PSP's cut is a platform expense outside the three-amount split; 2% matches the f8 seeds.
    pspFeeAmount: ((parseIrr(amounts.grossPriceIrr) * BigInt(200)) / RATE_SCALE).toString(),
    platformFeeRate: MOCK_PLATFORM_FEE_RATE,
    scheduledDate: request.requestedDate,
    scheduledTimeStart: request.requestedTimeStart,
    scheduledTimeEnd: request.requestedTimeEnd,
  });

  const converted = mockMarkBookingRequestConverted(request.id, booking.id);
  transaction.status = 'succeeded';
  transaction.bookingId = booking.id;

  invoices[booking.id] = {
    id: nextInvoiceId,
    bookingId: booking.id,
    invoiceNumber: `INV-${String(nextInvoiceId).padStart(10, '0')}`,
    issuingEntityType: 'platform',
    grossIrr: amounts.grossPriceIrr,
    platformCommissionIrr: amounts.commissionIrr,
    bnplCommissionIrr: null,
    vatRate: MOCK_VAT_RATE,
    vatIrr: amounts.vatIrr,
    moadianReferenceNumber: null,
    moadianStatus: 'pending',
    pdfUrl: null,
    issuedAt: new Date().toISOString(),
  };
  nextInvoiceId += 1;

  return toOutcome(converted, transaction);
}

/**
 * In-memory mock behind the `PaymentApi` seam — the missing conversion trigger between the f7 and f8
 * mock stores. It serves the unserved C6 summary (REQ-016), plays the PSP + webhook roles (initiate →
 * app-relative gateway-harness redirect → capture on the success return), enforces b10's idempotency
 * semantics (same-key retry reuses the attempt; a repeat initiate after capture is a `409`; a replayed
 * return is a no-op), and issues the b11-shaped invoice at capture. Swap to the real `clientApi` once
 * the upstream domains are real and REQ-016/017 land (`USE_PAYMENT_MOCK = false`).
 */
export const paymentMockApi: PaymentApi = {
  getCheckoutSummary: async (bookingRequestId) => {
    await sleep(MOCK_LATENCY_MS);
    const request = await bookingRequestsMockApi.get(bookingRequestId);
    const amounts = splitAmounts(requestGross(request));
    const summary: CheckoutSummaryDto = {
      bookingRequestId: request.id,
      requestStatus: request.status,
      nurseName: request.nurseName,
      patientName: request.patientName,
      variantLabel: request.variantLabel,
      variantPriceUnit: request.variantPriceUnit,
      sessionCount: SESSION_COUNT,
      requestedDate: request.requestedDate,
      requestedTimeStart: request.requestedTimeStart,
      requestedTimeEnd: request.requestedTimeEnd,
      paymentDeadlineAt: request.paymentDeadlineAt,
      serviceCostIrr: amounts.serviceCostIrr,
      commissionIrr: amounts.commissionIrr,
      vatIrr: amounts.vatIrr,
      vatRate: MOCK_VAT_RATE,
      totalIrr: amounts.grossPriceIrr,
      grossPriceIrr: amounts.grossPriceIrr,
      balinyaarCommissionIrr: amounts.balinyaarCommissionIrr,
      nursePayoutAmount: amounts.nursePayoutAmount,
    };
    return summary;
  },

  initiatePayment: async ({ bookingRequestId, idempotencyKey }: InitiatePaymentInput) => {
    await sleep(MOCK_LATENCY_MS);
    const request = await bookingRequestsMockApi.get(bookingRequestId);

    const existing = latestTransactionFor(bookingRequestId);
    if (request.status === 'converted' || existing?.status === 'succeeded') {
      throw new ApiError(409, 'Request already paid', 'already_paid');
    }
    if (request.status !== 'accepted_awaiting_payment') {
      // Covers the lapsed 30-minute window (`payment_deadline_expired`) and every other non-payable state.
      throw new ApiError(409, 'Request is not awaiting payment', 'not_awaiting_payment');
    }
    // b10 idempotency: a retried start with the same `Idempotency-Key` reuses the attempt/reference.
    if (existing && existing.idempotencyKey === idempotencyKey && existing.status === 'pending') {
      return toInitiateResult(existing);
    }

    const transaction: MockTransaction = {
      transactionId: nextTransactionId++,
      bookingRequestId,
      idempotencyKey,
      status: 'pending',
      gatewayReferenceCode: `mock-ref-${bookingRequestId}-${idempotencyKey.slice(0, 8)}`,
      grossPriceIrr: requestGross(request),
      bookingId: null,
    };
    transactions = [transaction, ...transactions];
    return toInitiateResult(transaction);
  },

  confirmGatewayReturn: async ({ bookingRequestId, transactionId, outcome }: ConfirmGatewayReturnInput) => {
    await sleep(MOCK_LATENCY_MS);
    const request = await bookingRequestsMockApi.get(bookingRequestId);
    const transaction =
      (transactionId != null ? transactions.find((t) => t.transactionId === transactionId) : undefined) ??
      latestTransactionFor(bookingRequestId);

    // Replays / double-taps converge (the webhook dedup analogue): if capture already happened, report it.
    if (request.status === 'converted' || transaction?.status === 'succeeded') {
      return toOutcome(request, transaction);
    }
    if (!transaction) throw new ApiError(404, 'Payment transaction not found', 'not_found');

    if (outcome === 'failure' || request.status !== 'accepted_awaiting_payment') {
      // A declined gateway, or the payment window lapsed while the customer was at the gateway.
      transaction.status = 'failed';
      return toOutcome(request, transaction);
    }
    return capture(request, transaction);
  },

  getPaymentOutcome: async (bookingRequestId) => {
    await sleep(MOCK_LATENCY_MS);
    const request = await bookingRequestsMockApi.get(bookingRequestId);
    return toOutcome(request, latestTransactionFor(bookingRequestId));
  },

  getInvoice: async (bookingId) => {
    await sleep(MOCK_LATENCY_MS);
    const invoice = invoices[bookingId];
    // b11: the invoice exists only once issued — a clean 404 until then (the UI shows "not issued yet").
    if (!invoice) throw new ApiError(404, 'Invoice not issued', 'not_issued');
    return { ...invoice };
  },
};

function toInitiateResult(transaction: MockTransaction): InitiatePaymentResult {
  const query = new URLSearchParams();
  query.set(CHECKOUT_QUERY_TRANSACTION_ID, String(transaction.transactionId));
  query.set(CHECKOUT_QUERY_REQUEST_ID, String(transaction.bookingRequestId));
  return {
    transactionId: transaction.transactionId,
    // App-relative (the checkout prepends the locale); the real PSP returns an absolute https URL.
    redirectUrl: `${ROUTES.CHECKOUT_GATEWAY}?${query.toString()}`,
    gatewayReferenceCode: transaction.gatewayReferenceCode,
  };
}
