import { api as client } from "../auth/http";
import { unwrap } from "@/utils/endpoint";
import { URL_CONSTANTS } from "@/constants/URLS";
import type { Freight } from "@edr/types";

const C = URL_CONSTANTS.CONTRACTS;

export interface ContractListFilter {
  status?: string;
  /** Comma-separated statuses for grouped tabs. */
  statuses?: string;
  /** Tab key for React Query cache (not sent to API). */
  tab?: string;
  companyId?: string;
  freightType?: string;
  tradeDirection?: string;
  contractKind?: string;
  paymentCurrency?: string;
  /** Created-at range (ISO strings, inclusive). */
  createdFrom?: string;
  createdTo?: string;
  /** Server-side free-text search (contract reference, company name). */
  search?: string;
  page?: number;
  pageSize?: number;
  sortBy?: string;
  sortOrder?: "ASC" | "DESC";
}

export interface PaginatedContracts {
  items: Freight.IContract[];
  total: number;
}

/** One line of the server-priced booking breakdown (mirrors PriceLineItemDto). */
export interface ShipmentPriceLine {
  code: string;
  description: string;
  amount: number;
  unitAmount: number;
  /** Rate unit as stored: PER_CONTAINER | PER_WAGON | PER_TON | PER_KM | FLAT | … */
  unit: string;
  quantity: number;
  currency: string;
}

/**
 * Pre-create validation + authoritative price preview for a booking under a
 * contract. `lineItems`/`totalAmount` are the full server-computed breakdown —
 * the same pricing pass the booking persists at create (rail freight,
 * first/last mile, overweight and every other surcharge). `pairingErrors` and
 * `capacityErrors` are HARD BLOCKS; `overweightLines` are warnings.
 */
export interface ShipmentValidation {
  overweightLines: Array<{
    containerTypeCode: string;
    totalVgmTons: number;
    maxAllowedTons: number;
    excessTons: number;
  }>;
  overweightSurchargeAmount: number;
  currency: string | null;
  pairingErrors: string[];
  /** Lines above the container type's hard max capacity — booking cannot be created. */
  capacityErrors?: string[];
  /** Containers already on another active booking for the same day + route — booking cannot be created. */
  containerClashErrors?: string[];
  /** EXPORT only: no single open train on the chosen day can carry the whole booking — booking cannot be created. */
  spaceErrors?: string[];
  lineItems?: ShipmentPriceLine[];
  totalAmount?: number;
}

export interface ContractListSummaryMetrics {
  inQueue: number;
  needsAction: number;
  urgent: number;
  completed: number;
}

export interface ContractListSummaryTabs {
  all: number;
  intake: number;
  in_approval: number;
  approved_contract: number;
  clearance: number;
  active: number;
  closed: number;
}

export interface ContractListSummary {
  metrics: ContractListSummaryMetrics;
  tabs: ContractListSummaryTabs;
}

export interface ContractView {
  contractId: string;
  reference: string;
  status: string;
  freightType: "BULK" | "CONTAINER";
  templateKey: string;
  title: string;
  html: string;
  canSignCustomer: boolean;
  canSignStaff: boolean;
  hasContractDocument: boolean;
  signatures: Array<{
    role: string;
    signerDisplayName: string;
    signedAt: string;
    signatureImageUrl?: string | null;
    stampImageUrl?: string | null;
  }>;
  savedSignature?: {
    signerDisplayName: string;
    signatureImageUrl?: string | null;
  } | null;
}

export interface SignContractPayload {
  role: "CUSTOMER" | "STAFF";
  signatureImageBase64: string;
  /** Company stamp/seal image; required to sign a contract. */
  stampImageBase64?: string;
  signerDisplayName: string;
  consentText?: string;
}

/**
 * One stored version of a clearance document. The current row plus every
 * superseded upload — staff corrections never erase what the customer sent.
 */
export interface ClearanceDocumentVersion {
  id: string;
  name: string;
  url: string;
  size: number;
  mimeType: string;
  uploadedAt: string;
  isCurrent: boolean;
  replacedAt: string | null;
  replacedByUserId: string | null;
  replaceReason: string | null;
}

