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 { MOCK_PLATFORM_FEE_RATE, MOCK_VAT_RATE } from '@/services/payment/constants';
import {
  BNPL_QUERY_PLAN,
  BNPL_QUERY_PROVIDER,
  BNPL_QUERY_REQUEST_ID,
  BNPL_QUERY_TRANSACTION_ID,
  MOCK_CREDIT_CEILING_IRR,
  MOCK_NOT_ELIGIBLE_LAST_DIGIT,
} from '../constants';
import type {
  AcceptBnplScheduleInput,
  BnplApi,
  BnplEligibilityResult,
  BnplInstallmentRow,
  BnplInstallmentStatus,
  BnplOptions,
  BnplOrderStatus,
  BnplPlanOption,
  BnplProvider,
  BnplSchedule,
  BnplSettlementResult,
  BnplStatus,
  CheckEligibilityInput,
  GetBnplScheduleInput,
  IssueBnplTokenInput,
  IssueBnplTokenResult,
  ProviderCode,
  WalletInstallmentPlan,
} 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: fractions 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 ZERO = BigInt(0);

function fractionPpm(fraction: number): bigint {
  return BigInt(Math.round(fraction * Number(RATE_SCALE)));
}

/** `YYYY-MM-DD` for a whole-month offset from today (mock schedule dates — runs client-side, `new Date()` is fine). */
function isoMonthsFromNow(months: number): string {
  const d = new Date();
  d.setMonth(d.getMonth() + months);
  return d.toISOString().slice(0, 10);
}

function todayIso(): string {
  return new Date().toISOString().slice(0, 10);
}

/**
 * A plan template — the provider-config a real BNPL adapter owns. `feePercent` is the customer-facing plan
 * fee (0 = بدون سود / interest-free, matching the interest-free-to-customer model for the ۴-قسط plans);
 * the wireframe shows دیجی‌پی's longer terms carrying a small fee (کارمزد ۴٪/۹٪). All amounts the client
 * shows are computed HERE (the server's job) and served as digit-strings — the client never derives them.
 */
interface PlanTemplate {
  planId: string;
  termMonths: number | null;
  installmentCount: number;
  feePercent: number;
  downPaymentPercent: number;
}

interface ProviderTemplate {
  providerCode: ProviderCode;
  plans: PlanTemplate[];
}

// Provider/plan set as **data** (per phase §Out-of-scope: treat the provider list as data, no multi-provider
// routing UI). دیجی‌پی (۳/۶/۱۲ ماهه), اسنپ‌پی (۴ قسط بدون سود), اقساط بالین‌یار (طرح داخلی) — matching the
// wireframe. Shapes/fees follow product/payments/bnpl-landscape.md; never hardcode a fee in the UI.
const PROVIDER_TEMPLATES: ProviderTemplate[] = [
  {
    providerCode: 'digipay',
    plans: [
      { planId: 'digipay_3m', termMonths: 3, installmentCount: 3, feePercent: 0, downPaymentPercent: 0.2 },
      { planId: 'digipay_6m', termMonths: 6, installmentCount: 6, feePercent: 0.04, downPaymentPercent: 0.2 },
      { planId: 'digipay_12m', termMonths: 12, installmentCount: 12, feePercent: 0.09, downPaymentPercent: 0.2 },
    ],
  },
  {
    providerCode: 'snapppay',
    plans: [{ planId: 'snapppay_4', termMonths: null, installmentCount: 4, feePercent: 0, downPaymentPercent: 0.25 }],
  },
  {
    providerCode: 'balinyaar',
    plans: [{ planId: 'balinyaar_4', termMonths: null, installmentCount: 4, feePercent: 0, downPaymentPercent: 0.25 }],
  },
];

/** The frozen order gross the provider finances: variant price × session count, never client-supplied. */
function requestGross(request: BookingRequestDto): string {
  if (request.variantPrice == null) {
    throw new ApiError(409, 'Request has no priced variant', 'unpriced_request');
  }
  return multiplyIrr(request.variantPrice, SESSION_COUNT);
}

/** Split a plan's amounts (all BigInt): total (= gross + customer-facing fee), down-payment, regular installment. */
function planAmounts(grossIrr: string, plan: PlanTemplate) {
  const gross = parseIrr(grossIrr);
  const total = gross + (gross * fractionPpm(plan.feePercent)) / RATE_SCALE;
  const downPayment = (total * fractionPpm(plan.downPaymentPercent)) / RATE_SCALE;
  const financed = total - downPayment;
  const regular = plan.installmentCount > 0 ? financed / BigInt(plan.installmentCount) : ZERO;
  // The final installment absorbs the integer-division remainder so the rows sum to `total` exactly.
  const last = financed - regular * BigInt(Math.max(plan.installmentCount - 1, 0));
  return { total, downPayment, regular, last };
}

