import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { DataSource, EntityManager, FindManyOptions, ILike } from 'typeorm';

import { deriveTradeDirection } from '../../common/derive-trade-direction.util';
import { Cargo } from '../cargoes/entities/cargoes.entity';
import { BulkInspectDto } from './dto/bulk-inspect.dto';
import { BulkReceiveDto } from './dto/bulk-receive.dto';
import { DeliverInventoryDto } from './dto/deliver-inventory.dto';
import { FilterWarehouseInventoryDto } from './dto/filter-inventory.dto';
import { InquiryWarehouseInventoryDto } from './dto/inquiry-inventory.dto';
import { LoadInventoryDto } from './dto/load-inventory.dto';
import { MoveInventoryDto } from './dto/move-inventory.dto';
import { ReceiveWarehouseInventoryDto } from './dto/receive-inventory.dto';
import { ReleaseOrderDto } from './dto/release-order.dto';
import { ReserveInventoryDto } from './dto/reserve-inventory.dto';
import { UnloadBookingDto } from './dto/unload-booking.dto';
import { WarehouseAllocationService } from './warehouse-allocation.service';
import { WarehouseInspectionService } from './warehouse-inspection.service';
import { WarehouseInvoiceService } from './warehouse-invoice.service';
import { WarehouseActivityLog } from './entities/warehouse-activity-log.entity';
import { WarehouseInventoryMovement } from './entities/warehouse-inventory-movement.entity';
import {
  WAREHOUSE_INVENTORY_TRANSITIONS,
  WarehouseInventory,
  WarehouseInventoryStatus,
} from './entities/warehouse-inventory.entity';
import { WarehouseLoading } from './entities/warehouse-loading.entity';
import { WarehouseYard } from './entities/warehouse-yard.entity';
import { WarehouseZone } from './entities/warehouse-zone.entity';
import { Warehouse } from './entities/warehouse.entity';
import { SchedulingReadFacade } from './scheduling-read.facade';
import { WarehouseActivityLogService } from './warehouse-activity-log.service';
import { WarehouseInventoryRepository } from './warehouse-inventory.repository';
import { WarehouseLoadingRepository } from './warehouse-loading.repository';

/** Wagon states that may receive a load (besides being part of an existing schedule). */
const LOADABLE_WAGON_STATUSES = ['AVAILABLE', 'IMPORT_READY', 'EXPORT_READY', 'ASSIGNED'];

export interface InventoryInquiryResult {
  id: string;
  bookingId: string | null;
  bookingNumber: string | null;
  customerName: string | null;
  containerNumber: string | null;
  cargoType: string | null;
  cargoDescription: string | null;
  goodsId: string | null;
  warehouse: { id: string; name: string; code: string } | null;
  yard: { id: string; name: string; code: string } | null;
  zone: { id: string; name: string; code: string } | null;
  status: string;
  quantity: number;
  weight: number;
  arrivedAt: Date | null;
  readyForLoadingAt: Date | null;
}

interface LocationNode {
  maxWeight?: number | null;
  capacityWeight?: number | null;
  maxVolume?: number | null;
  capacityContainers?: number | null;
  currentWeight: number;
  currentVolume: number;
  currentContainers: number;
}

// ── Batch 4.5 result/queue shapes ────────────────────────────────────────────
interface ArrivalQueueRow {
  bookingId: string;
  bookingReference: string;
  customer: string | null;
  cargo: string | null;
  container: string | null;
  arrivalDate: Date | null;
  bookingStatus: string;
  inventoryId: string | null;
  currentStatus: string | null;
  inspectionStatus: string | null;
  facility: string | null;
  warehouse: string | null;
  yard: string | null;
  zone: string | null;
}

export interface ArrivalQueueItem {
  bookingId: string;
  bookingReference: string;
  customer: string | null;
  cargo: string | null;
  container: string | null;
  facility: string | null;
  warehouse: string | null;
  yard: string | null;
  zone: string | null;
  inventoryId: string | null;
  currentStatus: string | null;
  arrivalDate: Date | null;
  inspectionStatus: string | null;
  unloaded: boolean;
}

interface DefaultLocation {
  warehouseId: string;
  yardId: string;
  zoneId: string;
  facilityId: string | null;
}

export interface AutoUnloadResult {
  processedCount: number;
  skippedCount: number;
  failedCount: number;
  results: { bookingId: string; inventoryId?: string; status: string; reason?: string }[];
}

export interface AutoLoadResult {
  loadedCount: number;
  skippedCount: number;
  results: { inventoryId: string; status: string; reason?: string }[];
}

// ── Receive (Import/Export bulk) shapes ──────────────────────────────────────
export interface EligibleBookingRow {
  id: string;
  reference: string;
  customerId: string | null;
  customer: string | null;
  direction: string;
  origin: string | null;
  destination: string | null;
  freightType: string | null;
  cargo: string | null;
  weight: string | null;
  paymentStatus: string;
  status: string;
}

export interface BulkReceiveResult {
  receivedCount: number;
  skippedCount: number;
  results: { bookingId: string; status: string; inventoryId?: string; reason?: string }[];
}

export interface LoadPassedExportResult {
  loadedCount: number;
  skippedCount: number;
  results: { inventoryId: string; status: string; reason?: string }[];
}

export interface BulkInspectResult {
  inspectedCount: number;
  skippedCount: number;
  results: { inventoryId: string; status: string; reason?: string }[];
}

export interface ReadyToLoadRow {
  id: string;
  bookingId: string | null;
  bookingReference: string | null;
  customerId: string | null;
  customerName: string | null;
  containerNumber: string | null;
  cargoType: string | null;
  weight: number | null;
  origin: string | null;
  destination: string | null;
  inspectionStatus: string | null;
  status: string;
}

export interface BulkDispatchResult {
  dispatchedCount: number;
  skippedCount: number;
  results: { inventoryId: string; status: string; reason?: string }[];
}

export interface AutoUnloadArrivedResult {
  unloadedCount: number;
  skippedCount: number;
  failedCount: number;
  results: { bookingId: string; inventoryId?: string; status: string; reason?: string }[];
}

export interface ImportUnloadedRow {
  id: string;
  bookingId: string | null;
  bookingReference: string | null;
  customerId: string | null;
  customerName: string | null;
  arrivalTime: string | null;
  containerNumber: string | null;
  cargoType: string | null;
  weight: number | null;
  trainSchedule: string | null;
  inspectionStatus: string | null;
  pickupOption: string;
  lastMileRequested: boolean;
  currentStatus: string;
}

@Injectable()
export class WarehouseInventoryService {
  constructor(
    private readonly dataSource: DataSource,
    private readonly inventoryRepository: WarehouseInventoryRepository,
    private readonly loadingRepository: WarehouseLoadingRepository,
    private readonly activityLog: WarehouseActivityLogService,
    private readonly scheduling: SchedulingReadFacade,
    private readonly allocation: WarehouseAllocationService,
    private readonly invoices: WarehouseInvoiceService,
    private readonly inspectionService: WarehouseInspectionService,
  ) {}

  /**
   * Batch 6 — final terminal release / gate clearance.
   * Blocked while an unpaid demurrage/storage invoice exists. Does NOT touch
   * inspection / storage / loading steps — only the final release.
   */
  async gateClearance(id: string, performedBy?: string): Promise<WarehouseInventory> {
    const item = await this.findById(id);
    const blocking = await this.invoices.findBlockingInvoice(id);
    if (blocking) {
      throw new BadRequestException(
        'Warehouse demurrage/storage fee must be paid before terminal release.',
      );
    }
    const now = new Date();
    await this.inventoryRepository.update(id, {
      gateClearedAt: now,
      releaseDate: item.releaseDate ?? now,
    });
    await this.activityLog.record({
      activityType: 'INVENTORY_DISPATCHED',
      inventoryId: id,
      warehouseId: item.warehouseId,
      description: 'Gate clearance / terminal release',
      performedBy,
    });
    return this.findById(id);
  }

  // ── Listing ────────────────────────────────────────────────────────────

