import type { BookingSessionStatus, BookingStatus } from '@/services/bookings/types';

/**
 * Refunds & cancellation domain (b11 contract `dev/contracts/domains/refunds-invoices.md`). This is the
 * **customer** half of the refund story: resolve the applicable cancellation policy by lead time, disclose
 * the fee/refund split before confirming, request the cancellation, then follow the read-only refund
 * status. The admin refund console (create/approve, leg-split editor, clawback banner, retry) is DEFERRED
 * to f15-b15.
 *
 * Load-bearing semantics (contract + product `07-cancellation-and-refunds.md` / `cancellation-and-payout.md`):
 *  - **Refunds are admin-only.** The customer can *request* a cancellation and *read* the refund's progress;
 *    it can never self-issue money. The copy reflects the admin-approved, ticket-linked reality.
 *  - **Money is IRR integer, on the wire as a digit-string.** Parse/format only via the `@/utils` BigInt
 *    helpers; Toman is display-only. The refund is the decomposition of `gross = commission + payout` —
 *    render `platformFeeRefundedIrr` / `nursePayoutRefundedIrr` as served; never recompute the split.
 *  - **Disclose the fee/refund % BEFORE confirm.** The policy (resolved by lead time + actor) and its
 *    refund % + fee % must be on screen and acknowledged before the cancellation can be submitted.
 *  - **BNPL is asynchronous.** For `bnpl_revert`, surface the `expectedCustomerRefundEta` (~7–10 business
 *    days) honestly — the money returns *through the provider*, never instantly, never Balinyaar → customer.
 *  - **Per-session, not all-or-nothing.** Only un-started sessions are refundable; completed-and-verified
 *    sessions stay payout-eligible and render as locked.
 *  - **Never render a label off a raw enum code** — codes map to i18n keys in both locales.
 */

/**
 * `refunds.status` (b11 contract enum, forward-only). A card refund goes `approved → succeeded`
 * immediately; a BNPL/manual refund sits in `processing` until the async customer cash-back reconciles.
 * The customer-facing UI maps these six codes onto three steps (see `refundCustomerStep`).
 */
export type RefundStatus = 'requested' | 'approved' | 'processing' | 'succeeded' | 'failed' | 'rejected';

/** `refunds.refund_channel` (b11). The data-model's `manual_bank` is served as the canonical `manual`. */
export type RefundChannel = 'psp_card' | 'bnpl_revert' | 'manual';

/** The three customer-facing refund steps the six contract statuses collapse onto. */
export const CUSTOMER_REFUND_STEP_ORDER = ['submitted', 'on_its_way', 'completed'] as const;
export type CustomerRefundStep = (typeof CUSTOMER_REFUND_STEP_ORDER)[number];

/**
 * Maps the contract `RefundStatus` onto the customer's mental model: *submitted → on its way → completed*,
 * with `failed`/`rejected` collapsing to a distinct error state (never a fourth happy step).
 */
export function refundCustomerStep(status: RefundStatus): CustomerRefundStep | 'failed' {
  switch (status) {
    case 'requested':
    case 'approved':
      return 'submitted';
    case 'processing':
      return 'on_its_way';
    case 'succeeded':
      return 'completed';
    case 'failed':
    case 'rejected':
      return 'failed';
  }
}

/** Zero-based `activeStep` for the three-step refund stepper (only meaningful for the non-`failed` steps). */
export function refundStepIndex(status: RefundStatus): number {
  const step = refundCustomerStep(status);
  if (step === 'failed') return CUSTOMER_REFUND_STEP_ORDER.length - 1;
  return CUSTOMER_REFUND_STEP_ORDER.indexOf(step);
}

/** `succeeded` (completed) or `failed`/`rejected` (dead) — nothing left to poll. */
export function isTerminalRefundStatus(status: RefundStatus): boolean {
  return status === 'succeeded' || status === 'failed' || status === 'rejected';
}

/**
 * Proposed cancellation-policy tier codes (REQ-020). The product docs describe the tiers (free > 24h,
 * partial < 24h, customer no-show) but pin **no** wire code-names, so these are the client-mock codes the
 * UI maps to i18n keys; when the backend defines the real `cancellation_policy_code` set the map updates.
 * **Never** render a label off the raw code.
 */
export type CancellationPolicyCode = 'free_24h' | 'partial_under_24h' | 'customer_no_show';

/** The lead-time bucket that resolved the tier — drives an explanatory i18n label, not the money. */
export type CancellationLeadTime = 'gt_24h' | 'lt_24h' | 'started';

