import type { Freight, PaginatedResponse } from "@edr/types";

import { URL_CONSTANTS } from "@/constants/URLS";
import { client } from "../utils/api";
import type { ContractView, SignContractPayload } from "./bookings.service";

const C = URL_CONSTANTS.CONTRACTS;

export type CreateContractPayload = Freight.CreateContractDto;
export type UpdateContractPayload = Freight.UpdateContractDto;

/** Document files keyed by upload field — same shape as booking documents. */
export type ContractDocuments = Record<string, File | File[] | null>;

/**
 * Unit-rate pricing returned by generate-price. Mirrors the contract
 * `pricing_breakdown` (doc §9.1) — line items with a per-unit price and NO
 * total, since quantities are unknown at the contract stage.
 */
export interface GenerateContractPriceResponse {
  contractId: string;
  currency: string;
  lineItems: Freight.ContractUnitRateLineItem[];
  warnings?: string[];
}

export interface SubmitContractResponse {
  contractId: string;
  status: Freight.ContractStatus;
  priceChanged: boolean;
  currency: string;
  lineItems?: Freight.ContractUnitRateLineItem[];
  message?: string;
}

/** A container line whose total VGM exceeds the weight-limit rule. */
export interface OverweightLine {
  containerTypeCode: string;
  totalVgmTons: number;
  maxAllowedTons: number;
  excessTons: number;
}

/**
 * One line of the server-priced booking breakdown — the exact line the booking
 * will persist at create time (rail freight, first/last mile, surcharges…).
 */
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-submit validation + authoritative price preview for a shipment booking.
 * `overweightLines` are WARNINGS only (an overweight surcharge applies — the
 * customer may still submit); `pairingErrors` are HARD BLOCKS (20ft containers
 * that cannot be balanced onto wagons) and must prevent booking.
 * `lineItems`/`totalAmount` are the full server-computed breakdown — the same
 * BookingPricingService pass that prices the booking on create, so the confirm
 * modal shows first/last mile, overweight, and every surcharge, not just the
 * container estimate.
 */