  findAll(filter: FilterWarehouseInventoryDto): Promise<WarehouseInventory[]> {
    const base = {
      ...(filter.warehouseId ? { warehouseId: filter.warehouseId } : {}),
      ...(filter.yardId ? { yardId: filter.yardId } : {}),
      ...(filter.zoneId ? { zoneId: filter.zoneId } : {}),
      ...(filter.bookingId ? { bookingId: filter.bookingId } : {}),
      ...(filter.cargoId ? { cargoId: filter.cargoId } : {}),
      ...(filter.containerId ? { containerId: filter.containerId } : {}),
      ...(filter.goodsId ? { goodsId: filter.goodsId } : {}),
      ...(filter.status ? { status: filter.status } : {}),
    };

    const search = filter.search?.trim();
    const where: FindManyOptions<WarehouseInventory>['where'] = search
      ? { ...base, notes: ILike(`%${search}%`) }
      : base;

    return this.inventoryRepository.findAll({
      where,
      relations: { warehouse: { facility: true }, yard: true, zone: true, booking: true },
      order: { createdAt: 'DESC' },
    });
  }

  findReadyForLoading(filter: FilterWarehouseInventoryDto): Promise<WarehouseInventory[]> {
    return this.findAll({ ...filter, status: 'READY_FOR_LOADING' });
  }

  async findById(id: string): Promise<WarehouseInventory> {
    const item = await this.inventoryRepository.findById(id, {
      relations: { warehouse: true, yard: true, zone: true },
    });

    if (!item) {
      throw new NotFoundException(`Inventory item ${id} not found`);
    }

    return item;
  }

  // ── Batch 4.5: Arrival / Unload / Load automation ──────────────────────────

  /** Bookings whose goods have arrived and may be unloaded into the warehouse. */
  private readonly ARRIVED_BOOKING_STATUSES = ['IN_TRANSIT'];

  /** Arrived bookings + their current inventory/inspection state (queue view). */
  async arrivalQueue(): Promise<ArrivalQueueItem[]> {
    const rows: ArrivalQueueRow[] = await this.dataSource.query(
      `SELECT b.id            AS "bookingId",
              b.reference      AS "bookingReference",
              company.name     AS "customer",
              b.cargo_free_text AS "cargo",
              ct.container_number AS "container",
              b.scheduled_date AS "arrivalDate",
              b.status         AS "bookingStatus",
              inv.id           AS "inventoryId",
              inv.status       AS "currentStatus",
              inv.inspection_status AS "inspectionStatus",
              fac.name         AS "facility",
              wh.name          AS "warehouse",
              yard.name        AS "yard",
              zone.name        AS "zone"
         FROM freight.bookings b
         LEFT JOIN freight.companies company ON company.id = b.company_id
         LEFT JOIN freight.warehouse_inventory inv ON inv.booking_id = b.id AND inv.deleted_at IS NULL
         LEFT JOIN freight.warehouses wh ON wh.id = inv.warehouse_id
         LEFT JOIN freight.warehouse_yards yard ON yard.id = inv.yard_id
         LEFT JOIN freight.warehouse_zones zone ON zone.id = inv.zone_id
         LEFT JOIN freight.facilities fac ON fac.id = wh.facility_id
         LEFT JOIN freight.containers ct ON ct.id = inv.container_id
        WHERE b.status = ANY($1) AND b.deleted_at IS NULL
        ORDER BY b.scheduled_date DESC NULLS LAST`,
      [this.ARRIVED_BOOKING_STATUSES],
    );

    return rows.map((r) => ({
      bookingId: r.bookingId,
      bookingReference: r.bookingReference,
      customer: r.customer ?? null,
      cargo: r.cargo ?? null,
      container: r.container ?? null,
      facility: r.facility ?? null,
      warehouse: r.warehouse ?? null,
      yard: r.yard ?? null,
      zone: r.zone ?? null,
      inventoryId: r.inventoryId ?? null,
      currentStatus: r.currentStatus ?? null,
      arrivalDate: r.arrivalDate ?? null,
      inspectionStatus: r.inspectionStatus ?? null,
      unloaded: Boolean(r.inventoryId),
    }));
  }

  /** First warehouse that has at least one yard + zone (fallback location for auto-unload). */
  private async pickDefaultLocation(): Promise<DefaultLocation | null> {
    const [row]: DefaultLocation[] = await this.dataSource.query(
      `SELECT wh.id AS "warehouseId", wh.facility_id AS "facilityId",
              yard.id AS "yardId", zone.id AS "zoneId"
         FROM freight.warehouses wh
         JOIN freight.warehouse_yards yard ON yard.warehouse_id = wh.id AND yard.deleted_at IS NULL
         JOIN freight.warehouse_zones zone ON zone.yard_id = yard.id AND zone.deleted_at IS NULL
        WHERE wh.deleted_at IS NULL
        ORDER BY wh.created_at ASC
        LIMIT 1`,
    );
    return row ?? null;
  }

  /** Bulk-create inventory (RECEIVED) for arrived bookings that are not yet unloaded. */
  async autoUnloadArrived(): Promise<AutoUnloadResult> {
    const arrived: {
      id: string;
      weight: string | null;
      freightType: string | null;
      tradeDirection: string | null;
      cargoTypeCode: string | null;
    }[] = await this.dataSource.query(
      `SELECT b.id, b.cargo_total_weight_vgm AS weight,
              b.freight_type AS "freightType", b.trade_direction AS "tradeDirection",
              cgt.code AS "cargoTypeCode"
         FROM freight.bookings b
         LEFT JOIN freight.warehouse_inventory inv ON inv.booking_id = b.id AND inv.deleted_at IS NULL
         LEFT JOIN freight.cargo_types cgt ON cgt.id = b.cargo_type_id
        WHERE b.status = ANY($1) AND b.deleted_at IS NULL AND inv.id IS NULL`,
      [this.ARRIVED_BOOKING_STATUSES],
    );

    const result: AutoUnloadResult = { processedCount: 0, skippedCount: 0, failedCount: 0, results: [] };

    if (arrived.length === 0) return result;

    const fallback = await this.pickDefaultLocation();

    for (const booking of arrived) {
      try {
        // Deterministic allocation by rules; fall back to default location if no rule resolves.
        const allocated = await this.allocation.resolveLocation({
          freightType: booking.freightType,
          tradeDirection: booking.tradeDirection,
          cargoTypeCode: booking.cargoTypeCode,
        });
        const location = allocated ?? fallback;
        if (!location) {
          result.failedCount += 1;
          result.results.push({ bookingId: booking.id, status: 'FAILED', reason: 'No warehouse/yard/zone configured' });
          continue;
        }
        const saved = await this.inventoryRepository.create({
          warehouseId: location.warehouseId,
          yardId: location.yardId,
          zoneId: location.zoneId,
          bookingId: booking.id,
          quantity: 1,
          weight: Number(booking.weight) || 0,
          status: 'RECEIVED',
          arrivedAt: new Date(),
          notes: allocated?.rule ? `Auto-unloaded → ${allocated.path}` : 'Auto-unloaded from arrival queue',
        });
        result.processedCount += 1;
        result.results.push({ bookingId: booking.id, inventoryId: saved.id, status: 'PROCESSED' });
      } catch (error) {
        result.failedCount += 1;
        result.results.push({
          bookingId: booking.id,
          status: 'FAILED',
          reason: error instanceof Error ? error.message : String(error),
        });
      }
    }

    return result;
  }

  /** Unload a single arrived booking into a chosen (or default) location. */
  async unloadBooking(bookingId: string, dto: UnloadBookingDto): Promise<WarehouseInventory> {
    const existing = await this.inventoryRepository.findAll({ where: { bookingId } });

    let location: DefaultLocation | null =
      dto.warehouseId && dto.yardId && dto.zoneId
        ? { warehouseId: dto.warehouseId, yardId: dto.yardId, zoneId: dto.zoneId, facilityId: dto.facilityId ?? null }
        : null;
    if (!location) location = await this.pickDefaultLocation();
    if (!location) {
      throw new BadRequestException('No warehouse/yard/zone provided or configured for unloading');
    }

    const arrivedAt = dto.unloadedAt ? new Date(dto.unloadedAt) : new Date();

    if (existing[0]) {
      await this.inventoryRepository.update(existing[0].id, {
        warehouseId: location.warehouseId,
        yardId: location.yardId,
        zoneId: location.zoneId,
        status: 'RECEIVED',
        arrivedAt,
        notes: dto.notes ?? existing[0].notes ?? 'Unloaded',
      });
      return this.findById(existing[0].id);
    }

    const saved = await this.inventoryRepository.create({
      warehouseId: location.warehouseId,
      yardId: location.yardId,
      zoneId: location.zoneId,
      bookingId,
      quantity: 1,
      weight: 0,
      status: 'RECEIVED',
      arrivedAt,
      notes: dto.notes ?? 'Unloaded',
    });
    return this.findById(saved.id);
  }

