import { Inject, Injectable } from "@nestjs/common";

import { CargoType } from "../rule-engine/entities/cargo-type.entity";
import { ContainerType } from "../rule-engine/entities/container-type.entity";
import {
  CARGO_TYPES_REPOSITORY,
  ICargoTypesRepository,
} from "../rule-engine/interfaces/cargo-types.repository.interface";
import {
  CONTAINER_TYPES_REPOSITORY,
  IContainerTypesRepository,
} from "../rule-engine/interfaces/container-types.repository.interface";
import {
  IServiceTypesRepository,
  SERVICE_TYPES_REPOSITORY,
} from "../rule-engine/interfaces/service-types.repository.interface";
import {
  IShippingLinesRepository,
  SHIPPING_LINES_REPOSITORY,
} from "../rule-engine/interfaces/shipping-lines.repository.interface";
import {
  IYardsRepository,
  YARDS_REPOSITORY,
} from "../rule-engine/interfaces/yards.repository.interface";
import {
  BookingReferenceCargoTypeChildDto,
  BookingReferenceCargoTypeGroupDto,
  BookingReferenceContainerSizeGroupDto,
  BookingReferenceContainerTypeDto,
  BookingReferenceDataDto,
  BookingReferenceServiceDto,
  BookingReferenceShippingLineDto,
  BookingReferenceYardDto,
} from "./dto/booking-reference-data.dto";

/**
 * Reference cargo tree: top-level groups, each carrying its selectable
 * commodities.
 *
 * `cargo_types` is an arbitrary-depth tree (Bulk → Steel Billet → S1 → …), but
 * only a LEAF is a real commodity — an intermediate node is a container for
 * finer types, and booking against it would be ambiguous. So each group's
 * `children` are all of its leaf descendants, flattened, whatever the depth.
 * Deep leaves carry their path below the group ("Steel Billet → S1") so a
 * generically-named leaf still reads unambiguously in a dropdown.
 *
 * A group with no active descendants is its own leaf and is emitted as its
 * single child — otherwise it is selectable as a group but offers no commodity,
 * which dead-ends every form that requires one.
 */
export function buildCargoTypeTree(
  rows: CargoType[],
): BookingReferenceCargoTypeGroupDto[] {
  const active = rows.filter((r) => r.isActive);

  const byOrder = (a: CargoType, b: CargoType) =>
    a.displayOrder - b.displayOrder || a.code.localeCompare(b.code);

  const childrenOf = new Map<string, CargoType[]>();
  for (const row of active) {
    if (!row.parentGroupId) continue;
    const siblings = childrenOf.get(row.parentGroupId) ?? [];
    siblings.push(row);
    childrenOf.set(row.parentGroupId, siblings);
  }
  for (const siblings of childrenOf.values()) siblings.sort(byOrder);

  const parents = active.filter((r) => !r.parentGroupId).sort(byOrder);

  /** Depth-first leaf walk; `trail` is the path below the group. */
  const collectLeaves = (
    node: CargoType,
    trail: string[],
    seen: Set<string>,
  ): BookingReferenceCargoTypeChildDto[] => {
    // Admin-entered parent pointers could in principle cycle — never loop.
    if (seen.has(node.id)) return [];
    seen.add(node.id);

    const kids = childrenOf.get(node.id) ?? [];
    if (kids.length === 0) {
      return [
        {
          id: node.id,
          name: [...trail, node.cargoTypeName].join(" → "),
          code: node.code,
          unit_of_measure: node.unitOfMeasure ?? null,
        },
      ];
    }
    const nextTrail = [...trail, node.cargoTypeName];
    return kids.flatMap((kid) => collectLeaves(kid, nextTrail, seen));
  };

  return parents.map((parent) => {
    const kids = childrenOf.get(parent.id) ?? [];
    const children =
      kids.length === 0
        ? // The group itself is the commodity.
          [
            {
              id: parent.id,
              name: parent.cargoTypeName,
              code: parent.code,
              unit_of_measure: parent.unitOfMeasure ?? null,
            },
          ]
        : kids.flatMap((kid) => collectLeaves(kid, [], new Set<string>()));

    return {
      id: parent.id,
      name: parent.cargoTypeName,
      code: parent.code,
      children,
    };
  });
}