export interface ShipmentValidation {
  overweightLines: OverweightLine[];
  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 ContractListFilter {
  status?: string;
  statuses?: string;
  /** ONE_TIME or GENERAL. */
  contractKind?: string;
  /** CONTAINER or BULK. */
  freightType?: string;
  /** IMPORT / EXPORT / DOMESTIC. */
  tradeDirection?: string;
  companyProfileId?: string;
  createdFrom?: string;
  createdTo?: string;
  /** Server-side free-text search (contract reference, company name). */
  search?: string;
  page?: number;
  pageSize?: number;
  sortBy?: string;
  sortOrder?: "ASC" | "DESC";
}

function appendScalar(formData: FormData, key: string, value: unknown) {
  if (value === undefined || value === null) return;
  if (typeof value === "boolean") {
    formData.append(key, value ? "true" : "false");
    return;
  }
  if (typeof value === "number") {
    formData.append(key, String(value));
    return;
  }
  if (typeof value === "string") {
    formData.append(key, value);
  }
}

function appendDocuments(formData: FormData, documents?: ContractDocuments) {
  if (!documents) return;
  for (const [key, fileOrFiles] of Object.entries(documents)) {
    if (!fileOrFiles) continue;
    if (Array.isArray(fileOrFiles)) {
      for (const f of fileOrFiles) formData.append(key, f);
    } else {
      formData.append(key, fileOrFiles);
    }
  }
}

/**
 * Flatten a contract payload (and optional intake documents) into multipart
 * FormData. The cargo-scope and route arrays are sent index-bracketed
 * (`cargoScope[0][containerSize]`, `routes[0][originYardId]`, …), mirroring the
 * booking form-data builder.
 */
export function buildContractFormData(
  payload: Partial<CreateContractPayload>,
  documents?: ContractDocuments,
): FormData {
  const formData = new FormData();
  const skipKeys = new Set(["cargoScope", "routes"]);

  for (const [key, value] of Object.entries(payload)) {
    if (skipKeys.has(key)) continue;
    appendScalar(formData, key, value);
  }

  payload.cargoScope?.forEach((scope, i) => {
    appendScalar(formData, `cargoScope[${i}][containerSize]`, scope.containerSize);
    appendScalar(formData, `cargoScope[${i}][cargoTypeId]`, scope.cargoTypeId);
    appendScalar(formData, `cargoScope[${i}][cargoFreeText]`, scope.cargoFreeText);
  });

  payload.routes?.forEach((route, i) => {
    appendScalar(formData, `routes[${i}][originYardId]`, route.originYardId);
    appendScalar(
      formData,
      `routes[${i}][destinationYardId]`,
      route.destinationYardId,
    );
    appendScalar(formData, `routes[${i}][km]`, route.km);
    appendScalar(formData, `routes[${i}][sortOrder]`, route.sortOrder ?? i);
  });

  appendDocuments(formData, documents);
  return formData;
}

export const contractsService = {
  list: async (
    filter: ContractListFilter | void = {},
  ): Promise<PaginatedResponse<Freight.IContract>> => {
    const { data } = await client.get(C.BASE, { params: filter ?? {} });
    return data.data;
  },

  listMy: async (
    filter: ContractListFilter | void = {},
  ): Promise<PaginatedResponse<Freight.IContract>> => {
    const { data } = await client.get(C.MY, { params: filter ?? {} });
    return data.data;
  },

  get: async (id: string): Promise<Freight.IContract> => {
    const { data } = await client.get(C.BY_ID(id));
    return data.data;
  },

  create: async (
    payload: CreateContractPayload,
    documents?: ContractDocuments,
  ): Promise<Freight.IContract> => {
    const formData = buildContractFormData(payload, documents);
    const { data } = await client.post(C.BASE, formData, {
      headers: { "Content-Type": "multipart/form-data" },
    });
    return data.data.contract ?? data.data;
  },

  update: async (
    id: string,
    payload: UpdateContractPayload,
    documents?: ContractDocuments,
  ): Promise<Freight.IContract> => {
    const formData = buildContractFormData(payload, documents);
    const { data } = await client.patch(C.BY_ID(id), formData, {
      headers: { "Content-Type": "multipart/form-data" },
    });
    return data.data.contract ?? data.data;
  },

  remove: async (id: string): Promise<void> => {
    await client.delete(C.BY_ID(id));
  },

  uploadDocuments: async (
    id: string,
    files: ContractDocuments,
  ): Promise<Freight.IContract> => {
    const formData = new FormData();
    appendDocuments(formData, files);
    const { data } = await client.post(C.DOCUMENTS(id), formData, {
      headers: { "Content-Type": "multipart/form-data" },
    });
    return data.data;
  },

  generatePrice: async (
    id: string,
  ): Promise<GenerateContractPriceResponse> => {
    const { data } = await client.post(C.GENERATE_PRICE(id));
    return data.data;
  },

  submit: async (id: string): Promise<SubmitContractResponse> => {
    const { data } = await client.post(C.SUBMIT(id));
    return data.data;
  },

  confirmSubmit: async (id: string): Promise<SubmitContractResponse> => {
    const { data } = await client.post(C.CONFIRM_SUBMIT(id));
    return data.data;
  },

  generateContract: async (id: string): Promise<Freight.IContract> => {
    const { data } = await client.post(C.CONTRACT_GENERATE(id));
    return data.data ?? data;
  },

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

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

  signContract: async (
    id: string,
    payload: SignContractPayload,
  ): Promise<Freight.IContract> => {
    const { data } = await client.post(C.CONTRACT_SIGN(id), payload);
    return data.data ?? data;
  },

  // Ask the server to send the signing OTP to the signer's own registered phone.
  // The client never picks the number (the server resolves it from the
  // authenticated user and verifies against the same one), so send and verify
  // can't disagree. Returns a masked hint.
  sendSigningOtp: async (id: string): Promise<{ sentTo: string }> => {
    const { data } = await client.post(C.CONTRACT_SEND_SIGNING_OTP(id));
    return data.data ?? data;
  },

  renew: async (
    id: string,
    dto: Freight.RenewContractDto,
  ): Promise<Freight.IContract> => {
    const { data } = await client.post(C.RENEW(id), dto);
    return data.data ?? data;
  },

  // ── Pre-booking clearance (Path B) ──
  getClearance: async (id: string): Promise<Freight.ContractClearanceView> => {
    const { data } = await client.get(C.CLEARANCE(id));
    return data.data ?? data;
  },

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

  /**
   * Reject the advised duty & tax with a reason. The clearance step goes back
   * to GL Ethiopia, who re-advises a corrected amount; this can repeat.
   */
  disputeContractDuty: async (
    id: string,
    note: string,
  ): Promise<Freight.IContract> => {
    const { data } = await client.post(C.CLEARANCE_DUTY_DISPUTE(id), { note });
    return data.data ?? data;
  },

  uploadContractDutySlip: async (
    id: string,
    file: File,
  ): Promise<Freight.IContract> => {
    const form = new FormData();
    form.append("file", file);
    const { data } = await client.post(C.CLEARANCE_DUTY_SLIP(id), form, {
      headers: { "Content-Type": "multipart/form-data" },
    });
    return data.data ?? data;
  },

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

  submitBookingRequest: async (
    id: string,
    dto: Freight.CreateBookingRequestDto,
  ): Promise<Freight.IBookingRequest> => {
    const { data } = await client.post(C.BOOKING_REQUESTS(id), dto);
    return data.data ?? data;
  },

  cancelBookingRequest: async (reqId: string): Promise<Freight.IBookingRequest> => {
    const { data } = await client.post(C.BOOKING_REQUEST_CANCEL(reqId), {});
    return data.data ?? data;
  },

  // ── Booking under contract (Path A customer) ──
  createBookingUnderContract: async (
    id: string,
    dto: Freight.CreateBookingUnderContractDto,
  ): Promise<Freight.IBooking> => {
    const { data } = await client.post(C.BOOKINGS(id), dto);
    return data.data.booking ?? data.data;
  },

  /**
   * One-click bare booking instance under a GENERAL non-customs contract — no
   * cargo, no date. The instance enters per-booking clearance; the customer
   * completes it (cargo + shipment day) once Operations finalizes.
   */
  initiateBookingUnderContract: async (
    id: string,
    contractRouteId?: string,
  ): Promise<Freight.IBooking> => {
    const { data } = await client.post(
      C.BOOKINGS_INITIATE(id),
      contractRouteId ? { contractRouteId } : {},
    );
    return data.data.booking ?? data.data;
  },

  /** Complete an initiated booking after clearance — same DTO as create. */
  completeBookingUnderContract: async (
    id: string,
    bookingId: string,
    dto: Freight.CreateBookingUnderContractDto,
  ): Promise<Freight.IBooking> => {
    const { data } = await client.post(C.BOOKINGS_COMPLETE(id, bookingId), dto);
    return data.data.booking ?? data.data;
  },

  /**
   * Pre-submit validation of a shipment booking (same DTO as
   * `createBookingUnderContract`). Returns overweight warnings and hard-block
   * 20ft wagon-pairing errors so the customer can be warned/blocked before the
   * booking is created.
   */
  validateShipment: async (
    id: string,
    dto: Freight.CreateBookingUnderContractDto,
  ): Promise<ShipmentValidation> => {
    const { data } = await client.post(C.VALIDATE_SHIPMENT(id), dto);
    return data.data ?? data;
  },

  // ── Milestones ──
  getContractMilestones: async (
    id: string,
  ): Promise<Freight.IClearanceMilestone[]> => {
    const { data } = await client.get(C.MILESTONES(id));
    return data.data ?? data;
  },

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

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

  /** Customer uploads the duty/tax payment slip (doc-triggers DUTY_TAX_PAID). */
  uploadDutySlip: async (
    bookingId: string,
    file: File,
  ): Promise<{ milestoneCompleted: boolean }> => {
    const form = new FormData();
    form.append("duty_tax_receipt", file);
    const { data } = await client.post(C.BOOKING_DUTY_SLIP(bookingId), form, {
      headers: { "Content-Type": "multipart/form-data" },
    });
    return data.data ?? data;
  },

  /** Customer attaches the payment slip for the GL final invoice (export). */
  uploadFinalInvoiceSlip: async (
    bookingId: string,
    file: File,
  ): Promise<{ uploaded: boolean }> => {
    const form = new FormData();
    form.append("file", file);
    const { data } = await client.post(
      C.BOOKING_FINAL_INVOICE_SLIP(bookingId),
      form,
      { headers: { "Content-Type": "multipart/form-data" } },
    );
    return data.data ?? data;
  },

  /** Customer attaches the slip for the post-arrival additional duty round (import). */
  uploadSecondDutySlip: async (
    bookingId: string,
    file: File,
  ): Promise<{ milestoneCompleted: boolean }> => {
    const form = new FormData();
    form.append("file", file);
    const { data } = await client.post(
      C.BOOKING_SECOND_DUTY_SLIP(bookingId),
      form,
      { headers: { "Content-Type": "multipart/form-data" } },
    );
    return data.data ?? data;
  },
};