  /** Auto-load all READY_FOR_LOADING inventory whose booking is PAID. Unpaid stay pending. */
  async autoLoadReady(): Promise<AutoLoadResult> {
    const ready = await this.inventoryRepository.findAll({ where: { status: 'READY_FOR_LOADING' } });
    const result: AutoLoadResult = { loadedCount: 0, skippedCount: 0, results: [] };

    for (const item of ready) {
      const bookingStatus = item.bookingId ? await this.getBookingStatus(item.bookingId) : null;
      if (bookingStatus !== 'PAID') {
        result.skippedCount += 1;
        result.results.push({ inventoryId: item.id, status: 'SKIPPED', reason: 'Booking not PAID' });
        continue;
      }
      await this.dataSource.transaction(async (manager) => {
        await manager.getRepository(WarehouseInventory).update(item.id, {
          status: 'LOADED',
          loadedAt: new Date(),
        });
        await this.activityLog.record(
          {
            activityType: 'INVENTORY_LOADED',
            inventoryId: item.id,
            warehouseId: item.warehouseId,
            description: 'Auto-loaded (PAID booking)',
          },
          manager,
        );
      });
      result.loadedCount += 1;
      result.results.push({ inventoryId: item.id, status: 'LOADED' });
    }

    return result;
  }

  // ── Receive (Import/Export bulk) ───────────────────────────────────────────

  /**
   * Eligible PAID bookings that have NOT been received yet, classified IMPORT/EXPORT by route
   * (origin/destination yard countries). Pass a direction to filter to one; omit it to return
   * all import + export bookings in a single call (DOMESTIC routes are excluded either way).
   */
  async eligibleBookings(direction?: 'IMPORT' | 'EXPORT'): Promise<EligibleBookingRow[]> {
    const rows: Array<
      EligibleBookingRow & { originCountry: string | null; destinationCountry: string | null }
    > = await this.dataSource.query(
      `SELECT b.id,
              b.reference              AS "reference",
              b.company_id             AS "customerId",
              company.name             AS "customer",
              oy.code                  AS "origin",
              dy.code                  AS "destination",
              oy.country               AS "originCountry",
              dy.country               AS "destinationCountry",
              b.freight_type           AS "freightType",
              COALESCE(ct.cargo_type_name, b.cargo_free_text) AS "cargo",
              b.cargo_total_weight_vgm AS "weight",
              b.payment_status         AS "paymentStatus",
              b.status                 AS "status"
         FROM freight.bookings b
         LEFT JOIN freight.companies company ON company.id = b.company_id
         LEFT JOIN freight.yards oy ON oy.id = b.origin_yard_id
         LEFT JOIN freight.yards dy ON dy.id = b.destination_yard_id
         LEFT JOIN freight.cargo_types ct ON ct.id = b.cargo_type_id
         LEFT JOIN freight.warehouse_inventory inv ON inv.booking_id = b.id AND inv.deleted_at IS NULL
        WHERE b.deleted_at IS NULL
          AND b.payment_status = 'PAID'
          AND inv.id IS NULL
        ORDER BY b.scheduled_date DESC NULLS LAST`,
    );

    // Direction is derived from the route (origin/destination yard countries), reusing deriveTradeDirection.
    return rows
      .map((r) => ({
        ...r,
        direction: deriveTradeDirection({ country: r.originCountry }, { country: r.destinationCountry }),
      }))
      .filter((r) =>
        direction ? r.direction === direction : r.direction === 'IMPORT' || r.direction === 'EXPORT',
      );
  }

  /** Bulk-receive eligible PAID bookings into a location. Skips duplicates / wrong direction. */
  async bulkReceive(dto: BulkReceiveDto): Promise<BulkReceiveResult> {
    const result: BulkReceiveResult = { receivedCount: 0, skippedCount: 0, results: [] };

    await this.dataSource.transaction(async (manager) => {
      await this.validateLocation(manager, {
        warehouseId: dto.warehouseId,
        yardId: dto.yardId,
        zoneId: dto.zoneId,
      });

      for (const bookingId of dto.bookingIds) {
        const skip = (reason: string) => {
          result.skippedCount += 1;
          result.results.push({ bookingId, status: 'SKIPPED', reason });
        };

        const [booking] = await manager.query(
          `SELECT b.payment_status AS "paymentStatus", b.cargo_total_weight_vgm AS "weight",
                  oy.country AS "originCountry", dy.country AS "destinationCountry"
             FROM freight.bookings b
             LEFT JOIN freight.yards oy ON oy.id = b.origin_yard_id
             LEFT JOIN freight.yards dy ON dy.id = b.destination_yard_id
            WHERE b.id = $1 AND b.deleted_at IS NULL LIMIT 1`,
          [bookingId],
        );
        if (!booking) { skip('Booking not found'); continue; }
        if (booking.paymentStatus !== 'PAID') { skip('Booking not PAID'); continue; }
        // Direction is derived from the route (yard countries), not the stored field.
        const bookingDirection = deriveTradeDirection(
          { country: booking.originCountry },
          { country: booking.destinationCountry },
        );
        if (bookingDirection !== dto.direction) {
          skip(`Booking route is ${bookingDirection}, not ${dto.direction}`);
          continue;
        }

        const existing = await manager.getRepository(WarehouseInventory).findOne({ where: { bookingId } });
        if (existing) { skip('Already received'); continue; }

        const saved = await manager.getRepository(WarehouseInventory).save(
          manager.getRepository(WarehouseInventory).create({
            warehouseId: dto.warehouseId,
            yardId: dto.yardId,
            zoneId: dto.zoneId,
            bookingId,
            quantity: 1,
            weight: Number(booking.weight) || 0,
            status: 'RECEIVED',
            arrivedAt: new Date(),
            notes: `Bulk received (${dto.direction})`,
          }),
        );

        await this.activityLog.record(
          {
            activityType: 'INVENTORY_RECEIVED',
            inventoryId: saved.id,
            warehouseId: dto.warehouseId,
            description: `Bulk received ${dto.direction} booking`,
            performedBy: dto.performedBy,
          },
          manager,
        );

        result.receivedCount += 1;
        result.results.push({ bookingId, status: 'RECEIVED', inventoryId: saved.id });
      }
    });

    return result;
  }

  /** Bulk-load all EXPORT inventory that passed inspection and is READY_FOR_LOADING. */
  async loadPassedExport(performedBy?: string): Promise<LoadPassedExportResult> {
    const ready = await this.inventoryRepository.findAll({ where: { status: 'READY_FOR_LOADING' } });
    const result: LoadPassedExportResult = { loadedCount: 0, skippedCount: 0, results: [] };

    for (const item of ready) {
      const skip = (reason: string) => {
        result.skippedCount += 1;
        result.results.push({ inventoryId: item.id, status: 'SKIPPED', reason });
      };

      if (item.inspectionStatus !== 'PASSED') { skip('Inspection not PASSED'); continue; }
      const direction = item.bookingId ? await this.getBookingDirection(item.bookingId) : null;
      if (direction !== 'EXPORT') { skip('Not an EXPORT item'); continue; }
      const bookingStatus = item.bookingId ? await this.getBookingStatus(item.bookingId) : null;
      if (bookingStatus !== 'PAID') { skip('Booking not PAID'); continue; }

      await this.dataSource.transaction(async (manager) => {
        await manager.getRepository(WarehouseInventory).update(item.id, {
          status: 'LOADED',
          loadedAt: new Date(),
        });
        await this.activityLog.record(
          {
            activityType: 'INVENTORY_LOADED',
            inventoryId: item.id,
            warehouseId: item.warehouseId,
            description: 'Bulk loaded (passed export)',
            performedBy,
          },
          manager,
        );
      });

      result.loadedCount += 1;
      result.results.push({ inventoryId: item.id, status: 'LOADED' });
    }

    return result;
  }

