import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { BookingType, CargoUnitOfMeasure } from '@edr/types';
import { DataSource } from 'typeorm';
import { DropdownSettingsService } from '../dropdown-settings/dropdown-settings.service';
import { Booking } from '../bookings/entities/booking.entity';
import { BookingOrder } from './entities/booking-order.entity';
import { ContractRouteLine } from './entities/contract-route-line.entity';
import {
  ContractQuantityLineView,
  ContractRouteLineView,
} from './dto/contract-view.dto';

/** Setting code holding the global ordering window (in months) for general contracts. */
export const CONTRACT_PERIOD_SETTING_CODE = 'general_contract_period';
/** Fallback when the setting is missing or unparseable. */
export const DEFAULT_CONTRACT_PERIOD_MONTHS = 3;

/**
 * Owns general-contract concerns that sit alongside the generic booking flow:
 * the configurable ordering period, post-payment activation, and computing the
 * remaining drawdown pool per contract.
 */
@Injectable()
export class GeneralContractService {
  private readonly logger = new Logger(GeneralContractService.name);

  constructor(
    private readonly dataSource: DataSource,
    private readonly dropdownSettings: DropdownSettingsService,
  ) {}

  isGeneralContract(booking: Pick<Booking, 'bookingType'>): boolean {
    return booking.bookingType === BookingType.GeneralContract;
  }

  /** The configured ordering window in months (defaults to 3). */
  async getPeriodMonths(): Promise<number> {
    try {
      const setting = await this.dropdownSettings.getByCode(
        CONTRACT_PERIOD_SETTING_CODE,
      );
      const raw = setting.children?.[0]?.value;
      const months = Number(raw);
      if (Number.isFinite(months) && months > 0) return months;
    } catch {
      // Setting not seeded yet — fall back to the default.
    }
    return DEFAULT_CONTRACT_PERIOD_MONTHS;
  }

  /**
   * Called when a general contract's payment succeeds: mark it ACTIVE (instead of
   * entering the train queue like a one-time booking) and stamp the ordering
   * window. Idempotent.
   */
  async activateAfterPayment(bookingId: string): Promise<void> {
    const repo = this.dataSource.getRepository(Booking);
    const booking = await repo.findOne({ where: { id: bookingId } });
    if (!booking || !this.isGeneralContract(booking)) return;
    if (booking.status === 'CONTRACT_ACTIVE' || booking.status === 'CONTRACT_CLOSED') {
      return;
    }

    const months = await this.getPeriodMonths();
    const expiresAt = new Date();
    expiresAt.setMonth(expiresAt.getMonth() + months);

    await repo.update(bookingId, {
      status: 'CONTRACT_ACTIVE',
      paymentStatus: 'PAID',
      expiresAt,
    });
    this.logger.log(
      `General contract ${booking.reference} ACTIVE — ordering window ${months} month(s) (expires ${expiresAt.toISOString()})`,
    );
  }

  /**
   * The drawdown pool for a contract: contracted vs. ordered vs. remaining,
   * per container type for CONTAINER contracts, or a single total line for
   * BULK/BREAK_BULK (keyed on a null container type).
   */
  async getQuantityLines(
    contractBookingId: string,
  ): Promise<ContractQuantityLineView[]> {
    const booking = await this.dataSource.getRepository(Booking).findOne({
      where: { id: contractBookingId },
      relations: { bookingContainers: { containerType: true }, cargoType: true },
    });
    if (!booking) throw new NotFoundException(`Contract ${contractBookingId} not found`);

    const ordered = await this.orderedByContainerType(contractBookingId);

    if (booking.freightType === 'CONTAINER') {
      return (booking.bookingContainers ?? []).map((c) => {
        const orderedQty = ordered.get(c.containerTypeId ?? '') ?? 0;
        const contracted = Number(c.quantity);
        return {
          containerTypeId: c.containerTypeId ?? null,
          containerTypeName: c.containerType?.label ?? null,
          unitOfMeasure: null,
          contractedQuantity: contracted,
          orderedQuantity: orderedQty,
          remainingQuantity: Math.max(0, contracted - orderedQty),
        };
      });
    }

    // BULK / BREAK_BULK — a single pool keyed on the contracted total weight/items.
    const orderedQty = ordered.get('') ?? 0;
    const contracted = Number(booking.cargoTotalWeightVgm);
    const uom: CargoUnitOfMeasure | null =
      (booking.cargoType?.unitOfMeasure as CargoUnitOfMeasure | undefined) ??
      CargoUnitOfMeasure.PerTon;
    return [
      {
        containerTypeId: null,
        containerTypeName: null,
        unitOfMeasure: uom,
        contractedQuantity: contracted,
        orderedQuantity: orderedQty,
        remainingQuantity: Math.max(0, contracted - orderedQty),
      },
    ];
  }