function toPlanOption(grossIrr: string, plan: PlanTemplate): BnplPlanOption {
  const { total, downPayment, regular } = planAmounts(grossIrr, plan);
  return {
    planId: plan.planId,
    termMonths: plan.termMonths,
    installmentCount: plan.installmentCount,
    feePercent: plan.feePercent,
    downPaymentPercent: plan.downPaymentPercent,
    monthlyAmountIrr: regular.toString(),
    downPaymentIrr: downPayment.toString(),
    totalIrr: total.toString(),
  };
}

/** Build the served repayment rows (down-payment today + N monthly installments; last absorbs the remainder). */
function buildScheduleRows(grossIrr: string, plan: PlanTemplate): BnplInstallmentRow[] {
  const { downPayment, regular, last } = planAmounts(grossIrr, plan);
  const rows: BnplInstallmentRow[] = [
    { sequence: 0, kind: 'down_payment', dueDate: todayIso(), amountIrr: downPayment.toString() },
  ];
  for (let i = 1; i <= plan.installmentCount; i += 1) {
    const amount = i === plan.installmentCount ? last : regular;
    rows.push({ sequence: i, kind: 'installment', dueDate: isoMonthsFromNow(i), amountIrr: amount.toString() });
  }
  return rows;
}

function findPlan(providerCode: ProviderCode, planId: string): PlanTemplate {
  const provider = PROVIDER_TEMPLATES.find((p) => p.providerCode === providerCode);
  const plan = provider?.plans.find((p) => p.planId === planId);
  if (!plan) throw new ApiError(400, 'Unknown BNPL plan', 'unknown_plan');
  return plan;
}

// The b8 booking amounts a settled order confirms (a settled BNPL order = a card payment net-of-fee, so the
// nurse payout is invariant to method — same split as the f9 capture). Commission net + VAT = balinyaar cut.
const FEE_RATE_PPM = fractionPpm(MOCK_PLATFORM_FEE_RATE);
const VAT_RATE_PPM = fractionPpm(MOCK_VAT_RATE);

function bookingAmounts(grossIrr: string) {
  const gross = parseIrr(grossIrr);
  const commissionNet = (gross * FEE_RATE_PPM) / RATE_SCALE;
  const vat = (commissionNet * VAT_RATE_PPM) / RATE_SCALE;
  const payout = gross - commissionNet - vat;
  return {
    grossPriceIrr: gross.toString(),
    balinyaarCommissionIrr: (commissionNet + vat).toString(),
    nursePayoutAmount: payout.toString(),
    pspFeeAmount: ((gross * BigInt(200)) / RATE_SCALE).toString(),
  };
}

interface MockBnplTx {
  bnplTransactionId: number;
  paymentTransactionId: number;
  bookingRequestId: number;
  providerCode: ProviderCode;
  planId: string;
  idempotencyKey: string;
  status: BnplStatus;
  externalPaymentToken: string;
  orderAmountIrr: string;
  totalIrr: string;
  installmentCount: number;
  schedule: BnplInstallmentRow[];
  bookingId: number | null;
  createdAt: string;
}

// Module-level stores (one browser session) — same singleton pattern as the f7/f8/f9 mocks, so the C6
// branch, the return settle, and the Wallet all observe the same order.
let transactions: MockBnplTx[] = [];
let walletPlans: WalletInstallmentPlan[] = [];
let nextBnplTxId = 71_001;
let nextPaymentTxId = 72_001;

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

const providerEarlyPayUrl = (providerCode: ProviderCode, txId: number): string =>
  `https://provider.example/${providerCode}/installments/${txId}/early-pay`;

/**
 * Seed one active plan so D5 shows a provider-reported outstanding balance out of the box (like the refunds
 * mock seeds a failed refund). A settled دیجی‌پی ۶-ماهه: قسط ۱ پرداخت‌شده, قسط ۲ سررسید نزدیک, ۳–۶ آینده —
 * the paid/due/future mix the wireframe shows. Outstanding = Σ of the not-yet-paid rows.
 */