  /** EXPORT inventory rows at a given status (route-derived direction), with booking detail. */
  private async exportInventoryByStatus(
    status: WarehouseInventoryStatus,
    requireInspectionPassed = false,
  ): Promise<ReadyToLoadRow[]> {
    const rows: Array<
      ReadyToLoadRow & { originCountry: string | null; destinationCountry: string | null }
    > = await this.dataSource.query(
      `SELECT inv.id,
              inv.booking_id           AS "bookingId",
              b.reference              AS "bookingReference",
              b.company_id             AS "customerId",
              company.name             AS "customerName",
              ct.container_number      AS "containerNumber",
              COALESCE(cgt.cargo_type_name, b.cargo_free_text) AS "cargoType",
              inv.weight               AS "weight",
              oy.code                  AS "origin",
              dy.code                  AS "destination",
              oy.country               AS "originCountry",
              dy.country               AS "destinationCountry",
              inv.inspection_status    AS "inspectionStatus",
              inv.status
         FROM freight.warehouse_inventory inv
         LEFT JOIN freight.bookings b ON b.id = inv.booking_id AND b.deleted_at IS NULL
         LEFT JOIN freight.companies company ON company.id = b.company_id
         LEFT JOIN freight.yards oy ON oy.id = b.origin_yard_id
         LEFT JOIN freight.yards dy ON dy.id = b.destination_yard_id
         LEFT JOIN freight.cargo_types cgt ON cgt.id = b.cargo_type_id
         LEFT JOIN freight.containers ct ON ct.id = inv.container_id
        WHERE inv.deleted_at IS NULL
          AND inv.status = $1
          ${requireInspectionPassed ? `AND inv.inspection_status = 'PASSED'` : ''}
        ORDER BY inv.created_at DESC`,
      [status],
    );

    return rows
      .filter((r) => {
        const dir = deriveTradeDirection({ country: r.originCountry }, { country: r.destinationCountry });
        return dir === 'EXPORT';
      })
      .map(({ originCountry: _oc, destinationCountry: _dc, ...rest }) => rest);
  }

  /** EXPORT inventory that passed inspection and is waiting to be loaded (READY_FOR_LOADING). */
  async readyToLoadExport(): Promise<ReadyToLoadRow[]> {
    return this.exportInventoryByStatus('READY_FOR_LOADING', true);
  }

  /** EXPORT inventory that has been loaded onto a wagon and is queued for dispatch (LOADED). */
  async loadedExport(): Promise<ReadyToLoadRow[]> {
    return this.exportInventoryByStatus('LOADED');
  }

  /** Shared query for the import queues — IMPORT inventory at the given statuses, inspection columns. */
  private async importQueueByStatuses(statuses: string[]): Promise<ImportUnloadedRow[]> {
    const rows: Array<
      ImportUnloadedRow & { originCountry: string | null; destinationCountry: string | null }
    > = await this.dataSource.query(
      `SELECT inv.id,
              inv.booking_id           AS "bookingId",
              b.reference              AS "bookingReference",
              b.company_id             AS "customerId",
              company.name             AS "customerName",
              COALESCE(inv.unloaded_at, inv.arrived_at) AS "arrivalTime",
              (SELECT c.container_number FROM freight.containers c
                WHERE c.booking_id = b.id AND c.deleted_at IS NULL
                ORDER BY c.container_number LIMIT 1) AS "containerNumber",
              COALESCE(cgt.cargo_type_name, b.cargo_free_text) AS "cargoType",
              inv.weight               AS "weight",
              ts.train_number          AS "trainSchedule",
              inv.inspection_status    AS "inspectionStatus",
              CASE WHEN b.last_mile_delivery_address IS NOT NULL
                   THEN 'DOOR_DELIVERY' ELSE 'TERMINAL_PICKUP' END AS "pickupOption",
              (b.last_mile_delivery_address IS NOT NULL) AS "lastMileRequested",
              inv.status               AS "currentStatus",
              oy.country               AS "originCountry",
              dy.country               AS "destinationCountry"
         FROM freight.warehouse_inventory inv
         LEFT JOIN freight.bookings b ON b.id = inv.booking_id AND b.deleted_at IS NULL
         LEFT JOIN freight.companies company ON company.id = b.company_id
         LEFT JOIN freight.cargo_types cgt ON cgt.id = b.cargo_type_id
         LEFT JOIN freight.yards oy ON oy.id = b.origin_yard_id
         LEFT JOIN freight.yards dy ON dy.id = b.destination_yard_id
         LEFT JOIN freight.train_schedule_bookings tsb ON tsb.booking_id = b.id AND tsb.deleted_at IS NULL
         LEFT JOIN freight.train_schedules ts ON ts.id = tsb.train_schedule_id
        WHERE inv.deleted_at IS NULL
          AND inv.status = ANY($1)
        ORDER BY inv.created_at DESC`,
      [statuses],
    );

    return rows
      .filter(
        (r) =>
          deriveTradeDirection({ country: r.originCountry }, { country: r.destinationCountry }) === 'IMPORT',
      )
      .map(({ originCountry: _oc, destinationCountry: _dc, ...rest }) => rest);
  }

  /**
   * Batch 9 — IMPORT inventory sitting in the Unloaded Queue (UNLOADED / destination-inspection
   * states), with the columns the inspection screen needs. Read-only.
   */
  importUnloadedQueue(): Promise<ImportUnloadedRow[]> {
    return this.importQueueByStatuses(['UNLOADED', 'DESTINATION_INSPECTION', 'UNDER_INSPECTION']);
  }

  /**
   * Batch 10 — IMPORT inventory that passed inspection and is PICKUP_READY (READY_FOR_PICKUP),
   * awaiting customer pickup / last mile / store / dispatch. Read-only.
   */
  importPickupReadyQueue(): Promise<ImportUnloadedRow[]> {
    return this.importQueueByStatuses(['READY_FOR_PICKUP']);
  }

  /**
   * Bulk-dispatch loaded EXPORT inventory. Reuses the single-item dispatch transition
   * (status LOADED → DISPATCHED, capacity freed, movement/activity logged). Items not
   * LOADED or not EXPORT are skipped. The train/schedule flow later moves DISPATCHED → IN_TRANSIT.
   */
  async bulkDispatchExport(inventoryIds: string[], performedBy?: string): Promise<BulkDispatchResult> {
    const result: BulkDispatchResult = { dispatchedCount: 0, skippedCount: 0, results: [] };

    for (const inventoryId of inventoryIds) {
      const skip = (reason: string) => {
        result.skippedCount += 1;
        result.results.push({ inventoryId, status: 'SKIPPED', reason });
      };

      const item = await this.inventoryRepository.findById(inventoryId);
      if (!item) { skip('Inventory not found'); continue; }
      if (item.status !== 'LOADED') { skip(`Status is ${item.status}, not LOADED`); continue; }
      const direction = item.bookingId ? await this.getBookingDirection(item.bookingId) : null;
      if (direction !== 'EXPORT') { skip('Not an EXPORT item'); continue; }

      try {
        await this.dispatch(inventoryId, performedBy);
        result.dispatchedCount += 1;
        result.results.push({ inventoryId, status: 'DISPATCHED' });
      } catch (error) {
        skip(error instanceof Error ? error.message : String(error));
      }
    }

    return result;
  }

  /** Booking statuses eligible to be unloaded off an arrived import train (Batch 8). */
  private readonly IMPORT_UNLOAD_ELIGIBLE_STATUSES = [
    'IN_TRANSIT',
    'ARRIVED_AT_INDODE',
    'ARRIVED_AT_DESTINATION',
    'ARRIVED_AT_FACILITY',
  ];

