import { clientFetch } from '@/lib/api/client';
import { unwrap, type ApiEnvelope } from '@/lib/api/types';
import { ApiError } from '@/lib/api/errors';
import type {
  AdminRefundResult,
  CancelBookingInput,
  CancellationPolicyPreview,
  InitiateRefundInput,
  RefundChannel,
  RefundPreview,
  RefundStatus,
  RefundSummary,
  RefundsApi,
} from '../types';

const BOOKINGS = '/api/v1/bookings';
const REFUNDS = '/api/v1/refunds';
const ADMIN_REFUNDS = '/api/v1/admin_refunds';

/**
 * The thin b11 customer refund payload (`GET refunds/{id}/status`) — the only refund shape the contract
 * exposes to a customer. The fee-leg decomposition + policy snapshot live on the admin-only
 * `RefundListItem`, so this maps into `RefundSummary` with those fields `null` until REQ-021 serves them.
 */
interface RefundStatusWire {
  id: number;
  bookingId: number;
  status: RefundStatus;
  refundChannel: RefundChannel;
  amount: string;
  expectedCustomerRefundEta: string | null;
  reference: string | null;
}

function toSummary(wire: RefundStatusWire): RefundSummary {
  return {
    id: wire.id,
    bookingId: wire.bookingId,
    refundStatus: wire.status,
    refundChannel: wire.refundChannel,
    totalRefundedIrr: wire.amount,
    expectedCustomerRefundEta: wire.expectedCustomerRefundEta,
    externalRevertReference: wire.reference,
    // REQ-021: the customer status carries no decomposition/policy/timestamps yet — the fee-split section
    // is hidden until these are served (the mock fills them so the transparency split demos end-to-end).
    refundPercentageApplied: null,
    cancellationPolicyCode: null,
    platformFeeRefundedIrr: null,
    nursePayoutRefundedIrr: null,
    createdAt: null,
    completedAt: null,
  };
}

/**
 * Real HTTP implementation of the `RefundsApi` seam. Only `getRefund` maps a **published** b11 route
 * (`GET refunds/{id}/status`, tenancy-scoped); the other three target contract gaps the frontend filed
 * (which is why the domain stays mock-primary — see `constants.ts`):
 *  - `resolveCancellationPolicy` → REQ-020 (`GET bookings/{id}/cancellation_policy`): b9 snapshots the
 *    policy only *after* a cancel; there is no pre-cancel preview resolving the tier by current lead time
 *    + per-session refundability.
 *  - `cancelBooking` → REQ-019 (`POST bookings/{id}/cancel`): b11 refunds are admin-only, no customer path.
 *  - `getRefundByBooking` → REQ-021 (`GET refunds/by_booking/{id}`): the customer cannot obtain a refund
 *    id from the admin-only worklist, so it needs to reach its refund from the booking. `404` = no refund.
 *
 * NOT the primary implementation this phase (`USE_REFUNDS_MOCK = true`).
 */
export const refundsClientApi: RefundsApi = {
  resolveCancellationPolicy: async (bookingId: number) =>
    unwrap(
      await clientFetch<ApiEnvelope<CancellationPolicyPreview>>(
        `${BOOKINGS}/${bookingId}/cancellation_policy`,
      ),
    ),

  cancelBooking: async ({ bookingId, sessionIds, reasonCategory, reasonNotes }: CancelBookingInput) =>
    toSummary(
      unwrap(
        await clientFetch<ApiEnvelope<RefundStatusWire>>(`${BOOKINGS}/${bookingId}/cancel`, {
          method: 'POST',
          body: JSON.stringify({ sessionIds, reasonCategory, reasonNotes }),
        }),
      ),
    ),

  getRefundByBooking: async (bookingId: number) => {
    try {
      return toSummary(
        unwrap(await clientFetch<ApiEnvelope<RefundStatusWire>>(`${REFUNDS}/by_booking/${bookingId}`)),
      );
    } catch (error) {
      // No refund for this booking (e.g. not cancelled) is a clean empty state, not a failure.
      if (error instanceof ApiError && error.status === 404) return null;
      throw error;
    }
  },

  getRefund: async (refundId: number) =>
    toSummary(unwrap(await clientFetch<ApiEnvelope<RefundStatusWire>>(`${REFUNDS}/${refundId}/status`))),

  // REQ-035: refund preview endpoint. b11 computes the fee-leg decomposition only *on create* (there is no
  // read-only preview route), yet the admin console must disclose the split before initiating. Filed as a
  // proposed `GET api/v1/admin_refunds/preview?booking_id=&ticket_id=`; the mock serves it today.
  getRefundPreview: async (bookingId: number, ticketId: number | null) => {
    const params = new URLSearchParams({ booking_id: String(bookingId) });
    if (ticketId != null) params.set('ticket_id', String(ticketId));
    return unwrap(
      await clientFetch<ApiEnvelope<RefundPreview>>(`${ADMIN_REFUNDS}/preview?${params.toString()}`),
    );
  },

  // Create + immediately execute a ticket-linked refund (b11 `POST api/v1/admin_refunds`). The response is
  // already `AdminRefundResult`-shaped (refundId/status/channel/decomposed legs/eta/clawbackId).
  initiateRefund: async (input: InitiateRefundInput) =>
    unwrap(
      await clientFetch<ApiEnvelope<AdminRefundResult>>(ADMIN_REFUNDS, {
        method: 'POST',
        body: JSON.stringify({
          bookingId: input.bookingId,
          ticketId: input.ticketId,
          refundPercentage: input.refundPercentage,
          refundChannel: input.refundChannel,
          reasonCategory: input.reasonCategory,
          reasonNotes: input.reasonNotes,
        }),
      }),
    ),

  // REQ-035: approve/reject a refund. b11 has no separate approve/reject step — `POST admin_refunds` both
  // creates and executes — so a failed-channel retry and an explicit rejection are proposed as
  // `POST api/v1/admin_refunds/{id}/approve` and `.../{id}/reject`; the mock serves both today.
  approveRefund: async (refundId: number) =>
    unwrap(
      await clientFetch<ApiEnvelope<AdminRefundResult>>(`${ADMIN_REFUNDS}/${refundId}/approve`, {
        method: 'POST',
      }),
    ),

  // REQ-035: see approveRefund. Reject records a reason and moves the refund to `rejected`.
  rejectRefund: async (refundId: number, reason: string) => {
    unwrap(
      await clientFetch<ApiEnvelope<true>>(`${ADMIN_REFUNDS}/${refundId}/reject`, {
        method: 'POST',
        body: JSON.stringify({ reason }),
      }),
    );
  },
};