  /**
   * Per-route drawdown pool for a multi-route general contract: contracted vs.
   * ordered vs. remaining, one entry per contracted route line. Returns [] for
   * single-route contracts (no route lines) — callers fall back to
   * {@link getQuantityLines}.
   */
  async getRouteLines(
    contractBookingId: string,
  ): Promise<ContractRouteLineView[]> {
    const routeLines = await this.dataSource
      .getRepository(ContractRouteLine)
      .find({
        where: { contractBookingId },
        relations: {
          originYard: true,
          destinationYard: true,
          containerType: true,
        },
        order: { createdAt: 'ASC' },
      });
    if (routeLines.length === 0) return [];

    const ordered = await this.orderedByRouteLine(contractBookingId);

    return routeLines.map((rl) => {
      const orderedQty = ordered.get(rl.id) ?? 0;
      const contracted = Number(rl.quantity);
      return {
        routeLineId: rl.id,
        originYardId: rl.originYardId,
        originYardName: rl.originYard?.label ?? null,
        destinationYardId: rl.destinationYardId,
        destinationYardName: rl.destinationYard?.label ?? null,
        containerTypeId: rl.containerTypeId ?? null,
        containerTypeName: rl.containerType?.label ?? null,
        contractedQuantity: contracted,
        orderedQuantity: orderedQty,
        remainingQuantity: Math.max(0, contracted - orderedQty),
        km: rl.km != null ? Number(rl.km) : null,
      };
    });
  }

  /** Sum of non-cancelled order quantities, keyed by route_line_id. */
  private async orderedByRouteLine(
    contractBookingId: string,
  ): Promise<Map<string, number>> {
    const rows = await this.dataSource
      .getRepository(BookingOrder)
      .createQueryBuilder('o')
      .innerJoin('o.lines', 'line')
      .select('o.route_line_id', 'key')
      .addSelect('SUM(line.quantity)', 'total')
      .where('o.contract_booking_id = :contractBookingId', { contractBookingId })
      .andWhere('o.route_line_id IS NOT NULL')
      .andWhere(`o.status NOT IN ('CANCELLED', 'REJECTED')`)
      .groupBy('o.route_line_id')
      .getRawMany<{ key: string; total: string }>();

    const map = new Map<string, number>();
    for (const row of rows) if (row.key) map.set(row.key, Number(row.total));
    return map;
  }

  /** Sum of non-cancelled order line quantities, keyed by container type id ('' = bulk). */
  private async orderedByContainerType(
    contractBookingId: string,
  ): Promise<Map<string, number>> {
    const rows = await this.dataSource
      .getRepository(BookingOrder)
      .createQueryBuilder('o')
      .innerJoin('o.lines', 'line')
      .select('COALESCE(line.container_type_id::text, :empty)', 'key')
      .addSelect('SUM(line.quantity)', 'total')
      .where('o.contract_booking_id = :contractBookingId', { contractBookingId })
      .andWhere(`o.status NOT IN ('CANCELLED', 'REJECTED')`)
      .setParameter('empty', '')
      .groupBy('key')
      .getRawMany<{ key: string; total: string }>();

    const map = new Map<string, number>();
    for (const row of rows) map.set(row.key ?? '', Number(row.total));
    return map;
  }

  /** Convenience: how many units remain for a given container type ('' = bulk). */
  async remainingFor(
    contractBookingId: string,
    containerTypeKey: string,
  ): Promise<number> {
    const lines = await this.getQuantityLines(contractBookingId);
    const line = lines.find(
      (l) => (l.containerTypeId ?? '') === containerTypeKey,
    );
    return line?.remainingQuantity ?? 0;
  }

  /** True once every contracted line is fully drawn down. */
  async isExhausted(contractBookingId: string): Promise<boolean> {
    // Multi-route contracts are exhausted when every route line is drawn down;
    // single-route contracts fall back to the per-container-type pool.
    const routeLines = await this.getRouteLines(contractBookingId);
    if (routeLines.length > 0) {
      return routeLines.every((l) => l.remainingQuantity <= 0);
    }
    const lines = await this.getQuantityLines(contractBookingId);
    return lines.every((l) => l.remainingQuantity <= 0);
  }
}