  /**
   * Batch 8 — unload all eligible assigned bookings of an ARRIVED import train into UNLOADED state.
   * Reuses the allocation + inventory + activity-log plumbing. Does NOT store and does NOT inspect —
   * items land in UNLOADED so the operator drives store/inspect/reserve/dispatch from the queue.
   */
  async autoUnloadArrivedBookings(
    scheduleId: string,
    performedBy?: string,
  ): Promise<AutoUnloadArrivedResult> {
    const result: AutoUnloadArrivedResult = { unloadedCount: 0, skippedCount: 0, failedCount: 0, results: [] };

    // 1. Schedule must exist, be ARRIVED, and be an IMPORT route (derived from station countries).
    const [schedule] = await this.dataSource.query(
      `SELECT ts.id, ts.status, oy.country AS "originCountry", dy.country AS "destinationCountry"
         FROM freight.train_schedules ts
         LEFT JOIN freight.yards oy ON oy.id = ts.origin_station_id
         LEFT JOIN freight.yards dy ON dy.id = ts.destination_station_id
        WHERE ts.id = $1 AND ts.deleted_at IS NULL
        LIMIT 1`,
      [scheduleId],
    );
    if (!schedule) {
      throw new NotFoundException(`Train schedule ${scheduleId} not found`);
    }
    if (schedule.status !== 'ARRIVED') {
      throw new BadRequestException(`Train schedule is ${schedule.status}, not ARRIVED`);
    }
    const direction = deriveTradeDirection(
      { country: schedule.originCountry },
      { country: schedule.destinationCountry },
    );
    if (direction !== 'IMPORT') {
      throw new BadRequestException(`Train schedule route is ${direction}, not IMPORT`);
    }

    // 2. Assigned bookings on this train.
    const bookings: {
      id: string;
      status: string;
      weight: string | null;
      freightType: string | null;
      tradeDirection: string | null;
      cargoTypeCode: string | null;
    }[] = await this.dataSource.query(
      `SELECT b.id, b.status, b.cargo_total_weight_vgm AS weight,
              b.freight_type AS "freightType", b.trade_direction AS "tradeDirection",
              cgt.code AS "cargoTypeCode"
         FROM freight.train_schedule_bookings tsb
         JOIN freight.bookings b ON b.id = tsb.booking_id AND b.deleted_at IS NULL
         LEFT JOIN freight.cargo_types cgt ON cgt.id = b.cargo_type_id
        WHERE tsb.train_schedule_id = $1 AND tsb.deleted_at IS NULL`,
      [scheduleId],
    );

    const fallback = await this.pickDefaultLocation();
    const now = new Date();

    for (const booking of bookings) {
      const skip = (reason: string) => {
        result.skippedCount += 1;
        result.results.push({ bookingId: booking.id, status: 'SKIPPED', reason });
      };
      const fail = (reason: string) => {
        result.failedCount += 1;
        result.results.push({ bookingId: booking.id, status: 'FAILED', reason });
      };

      if (!this.IMPORT_UNLOAD_ELIGIBLE_STATUSES.includes(booking.status)) {
        skip(`Booking status ${booking.status} is not unload-eligible`);
        continue;
      }

      try {
        const existing = (await this.inventoryRepository.findAll({ where: { bookingId: booking.id } }))[0];

        // Already unloaded or further along — leave it (do not regress the lifecycle).
        if (existing && existing.status !== 'RECEIVED') {
          skip(`Inventory already ${existing.status}`);
          continue;
        }

        if (existing) {
          await this.inventoryRepository.update(existing.id, {
            status: 'UNLOADED',
            unloadedAt: now,
            arrivedAt: existing.arrivedAt ?? now,
          });
          await this.activityLog.record({
            activityType: 'INVENTORY_UNLOADED',
            inventoryId: existing.id,
            warehouseId: existing.warehouseId,
            description: 'Unloaded from arrived import train',
            performedBy,
          });
          result.unloadedCount += 1;
          result.results.push({ bookingId: booking.id, inventoryId: existing.id, status: 'UNLOADED' });
          continue;
        }

        // No inventory yet — create it at the allocated (or default) location, in UNLOADED state.
        const allocated = await this.allocation.resolveLocation({
          freightType: booking.freightType,
          tradeDirection: booking.tradeDirection,
          cargoTypeCode: booking.cargoTypeCode,
        });
        const location = allocated ?? fallback;
        if (!location) {
          fail('No warehouse/yard/zone configured');
          continue;
        }

        const saved = await this.inventoryRepository.create({
          warehouseId: location.warehouseId,
          yardId: location.yardId,
          zoneId: location.zoneId,
          bookingId: booking.id,
          quantity: 1,
          weight: Number(booking.weight) || 0,
          status: 'UNLOADED',
          arrivedAt: now,
          unloadedAt: now,
          notes: allocated?.rule ? `Unloaded → ${allocated.path}` : 'Unloaded from arrived import train',
        });
        await this.activityLog.record({
          activityType: 'INVENTORY_UNLOADED',
          inventoryId: saved.id,
          warehouseId: saved.warehouseId,
          description: 'Unloaded from arrived import train',
          performedBy,
        });
        result.unloadedCount += 1;
        result.results.push({ bookingId: booking.id, inventoryId: saved.id, status: 'UNLOADED' });
      } catch (error) {
        fail(error instanceof Error ? error.message : String(error));
      }
    }

    return result;
  }

  /**
   * Bulk-mark received items inspection PASSED (reusing the inspection service to create a minimal
   * report + sync inspectionStatus/inspectedAt). EXPORT items advance straight to READY_FOR_LOADING.
   * For damage / weight-loss / images, use the per-item Inspect / Report action instead.
   */
  async bulkMarkInspected(dto: BulkInspectDto): Promise<BulkInspectResult> {
    const result: BulkInspectResult = { inspectedCount: 0, skippedCount: 0, results: [] };
    // UNLOADED added for Batch 9 import destination inspection (arrived-train unload landing state).
    const eligible = ['UNLOADED', 'RECEIVED', 'STORED', 'RESERVED'];

    for (const inventoryId of dto.inventoryIds) {
      const skip = (reason: string) => {
        result.skippedCount += 1;
        result.results.push({ inventoryId, status: 'SKIPPED', reason });
      };

      const item = await this.inventoryRepository.findById(inventoryId);
      if (!item) { skip('Inventory not found'); continue; }
      if (item.inspectionStatus === 'PASSED') { skip('Already inspected'); continue; }
      if (!eligible.includes(item.status)) { skip(`Status ${item.status} not eligible for inspection`); continue; }

      // Reuse the existing inspection service: creates a minimal PASSED report + sets inspectionStatus/inspectedAt.
      await this.inspectionService.create(inventoryId, {
        reportType: 'INSPECTION',
        inspectionStatus: 'PASSED',
        remarks: dto.remarks ?? 'Bulk marked inspected (PASSED).',
        inspectedById: dto.inspectedBy,
      });

      // A passed item advances by trade direction:
      //   EXPORT → Ready To Load (READY_FOR_LOADING)
      //   IMPORT → Pickup Ready (READY_FOR_PICKUP) — NOT ready-for-loading.
      const direction = item.bookingId ? await this.getBookingDirection(item.bookingId) : null;
      if (direction === 'EXPORT') {
        await this.dataSource.transaction(async (manager) => {
          await manager.getRepository(WarehouseInventory).update(inventoryId, {
            status: 'READY_FOR_LOADING',
            readyForLoadingAt: new Date(),
          });
          await this.activityLog.record(
            {
              activityType: 'READY_FOR_LOADING',
              inventoryId,
              warehouseId: item.warehouseId,
              description: 'Inspection passed → ready for loading',
              performedBy: dto.inspectedBy,
            },
            manager,
          );
        });
        result.results.push({ inventoryId, status: 'READY_FOR_LOADING' });
      } else if (direction === 'IMPORT') {
        await this.dataSource.transaction(async (manager) => {
          await manager.getRepository(WarehouseInventory).update(inventoryId, {
            status: 'READY_FOR_PICKUP',
            readyForPickupAt: new Date(),
          });
          await this.activityLog.record(
            {
              activityType: 'READY_FOR_PICKUP',
              inventoryId,
              warehouseId: item.warehouseId,
              description: 'Destination inspection passed → pickup ready',
              performedBy: dto.inspectedBy,
            },
            manager,
          );
        });
        result.results.push({ inventoryId, status: 'READY_FOR_PICKUP' });
      } else {
        result.results.push({ inventoryId, status: 'INSPECTED' });
      }
      result.inspectedCount += 1;
    }

    return result;
  }

  // ── Receive ──────────────────────────────────────────────────────────────