function seedWallet(): void {
  const rows: BnplInstallmentRow[] = [
    { sequence: 0, kind: 'down_payment', dueDate: isoMonthsFromNow(-1), amountIrr: '4660000', status: 'paid' },
    { sequence: 1, kind: 'installment', dueDate: isoMonthsFromNow(-1), amountIrr: '4040000', status: 'paid' },
    { sequence: 2, kind: 'installment', dueDate: isoMonthsFromNow(0), amountIrr: '4040000', status: 'due_soon' },
    { sequence: 3, kind: 'installment', dueDate: isoMonthsFromNow(1), amountIrr: '4040000', status: 'upcoming' },
    { sequence: 4, kind: 'installment', dueDate: isoMonthsFromNow(2), amountIrr: '4040000', status: 'upcoming' },
    { sequence: 5, kind: 'installment', dueDate: isoMonthsFromNow(3), amountIrr: '4040000', status: 'upcoming' },
    { sequence: 6, kind: 'installment', dueDate: isoMonthsFromNow(4), amountIrr: '4040000', status: 'upcoming' },
  ];
  const outstanding = rows
    .filter((r) => r.status !== 'paid')
    .reduce((acc, r) => acc + parseIrr(r.amountIrr), ZERO);
  const nextDue = rows.find((r) => r.status === 'due_soon') ?? rows.find((r) => r.status === 'upcoming') ?? null;
  walletPlans = [
    {
      bnplTransactionId: 70_900,
      bookingId: 5001,
      providerCode: 'digipay',
      status: 'settled',
      serviceLabel: 'مراقبت سالمند — شیفت روز',
      outstandingBalanceIrr: outstanding.toString(),
      installmentCount: 6,
      nextDueDate: nextDue?.dueDate ?? null,
      nextAmountIrr: nextDue?.amountIrr ?? null,
      installments: rows,
      earlyPayUrl: providerEarlyPayUrl('digipay', 70_900),
      createdAt: isoMonthsFromNow(-1),
    },
  ];
}

seedWallet();

/** Provider-reported due status for a freshly settled plan: down-payment paid, next installment due-soon, rest upcoming. */
function toWalletRows(schedule: BnplInstallmentRow[]): BnplInstallmentRow[] {
  return schedule.map((row) => {
    let status: BnplInstallmentStatus;
    if (row.kind === 'down_payment') status = 'paid';
    else if (row.sequence === 1) status = 'due_soon';
    else status = 'upcoming';
    return { ...row, status };
  });
}

/** The settle side effect: convert the request, insert the confirmed booking (net-of-fee), seed the Wallet plan. */
function settle(request: BookingRequestDto, tx: MockBnplTx): BnplSettlementResult {
  const amounts = bookingAmounts(tx.orderAmountIrr);
  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,
    pspFeeAmount: amounts.pspFeeAmount,
    platformFeeRate: MOCK_PLATFORM_FEE_RATE,
    scheduledDate: request.requestedDate,
    scheduledTimeStart: request.requestedTimeStart,
    scheduledTimeEnd: request.requestedTimeEnd,
  });

  const converted = mockMarkBookingRequestConverted(request.id, booking.id);
  tx.status = 'settled';
  tx.bookingId = booking.id;

  const walletRows = toWalletRows(tx.schedule);
  const outstanding = walletRows
    .filter((r) => r.status !== 'paid')
    .reduce((acc, r) => acc + parseIrr(r.amountIrr), ZERO);
  const nextDue = walletRows.find((r) => r.status !== 'paid') ?? null;
  walletPlans = [
    {
      bnplTransactionId: tx.bnplTransactionId,
      bookingId: booking.id,
      providerCode: tx.providerCode,
      status: 'settled',
      serviceLabel: request.variantLabel,
      outstandingBalanceIrr: outstanding.toString(),
      installmentCount: tx.installmentCount,
      nextDueDate: nextDue?.dueDate ?? null,
      nextAmountIrr: nextDue?.amountIrr ?? null,
      installments: walletRows,
      earlyPayUrl: providerEarlyPayUrl(tx.providerCode, tx.bnplTransactionId),
      createdAt: new Date().toISOString(),
    },
    ...walletPlans,
  ];

  return toSettlementResult(converted, tx);
}

function toSettlementResult(request: BookingRequestDto, tx: MockBnplTx): BnplSettlementResult {
  return {
    bnplTransactionId: tx.bnplTransactionId,
    bookingRequestId: request.id,
    requestStatus: request.status,
    status: tx.status,
    bookingId: request.bookingId ?? tx.bookingId,
    providerCode: tx.providerCode,
  };
}