/** Whether the whole booking or only the remaining (un-started) sessions are being cancelled. */
export type CancellationScope = 'whole_booking' | 'remaining_sessions';

/** Why a session is refundable or locked — maps to an i18n reason chip. `un_started` ⇔ refundable. */
export type CancellationSessionReason = 'un_started' | BookingSessionStatus;

/** One session's refundability in the cancellation preview (refundable ⇔ un-started; locked otherwise). */
export interface CancellationSessionPreview {
  bookingSessionId: number;
  sessionIndex: number;
  /** ISO date `YYYY-MM-DD`. */
  scheduledDate: string;
  refundable: boolean;
  /** `un_started` when refundable; otherwise the blocking session status (`completed`/`in_progress`/…). */
  reasonCode: CancellationSessionReason;
}

/**
 * The resolved cancellation preview shown BEFORE confirm (REQ-020 — not served by b11 yet, mock-primary).
 * `refundAmountIrr + feeAmountIrr = refundableAmountIrr` by construction, so the disclosure's
 * `PriceBreakdown` (refund-vs-fee split) reconciles to the rial. The fee-leg decomposition
 * (`platformFeeRefundedIrr` / `nursePayoutRefundedIrr`) is served, never recomputed client-side.
 */
export interface CancellationPolicyPreview {
  bookingId: number;
  /** `false` when nothing is refundable (already cancelled/completed, or no un-started sessions). */
  cancellable: boolean;
  cancellationPolicyCode: CancellationPolicyCode;
  /** 0–1 fraction of the refundable amount returned to the customer. */
  refundPercentageApplied: number;
  /** 0–1 fraction retained as the cancellation fee/penalty (`= 1 - refundPercentageApplied`). */
  feePercentage: number;
  /** IRR digit-string — the amount refunded to the customer. */
  refundAmountIrr: string;
  /** IRR digit-string — the amount retained as the fee. */
  feeAmountIrr: string;
  /** IRR digit-string — the base being decided (`refundAmountIrr + feeAmountIrr`; the un-started gross). */
  refundableAmountIrr: string;
  /** Decomposition of `refundAmountIrr` across the two fee legs (served, never recomputed). */
  platformFeeRefundedIrr: string;
  nursePayoutRefundedIrr: string;
  appliesTo: CancellationScope;
  leadTimeLabel: CancellationLeadTime;
  /** The channel the refund will run through — drives the ETA preview wording. */
  refundChannel: RefundChannel;
  /** Populated only for `bnpl_revert` (the ~7–10 business-day window); a date `YYYY-MM-DD`. */
  expectedCustomerRefundEta: string | null;
  /** The refundable session ids the confirm submits (all un-started sessions). */
  refundableSessionIds: number[];
  sessions: CancellationSessionPreview[];
}

/** The customer's stated reason category for cancelling — maps to an i18n label, never rendered raw. */
export type CancelReasonCategory =
  | 'changed_mind'
  | 'schedule_conflict'
  | 'found_other_care'
  | 'other';

/** `POST bookings/{bookingId}/cancel` input (REQ-019 — customer-initiated; not served by b11 yet). */
export interface CancelBookingInput {
  bookingId: number;
  /** The un-started sessions to cancel (whole-remaining by default); omitted = all refundable. */
  sessionIds?: number[];
  reasonCategory: CancelReasonCategory;
  reasonNotes?: string;
}

/**
 * The customer-facing refund view. A superset of the thin b11 `GET refunds/{id}/status` payload
 * (`{ id, bookingId, status, refundChannel, amount, expectedCustomerRefundEta, reference }`) — the mock
 * fills the whole shape; the real `clientApi` maps the thin contract and leaves the decomposition fields
 * `null` until REQ-021 exposes them to the customer. The UI renders the fee-leg split only when present.
 */
export interface RefundSummary {
  id: number;
  bookingId: number;
  refundStatus: RefundStatus;
  refundChannel: RefundChannel;
  /** IRR digit-string — the total refunded to the customer (the contract's `amount`). */
  totalRefundedIrr: string;
  /** Populated for `bnpl_revert` (the ~7–10 business-day window); a date `YYYY-MM-DD`. */
  expectedCustomerRefundEta: string | null;
  /** Opaque, **masked** (last 4 only) external reference — never parse it. */
  externalRevertReference: string | null;
  /** --- Fee-leg decomposition + policy snapshot (REQ-021: `null` on the real path until served). --- */
  refundPercentageApplied: number | null;
  cancellationPolicyCode: string | null;
  platformFeeRefundedIrr: string | null;
  nursePayoutRefundedIrr: string | null;
  createdAt: string | null;
  completedAt: string | null;
}

