import { clientFetch } from '@/lib/api/client';
import { unwrap, type ApiEnvelope, type Paginated } from '@/lib/api/types';
import { PAYOUTS_PAGE_SIZE } from '../constants';
import type {
  AdminPayoutBatchDetail,
  AdminPayoutRow,
  EarningsListParams,
  EligibleNurseEarnings,
  NurseEarningsItem,
  NurseEarningsSummary,
  NursePayoutDetail,
  NursePayoutHistoryItem,
  PayoutBatchFilters,
  PayoutBatchStatus,
  PayoutBatchSummary,
  PayoutStatus,
  PayoutsApi,
} from '../types';
import type { PageParams } from '@/lib/api/types';

const NURSE_PAYOUTS = '/api/v1/nurse_payouts';
const ADMIN_PAYOUTS = '/api/v1/admin_payouts';

/** The header b13's process/retry read the per-run idempotency key from (same convention as b10/b12). */
const IDEMPOTENCY_KEY_HEADER = 'Idempotency-Key';

/**
 * The b13 nurse history payload (`GET nurse_payouts/history` → `NursePayoutHistoryDto`). Note it carries
 * **no** `failureReason` — that field lives on the admin-only `PayoutDto`, so it maps to `null` until
 * REQ-025 adds it to the nurse read (a `failed` payout still renders; its reason is just absent).
 */
interface NursePayoutHistoryWire {
  id: number;
  batchId: number;
  status: PayoutStatus;
  grossEarningsIrr: string;
  clawbackAppliedIrr: string;
  netAmountIrr: string;
  maskedIban: string;
  transferReference: string | null;
  paidAt: string | null;
  periodStart: string;
  periodEnd: string;
}

function toHistoryItem(wire: NursePayoutHistoryWire): NursePayoutHistoryItem {
  return {
    id: wire.id,
    batchId: wire.batchId,
    status: wire.status,
    grossEarningsIrr: wire.grossEarningsIrr,
    clawbackAppliedIrr: wire.clawbackAppliedIrr,
    netAmountIrr: wire.netAmountIrr,
    maskedIban: wire.maskedIban,
    transferReference: wire.transferReference,
    paidAt: wire.paidAt,
    periodStart: wire.periodStart,
    periodEnd: wire.periodEnd,
    // REQ-025: the nurse history DTO carries no failure reason yet (it is on the admin PayoutDto).
    failureReason: null,
  };
}

// ── Admin batch wire DTOs (`admin_payouts/*`) ─────────────────────────────────────────────────────────

/** `PayoutBatchDto` — note it has `initiatedByAdminId` but **no** `holidayShifted` flag (REQ-036). */
interface PayoutBatchWire {
  id: number;
  periodStart: string;
  periodEnd: string;
  processingDate: string;
  totalAmount: string;
  payoutCount: number;
  status: PayoutBatchStatus;
  processedAt: string | null;
  failureNotes: string | null;
  createdAt: string;
}

/** `PayoutDto` — note the transferred amount is `amount`, mapped to `amountIrr`. */
interface PayoutWire {
  id: number;
  nurseId: number;
  nurseName: string | null;
  maskedIban: string;
  grossEarningsIrr: string;
  clawbackAppliedIrr: string;
  netAmountIrr: string;
  amount: string;
  status: PayoutStatus;
  transferReference: string | null;
  paidAt: string | null;
  failureReason: string | null;
  bookings: { bookingId: number; sessionId: number | null; payoutAmountIrr: string }[];
}

interface PayoutBatchDetailWire {
  batch: PayoutBatchWire;
  payouts: PayoutWire[];
  total: number;
  page: number;
  pageSize: number;
}

interface GeneratePayoutBatchResultWire {
  batch: PayoutBatchWire;
}

function toBatchSummary(wire: PayoutBatchWire): PayoutBatchSummary {
  return {
    id: wire.id,
    periodStart: wire.periodStart,
    periodEnd: wire.periodEnd,
    processingDate: wire.processingDate,
    totalAmount: wire.totalAmount,
    payoutCount: wire.payoutCount,
    status: wire.status,
    processedAt: wire.processedAt,
    failureNotes: wire.failureNotes,
    createdAt: wire.createdAt,
    // REQ-036: PayoutBatchDto exposes no holidayShifted flag — a single preview endpoint returning
    // eligible+skipped+processingDate+holidayShifted would carry it. Defaults false until then.
    holidayShifted: false,
  };
}

function toPayoutRow(wire: PayoutWire): AdminPayoutRow {
  return {
    id: wire.id,
    nurseId: wire.nurseId,
    nurseName: wire.nurseName,
    maskedIban: wire.maskedIban,
    grossEarningsIrr: wire.grossEarningsIrr,
    clawbackAppliedIrr: wire.clawbackAppliedIrr,
    netAmountIrr: wire.netAmountIrr,
    amountIrr: wire.amount,
    status: wire.status,
    transferReference: wire.transferReference,
    paidAt: wire.paidAt,
    failureReason: wire.failureReason,
    bookings: wire.bookings.map((b) => ({
      bookingId: b.bookingId,
      sessionId: b.sessionId,
      payoutAmountIrr: b.payoutAmountIrr,
    })),
  };
}

/**
 * Real HTTP implementation of the `PayoutsApi` seam (b13 contract `dev/contracts/domains/payouts.md`,
 * swagger `dev/contracts/openapi/swagger.v1.json`). Only `getNursePayoutHistory` maps a **published** nurse
 * route; the other three target contract gaps the frontend filed (**REQ-025**), which is why the domain
 * stays mock-primary (see `constants.ts`):
 *  - `getNurseEarningsBalance` → the four-bucket summary + signed net balance (no nurse endpoint today).
 *  - `getNurseEarnings` → the per-booking earnings list + money-state (no nurse endpoint today).
 *  - `getNursePayoutDetail` → a nurse-scoped payout detail with batch context + booking links (the b13
 *    `batches/{id}` detail is **admin-only**).
 *
 * NOT the primary implementation this phase (`USE_PAYOUTS_MOCK = true`). Routes are `snake_case`; ids come
 * from the route; `clientFetch` returns the raw envelope so we `unwrap()`.
 */