function toInitiateResult(tx: MockBnplTx): IssueBnplTokenResult {
  const query = new URLSearchParams();
  query.set(BNPL_QUERY_TRANSACTION_ID, String(tx.bnplTransactionId));
  query.set(BNPL_QUERY_REQUEST_ID, String(tx.bookingRequestId));
  query.set(BNPL_QUERY_PROVIDER, tx.providerCode);
  query.set(BNPL_QUERY_PLAN, tx.planId);
  return {
    bnplTransactionId: tx.bnplTransactionId,
    paymentTransactionId: tx.paymentTransactionId,
    status: tx.status,
    externalPaymentToken: tx.externalPaymentToken,
    // App-relative (the wizard prepends the locale); the real provider returns an absolute https URL.
    redirectUrl: `${ROUTES.CHECKOUT_BNPL_GATEWAY}?${query.toString()}`,
  };
}

/**
 * In-memory mock behind the `BnplApi` seam — the whole D1–D5 installment surface b12 doesn't serve
 * (no provider/plan options, no repayment schedule, no provider-reported Wallet status → REQ-022/023/024).
 * It reads the frozen request gross from the shared f7 store, plays the provider (eligibility verdict,
 * plan math, token/redirect), and on settle (the down-payment-cleared analogue of the webhook) runs the
 * SAME conversion bridge f9 uses — a settled BNPL order is a card payment net-of-fee — then seeds a
 * provider-reported Wallet plan. Swap to the real `clientApi` once the upstream is real and the REQs land
 * (`USE_BNPL_MOCK = false`).
 */