  async receive(dto: ReceiveWarehouseInventoryDto): Promise<WarehouseInventory> {
    const weight = Number(dto.weight) || 0;
    const volume = Number(dto.volume) || 0;
    const containerCount = dto.containerId ? Math.round(Number(dto.quantity) || 0) : 0;

    const id = await this.dataSource.transaction(async (manager) => {
      const { warehouse, yard, zone } = await this.validateLocation(manager, dto);

      if (dto.bookingId) {
        await this.assertBookingExists(manager, dto.bookingId);
      }

      this.assertCapacity('Warehouse', warehouse, weight, volume, containerCount);
      this.assertCapacity('Yard', yard, weight, volume, containerCount);
      this.assertCapacity('Zone', zone, weight, volume, containerCount);

      const now = new Date();
      const saved = await manager.getRepository(WarehouseInventory).save(
        manager.getRepository(WarehouseInventory).create({
          warehouseId: dto.warehouseId,
          yardId: dto.yardId,
          zoneId: dto.zoneId,
          bookingId: dto.bookingId ?? null,
          cargoId: dto.cargoId ?? null,
          containerId: dto.containerId ?? null,
          goodsId: dto.goodsId ?? null,
          quantity: Number(dto.quantity) || 0,
          weight,
          volume: dto.volume ?? null,
          status: 'RECEIVED',
          arrivedAt: now,
          notes: dto.notes?.trim() ?? null,
        }),
      );

      await this.applyCapacityDelta(manager, dto.warehouseId, dto.yardId, dto.zoneId, weight, volume, containerCount, +1);

      await this.activityLog.record(
        {
          activityType: 'INVENTORY_RECEIVED',
          inventoryId: saved.id,
          warehouseId: dto.warehouseId,
          description: `Received ${weight}kg at warehouse location`,
          performedBy: dto.performedBy,
        },
        manager,
      );

      return saved.id;
    });

    return this.findById(id);
  }

  // ── Lifecycle transitions ────────────────────────────────────────────────

  store(id: string, performedBy?: string): Promise<WarehouseInventory> {
    return this.transition(id, 'STORED', {
      timestampField: 'storedAt',
      activityType: 'INVENTORY_STORED',
      description: 'Inventory stored',
      performedBy,
    });
  }

  async reserve(dto: ReserveInventoryDto): Promise<WarehouseInventory> {
    const item = await this.findById(dto.inventoryId);

    if (item.status !== 'STORED') {
      throw new BadRequestException(`Inventory must be STORED to reserve (current: ${item.status})`);
    }

    const status = await this.getBookingStatus(dto.bookingId);
    if (!status) {
      throw new NotFoundException(`Booking ${dto.bookingId} not found`);
    }
    if (status !== 'PAID') {
      throw new BadRequestException(`Booking must be PAID to reserve inventory (current: ${status})`);
    }

    await this.dataSource.transaction(async (manager) => {
      await manager.getRepository(WarehouseInventory).update(dto.inventoryId, {
        status: 'RESERVED',
        bookingId: dto.bookingId,
        reservedAt: new Date(),
      });
      await this.activityLog.record(
        {
          activityType: 'INVENTORY_RESERVED',
          inventoryId: dto.inventoryId,
          warehouseId: item.warehouseId,
          description: `Reserved for booking ${dto.bookingId}`,
          performedBy: dto.performedBy,
        },
        manager,
      );
    });

    return this.findById(dto.inventoryId);
  }

  async readyForLoading(id: string, performedBy?: string): Promise<WarehouseInventory> {
    const item = await this.findById(id);
    if (!item.bookingId || !item.warehouseId || !item.yardId || !item.zoneId) {
      throw new BadRequestException('Inventory must have booking, warehouse, yard and zone before loading prep');
    }
    if (item.inspectionStatus !== 'PASSED') {
      throw new BadRequestException('Inventory must pass inspection before it can be marked ready for loading');
    }
    return this.transition(id, 'READY_FOR_LOADING', {
      timestampField: 'readyForLoadingAt',
      activityType: 'READY_FOR_LOADING',
      description: 'Inventory ready for loading',
      performedBy,
      preloaded: item,
    });
  }

  // ── Import branch (READY_FOR_PICKUP → DELIVERED) ───────────────────────────

  /** Mark inspected IMPORT inventory ready for customer pickup (RECEIVED → READY_FOR_PICKUP). */
  async readyForPickup(id: string, performedBy?: string): Promise<WarehouseInventory> {
    const item = await this.findById(id);

    if (item.inspectionStatus !== 'PASSED') {
      throw new BadRequestException('Inventory must pass inspection before it can be marked ready for pickup');
    }

    const direction = item.bookingId ? await this.getBookingDirection(item.bookingId) : null;
    if (direction !== 'IMPORT') {
      throw new BadRequestException('Only IMPORT inventory can be marked ready for pickup');
    }

    return this.transition(id, 'READY_FOR_PICKUP', {
      timestampField: 'readyForPickupAt',
      activityType: 'READY_FOR_PICKUP',
      description: 'Inventory ready for customer pickup',
      performedBy,
      preloaded: item,
    });
  }

  /** Record a DO / release order sent to the customer. Item stays READY_FOR_PICKUP. */
  async release(id: string, dto: ReleaseOrderDto): Promise<WarehouseInventory> {
    const item = await this.findById(id);
    if (item.status !== 'READY_FOR_PICKUP') {
      throw new BadRequestException(
        `Inventory must be READY_FOR_PICKUP to issue a release order (current: ${item.status})`,
      );
    }

    const releaseDate = dto.releaseDate ? new Date(dto.releaseDate) : new Date();
    const reference = dto.reference?.trim() || null;

    await this.dataSource.transaction(async (manager) => {
      await manager.getRepository(WarehouseInventory).update(id, {
        releaseDate,
        releaseOrderReference: reference,
      });
      await this.activityLog.record(
        {
          activityType: 'INVENTORY_RELEASED',
          inventoryId: id,
          warehouseId: item.warehouseId,
          description: reference
            ? `Release order ${reference} sent to customer`
            : 'Release order sent to customer',
          performedBy: dto.performedBy,
        },
        manager,
      );
    });

    return this.findById(id);
  }

  /** Hand import goods to the customer + capture proof of delivery (READY_FOR_PICKUP → DELIVERED). */
  async deliver(id: string, dto: DeliverInventoryDto): Promise<WarehouseInventory> {
    const item = await this.findById(id);
    this.assertTransition(item.status, 'DELIVERED');

    if (!item.releaseDate) {
      throw new BadRequestException('A release order must be issued before the goods can be delivered');
    }

    const receiverName = dto.receiverName.trim();
    const deliveredAt = dto.deliveredAt ? new Date(dto.deliveredAt) : new Date();
    const weight = Number(item.weight) || 0;
    const volume = Number(item.volume) || 0;
    const containerCount = item.containerId ? Math.round(Number(item.quantity) || 0) : 0;

    await this.dataSource.transaction(async (manager) => {
      await manager.getRepository(WarehouseInventory).update(id, {
        status: 'DELIVERED',
        deliveredAt,
      });

      // Goods physically leave the warehouse on pickup — free up capacity.
      await this.applyCapacityDelta(manager, item.warehouseId, item.yardId, item.zoneId, weight, volume, containerCount, -1);

      // Proof of delivery is captured on the linked cargo.
      if (item.cargoId) {
        await manager.getRepository(Cargo).update(item.cargoId, {
          receiverName,
          deliveredAt,
          deliveryRemarks: dto.remarks?.trim() ?? null,
        });
      }

      await this.activityLog.record(
        {
          activityType: 'INVENTORY_DELIVERED',
          inventoryId: id,
          warehouseId: item.warehouseId,
          description: `Delivered to ${receiverName}`,
          performedBy: dto.performedBy,
        },
        manager,
      );
    });

    return this.findById(id);
  }