async function postContract<T>(url: string, body?: unknown): Promise<T> {
  const response = await client.post<T>(url, body ?? {});
  return unwrap(response.data);
}

function buildListParams(filter?: ContractListFilter) {
  const params: Record<string, string | number | boolean | undefined> = {};
  if (filter) {
    if (filter.statuses) params.statuses = filter.statuses;
    else if (filter.status) params.status = filter.status;
    if (filter.search) params.search = filter.search;
    if (filter.page != null) params.page = filter.page;
    if (filter.pageSize != null) params.pageSize = filter.pageSize;
    if (filter.sortBy) params.sortBy = filter.sortBy;
    if (filter.sortOrder) params.sortOrder = filter.sortOrder;
    if (filter.companyId) params.companyId = filter.companyId;
    if (filter.freightType) params.freightType = filter.freightType;
    if (filter.tradeDirection) params.tradeDirection = filter.tradeDirection;
    if (filter.contractKind) params.contractKind = filter.contractKind;
    if (filter.paymentCurrency) params.paymentCurrency = filter.paymentCurrency;
    if (filter.createdFrom) params.createdFrom = filter.createdFrom;
    if (filter.createdTo) params.createdTo = filter.createdTo;
  }
  return params;
}

export const contractsService = {
  getListSummary: async (
    filter?: ContractListFilter,
  ): Promise<ContractListSummary> => {
    const response = await client.get<ContractListSummary>(C.LIST_SUMMARY, {
      params: buildListParams(filter),
    });
    return unwrap(response.data) as ContractListSummary;
  },

  list: async (filter?: ContractListFilter): Promise<PaginatedContracts> => {
    const response = await client.get<PaginatedContracts>(C.BASE, {
      params: buildListParams(filter),
    });
    const data = unwrap(response.data);
    return {
      items: (data.items ?? []) as Freight.IContract[],
      total: data.total ?? 0,
    };
  },

  getById: async (
    id: string,
    opts?: { suppressErrorModal?: boolean },
  ): Promise<Freight.IContract> => {
    const response = await client.get<Freight.IContract>(C.BY_ID(id), opts);
    return unwrap(response.data) as Freight.IContract;
  },

  // ── Staff review ──
  staffAccept: (
    id: string,
    validityDays: number,
    documentSnapshot?: Freight.IContractDocumentSnapshot,
    window?: { validFrom?: string; validUntil?: string },
  ) =>
    postContract<Freight.IContract>(C.STAFF_ACCEPT(id), {
      validityDays,
      documentSnapshot,
      ...window,
    }),

  /** The editable per-contract document draft (snapshot or live template). */
  getContractDocumentDraft: async (
    id: string,
  ): Promise<Freight.IContractDocumentDraft> => {
    const response = await client.get(C.CONTRACT_DOCUMENT_DRAFT(id));
    return unwrap(response.data) as Freight.IContractDocumentDraft;
  },

  /** Audit trail of edits to this contract's document, newest first. */
  getContractDocumentRevisions: async (
    id: string,
  ): Promise<Freight.IContractDocumentRevision[]> => {
    const response = await client.get(C.CONTRACT_DOCUMENT_REVISIONS(id));
    return unwrap(response.data) as Freight.IContractDocumentRevision[];
  },

  /** Save this contract's edited document articles (never touches the templates). */
  updateContractDocument: async (
    id: string,
    snapshot: Freight.IContractDocumentSnapshot,
  ): Promise<Freight.IContract> => {
    const response = await client.put(
      C.CONTRACT_DOCUMENT_ARTICLES(id),
      snapshot,
    );
    return unwrap(response.data) as Freight.IContract;
  },

  requestChanges: (id: string, note: string) =>
    postContract<Freight.IContract>(C.STAFF_REQUEST_CHANGES(id), { note }),

  reject: (id: string, reason: string) =>
    postContract<Freight.IContract>(C.STAFF_REJECT(id), { reason }),

  /**
   * Approve the next pending step. The server resolves the step's required role
   * and authorizes against it — the client never declares its own role.
   */
  approveStep: ({ id, stepId }: { id: string; stepId: string }) =>
    postContract<Freight.IContract>(C.APPROVE_STEP(id, stepId)),

  /**
   * Reject the current step. Without `returnToStepId` the contract is rejected
   * to the customer (terminal). With it, the contract is sent back to that
   * earlier approved step and the chain re-runs from there.
   */
  rejectStep: ({
    id,
    stepId,
    reason,
    returnToStepId,
  }: {
    id: string;
    stepId: string;
    reason: string;
    returnToStepId?: string;
  }) =>
    postContract<Freight.IContract>(C.REJECT_STEP(id, stepId), {
      reason,
      ...(returnToStepId ? { returnToStepId } : {}),
    }),

  // ── Contract document ──
  generateContract: (id: string) =>
    postContract<Freight.IContract>(C.CONTRACT_GENERATE(id)),

  getContractView: async (id: string): Promise<ContractView> => {
    const response = await client.get<ContractView>(C.CONTRACT_VIEW(id));
    return unwrap(response.data) as ContractView;
  },

  downloadContractDocument: async (id: string): Promise<Blob> => {
    const response = await client.get(C.CONTRACT_DOCUMENT(id), {
      responseType: "blob",
    });
    return response.data as Blob;
  },

  signContract: (id: string, payload: SignContractPayload) =>
    postContract<Freight.IContract>(C.CONTRACT_SIGN(id), payload),

  // ── Pre-booking clearance (Path B — GL ET) ──
  getClearanceQueue: async (): Promise<PaginatedContracts> => {
    const response = await client.get<PaginatedContracts>(C.CLEARANCE_QUEUE);
    const data = unwrap(response.data);
    return {
      items: (data.items ?? []) as Freight.IContract[],
      total: data.total ?? 0,
    };
  },

  getClearance: async (
    id: string,
    opts?: { suppressErrorModal?: boolean },
  ): Promise<Freight.ContractClearanceView> => {
    const response = await client.get(C.CLEARANCE(id), opts);
    return unwrap(response.data) as Freight.ContractClearanceView;
  },

  reviewClearanceDocument: (
    id: string,
    payload: { fileKey: string; status: "APPROVED" | "QUERIED"; note?: string },
  ) =>
    postContract<Freight.IContract>(C.CLEARANCE_REVIEW(id), payload),

  /** GL ET asks Djibouti to name the officer handling the shipment in transit. */
  requestTransitAssignee: (id: string, note?: string) =>
    postContract<Freight.IContract>(
      C.CLEARANCE_TRANSIT_ASSIGNEE_REQUEST(id),
      { note },
    ),

  /** GL Djibouti names (or changes) that officer — unblocks the declaration. */
  assignTransitAssignee: (id: string, assignee: string) =>
    postContract<Freight.IContract>(C.CLEARANCE_TRANSIT_ASSIGNEE_ASSIGN(id), {
      assignee,
    }),

  /**
   * Replace a clearance document in place. The customer's original is retired
   * into the version history rather than overwritten, and the new file comes
   * back unreviewed so it still has to be approved.
   */
  replaceClearanceDocument: async (
    id: string,
    fileKey: string,
    file: File,
    reason: string,
  ): Promise<Freight.IContract> => {
    const form = new FormData();
    form.append("file", file);
    form.append("reason", reason);
    const response = await client.post(
      C.CLEARANCE_DOC_REPLACE(id, fileKey),
      form,
      { headers: { "Content-Type": "multipart/form-data" } },
    );
    return unwrap(response.data) as Freight.IContract;
  },

  /** Every stored version of one clearance document, newest first. */
  getClearanceDocumentVersions: async (
    id: string,
    fileKey: string,
  ): Promise<ClearanceDocumentVersion[]> => {
    const response = await client.get(C.CLEARANCE_DOC_VERSIONS(id, fileKey));
    return (unwrap(response.data) as ClearanceDocumentVersion[]) ?? [];
  },

  uploadClearanceOutput: async (
    id: string,
    files: Record<string, File | null>,
  ): Promise<Freight.IContract> => {
    const form = new FormData();
    for (const [key, file] of Object.entries(files)) {
      if (file) form.append(key, file);
    }
    const response = await client.post(C.CLEARANCE_OUTPUT_DOCUMENTS(id), form, {
      headers: { "Content-Type": "multipart/form-data" },
    });
    return unwrap(response.data) as Freight.IContract;
  },

  finalizeClearance: (id: string) =>
    postContract<Freight.IContract>(C.CLEARANCE_FINALIZE(id)),

  getEtClearanceQueue: async (): Promise<PaginatedContracts> => {
    const response = await client.get<PaginatedContracts>(C.CLEARANCE_ET_QUEUE);
    const data = unwrap(response.data);
    return {
      items: (data.items ?? []) as Freight.IContract[],
      total: data.total ?? 0,
    };
  },

  getDjClearanceQueue: async (): Promise<PaginatedContracts> => {
    const response = await client.get<PaginatedContracts>(C.CLEARANCE_DJ_QUEUE);
    const data = unwrap(response.data);
    return {
      items: (data.items ?? []) as Freight.IContract[],
      total: data.total ?? 0,
    };
  },

  uploadDeclaration: async (
    id: string,
    files: Record<string, File | null>,
  ): Promise<Freight.IContract> => {
    const form = new FormData();
    for (const [key, file] of Object.entries(files)) {
      if (file) form.append(key, file);
    }
    const response = await client.post(C.CLEARANCE_DECLARATION(id), form, {
      headers: { "Content-Type": "multipart/form-data" },
    });
    return unwrap(response.data) as Freight.IContract;
  },

  adviseContractDuty: async (
    id: string,
    payload: {
      dutyRequired: boolean;
      amount?: number;
      currency?: string;
      declarationSerial?: string;
      attachment?: File | null;
    },
  ): Promise<Freight.IContract> => {
    const form = new FormData();
    form.append("dutyRequired", String(payload.dutyRequired));
    if (payload.amount != null) form.append("amount", String(payload.amount));
    if (payload.currency) form.append("currency", payload.currency);
    if (payload.declarationSerial) {
      form.append("declarationSerial", payload.declarationSerial);
    }
    if (payload.attachment) form.append("attachment", payload.attachment);
    const response = await client.post(C.CLEARANCE_DUTY(id), form, {
      headers: { "Content-Type": "multipart/form-data" },
    });
    return unwrap(response.data) as Freight.IContract;
  },

  finalizePreClearance: (id: string) =>
    postContract<Freight.IContract>(C.CLEARANCE_FINALIZE_PRE(id)),

  uploadContractTransitPermit: async (
    id: string,
    files: Record<string, File | null>,
  ): Promise<Freight.IContract> => {
    const form = new FormData();
    for (const [key, file] of Object.entries(files)) {
      if (file) form.append(key, file);
    }
    const response = await client.post(C.CLEARANCE_TRANSIT_PERMIT(id), form, {
      headers: { "Content-Type": "multipart/form-data" },
    });
    return unwrap(response.data) as Freight.IContract;
  },

  uploadDeliveryOrder: async (
    id: string,
    file: File,
    dates: { vesselArrivalDate: string; doCollectedDate: string },
  ): Promise<Freight.IContract> => {
    const form = new FormData();
    form.append("file", file);
    form.append("vesselArrivalDate", dates.vesselArrivalDate);
    form.append("doCollectedDate", dates.doCollectedDate);
    const response = await client.post(C.CLEARANCE_DELIVERY_ORDER(id), form, {
      headers: { "Content-Type": "multipart/form-data" },
    });
    return unwrap(response.data) as Freight.IContract;
  },

  uploadReleaseOrder: async (
    id: string,
    file: File,
    vesselDepartureDate: string,
  ): Promise<{ contract: Freight.IContract; hold: boolean; holdReason?: string }> => {
    const form = new FormData();
    form.append("file", file);
    form.append("vesselDepartureDate", vesselDepartureDate);
    const response = await client.post(C.CLEARANCE_RELEASE_ORDER(id), form, {
      headers: { "Content-Type": "multipart/form-data" },
    });
    return unwrap(response.data) as {
      contract: Freight.IContract;
      hold: boolean;
      holdReason?: string;
    };
  },

  requestRoAmendment: (id: string, note?: string) =>
    postContract<Freight.IContract>(C.CLEARANCE_RO_AMENDMENT(id), { note }),

  confirmExportRelease: (id: string) =>
    postContract<Freight.IContract>(C.CLEARANCE_EXPORT_RELEASE(id)),

  finalizeExportClearance: (id: string) =>
    postContract<Freight.IContract>(C.CLEARANCE_FINALIZE_EXPORT(id)),

  uploadTransportDocument: async (
    bookingId: string,
    files: Record<string, File | null>,
  ) => {
    const form = new FormData();
    for (const [key, file] of Object.entries(files)) {
      if (file) form.append(key, file);
    }
    const response = await client.post(C.BOOKING_TRANSPORT_DOCUMENT(bookingId), form, {
      headers: { "Content-Type": "multipart/form-data" },
    });
    return unwrap(response.data);
  },

  /** GL Djibouti uploads T1 transit documents (multi-file, post wagon allocation). */
  uploadT1Documents: async (
    bookingId: string,
    files: Record<string, File | null>,
  ) => {
    const form = new FormData();
    for (const [key, file] of Object.entries(files)) {
      if (file) form.append(key, file);
    }
    const response = await client.post(C.BOOKING_T1_DOCUMENTS(bookingId), form, {
      headers: { "Content-Type": "multipart/form-data" },
    });
    return unwrap(response.data);
  },

  /** GL Ethiopia closes (accepts) the T1 document set after the train arrives. */
  closeT1: async (bookingId: string): Promise<Freight.ClearanceT1State> => {
    const response = await client.post(C.BOOKING_T1_CLOSE(bookingId));
    return unwrap(response.data) as Freight.ClearanceT1State;
  },

  /** GL DJ raises the post-offload final invoice (amount + invoice document). */
  sendFinalInvoice: async (
    bookingId: string,
    payload: { amount: number; currency: string; description?: string; file: File },
  ): Promise<Freight.ClearanceFinalInvoiceSummary> => {
    const form = new FormData();
    form.append("amount", String(payload.amount));
    form.append("currency", payload.currency);
    if (payload.description) form.append("description", payload.description);
    form.append("file", payload.file);
    const response = await client.post(C.BOOKING_FINAL_INVOICE(bookingId), form, {
      headers: { "Content-Type": "multipart/form-data" },
    });
    return unwrap(response.data) as Freight.ClearanceFinalInvoiceSummary;
  },

  /** GL (ET or DJ) confirms the payment slip — settles the final invoice. */
  confirmFinalInvoicePaid: async (
    bookingId: string,
  ): Promise<Freight.ClearanceFinalInvoiceSummary> => {
    const response = await client.post(C.BOOKING_FINAL_INVOICE_CONFIRM(bookingId));
    return unwrap(response.data) as Freight.ClearanceFinalInvoiceSummary;
  },

  /** GL ET advises (or skips) the post-arrival additional duty/tax round (import). */
  adviseSecondDuty: async (
    bookingId: string,
    payload: {
      dutyRequired: boolean;
      amount?: number;
      currency?: string;
      declarationSerial?: string;
      attachment?: File | null;
    },
  ): Promise<{ advised: boolean; skipped: boolean }> => {
    const form = new FormData();
    form.append("dutyRequired", String(payload.dutyRequired));
    if (payload.amount != null) form.append("amount", String(payload.amount));
    if (payload.currency) form.append("currency", payload.currency);
    if (payload.declarationSerial)
      form.append("declarationSerial", payload.declarationSerial);
    if (payload.attachment) form.append("attachment", payload.attachment);
    const response = await client.post(C.BOOKING_SECOND_DUTY(bookingId), form, {
      headers: { "Content-Type": "multipart/form-data" },
    });
    return unwrap(response.data) as { advised: boolean; skipped: boolean };
  },

  // ── Path A self-clearance (Operations review) ──
  getOpsClearanceQueue: async (filter?: {
    page?: number;
    pageSize?: number;
    search?: string;
    /** Comma-separated ops-clearance lifecycle statuses; omitted → under-review queue. */
    statuses?: string;
  }): Promise<PaginatedContracts> => {
    const response = await client.get<PaginatedContracts>(
      C.OPS_CLEARANCE_QUEUE,
      { params: filter },
    );
    const data = unwrap(response.data);
    return {
      items: (data.items ?? []) as Freight.IContract[],
      total: data.total ?? 0,
    };
  },

  getClearanceHistory: async (): Promise<PaginatedContracts> => {
    const response = await client.get<PaginatedContracts>(C.CLEARANCE_HISTORY);
    const data = unwrap(response.data);
    return { items: (data.items ?? []) as Freight.IContract[], total: data.total ?? 0 };
  },

  getOpsClearanceHistory: async (filter?: {
    page?: number;
    pageSize?: number;
    search?: string;
  }): Promise<PaginatedContracts> => {
    const response = await client.get<PaginatedContracts>(
      C.OPS_CLEARANCE_HISTORY,
      { params: filter },
    );
    const data = unwrap(response.data);
    return { items: (data.items ?? []) as Freight.IContract[], total: data.total ?? 0 };
  },

  opsReviewClearanceDocument: (
    id: string,
    payload: { fileKey: string; status: "APPROVED" | "QUERIED"; note?: string },
  ) => postContract<Freight.IContract>(C.OPS_CLEARANCE_REVIEW(id), payload),

  opsFinalizeClearance: (id: string) =>
    postContract<Freight.IContract>(C.OPS_CLEARANCE_FINALIZE(id)),

  // ── Booking under contract (GL ET — Path B) ──
  // The API returns { booking, warnings } (CreateBookingUnderContractResult) —
  // unwrap to the booking itself so callers can use its id directly.
  createBookingUnderContract: async (
    id: string,
    payload: Freight.CreateBookingUnderContractDto,
  ): Promise<{ id: string; reference: string; warnings?: string[] }> => {
    const result = await postContract<{
      booking?: { id: string; reference: string };
      id?: string;
      reference?: string;
      warnings?: string[];
    }>(C.BOOKINGS(id), payload);
    const booking = result.booking ?? result;
    return {
      id: booking.id ?? "",
      reference: booking.reference ?? "",
      warnings: result.warnings,
    };
  },

  /**
   * Complete a bare initiated booking instance once its per-booking clearance
   * is CLEARANCE_READY — same payload as create; the API persists cargo,
   * prices, invoices, checks the booking window and moves the booking to the
   * operations queue.
   */
  completeBookingUnderContract: async (
    id: string,
    bookingId: string,
    payload: Freight.CreateBookingUnderContractDto,
  ): Promise<{ id: string; reference: string; warnings?: string[] }> => {
    const result = await postContract<{
      booking?: { id: string; reference: string };
      id?: string;
      reference?: string;
      warnings?: string[];
    }>(C.BOOKINGS_COMPLETE(id, bookingId), payload);
    const booking = result.booking ?? result;
    return {
      id: booking.id ?? "",
      reference: booking.reference ?? "",
      warnings: result.warnings,
    };
  },

  /**
   * Pre-create validation + authoritative price preview: the same
   * BookingPricingService pass that prices the booking on create (rail +
   * first/last mile + every surcharge), plus overweight warnings and 20ft
   * pairing hard-blocks. Shown in the GL price-confirm modal.
   */
  validateShipment: (
    id: string,
    payload: Freight.CreateBookingUnderContractDto,
  ) =>
    postContract<ShipmentValidation>(C.VALIDATE_SHIPMENT(id), payload),

  /** Remaining bookable quantity per cargo line (GENERAL draw-down cap). */
  getCapacity: async (id: string): Promise<Freight.ContractCapacityLine[]> => {
    const response = await client.get(C.CAPACITY(id));
    return (unwrap(response.data) ?? []) as Freight.ContractCapacityLine[];
  },

  // ── Shipment requests (GENERAL + customs) ──
  /** GL queue of pending shipment requests across contracts. */
  getBookingRequestQueue: async (): Promise<Freight.IBookingRequest[]> => {
    const response = await client.get(C.BOOKING_REQUEST_QUEUE);
    return (unwrap(response.data) ?? []) as Freight.IBookingRequest[];
  },

  listBookingRequests: async (
    id: string,
  ): Promise<Freight.IBookingRequest[]> => {
    const response = await client.get(C.BOOKING_REQUESTS(id));
    return (unwrap(response.data) ?? []) as Freight.IBookingRequest[];
  },

  getBookingRequest: async (
    reqId: string,
  ): Promise<Freight.IBookingRequest> => {
    const response = await client.get(C.BOOKING_REQUEST_BY_ID(reqId));
    return unwrap(response.data) as Freight.IBookingRequest;
  },

  acceptBookingRequest: (reqId: string, bookingId: string) =>
    postContract<Freight.IBookingRequest>(C.BOOKING_REQUEST_ACCEPT(reqId), {
      bookingId,
    }),

  rejectBookingRequest: (reqId: string, note?: string) =>
    postContract<Freight.IBookingRequest>(C.BOOKING_REQUEST_REJECT(reqId), {
      note,
    }),

  // ── Clearance milestones ──
  listMilestonesForContract: async (
    id: string,
  ): Promise<Freight.IClearanceMilestone[]> => {
    const response = await client.get(C.MILESTONES(id));
    return (unwrap(response.data) ?? []) as Freight.IClearanceMilestone[];
  },

  listMilestonesForBooking: async (
    bookingId: string,
  ): Promise<Freight.IClearanceMilestone[]> => {
    const response = await client.get(C.BOOKING_MILESTONES(bookingId));
    return (unwrap(response.data) ?? []) as Freight.IClearanceMilestone[];
  },

  completeMilestone: (bookingId: string, code: string, note?: string) =>
    postContract<Freight.IClearanceMilestone>(
      C.COMPLETE_BOOKING_MILESTONE(bookingId, code),
      { note },
    ),

  // ── GL post-booking operational actions ──
  assignRisk: (
    bookingId: string,
    payload: { riskLevel: Freight.CustomsRiskLevel; note?: string },
  ) =>
    postContract<Freight.IClearanceMilestone>(
      C.BOOKING_RISK(bookingId),
      payload,
    ),

  adviseDuty: (
    bookingId: string,
    payload: {
      amount: number;
      currency: string;
      declarationSerial?: string;
      note?: string;
    },
  ) =>
    postContract<Freight.IClearanceMilestone>(
      C.BOOKING_DUTY(bookingId),
      payload,
    ),

  assignStation: (
    bookingId: string,
    payload: { stationYardId: string; staffId?: string },
  ) => postContract(C.BOOKING_STATION_ASSIGN(bookingId), payload),

  uploadGlDocuments: async (
    bookingId: string,
    files: Record<string, File | null>,
  ): Promise<{ uploaded: number; completedMilestones: string[] }> => {
    const form = new FormData();
    for (const [key, file] of Object.entries(files)) {
      if (file) form.append(key, file);
    }
    const response = await client.post(C.BOOKING_GL_DOCUMENTS(bookingId), form, {
      headers: { "Content-Type": "multipart/form-data" },
    });
    return unwrap(response.data) as {
      uploaded: number;
      completedMilestones: string[];
    };
  },

  listIncidents: async (
    bookingId: string,
  ): Promise<Freight.IClearanceIncident[]> => {
    const response = await client.get(C.BOOKING_INCIDENTS(bookingId));
    return (unwrap(response.data) ?? []) as Freight.IClearanceIncident[];
  },

  reportIncident: async (
    bookingId: string,
    payload: {
      incidentType: Freight.IncidentType;
      description: string;
      photos: File[];
    },
  ): Promise<Freight.IClearanceIncident> => {
    const form = new FormData();
    form.append("incidentType", payload.incidentType);
    form.append("description", payload.description);
    for (const photo of payload.photos) form.append("photos", photo);
    const response = await client.post(C.BOOKING_INCIDENTS(bookingId), form, {
      headers: { "Content-Type": "multipart/form-data" },
    });
    return unwrap(response.data) as Freight.IClearanceIncident;
  },
};
