import { BadRequestException } from '@nestjs/common';
import {
  catalogEntriesForTradeDirection,
  declarationFileLabel,
  isDeclarationFileCode,
  isImportTransitPermitFileCode,
  isExportTransportFileCode,
  isT1TransportFileCode,
  exportTransportFileLabel,
  t1TransportFileLabel,
  transitPermitFileLabel,
  type ClearanceWorkflowFile,
} from '@edr/types';

/** Require at least one declaration file in the upload batch. */
export function assertDeclarationFiles(files: Express.Multer.File[]): void {
  if (files.length === 0) {
    throw new BadRequestException('No declaration documents uploaded');
  }
}

/** Assign stable `declaration_*` codes so multi-file uploads always pass validation. */
export function normalizeDeclarationFieldNames(
  files: Express.Multer.File[],
): Express.Multer.File[] {
  return files.map((file, index) => ({
    ...file,
    fieldname: `declaration_${index}`,
  }));
}

type DeclarationFileStore = {
  findByResource(
    resourceId: string,
    resource: string,
  ): Promise<Array<{ code?: string | null }>>;
  deleteByCode(resourceId: string, resource: string, code: string): Promise<void>;
  upload(input: {
    resourceId: string;
    resource: string;
    code: string;
    file: Express.Multer.File;
  }): Promise<unknown>;
};

/** Replace all declaration files on a resource with a new multi-file upload batch. */
export async function persistDeclarationUploads(
  store: DeclarationFileStore,
  resourceId: string,
  resource: string,
  files: Express.Multer.File[],
): Promise<void> {
  const normalized = normalizeDeclarationFieldNames(files);
  assertDeclarationFiles(normalized);

  const existing = await store.findByResource(resourceId, resource);
  await Promise.all(
    existing
      .filter((f) => f.code && isDeclarationFileCode(f.code))
      .map((f) => store.deleteByCode(resourceId, resource, f.code!)),
  );

  await Promise.all(
    normalized.map((file, index) =>
      store.upload({
        resourceId,
        resource,
        code: `declaration_${index}`,
        file,
      }),
    ),
  );
}

/** Require at least one transit permit file in the upload batch. */
export function assertTransitPermitFiles(files: Express.Multer.File[]): void {
  if (files.length === 0) {
    throw new BadRequestException('No transit permit documents uploaded');
  }
}

/** Assign stable `transit_permit_*` codes for multi-file import transit uploads. */
export function normalizeTransitPermitFieldNames(
  files: Express.Multer.File[],
): Express.Multer.File[] {
  return files.map((file, index) => ({
    ...file,
    fieldname: `transit_permit_${index}`,
  }));
}

/** Replace all import transit permit files on a resource with a new multi-file batch. */
export async function persistTransitPermitUploads(
  store: DeclarationFileStore,
  resourceId: string,
  resource: string,
  files: Express.Multer.File[],
): Promise<void> {
  const normalized = normalizeTransitPermitFieldNames(files);
  assertTransitPermitFiles(normalized);

  const existing = await store.findByResource(resourceId, resource);
  await Promise.all(
    existing
      .filter((f) => f.code && isImportTransitPermitFileCode(f.code))
      .map((f) => store.deleteByCode(resourceId, resource, f.code!)),
  );

  await Promise.all(
    normalized.map((file, index) =>
      store.upload({
        resourceId,
        resource,
        code: `transit_permit_${index}`,
        file,
      }),
    ),
  );
}

/** Require at least one export transport document in the upload batch. */
export function assertExportTransportFiles(files: Express.Multer.File[]): void {
  if (files.length === 0) {
    throw new BadRequestException('No transit permit documents uploaded');
  }
}

export function normalizeExportTransportFieldNames(
  files: Express.Multer.File[],
): Express.Multer.File[] {
  return files.map((file, index) => ({
    ...file,
    fieldname: `export_transport_document_${index}`,
  }));
}

/** Replace all export transport documents on a booking with a new multi-file batch. */
export async function persistExportTransportUploads(
  store: DeclarationFileStore,
  bookingId: string,
  files: Express.Multer.File[],
): Promise<void> {
  const normalized = normalizeExportTransportFieldNames(files);
  assertExportTransportFiles(normalized);

  const existing = await store.findByResource(bookingId, 'bookings');
  await Promise.all(
    existing
      .filter((f) => f.code && isExportTransportFileCode(f.code))
      .map((f) => store.deleteByCode(bookingId, 'bookings', f.code!)),
  );

  await Promise.all(
    normalized.map((file, index) =>
      store.upload({
        resourceId: bookingId,
        resource: 'bookings',
        code: `export_transport_document_${index}`,
        file,
      }),
    ),
  );
}