  /**
   * Load READY_FOR_LOADING inventory onto a wagon. Creates a WarehouseLoading record.
   * Reads wagon/schedule data read-only — never modifies scheduling.
   */
  async load(id: string, dto: LoadInventoryDto): Promise<WarehouseInventory> {
    const item = await this.findById(id);

    // 1. inventory status must be READY_FOR_LOADING (and not already LOADED).
    this.assertTransition(item.status, 'LOADED');

    // 2. inventory is at a valid warehouse/yard/zone location.
    if (!item.warehouseId || !item.yardId || !item.zoneId) {
      throw new BadRequestException('Inventory must be at a warehouse/yard/zone before loading');
    }

    // 3. wagon must exist.
    const wagon = await this.scheduling.findWagon(dto.wagonId);
    if (!wagon) {
      throw new NotFoundException(`Wagon ${dto.wagonId} not found`);
    }

    // 4. wagon must be available, or already selected by an existing train schedule.
    const scheduled = await this.scheduling.isWagonScheduled(dto.wagonId);
    if (!LOADABLE_WAGON_STATUSES.includes(wagon.status) && !scheduled) {
      throw new BadRequestException(
        `Wagon ${wagon.wagonNumber} is not available for loading (status: ${wagon.status})`,
      );
    }

    // 5. inventory must not already have a loading record.
    const existing = await this.loadingRepository.findAll({ where: { warehouseInventoryId: id } });
    if (existing.length > 0) {
      throw new BadRequestException('Inventory has already been loaded');
    }

    const loadedWeight = dto.loadedWeight ?? (Number(item.weight) || 0);

    await this.dataSource.transaction(async (manager) => {
      const now = new Date();
      await manager.getRepository(WarehouseInventory).update(id, {
        status: 'LOADED',
        loadedAt: now,
      });

      await manager.getRepository(WarehouseLoading).save(
        manager.getRepository(WarehouseLoading).create({
          warehouseInventoryId: id,
          bookingId: item.bookingId ?? null,
          wagonId: dto.wagonId,
          loadedAt: now,
          loadedBy: dto.loadedBy ?? null,
          loadedWeight,
          notes: dto.notes?.trim() ?? null,
        }),
      );

      await this.activityLog.record(
        {
          activityType: 'INVENTORY_LOADED',
          inventoryId: id,
          warehouseId: item.warehouseId,
          description: `Loaded onto wagon ${wagon.wagonNumber}`,
          performedBy: dto.loadedBy,
        },
        manager,
      );
    });

    return this.findById(id);
  }

  // ── Loading records (Batch 3) ─────────────────────────────────────────────

  async findLoadings(
    filter: { bookingId?: string; wagonId?: string },
  ): Promise<Array<WarehouseLoading & { wagonNumber: string | null }>> {
    const where = {
      ...(filter.bookingId ? { bookingId: filter.bookingId } : {}),
      ...(filter.wagonId ? { wagonId: filter.wagonId } : {}),
    };
    const loadings = await this.loadingRepository.findAll({
      where,
      relations: { inventory: { warehouse: true, yard: true, zone: true } },
      order: { loadedAt: 'DESC' },
    });

    // Enrich with wagon numbers (read-only lookup into the scheduling domain).
    const wagonIds = [...new Set(loadings.map((l) => l.wagonId))];
    const wagonNumbers = new Map<string, string>();
    if (wagonIds.length > 0) {
      const rows: Array<{ id: string; wagon_number: string }> = await this.dataSource.query(
        'SELECT id, wagon_number FROM freight.wagons WHERE id = ANY($1)',
        [wagonIds],
      );
      rows.forEach((r) => wagonNumbers.set(r.id, r.wagon_number));
    }

    return loadings.map((loading) =>
      Object.assign(loading, { wagonNumber: wagonNumbers.get(loading.wagonId) ?? null }),
    );
  }

  findLoadingsByInventory(inventoryId: string): Promise<WarehouseLoading[]> {
    return this.loadingRepository.findAll({
      where: { warehouseInventoryId: inventoryId },
      order: { loadedAt: 'DESC' },
    });
  }

  async dispatch(id: string, performedBy?: string): Promise<WarehouseInventory> {
    const item = await this.findById(id);
    this.assertTransition(item.status, 'DISPATCHED');

    const weight = Number(item.weight) || 0;
    const volume = Number(item.volume) || 0;
    const containerCount = item.containerId ? Math.round(Number(item.quantity) || 0) : 0;

    await this.dataSource.transaction(async (manager) => {
      await manager.getRepository(WarehouseInventory).update(id, {
        status: 'DISPATCHED',
        dispatchedAt: new Date(),
      });
      // Item physically leaves the warehouse — free up capacity.
      await this.applyCapacityDelta(manager, item.warehouseId, item.yardId, item.zoneId, weight, volume, containerCount, -1);
      await this.activityLog.record(
        {
          activityType: 'INVENTORY_DISPATCHED',
          inventoryId: id,
          warehouseId: item.warehouseId,
          description: 'Inventory dispatched',
          performedBy,
        },
        manager,
      );
    });

    return this.findById(id);
  }

  // ── Movement ──────────────────────────────────────────────────────────────

  async move(id: string, dto: MoveInventoryDto): Promise<WarehouseInventory> {
    const item = await this.findById(id);
    if (item.status === 'DISPATCHED') {
      throw new BadRequestException('Dispatched inventory cannot be moved');
    }

    const weight = Number(item.weight) || 0;
    const volume = Number(item.volume) || 0;
    const containerCount = item.containerId ? Math.round(Number(item.quantity) || 0) : 0;

    const from = { warehouseId: item.warehouseId, yardId: item.yardId, zoneId: item.zoneId };

    await this.dataSource.transaction(async (manager) => {
      const { warehouse } = await this.validateLocation(manager, dto);

      // Capacity check at the destination (item is added there).
      const dest = await this.loadLocation(manager, dto);
      this.assertCapacity('Warehouse', dest.warehouse, weight, volume, containerCount);
      this.assertCapacity('Yard', dest.yard, weight, volume, containerCount);
      this.assertCapacity('Zone', dest.zone, weight, volume, containerCount);

      // Free the old location, occupy the new one.
      await this.applyCapacityDelta(manager, from.warehouseId, from.yardId, from.zoneId, weight, volume, containerCount, -1);
      await this.applyCapacityDelta(manager, dto.warehouseId, dto.yardId, dto.zoneId, weight, volume, containerCount, +1);

      await manager.getRepository(WarehouseInventory).update(id, {
        warehouseId: dto.warehouseId,
        yardId: dto.yardId,
        zoneId: dto.zoneId,
      });

      await manager.getRepository(WarehouseInventoryMovement).save(
        manager.getRepository(WarehouseInventoryMovement).create({
          inventoryId: id,
          fromWarehouseId: from.warehouseId,
          fromYardId: from.yardId,
          fromZoneId: from.zoneId,
          toWarehouseId: dto.warehouseId,
          toYardId: dto.yardId,
          toZoneId: dto.zoneId,
          remarks: dto.remarks?.trim() ?? null,
          movedBy: dto.movedBy ?? 'system',
          movedAt: new Date(),
        }),
      );

      await this.activityLog.record(
        {
          activityType: 'INVENTORY_MOVED',
          inventoryId: id,
          warehouseId: warehouse.id,
          description: dto.remarks?.trim() || 'Inventory moved',
          performedBy: dto.movedBy,
        },
        manager,
      );
    });

    return this.findById(id);
  }

  findMovements(id: string): Promise<WarehouseInventoryMovement[]> {
    return this.dataSource.getRepository(WarehouseInventoryMovement).find({
      where: { inventoryId: id },
      order: { movedAt: 'DESC' },
    });
  }

  findActivity(id: string): Promise<WarehouseActivityLog[]> {
    return this.activityLog.findByInventory(id);
  }

  // ── Inquiry (Batch 1) ──────────────────────────────────────────────────