export const bnplMockApi: BnplApi = {
  getBnplOptions: async (bookingRequestId) => {
    await sleep(MOCK_LATENCY_MS);
    const request = await bookingRequestsMockApi.get(bookingRequestId);
    const gross = requestGross(request);
    const providers: BnplProvider[] = PROVIDER_TEMPLATES.map((p) => ({
      providerCode: p.providerCode,
      tagline: null,
      plans: p.plans.map((plan) => toPlanOption(gross, plan)),
    }));
    const options: BnplOptions = {
      bookingRequestId: request.id,
      requestStatus: request.status,
      orderAmountIrr: gross,
      // The §1 branch flag — mock always eligible; D3 is the real gate. Never a client-side threshold.
      bnplEligible: true,
      providers,
    };
    return options;
  },

  checkEligibility: async ({ bookingRequestId, providerCode, nationalId }: CheckEligibilityInput) => {
    await sleep(MOCK_LATENCY_MS);
    const request = await bookingRequestsMockApi.get(bookingRequestId);
    const gross = parseIrr(requestGross(request));
    const ceiling = parseIrr(MOCK_CREDIT_CEILING_IRR);

    // Deterministic declined branches so a tester can exercise both (§7 step 3): last digit 0 → declined;
    // an order above the ceiling → ceiling_exceeded. The real credit check is the provider's — the client
    // only surfaces the verdict, never pre-judges it.
    if (nationalId.endsWith(MOCK_NOT_ELIGIBLE_LAST_DIGIT)) {
      return declined('not_eligible');
    }
    if (gross > ceiling) {
      return declined('ceiling_exceeded');
    }
    const provider = PROVIDER_TEMPLATES.find((p) => p.providerCode === providerCode);
    const installmentCount = provider?.plans[0]?.installmentCount ?? 4;
    const result: BnplEligibilityResult = {
      eligibilityStatus: 'eligible',
      isEligible: true,
      installmentCount,
      planSummary: null,
      creditCeilingIrr: MOCK_CREDIT_CEILING_IRR,
    };
    return result;
  },

  getBnplSchedule: async ({ bookingRequestId, providerCode, planId }: GetBnplScheduleInput) => {
    await sleep(MOCK_LATENCY_MS);
    const request = await bookingRequestsMockApi.get(bookingRequestId);
    const plan = findPlan(providerCode, planId);
    const gross = requestGross(request);
    const rows = buildScheduleRows(gross, plan);
    const { total, downPayment } = planAmounts(gross, plan);
    const schedule: BnplSchedule = {
      bookingRequestId,
      providerCode,
      planId,
      totalIrr: total.toString(),
      downPaymentIrr: downPayment.toString(),
      installmentCount: plan.installmentCount,
      rows,
    };
    return schedule;
  },

  issueBnplToken: async ({ bookingRequestId, providerCode, planId, idempotencyKey }: IssueBnplTokenInput) => {
    await sleep(MOCK_LATENCY_MS);
    const request = await bookingRequestsMockApi.get(bookingRequestId);
    const existing = latestTxFor(bookingRequestId);
    if (request.status === 'converted' || existing?.status === 'settled') {
      throw new ApiError(409, 'Request already paid', 'already_paid');
    }
    if (request.status !== 'accepted_awaiting_payment') {
      throw new ApiError(409, 'Request is not awaiting payment', 'not_awaiting_payment');
    }
    // b12 idempotency: a retried initiate with the same key reuses the token/order.
    if (existing && existing.idempotencyKey === idempotencyKey && existing.status === 'token_issued') {
      return toInitiateResult(existing);
    }

    const plan = findPlan(providerCode, planId);
    const gross = requestGross(request);
    const { total } = planAmounts(gross, plan);
    const bnplTransactionId = nextBnplTxId++;
    const tx: MockBnplTx = {
      bnplTransactionId,
      paymentTransactionId: nextPaymentTxId++,
      bookingRequestId,
      providerCode,
      planId,
      idempotencyKey,
      status: 'token_issued',
      externalPaymentToken: `mock-bnpl-token-${gross}-bnpl-br-${bookingRequestId}`,
      orderAmountIrr: gross,
      totalIrr: total.toString(),
      installmentCount: plan.installmentCount,
      schedule: buildScheduleRows(gross, plan),
      bookingId: null,
      createdAt: new Date().toISOString(),
    };
    transactions = [tx, ...transactions];
    return toInitiateResult(tx);
  },

  acceptBnplSchedule: async ({ bookingRequestId, bnplTransactionId, outcome }: AcceptBnplScheduleInput) => {
    await sleep(MOCK_LATENCY_MS);
    const request = await bookingRequestsMockApi.get(bookingRequestId);
    const tx =
      (bnplTransactionId != null ? transactions.find((t) => t.bnplTransactionId === bnplTransactionId) : undefined) ??
      latestTxFor(bookingRequestId);

    // Replays / double-taps converge (the webhook-dedup analogue): if settlement already happened, report it.
    if (request.status === 'converted' || tx?.status === 'settled') {
      return toSettlementResult(request, tx ?? fallbackTx(bookingRequestId));
    }
    if (!tx) throw new ApiError(404, 'BNPL order not found', 'not_found');

    if (outcome === 'failure' || request.status !== 'accepted_awaiting_payment') {
      // Provider declined the order, or the payment window lapsed during the handoff.
      tx.status = 'failed';
      return toSettlementResult(request, tx);
    }
    return settle(request, tx);
  },

  getBnplOrder: async (bookingRequestId) => {
    await sleep(MOCK_LATENCY_MS);
    const request = await bookingRequestsMockApi.get(bookingRequestId);
    const tx = latestTxFor(bookingRequestId);
    if (!tx) throw new ApiError(404, 'BNPL order not found', 'not_found');
    const order: BnplOrderStatus = {
      id: tx.bnplTransactionId,
      paymentTransactionId: tx.paymentTransactionId,
      bookingId: request.bookingId ?? tx.bookingId,
      providerCode: tx.providerCode,
      status: tx.status,
      eligibilityStatus: 'eligible',
      orderAmountIrr: tx.orderAmountIrr,
      settledAmountIrr: tx.status === 'settled' ? bookingAmounts(tx.orderAmountIrr).nursePayoutAmount : null,
      bnplCommissionIrr: tx.status === 'settled' ? bookingAmounts(tx.orderAmountIrr).balinyaarCommissionIrr : null,
      currency: 'IRR',
      installmentCount: tx.installmentCount,
      settledAt: tx.status === 'settled' ? new Date().toISOString() : null,
      createdAt: tx.createdAt,
    };
    return order;
  },

  getWalletInstallments: async () => {
    await sleep(MOCK_LATENCY_MS);
    return walletPlans.map((p) => ({ ...p, installments: p.installments.map((r) => ({ ...r })) }));
  },
};

function declined(status: 'not_eligible' | 'ceiling_exceeded'): BnplEligibilityResult {
  return {
    eligibilityStatus: status,
    isEligible: false,
    installmentCount: 0,
    planSummary: null,
    creditCeilingIrr: null,
  };
}

/** A minimal tx stand-in for a settlement replay where the request already converted but no tx row survives. */
function fallbackTx(bookingRequestId: number): MockBnplTx {
  return {
    bnplTransactionId: 0,
    paymentTransactionId: 0,
    bookingRequestId,
    providerCode: 'digipay',
    planId: 'digipay_6m',
    idempotencyKey: '',
    status: 'settled',
    externalPaymentToken: '',
    orderAmountIrr: '0',
    totalIrr: '0',
    installmentCount: 0,
    schedule: [],
    bookingId: null,
    createdAt: new Date().toISOString(),
  };
}