/** Require at least one T1 transport document in the upload batch. */
export function assertT1TransportFiles(files: Express.Multer.File[]): void {
  if (files.length === 0) {
    throw new BadRequestException('No T1 transport documents uploaded');
  }
}

export function normalizeT1TransportFieldNames(
  files: Express.Multer.File[],
): Express.Multer.File[] {
  return files.map((file, index) => ({
    ...file,
    fieldname: `t1_transport_document_${index}`,
  }));
}

/** Replace all T1 transport documents on a booking with a new multi-file batch. */
export async function persistT1TransportUploads(
  store: DeclarationFileStore,
  bookingId: string,
  files: Express.Multer.File[],
): Promise<void> {
  const normalized = normalizeT1TransportFieldNames(files);
  assertT1TransportFiles(normalized);

  const existing = await store.findByResource(bookingId, 'bookings');
  await Promise.all(
    existing
      .filter((f) => f.code && isT1TransportFileCode(f.code))
      .map((f) => store.deleteByCode(bookingId, 'bookings', f.code!)),
  );

  await Promise.all(
    normalized.map((file, index) =>
      store.upload({
        resourceId: bookingId,
        resource: 'bookings',
        code: `t1_transport_document_${index}`,
        file,
      }),
    ),
  );
}

export function parseDutyRequiredForm(value: string | boolean | undefined): boolean {
  if (typeof value === 'boolean') return value;
  if (value === undefined || value === '') return false;
  return value === 'true' || value === '1';
}

type DjQueueMilestone = {
  ownerRegion?: string | null;
  status: string;
};

type DjQueueCycle = {
  preClearanceFinalizedAt?: Date | null;
  roHoldReason?: string | null;
} | null | undefined;

/** Whether a customs clearance item belongs on the persistent GL Djibouti list. */
export function belongsOnDjClearanceQueue(
  tradeDirection: string | null | undefined,
  cycle: DjQueueCycle,
  milestones: DjQueueMilestone[],
  extras?: {
    roHoldReason?: string | null;
    preClearanceFinalizedAt?: Date | null;
  },
): boolean {
  const roHold = cycle?.roHoldReason ?? extras?.roHoldReason;
  if (roHold) return true;

  const hasDjActivity = milestones.some(
    (m) => m.ownerRegion === 'DJ' && (m.status === 'COMPLETED' || m.status === 'PENDING'),
  );
  if (hasDjActivity) return true;

  // Import DO upload is un-gated — Djibouti GL must see import customs items from
  // the start, not only after Ethiopia finalizes pre-clearance.
  if (tradeDirection === 'IMPORT') return true;

  return false;
}

/** Contract statuses for persistent phased customs clearance lists (ET + DJ). */
export const PHASED_CUSTOMS_CONTRACT_QUEUE_STATUSES = [
  'AWAITING_CLEARANCE_DOCUMENTS',
  'CLEARANCE_UNDER_REVIEW',
  'CLEARANCE_READY_FOR_BOOKING',
  'ACTIVE_SHIPMENT_IN_PROGRESS',
  'FULLY_EXECUTED',
  'CONTRACT_ACTIVE',
  'CONTRACT_CLOSED',
  // Terminal contracts stay on the list — the clearance hub is GL's history of
  // everything that passed through, not just the live work queue.
  'EXPIRED',
  'CANCELLED',
  'REJECTED',
] as const;

/** Whether a customs clearance item belongs on the persistent GL Ethiopia list. */
export function belongsOnEtClearanceQueue(milestones: DjQueueMilestone[]): boolean {
  return milestones.some((m) => m.status === 'PENDING' || m.status === 'COMPLETED');
}

/** Contract statuses that may appear on the GL Djibouti clearance list (includes post-booking). */
export const DJ_CONTRACT_QUEUE_STATUSES = PHASED_CUSTOMS_CONTRACT_QUEUE_STATUSES;