/**
 * Whether a booking can still be cancelled by the customer (has an un-started remainder). `confirmed` or
 * `in_progress` are candidates; the resolved preview reports `cancellable: false` if no un-started session
 * actually remains. Terminal/settled states (`completed`/`closed`/`disputed`/`cancelled`/`pending_payment`)
 * are never customer-cancellable here.
 */
export function isBookingCancellable(status: BookingStatus): boolean {
  return status === 'confirmed' || status === 'in_progress';
}

/* ----------------------------------------------------------------------------------------------------
 * Admin refund tooling (b11 `POST`/`GET api/v1/admin_refunds`). The console half of the story: an admin
 * previews the fee-leg decomposition, then initiates (create + immediately execute) a **ticket-linked**
 * refund — every initiate carries a `ticketId`. The server computes the split; the client only renders it.
 * Money stays an IRR digit-string; the client NEVER recomputes the percentage or the fee/payout legs —
 * `platformFeeRefundedIrr + nursePayoutRefundedIrr === amount` is a server invariant, rendered as served.
 * -------------------------------------------------------------------------------------------------- */

/**
 * The server's refund decomposition preview for a booking (admin console, before initiate). Reconciles by
 * construction: `platformFeeRefundedIrr + nursePayoutRefundedIrr === amountIrr`. `willCreateClawback` warns
 * that the nurse was already paid (a post-payout refund opens a `pending` clawback + support alert); the
 * `bnpl_revert` channel carries the ~7–10-business-day `expectedCustomerRefundEta` (a `YYYY-MM-DD` date).
 */
export interface RefundPreview {
  bookingId: number;
  refundPercentageApplied: number;
  amountIrr: string;
  platformFeeRefundedIrr: string;
  nursePayoutRefundedIrr: string;
  refundChannel: RefundChannel; // 'psp_card' | 'bnpl_revert' | 'manual'
  expectedCustomerRefundEta: string | null;
  willCreateClawback: boolean;
  cancellationPolicyCode: string | null;
}

/**
 * `POST api/v1/admin_refunds` input. Supply **either** `refundPercentage` (0–1) **or** neither (the server
 * falls back to the booking's b9 cancellation-snapshot percentage). `ticketId` links the refund to its
 * support ticket — nullable only until `refund_ticket_required` config is enforced (b15).
 */
export interface InitiateRefundInput {
  bookingId: number;
  ticketId: number | null;
  refundPercentage?: number;
  refundChannel?: RefundChannel;
  reasonCategory?: string;
  reasonNotes?: string;
}

/**
 * The `POST api/v1/admin_refunds` result — the refund was created **and executed**. A card refund returns
 * `succeeded` immediately (no ETA); a BNPL/manual refund returns `processing` with an
 * `expectedCustomerRefundEta`; a post-payout refund sets `clawbackId`. `status` reuses the domain's
 * `RefundStatus` (a channel refusal returns `failed`, retryable via approve).
 */
export interface AdminRefundResult {
  refundId: number;
  bookingId: number;
  status: RefundStatus;
  refundChannel: RefundChannel;
  amount: string;
  platformFeeRefundedIrr: string;
  nursePayoutRefundedIrr: string;
  expectedCustomerRefundEta: string | null;
  clawbackId: number | null;
}

/**
 * The refunds API seam — the real HTTP client and the in-memory mock both implement this interface;
 * selection is by config (`USE_REFUNDS_MOCK`), never scattered `if (mock)` checks.
 */
export interface RefundsApi {
  resolveCancellationPolicy(bookingId: number): Promise<CancellationPolicyPreview>;
  cancelBooking(input: CancelBookingInput): Promise<RefundSummary>;
  /** `null` when the booking has no refund (e.g. not cancelled) — a clean empty state, not an error. */
  getRefundByBooking(bookingId: number): Promise<RefundSummary | null>;
  getRefund(refundId: number): Promise<RefundSummary>;

  /* --- Admin refund tooling (b11 admin_refunds; every initiate is ticket-linked). --- */
  /** The server's fee-leg decomposition preview for a booking (`ticketId` = the linking ticket, or null). */
  getRefundPreview(bookingId: number, ticketId: number | null): Promise<RefundPreview>;
  initiateRefund(input: InitiateRefundInput): Promise<AdminRefundResult>;
  approveRefund(refundId: number): Promise<AdminRefundResult>;
  rejectRefund(refundId: number, reason: string): Promise<void>;
}