  async inquiry(filter: InquiryWarehouseInventoryDto): Promise<InventoryInquiryResult[]> {
    const qb = this.dataSource
      .getRepository(WarehouseInventory)
      .createQueryBuilder('inv')
      .leftJoinAndSelect('inv.warehouse', 'warehouse')
      .leftJoinAndSelect('inv.yard', 'yard')
      .leftJoinAndSelect('inv.zone', 'zone')
      .leftJoin('freight.bookings', 'booking', 'booking.id = inv.booking_id')
      .leftJoin('freight.companies', 'company', 'company.id = booking.company_id')
      .leftJoin('freight.containers', 'container', 'container.id = inv.container_id')
      .leftJoin('freight.cargoes', 'cargo', 'cargo.id = inv.cargo_id')
      .leftJoin('freight.cargo_types', 'cargo_type', 'cargo_type.id = cargo.cargo_type_id')
      .addSelect('booking.reference', 'b_reference')
      .addSelect('company.name', 'c_name')
      .addSelect('container.container_number', 'ct_number')
      .addSelect('cargo.description', 'cg_description')
      .addSelect('cargo_type.cargo_type_name', 'cgt_name')
      .orderBy('inv.created_at', 'DESC');

    if (filter.bookingNumber?.trim()) {
      qb.andWhere('booking.reference ILIKE :bn', { bn: `%${filter.bookingNumber.trim()}%` });
    }
    if (filter.containerNumber?.trim()) {
      qb.andWhere('container.container_number ILIKE :cn', { cn: `%${filter.containerNumber.trim()}%` });
    }
    if (filter.cargoType?.trim()) {
      qb.andWhere('cargo_type.cargo_type_name ILIKE :ctype', { ctype: `%${filter.cargoType.trim()}%` });
    }
    if (filter.goodsName?.trim()) {
      qb.andWhere('(inv.notes ILIKE :gn OR cargo.description ILIKE :gn)', { gn: `%${filter.goodsName.trim()}%` });
    }
    if (filter.warehouseId) qb.andWhere('inv.warehouse_id = :wid', { wid: filter.warehouseId });
    if (filter.yardId) qb.andWhere('inv.yard_id = :yid', { yid: filter.yardId });
    if (filter.zoneId) qb.andWhere('inv.zone_id = :zid', { zid: filter.zoneId });
    if (filter.status) qb.andWhere('inv.status = :status', { status: filter.status });

    const { entities, raw } = await qb.getRawAndEntities();

    return entities.map((inv, index) => {
      const row = raw[index] ?? {};
      return {
        id: inv.id,
        bookingId: inv.bookingId ?? null,
        bookingNumber: row.b_reference ?? null,
        customerName: row.c_name ?? null,
        containerNumber: row.ct_number ?? null,
        cargoType: row.cgt_name ?? null,
        cargoDescription: row.cg_description ?? null,
        goodsId: inv.goodsId ?? null,
        warehouse: inv.warehouse
          ? { id: inv.warehouse.id, name: inv.warehouse.name, code: inv.warehouse.code }
          : null,
        yard: inv.yard ? { id: inv.yard.id, name: inv.yard.name, code: inv.yard.code } : null,
        zone: inv.zone ? { id: inv.zone.id, name: inv.zone.name, code: inv.zone.code } : null,
        status: inv.status,
        quantity: Number(inv.quantity),
        weight: Number(inv.weight),
        arrivedAt: inv.arrivedAt ?? null,
        readyForLoadingAt: inv.readyForLoadingAt ?? null,
      };
    });
  }

  // ── Helpers ──────────────────────────────────────────────────────────────

  private async transition(
    id: string,
    to: WarehouseInventoryStatus,
    opts: {
      timestampField: keyof WarehouseInventory;
      activityType: Parameters<WarehouseActivityLogService['record']>[0]['activityType'];
      description: string;
      performedBy?: string;
      preloaded?: WarehouseInventory;
    },
  ): Promise<WarehouseInventory> {
    const item = opts.preloaded ?? (await this.findById(id));
    this.assertTransition(item.status, to);

    await this.dataSource.transaction(async (manager) => {
      await manager.getRepository(WarehouseInventory).update(id, {
        status: to,
        [opts.timestampField]: new Date(),
      });
      await this.activityLog.record(
        {
          activityType: opts.activityType,
          inventoryId: id,
          warehouseId: item.warehouseId,
          description: opts.description,
          performedBy: opts.performedBy,
        },
        manager,
      );
    });

    return this.findById(id);
  }

  private assertTransition(from: WarehouseInventoryStatus, to: WarehouseInventoryStatus): void {
    if (!WAREHOUSE_INVENTORY_TRANSITIONS[from]?.includes(to)) {
      throw new BadRequestException(`Invalid transition ${from} → ${to}`);
    }
  }

  private async validateLocation(
    manager: EntityManager,
    dto: { warehouseId: string; yardId: string; zoneId: string },
  ): Promise<{ warehouse: Warehouse; yard: WarehouseYard; zone: WarehouseZone }> {
    const { warehouse, yard, zone } = await this.loadLocation(manager, dto);

    if (warehouse.status !== 'ACTIVE') throw new BadRequestException('Warehouse is not ACTIVE');
    if (yard.warehouseId !== warehouse.id) throw new BadRequestException('Yard does not belong to the selected warehouse');
    if (yard.status !== 'ACTIVE') throw new BadRequestException('Yard is not ACTIVE');
    if (zone.yardId !== yard.id) throw new BadRequestException('Zone does not belong to the selected yard');
    if (zone.status !== 'ACTIVE') throw new BadRequestException('Zone is not ACTIVE');

    return { warehouse, yard, zone };
  }

  private async loadLocation(
    manager: EntityManager,
    dto: { warehouseId: string; yardId: string; zoneId: string },
  ): Promise<{ warehouse: Warehouse; yard: WarehouseYard; zone: WarehouseZone }> {
    const warehouse = await manager.getRepository(Warehouse).findOne({ where: { id: dto.warehouseId } });
    if (!warehouse) throw new NotFoundException(`Warehouse ${dto.warehouseId} not found`);
    const yard = await manager.getRepository(WarehouseYard).findOne({ where: { id: dto.yardId } });
    if (!yard) throw new NotFoundException(`Yard ${dto.yardId} not found`);
    const zone = await manager.getRepository(WarehouseZone).findOne({ where: { id: dto.zoneId } });
    if (!zone) throw new NotFoundException(`Zone ${dto.zoneId} not found`);
    return { warehouse, yard, zone };
  }

  private async assertBookingExists(manager: EntityManager, bookingId: string): Promise<void> {
    const rows = await manager.query(
      'SELECT id FROM freight.bookings WHERE id = $1 AND deleted_at IS NULL LIMIT 1',
      [bookingId],
    );
    if (!rows || rows.length === 0) {
      throw new NotFoundException(`Booking ${bookingId} not found`);
    }
  }

  private async getBookingStatus(bookingId: string): Promise<string | null> {
    const rows = await this.dataSource.query(
      'SELECT status FROM freight.bookings WHERE id = $1 AND deleted_at IS NULL LIMIT 1',
      [bookingId],
    );
    return rows?.[0]?.status ?? null;
  }

  /** IMPORT | EXPORT | DOMESTIC derived from the booking ROUTE (yard countries), or null if missing. */
  private async getBookingDirection(bookingId: string): Promise<string | null> {
    const rows = await this.dataSource.query(
      `SELECT oy.country AS "originCountry", dy.country AS "destinationCountry"
         FROM freight.bookings b
         LEFT JOIN freight.yards oy ON oy.id = b.origin_yard_id
         LEFT JOIN freight.yards dy ON dy.id = b.destination_yard_id
        WHERE b.id = $1 AND b.deleted_at IS NULL LIMIT 1`,
      [bookingId],
    );
    if (!rows?.[0]) return null;
    return deriveTradeDirection(
      { country: rows[0].originCountry },
      { country: rows[0].destinationCountry },
    );
  }

  private assertCapacity(
    label: string,
    node: LocationNode,
    weightAdd: number,
    volumeAdd: number,
    containerAdd: number,
  ): void {
    const maxWeight = node.maxWeight ?? node.capacityWeight;
    if (maxWeight != null) {
      const projected = Number(node.currentWeight) + weightAdd;
      if (projected > Number(maxWeight)) {
        throw new BadRequestException(`${label} weight capacity exceeded (${projected} / ${maxWeight})`);
      }
    }
    if (node.maxVolume != null && volumeAdd > 0) {
      const projected = Number(node.currentVolume) + volumeAdd;
      if (projected > Number(node.maxVolume)) {
        throw new BadRequestException(`${label} volume capacity exceeded (${projected} / ${node.maxVolume})`);
      }
    }
    if (node.capacityContainers != null && containerAdd > 0) {
      const projected = Number(node.currentContainers) + containerAdd;
      if (projected > Number(node.capacityContainers)) {
        throw new BadRequestException(`${label} container capacity exceeded (${projected} / ${node.capacityContainers})`);
      }
    }
  }

  private async applyCapacityDelta(
    manager: EntityManager,
    warehouseId: string,
    yardId: string,
    zoneId: string,
    weight: number,
    volume: number,
    containers: number,
    sign: 1 | -1,
  ): Promise<void> {
    const apply = sign === 1 ? manager.increment.bind(manager) : manager.decrement.bind(manager);
    const targets: Array<[typeof Warehouse | typeof WarehouseYard | typeof WarehouseZone, string]> = [
      [Warehouse, warehouseId],
      [WarehouseYard, yardId],
      [WarehouseZone, zoneId],
    ];

    for (const [entity, id] of targets) {
      if (weight) await apply(entity, { id }, 'currentWeight', weight);
      if (volume) await apply(entity, { id }, 'currentVolume', volume);
      if (containers) await apply(entity, { id }, 'currentContainers', containers);
    }
  }
}