/** Booking statuses for persistent phased customs clearance lists (ET + DJ). */
export const PHASED_CUSTOMS_BOOKING_QUEUE_STATUSES = [
  'AWAITING_DOCUMENTS',
  'DOCUMENTS_UNDER_REVIEW',
  'CLEARANCE_READY',
  'FULLY_EXECUTED',
  'OPERATION_REQUEST_PENDING',
  'OPERATION_CHANGES_REQUESTED',
  'ROAD_DISPATCH_PENDING',
  // Payment phase — the booking is selected/awaiting the customer's payment.
  'SELECTED_FOR_BATCH',
  'PNR_GENERATED',
  'AWAITING_PAYMENT',
  'PAYMENT_VERIFICATION_IN_PROGRESS',
  'IN_TRANSIT',
  'ARRIVED',
  'PAID',
  'COMPLETED',
  'CONTRACT_ACTIVE',
  'CONTRACT_CLOSED',
  // Terminal bookings stay on the list — EXPIRED especially: GL rebooks it
  // from here, and the hub doubles as clearance history.
  'EXPIRED',
  'CANCELLED',
  'REJECTED',
] as const;

/** Booking statuses that may appear on the GL Djibouti clearance list (includes post-clearance). */
export const DJ_BOOKING_QUEUE_STATUSES = PHASED_CUSTOMS_BOOKING_QUEUE_STATUSES;

/** Build labeled phased-customs file rows from resource files. */
export function buildWorkflowFiles(
  files: Array<{ code?: string | null; id: string; name: string; url: string }>,
  tradeDirection: string,
): ClearanceWorkflowFile[] {
  const fileByCode = new Map(
    files.filter((f) => f.code).map((f) => [f.code as string, f]),
  );
  const out: ClearanceWorkflowFile[] = [];
  const included = new Set<string>();

  for (const entry of catalogEntriesForTradeDirection(tradeDirection)) {
    const file = fileByCode.get(entry.code) ?? null;
    if (!file) continue;
    included.add(entry.code);
    out.push({
      code: entry.code,
      label: entry.label,
      uploadedBy: entry.uploadedBy,
      category: entry.category,
      file: { id: file.id, name: file.name, url: file.url },
    });
  }

  const extraDeclarations = files
    .filter((f) => f.code && isDeclarationFileCode(f.code) && !included.has(f.code))
    .sort((a, b) => (a.code ?? '').localeCompare(b.code ?? ''));

  extraDeclarations.forEach((file, index) => {
    if (!file.code) return;
    included.add(file.code);
    out.push({
      code: file.code,
      label: declarationFileLabel(file.code, index),
      uploadedBy: 'gl_et',
      category: 'declaration',
      file: { id: file.id, name: file.name, url: file.url },
    });
  });

  if (tradeDirection === 'IMPORT') {
    const extraTransit = files
      .filter((f) => f.code && isImportTransitPermitFileCode(f.code) && !included.has(f.code))
      .sort((a, b) => (a.code ?? '').localeCompare(b.code ?? ''));

    extraTransit.forEach((file, index) => {
      if (!file.code) return;
      included.add(file.code);
      out.push({
        code: file.code,
        label: transitPermitFileLabel(file.code, index),
        uploadedBy: 'gl_et',
        category: 'transit',
        file: { id: file.id, name: file.name, url: file.url },
      });
    });

    const extraT1 = files
      .filter((f) => f.code && isT1TransportFileCode(f.code) && !included.has(f.code))
      .sort((a, b) => (a.code ?? '').localeCompare(b.code ?? ''));

    extraT1.forEach((file, index) => {
      if (!file.code) return;
      included.add(file.code);
      out.push({
        code: file.code,
        label: t1TransportFileLabel(file.code, index),
        uploadedBy: 'gl_dj',
        category: 'djibouti',
        file: { id: file.id, name: file.name, url: file.url },
      });
    });
  }

  if (tradeDirection === 'EXPORT') {
    const extraExportTransport = files
      .filter((f) => f.code && isExportTransportFileCode(f.code) && !included.has(f.code))
      .sort((a, b) => (a.code ?? '').localeCompare(b.code ?? ''));

    extraExportTransport.forEach((file, index) => {
      if (!file.code) return;
      included.add(file.code);
      out.push({
        code: file.code,
        label: exportTransportFileLabel(file.code, index),
        uploadedBy: 'gl_et',
        category: 'transit',
        file: { id: file.id, name: file.name, url: file.url },
      });
    });
  }

  return out;
}
