import {
  BadRequestException,
  forwardRef,
  Inject,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { DataSource } from 'typeorm';
import { BookingsRepository } from '../bookings/bookings.repository';
import { BookingPricingService } from '../bookings/booking-pricing.service';
import { clearanceCodesForBooking } from '../bookings/clearance.util';
import { Booking } from '../bookings/entities/booking.entity';
import { BookingContainer } from '../bookings/entities/booking-container.entity';
import { CompaniesService } from '../companies/companies.service';
import { ContainerType } from '../rule-engine/entities/container-type.entity';
import { RatesService } from '../rule-engine/services/rates.service';
import { TrainSchedulingService } from '../train-scheduling/train-scheduling.service';
import { eatDay } from '../train-scheduling/batch-window.util';
import { BookingOrdersRepository } from './booking-orders.repository';
import { CreateBookingOrderDto } from './dto/create-booking-order.dto';
import { BookingOrder } from './entities/booking-order.entity';
import { BookingOrderLine } from './entities/booking-order-line.entity';
import { GeneralContractService } from './general-contract.service';
import { isRoadService, roadKmPrice } from './road.util';

@Injectable()
export class BookingOrdersService {
  private readonly logger = new Logger(BookingOrdersService.name);

  constructor(
    private readonly dataSource: DataSource,
    private readonly ordersRepository: BookingOrdersRepository,
    private readonly bookingsRepository: BookingsRepository,
    private readonly companiesService: CompaniesService,
    private readonly generalContractService: GeneralContractService,
    private readonly pricingService: BookingPricingService,
    private readonly ratesService: RatesService,
    @Inject(forwardRef(() => TrainSchedulingService))
    private readonly trainSchedulingService: TrainSchedulingService,
  ) {}

  /** Orders placed against a contract, with their lines and child booking. */
  listByContract(contractBookingId: string): Promise<BookingOrder[]> {
    return this.ordersRepository.findByContract(contractBookingId);
  }

  findById(id: string): Promise<BookingOrder | null> {
    return this.ordersRepository.findById(id);
  }

  /**
   * Place a drawdown order against an ACTIVE general contract.
   *
   * Validates the requested quantities against the remaining pool, then spawns a
   * ONE_TIME child Booking (PAID + FULLY_EXECUTED, inheriting the contract's
   * route/cargo/service) so it flows through the existing train-scheduling
   * pipeline. The order row is the ledger entry linking contract → child booking.
   */
  async create(
    dto: CreateBookingOrderDto,
    userId?: string,
  ): Promise<BookingOrder> {
    const contract = await this.bookingsRepository.findById(dto.contractBookingId);
    if (!contract) {
      throw new NotFoundException(`Contract ${dto.contractBookingId} not found`);
    }
    if (!this.generalContractService.isGeneralContract(contract)) {
      throw new BadRequestException('Booking is not a general contract');
    }
    if (contract.status !== 'CONTRACT_ACTIVE') {
      throw new BadRequestException(
        `Contract is ${contract.status} — orders can only be placed against an ACTIVE contract`,
      );
    }
    if (contract.expiresAt && contract.expiresAt.getTime() <= Date.now()) {
      throw new BadRequestException('Contract ordering window has expired');
    }

    // The customer placing the order must own the contract.
    if (userId && !(await this.userOwnsContract(userId, contract))) {
      throw new BadRequestException('You do not have access to this contract');
    }

    // Resolve the route the order ships on: a chosen contract route line for a
    // multi-route contract, else the contract's own origin/destination.
    const routeLines = await this.generalContractService.getRouteLines(
      contract.id,
    );
    let originYardId = contract.originYardId;
    let destinationYardId = contract.destinationYardId;
    let routeLineId: string | null = null;
    let routeKm: number | null = null;

    if (routeLines.length > 0) {
      if (!dto.routeLineId) {
        throw new BadRequestException(
          'This contract has multiple routes — select a route to draw from',
        );
      }
      const chosen = routeLines.find((r) => r.routeLineId === dto.routeLineId);
      if (!chosen) {
        throw new BadRequestException(
          'Selected route is not part of this contract',
        );
      }
      originYardId = chosen.originYardId;
      destinationYardId = chosen.destinationYardId;
      routeLineId = chosen.routeLineId;
      routeKm = chosen.km ?? null;
    }

    // Validate the route has a departure on the chosen day.
    const day = eatDay(new Date(dto.scheduledDate));
    const hasDeparture =
      await this.trainSchedulingService.existsOpenScheduleOnRouteDay(
        originYardId,
        destinationYardId,
        day,
      );
    if (!hasDeparture) {
      throw new BadRequestException(
        'No departures available on the selected day for this route',
      );
    }

    const isContainer = contract.freightType === 'CONTAINER';
    const orderTotal = dto.lines.reduce((sum, l) => sum + l.quantity, 0);

    // Hazardous/reefer counts the customer entered cannot exceed the line they
    // belong to. Validated for every order regardless of routing.
    for (const line of dto.lines) {
      const haz = line.hazardousQuantity ?? 0;
      const reefer = line.reeferQuantity ?? 0;
      if (haz < 0 || reefer < 0) {
        throw new BadRequestException('Hazardous/reefer quantities cannot be negative');
      }
      if (haz > line.quantity || reefer > line.quantity) {
        throw new BadRequestException(
          'Hazardous/reefer quantity cannot exceed the line quantity',
        );
      }
    }

    if (routeLineId) {
      // Multi-route: validate against the chosen route line's remaining pool.
      for (const line of dto.lines) {
        if (line.quantity <= 0) {
          throw new BadRequestException('Order quantities must be greater than zero');
        }
      }
      const chosen = routeLines.find((r) => r.routeLineId === routeLineId)!;
      if (orderTotal > chosen.remainingQuantity) {
        throw new BadRequestException(
          `Requested ${orderTotal} exceeds remaining ${chosen.remainingQuantity} for this route`,
        );
      }
    } else {
      // Single-route: validate each line against the per-container-type pool.
      const poolLines = await this.generalContractService.getQuantityLines(
        contract.id,
      );
      for (const line of dto.lines) {
        if (line.quantity <= 0) {
          throw new BadRequestException('Order quantities must be greater than zero');
        }
        const key = isContainer ? (line.containerTypeId ?? '') : '';
        const poolLine = poolLines.find((p) => (p.containerTypeId ?? '') === key);
        if (!poolLine) {
          throw new BadRequestException(
            isContainer
              ? `Container type ${line.containerTypeId} is not part of this contract`
              : 'This contract has no matching quantity pool',
          );
        }
        if (line.quantity > poolLine.remainingQuantity) {
          throw new BadRequestException(
            `Requested ${line.quantity} exceeds remaining ${poolLine.remainingQuantity}` +
              (poolLine.containerTypeName ? ` for ${poolLine.containerTypeName}` : ''),
          );
        }
      }
    }

    // Persist the order + its child shipment booking atomically.
    const order = await this.dataSource.transaction(async (manager) => {
      const childBooking = await this.spawnChildBooking(
        contract,
        dto,
        { originYardId, destinationYardId, km: routeKm },
        manager,
      );

      const reference = await this.generateReference();
      const orderRow = manager.create(BookingOrder, {
        reference,
        contractBookingId: contract.id,
        bookingId: childBooking.id,
        routeLineId,
        companyId: contract.companyId ?? null,
        scheduledDate: new Date(dto.scheduledDate),
        // The order is a ledger row; the child booking drives the workflow
        // (review → pay → allocate), so the order tracks PENDING until done.
        status: 'PENDING',
        schedulingStatus: 'NOT_SCHEDULED',
      });
      const savedOrder = await manager.save(orderRow);

      const lines = dto.lines.map((l) =>
        manager.create(BookingOrderLine, {
          orderId: savedOrder.id,
          containerTypeId: isContainer ? (l.containerTypeId ?? null) : null,
          quantity: l.quantity,
          hazardousQuantity: l.hazardousQuantity ?? 0,
          reeferQuantity: l.reeferQuantity ?? 0,
        }),
      );
      await manager.save(lines);
      savedOrder.lines = lines;
      return savedOrder;
    });

    // The child does NOT enter the train batch pool here. It is priced and
    // unpaid, awaiting Marketing review (OPERATION_REQUEST_PENDING) or customs
    // clearance first; the batch enqueue happens only on accept.

    // Close the contract once its pool is exhausted (pending orders count, so
    // the pool reserves quantity as soon as an order is placed).
    if (await this.generalContractService.isExhausted(contract.id)) {
      await this.dataSource
        .getRepository(Booking)
        .update(contract.id, { status: 'CONTRACT_CLOSED' });
      this.logger.log(
        `Contract ${contract.reference} CLOSED — quantity exhausted`,
      );
    }

    return (await this.ordersRepository.findById(order.id)) ?? order;
  }

  /**
   * Create the ONE_TIME child booking for an order, inheriting the contract's
   * shipment context. Unlike the contract (which is no longer paid up front),
   * the child is PRICED and UNPAID and waits for Marketing review — going
   * through the customs clearance gate first when the service includes customs,
   * mirroring a one-time booking. It only enters the train pool on accept.
   */
  private async spawnChildBooking(
    contract: Booking,
    dto: CreateBookingOrderDto,
    route: { originYardId: string; destinationYardId: string; km: number | null },
    manager: import('typeorm').EntityManager,
  ): Promise<Booking> {
    const reference = await this.generateChildBookingReference();
    const isContainer = contract.freightType === 'CONTAINER';

    // Sum line quantities × the contract's per-unit weight for the child total.
    const containerByType = new Map(
      (contract.bookingContainers ?? []).map((c) => [c.containerTypeId, c]),
    );
    let totalWeight = 0;
    if (isContainer) {
      for (const line of dto.lines) {
        const src = containerByType.get(line.containerTypeId ?? '');
        const vgmPerUnit = src ? Number(src.vgmPerUnitTons) : 0;
        totalWeight += vgmPerUnit * line.quantity;
      }
    } else {
      totalWeight = dto.lines.reduce((sum, l) => sum + l.quantity, 0);
    }

    // Per-order hazardous/reefer: set the child flags from the order's line
    // counts so the HAZARD_SURCHARGE / REEFER_SURCHARGE rates apply.
    const hasHazardous = dto.lines.some((l) => (l.hazardousQuantity ?? 0) > 0);
    const hasReefer = dto.lines.some((l) => (l.reeferQuantity ?? 0) > 0);

    // Customs orders flow through the one-time clearance gate first; others go
    // straight to operations review with the chosen shipment day.
    const { includesCustoms } = clearanceCodesForBooking(contract);
    const spawnStatus = includesCustoms
      ? 'AWAITING_DOCUMENTS'
      : 'OPERATION_REQUEST_PENDING';

    const child = manager.create(Booking, {
      reference,
      companyId: contract.companyId ?? null,
      companyProfileId: contract.companyProfileId ?? null,
      isGovernment: contract.isGovernment,
      governmentInstitution: contract.governmentInstitution ?? null,
      contractType: contract.contractType,
      previousContractId: contract.id,
      serviceTypeId: contract.serviceTypeId,
      firstMilePickupAddress: contract.firstMilePickupAddress ?? null,
      lastMileDeliveryAddress: contract.lastMileDeliveryAddress ?? null,
      equipmentReturn: contract.equipmentReturn,
      originYardId: route.originYardId,
      destinationYardId: route.destinationYardId,
      tradeDirection: contract.tradeDirection,
      freightType: contract.freightType,
      cargoTypeId: contract.cargoTypeId ?? null,
      cargoFreeText: contract.cargoFreeText ?? null,
      shippingLineId: contract.shippingLineId ?? null,
      cargoTotalWeightVgm: totalWeight,
      isHazardous: hasHazardous,
      isReefer: hasReefer,
      paymentCurrency: contract.paymentCurrency,
      bookingType: 'ONE_TIME',
      scheduledDate: new Date(dto.scheduledDate),
      // Priced + unpaid: the customer pays this order on its own.
      status: spawnStatus,
      paymentStatus: 'PENDING',
      priorityScore: contract.priorityScore,
      totalAmount: 0,
      schedulingStatus: 'NOT_SCHEDULED',
    });
    const savedChild = await manager.save(child);

    if (isContainer) {
      for (const line of dto.lines) {
        const src = containerByType.get(line.containerTypeId ?? '');
        const ct = line.containerTypeId
          ? await manager.getRepository(ContainerType).findOne({
              where: { id: line.containerTypeId },
            })
          : null;
        const wagonsPerUnit = ct ? Number(ct.wagonsPerUnit) : 1;
        const vgmPerUnit = src ? Number(src.vgmPerUnitTons) : 0;
        const row = manager.create(BookingContainer, {
          bookingId: savedChild.id,
          containerTypeId: line.containerTypeId ?? null,
          quantity: line.quantity,
          vgmPerUnitTons: vgmPerUnit,
          totalVgmTons: vgmPerUnit * line.quantity,
          wagonsRequired: Math.ceil(line.quantity * wagonsPerUnit),
          isOverweight: false,
        });
        await manager.save(row);
      }
    }

    // Price the order: base freight for the drawn quantity + haz/reefer
    // surcharges, plus a road KM charge when the service ships by road.
    const roadKm = isRoadService(contract.serviceType) ? route.km : null;
    await this.priceChildBooking(savedChild.id, roadKm, manager);

    return savedChild;
  }

  /**
   * Compute and persist the child order's price (base + surcharges) inside the
   * order transaction. The contract is no longer paid up front, so each order
   * carries its own total that the customer pays.
   */
  private async priceChildBooking(
    childId: string,
    roadKm: number | null,
    manager: import('typeorm').EntityManager,
  ): Promise<void> {
    const child = await manager.getRepository(Booking).findOne({
      where: { id: childId },
      relations: { bookingContainers: true },
    });
    if (!child) return;

    try {
      const computed = await this.pricingService.computePriceForBooking(child);
      const lineItems = [...computed.lineItems];
      let total = computed.totalAmount;

      // Road KM charge: distance × the live PER_KM rate, added as its own line.
      if (roadKm && roadKm > 0) {
        const perKmRate = await this.findPerKmRate(child.paymentCurrency);
        const kmAmount = roadKmPrice(roadKm, perKmRate);
        if (kmAmount > 0) {
          lineItems.push({
            code: 'ROAD_KM',
            description: `Road transport (${roadKm} km)`,
            amount: kmAmount,
            unitAmount: perKmRate!,
            unit: 'PER_KM',
            quantity: roadKm,
            currency: child.paymentCurrency,
          });
          total += kmAmount;
        }
      }

      await manager.getRepository(Booking).update(childId, {
        totalAmount: total,
        priorityScore: computed.priorityScore,
        pricingBreakdown: {
          lineItems,
          totalAmount: total,
          currency: computed.currency,
          generatedAt: new Date().toISOString(),
        },
      } as never);
    } catch (err) {
      this.logger.error(
        `Pricing child order ${childId} failed: ${err instanceof Error ? err.message : String(err)}`,
      );
    }
  }

  /** The live PER_KM rate value for road billing, in the given currency. */
  private async findPerKmRate(currency: string): Promise<number | null> {
    const rates = await this.ratesService.findLiveRates();
    const rate = rates.find(
      (r) => r.rateUnit === 'PER_KM' && r.currency === currency,
    );
    return rate ? Number(rate.rateValue) : null;
  }

  private async userOwnsContract(
    userId: string,
    contract: Booking,
  ): Promise<boolean> {
    if (!contract.companyId) return true; // government / staff-created
    try {
      const { company } = await this.companiesService.getCompanyInfoByUserId(
        userId,
      );
      return company.id === contract.companyId;
    } catch {
      return false;
    }
  }

  private async generateReference(): Promise<string> {
    const year = new Date().getFullYear();
    const count = await this.ordersRepository.countByYear(year);
    return `ORD-${year}-${String(count + 1).padStart(6, '0')}`;
  }

  private async generateChildBookingReference(): Promise<string> {
    const year = new Date().getFullYear();
    const count = await this.bookingsRepository.countByYear(year);
    return `BK-${year}-${String(count + 1).padStart(6, '0')}`;
  }
}