export function groupContainersBySize(
  rows: ContainerType[],
): BookingReferenceContainerSizeGroupDto[] {
  const active = rows.filter((r) => r.isActive);
  const bySize = new Map<string, ContainerType[]>();

  for (const ct of active) {
    const sizeKey =
      ct.sizeFt != null && ct.sizeFt > 0 ? `${ct.sizeFt}ft` : "other";
    const list = bySize.get(sizeKey) ?? [];
    list.push(ct);
    bySize.set(sizeKey, list);
  }

  const sortSizeKey = (key: string): number => {
    if (key === "other") return Number.MAX_SAFE_INTEGER;
    const n = parseInt(key, 10);
    return Number.isNaN(n) ? Number.MAX_SAFE_INTEGER - 1 : n;
  };

  return [...bySize.entries()]
    .sort(([a], [b]) => sortSizeKey(a) - sortSizeKey(b))
    .map(([size, types]) => ({
      size,
      types: types
        .sort(
          (a, b) =>
            (a.displayOrder ?? 0) - (b.displayOrder ?? 0) ||
            a.code.localeCompare(b.code),
        )
        .map(
          (ct): BookingReferenceContainerTypeDto => ({
            id: ct.id,
            name: ct.label?.trim() ? ct.label : ct.code,
            code: ct.code,
            is_reefer: ct.isReefer ?? false,
          }),
        ),
    }));
}

@Injectable()
export class BookingReferenceDataService {
  constructor(
    @Inject(YARDS_REPOSITORY)
    private readonly yardsRepository: IYardsRepository,
    @Inject(CONTAINER_TYPES_REPOSITORY)
    private readonly containerTypesRepository: IContainerTypesRepository,
    @Inject(SERVICE_TYPES_REPOSITORY)
    private readonly serviceTypesRepository: IServiceTypesRepository,
    @Inject(SHIPPING_LINES_REPOSITORY)
    private readonly shippingLinesRepository: IShippingLinesRepository,
    @Inject(CARGO_TYPES_REPOSITORY)
    private readonly cargoTypesRepository: ICargoTypesRepository,
  ) { }

  async getReferenceData(): Promise<BookingReferenceDataDto> {
    const [yards, containerTypes, serviceTypes, shippingLines, cargoTypes] =
      await Promise.all([
        this.yardsRepository.findAll({
          where: { isActive: true },
          order: { displayOrder: "ASC", code: "ASC" },
        }),
        this.containerTypesRepository.findAll({
          where: { isActive: true },
          order: { displayOrder: "ASC", code: "ASC" },
        }),
        this.serviceTypesRepository.findAll({
          where: { isActive: true },
          order: { displayOrder: "ASC", code: "ASC" },
        }),
        this.shippingLinesRepository.findAll({
          where: { isActive: true },
          order: { label: "ASC", code: "ASC" },
        }),
        this.cargoTypesRepository.findAll({
          where: { isActive: true },
          order: { displayOrder: "ASC", code: "ASC" },
        }),
      ]);

    return {
      yard: yards.map(
        (y): BookingReferenceYardDto => ({
          id: y.id,
          name: y.label,
          code: y.code,
          country: y.country,
        }),
      ),
      containers: groupContainersBySize(containerTypes),
      service: serviceTypes.map(
        (s): BookingReferenceServiceDto => ({
          name: s.serviceName,
          ...s,
        }),
      ),
      shipping_line: shippingLines.map(
        (sl): BookingReferenceShippingLineDto => ({
          id: sl.id,
          name: sl.label,
          code: sl.code,
        }),
      ),
      cargo_type: buildCargoTypeTree(cargoTypes),
    };
  }
}