export const payoutsClientApi: PayoutsApi = {
  getNurseEarningsBalance: async () =>
    unwrap(await clientFetch<ApiEnvelope<NurseEarningsSummary>>(`${NURSE_PAYOUTS}/earnings_balance`)),

  getNurseEarnings: async (params: EarningsListParams): Promise<Paginated<NurseEarningsItem>> => {
    const query = new URLSearchParams();
    if (params.state) query.set('state', params.state);
    query.set('page', String(params.page ?? 1));
    query.set('pageSize', String(params.pageSize ?? PAYOUTS_PAGE_SIZE));
    return unwrap(
      await clientFetch<ApiEnvelope<Paginated<NurseEarningsItem>>>(`${NURSE_PAYOUTS}/earnings?${query.toString()}`),
    );
  },

  getNursePayoutHistory: async (params: PageParams): Promise<Paginated<NursePayoutHistoryItem>> => {
    const query = new URLSearchParams();
    query.set('page', String(params.page ?? 1));
    query.set('pageSize', String(params.pageSize ?? PAYOUTS_PAGE_SIZE));
    const page = unwrap(
      await clientFetch<ApiEnvelope<Paginated<NursePayoutHistoryWire>>>(`${NURSE_PAYOUTS}/history?${query.toString()}`),
    );
    return { ...page, items: page.items.map(toHistoryItem) };
  },

  getNursePayoutDetail: async (payoutId: number) =>
    unwrap(await clientFetch<ApiEnvelope<NursePayoutDetail>>(`${NURSE_PAYOUTS}/${payoutId}`)),

  // ── Admin batch actions (`admin_payouts/*`) ─────────────────────────────────────────────────────────

  listPayoutBatches: async (
    filters: PayoutBatchFilters,
    params: PageParams,
  ): Promise<Paginated<PayoutBatchSummary>> => {
    const query = new URLSearchParams();
    if (filters.status) query.set('status', filters.status);
    query.set('page', String(params.page ?? 1));
    query.set('pageSize', String(params.pageSize ?? PAYOUTS_PAGE_SIZE));
    const page = unwrap(
      await clientFetch<ApiEnvelope<Paginated<PayoutBatchWire>>>(`${ADMIN_PAYOUTS}/batches?${query.toString()}`),
    );
    return { ...page, items: page.items.map(toBatchSummary) };
  },

  previewPayoutBatch: async (periodStart: string, periodEnd: string) => {
    const query = new URLSearchParams({ periodStart, periodEnd });
    const page = unwrap(
      await clientFetch<ApiEnvelope<Paginated<EligibleNurseEarnings>>>(
        `${ADMIN_PAYOUTS}/eligible?${query.toString()}`,
      ),
    );
    const eligible = page.items;
    const totalNet = eligible.reduce((sum, e) => sum + BigInt(e.netAmountIrr), BigInt(0));
    // REQ-036: a single preview endpoint returning eligible + skipped + processingDate + holidayShifted in
    // one shot. The b13 `eligible` read is paged and returns only the eligible rows — no skipped list (that
    // is materialized by the generate call) and no processingDate/holidayShifted — so carry what we can:
    // processingDate falls back to periodEnd and skipped is empty until the preview route lands.
    return {
      periodStart,
      periodEnd,
      processingDate: periodEnd,
      holidayShifted: false,
      eligible,
      skipped: [],
      totalNetIrr: String(totalNet),
    };
  },

  runPayoutBatch: async (periodStart: string, periodEnd: string, idempotencyKey: string) => {
    const result = unwrap(
      await clientFetch<ApiEnvelope<GeneratePayoutBatchResultWire>>(`${ADMIN_PAYOUTS}/batches`, {
        method: 'POST',
        headers: { [IDEMPOTENCY_KEY_HEADER]: idempotencyKey },
        body: JSON.stringify({ periodStart, periodEnd }),
      }),
    );
    return toBatchSummary(result.batch);
  },

  getPayoutBatchDetail: async (batchId: number, page: number): Promise<AdminPayoutBatchDetail> => {
    const query = new URLSearchParams({ page: String(page) });
    const detail = unwrap(
      await clientFetch<ApiEnvelope<PayoutBatchDetailWire>>(
        `${ADMIN_PAYOUTS}/batches/${batchId}?${query.toString()}`,
      ),
    );
    return {
      batch: toBatchSummary(detail.batch),
      payouts: detail.payouts.map(toPayoutRow),
      total: detail.total,
      page: detail.page,
      pageSize: detail.pageSize,
    };
  },

  retryPayout: async (payoutId: number, idempotencyKey: string) => {
    await clientFetch<ApiEnvelope<boolean>>(`${ADMIN_PAYOUTS}/${payoutId}/retry`, {
      method: 'POST',
      headers: { [IDEMPOTENCY_KEY_HEADER]: idempotencyKey },
    });
  },

  recordTransferReference: async (payoutId: number, reference: string) => {
    // REQ-036: b13 has `mark_failed` but no record-transfer-reference route — a manually reconciled bank
    // transfer reference has nowhere to land. Proposed action-style slug; 404s until the backend adds it.
    await clientFetch<ApiEnvelope<boolean>>(`${ADMIN_PAYOUTS}/${payoutId}/transfer_reference`, {
      method: 'POST',
      body: JSON.stringify({ reference }),
    });
  },
};
