import { Injectable, NotFoundException } from '@nestjs/common';

import { ContainerTypesService } from '../rule-engine/services/container-types.service';
import { RatesService } from '../rule-engine/services/rates.service';
import { Rate } from '../rule-engine/entities/rate.entity';
import { ExchangeService } from '@edr/api-common';
import {
  AppliedCargoModifier,
  BookingEvaluationInput,
  RuleEngineService,
} from '../rule-engine/rule-engine.service';
import { BookingsRepository } from './bookings.repository';
import {
  containersPerWagon,
  wagonRemainder,
} from './consolidation.service';
import { GeneratePriceResponseDto, PriceLineItemDto } from './dto/generate-price-response.dto';
import { Booking } from './entities/booking.entity';
import { assertBookingStatus } from './booking-status.util';

export interface ComputedPriceResult {
  lineItems: PriceLineItemDto[];
  totalAmount: number;
  currency: string;
  usedRates: Rate[];
  appliedModifiers: AppliedCargoModifier[];
  priorityScore: number;
  warnings: string[];
  hardBlocked: string[];
}

type StoredPricingBreakdown = {
  lineItems?: PriceLineItemDto[];
  totalAmount?: number;
  currency?: string;
  generatedAt?: string;
} | null;

/** Friendly labels for the per-unit rate card shown at the confirm step. */
const SURCHARGE_LABELS: Record<string, string> = {
  HAZARD_SURCHARGE: 'Hazardous cargo',
  HAZARDOUS_CARGO: 'Hazardous cargo',
  REEFER_SURCHARGE: 'Refrigerated (reefer)',
  REEFER_CARGO: 'Refrigerated (reefer)',
  OVERWEIGHT_PER_TON: 'Overweight excess',
  DOUBLE_HANDLING: 'Double handling',
  LASHING: 'Lashing',
  PIL_EXTRA_FEE: 'Shipping line fee',
};

function surchargeLabel(code: string): string {
  return (
    SURCHARGE_LABELS[code] ??
    code
      .toLowerCase()
      .split('_')
      .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
      .join(' ')
  );
}

@Injectable()
export class BookingPricingService {
  constructor(
    private readonly bookingsRepository: BookingsRepository,
    private readonly ruleEngineService: RuleEngineService,
    private readonly containerTypesService: ContainerTypesService,
    private readonly ratesService: RatesService,
    private readonly exchangeService: ExchangeService,
  ) {}

  async generatePrice(bookingId: string): Promise<GeneratePriceResponseDto> {
    const booking = await this.requireBooking(bookingId);
    assertBookingStatus(booking, ['DRAFT', 'CHANGES_REQUESTED']);

    const computed = await this.computePriceForBooking(booking);
    this.ruleEngineService.assertNoHardBlocks({
      priorityScore: computed.priorityScore,
      appliedModifiers: computed.appliedModifiers,
      containerWeightResults: [],
      warnings: computed.warnings,
      hardBlocked: computed.hardBlocked,
      requiresDirectorApproval: false,
    });

    await this.bookingsRepository.update(bookingId, {
      totalAmount: computed.totalAmount,
      priorityScore: computed.priorityScore,
      pricingBreakdown: {
        lineItems: computed.lineItems,
        totalAmount: computed.totalAmount,
        currency: computed.currency,
        generatedAt: new Date().toISOString(),
      },
    } as never);

    return {
      bookingId,
      totalAmount: computed.totalAmount,
      currency: computed.currency,
      lineItems: computed.lineItems,
      warnings: computed.warnings,
    };
  }

  async computePriceForBooking(booking: Booking): Promise<ComputedPriceResult> {
    const evalInput = await this.buildEvalInputForBooking(booking);
    const ruleResult = await this.ruleEngineService.evaluate(evalInput);

    const paymentCurrency = booking.paymentCurrency;
    const isEtbBooking = paymentCurrency === 'ETB';
    const usdToEtb = isEtbBooking ? await this.exchangeService.getRate('USD', 'ETB') : 1;

    const lineItems: PriceLineItemDto[] = [];
    let total = 0;

    const { lineItems: baseLines, usedRates: baseRates } =
      await this.computeBaseRailLinesWithRates(booking, evalInput);
    for (const line of baseLines) {
      lineItems.push(line);
      total += line.amount;
    }

    const liveRates = await this.ratesService.findLiveRates();
    const rateById = new Map(liveRates.map((r) => [r.id, r]));
    const usedRatesMap = new Map(baseRates.map((r) => [r.id, r]));

    for (const mod of ruleResult.appliedModifiers) {
      const usdAmount = mod.calculatedAmount;
      const convertedAmount = isEtbBooking ? Math.round(usdAmount * usdToEtb) : usdAmount;

      const rate = rateById.get(mod.rateId);
      const unit = rate?.rateUnit ?? 'FLAT';
      const unitUsd = rate ? Number(rate.rateValue) : usdAmount;
      const unitAmount = isEtbBooking ? Math.round(unitUsd * usdToEtb) : unitUsd;
      // Per-unit count: FLAT and PER_INVOICE are billed once (qty 1); an
      // explicit trigger (e.g. overweight tons) wins when present; otherwise
      // derive from total ÷ unit price.
      const quantity =
        unit === 'FLAT' || unit === 'PER_INVOICE'
          ? 1
          : mod.triggerValue != null && mod.triggerValue > 0
            ? mod.triggerValue
            : unitUsd > 0
              ? Math.max(1, Math.round(usdAmount / unitUsd))
              : 1;

      const item: PriceLineItemDto = {
        code: mod.surchargeCode,
        description: surchargeLabel(mod.surchargeCode),
        amount: convertedAmount,
        unitAmount,
        unit,
        quantity,
        currency: paymentCurrency,
      };
      lineItems.push(item);
      total += convertedAmount;

      if (rate) usedRatesMap.set(rate.id, rate);
    }

    return {
      lineItems,
      totalAmount: total,
      currency: booking.paymentCurrency,
      usedRates: [...usedRatesMap.values()],
      appliedModifiers: ruleResult.appliedModifiers,
      priorityScore: ruleResult.priorityScore,
      warnings: ruleResult.warnings,
      hardBlocked: ruleResult.hardBlocked,
    };
  }

  pricesMatch(stored: StoredPricingBreakdown, computed: ComputedPriceResult): boolean {
    if (!stored?.lineItems?.length) return false;
    if (Number(stored.totalAmount) !== computed.totalAmount) return false;
    return (
      this.lineItemsSignature(stored.lineItems) ===
      this.lineItemsSignature(computed.lineItems)
    );
  }

  async createPricingSnapshots(
    bookingId: string,
    usedRates: Rate[],
    appliedModifiers: AppliedCargoModifier[],
  ): Promise<void> {
    await this.bookingsRepository.clearPricingArtifacts(bookingId);
    const snapshots = await this.ruleEngineService.snapshotRates(bookingId, usedRates);

    const snapshotByRateId = new Map(snapshots.map((s) => [s.rateId, s.id]));
    const rows = appliedModifiers
      .map((m) => {
        const snapshotId = snapshotByRateId.get(m.rateId);
        if (!snapshotId) return null;
        return {
          bookingId,
          rateId: m.rateId,
          triggerValue: m.triggerValue,
          calculatedAmount: m.calculatedAmount,
          rateSnapshotId: snapshotId,
        };
      })
      .filter((r): r is NonNullable<typeof r> => r !== null);

    if (rows.length > 0) {
      await this.bookingsRepository.createCargoModifiers(rows);
    }
  }

  async buildEvalInputForBooking(booking: Booking): Promise<BookingEvaluationInput> {
    const lines = await Promise.all(
      (booking.bookingContainers ?? [])
        .filter((bc): bc is typeof bc & { containerTypeId: string } => bc.containerTypeId != null)
        .map(async (bc) => {
          const ct = await this.containerTypesService.findById(bc.containerTypeId);
          const vgm = Number(bc.vgmPerUnitTons);
          const qty = bc.quantity;
          return {
            container: {
              containerTypeId: bc.containerTypeId,
              quantity: qty,
              vgmPerUnitTons: vgm,
              totalVgmTons: qty * vgm,
              isReefer: ct.isReefer,
            },
            perWagon: containersPerWagon(Number(ct.wagonsPerUnit)),
            quantity: qty,
          };
        }),
    );
    const containers = lines.map((l) => l.container);
    // Wagon count is persisted per container line at booking creation; sum it.
    const totalWagons =
      booking.freightType === 'CONTAINER'
        ? Math.ceil(
            (booking.bookingContainers ?? []).reduce(
              (sum, bc) => sum + Number(bc.wagonsRequired ?? 0),
              0,
            ),
          )
        : 0;

    // Consolidation is system-managed: the CONSOLIDATION surcharge fires whenever
    // a container type leaves a wagon partially filled. Aggregate by type first —
    // two lines of the same type share wagons, so 2× 20FT (= one full wagon) must
    // NOT count as a partial wagon. Mirrors ConsolidationService.slotsFromContainerLines.
    const remainderByType = new Map<string, { quantity: number; perWagon: number }>();
    for (const l of lines) {
      const prev = remainderByType.get(l.container.containerTypeId);
      remainderByType.set(l.container.containerTypeId, {
        quantity: (prev?.quantity ?? 0) + Number(l.quantity || 0),
        perWagon: l.perWagon,
      });
    }
    const allowConsolidation =
      booking.freightType === 'CONTAINER' &&
      [...remainderByType.values()].some(
        (t) => wagonRemainder(t.quantity, t.perWagon) > 0,
      );

    return {
      freightType: booking.freightType as 'CONTAINER' | 'BULK',
      cargoTypeId: booking.cargoTypeId ?? null,
      serviceTypeId: booking.serviceTypeId,
      paymentCurrency: booking.paymentCurrency,
      tradeDirection: booking.tradeDirection,
      // Coerce defensively in case the stored flag is a string ("true"/"false").
      isHazardous: booking.isHazardous === true || (booking.isHazardous as unknown) === 'true',
      // Booking-level reefer flag (set by contract drawdown orders that carry a
      // reefer quantity) applies the REEFER surcharge even for non-reefer
      // container types. ORed with per-container reefer in the engine.
      isReefer: booking.isReefer === true || (booking.isReefer as unknown) === 'true',
      isGovernment: booking.isGovernment,
      allowConsolidation,
      shippingLineId: booking.shippingLineId,
      totalWagons,
      containers,
    };
  }

  private async requireBooking(id: string): Promise<Booking> {
    const booking = await this.bookingsRepository.findByIdWithFiles(id);
    if (!booking) throw new NotFoundException(`Booking ${id} not found`);
    return booking;
  }

  /** Line items for contract schedule (uses stored breakdown or recomputes). */
  async computeContractLineItems(booking: Booking): Promise<{
    lineItems: PriceLineItemDto[];
    totalAmount: number;
    currency: string;
  }> {
    const stored = booking.pricingBreakdown as StoredPricingBreakdown;

    if (stored?.lineItems?.length) {
      return {
        lineItems: stored.lineItems,
        totalAmount: Number(stored.totalAmount ?? booking.totalAmount),
        currency: stored.currency ?? booking.paymentCurrency,
      };
    }

    const computed = await this.computePriceForBooking(booking);

    if (computed.lineItems.length === 0) {
      const total = Number(booking.totalAmount);
      return {
        lineItems: [
          {
            code: 'TOTAL',
            description: 'Contract total',
            amount: total,
            unitAmount: total,
            unit: 'FLAT',
            quantity: 1,
            currency: booking.paymentCurrency,
          },
        ],
        totalAmount: total,
        currency: booking.paymentCurrency,
      };
    }

    return {
      lineItems: computed.lineItems,
      totalAmount: computed.totalAmount || Number(booking.totalAmount),
      currency: computed.currency,
    };
  }

  /**
   * Recompute priority on submit.
   *
   * The full priority model is additive and capped at 100:
   *   service-type bonus (≤ 15) + wagon block (≤ 50) + currency block (≤ 35).
   * All three components are produced by RuleEngineService.evaluate, so submit
   * simply re-runs the engine — there is no extra submit-time inflation.
   */
  async computeSubmitPriorityScore(booking: Booking): Promise<number> {
    const evalInput = await this.buildEvalInputForBooking(booking);
    const ruleResult = await this.ruleEngineService.evaluate(evalInput);
    return ruleResult.priorityScore;
  }

  private async computeBaseRailLinesWithRates(
    booking: Booking,
    evalInput: BookingEvaluationInput,
  ): Promise<{ lineItems: PriceLineItemDto[]; usedRates: Rate[] }> {
    const liveRates = await this.ratesService.findLiveRates();
    const paymentCurrency = booking.paymentCurrency;
    const isEtbBooking = paymentCurrency === 'ETB';
    const usdToEtb = isEtbBooking ? await this.exchangeService.getRate('USD', 'ETB') : 1;
    const isBulk = booking.freightType === 'BULK';

    const rateType =
      booking.tradeDirection === 'IMPORT'
        ? isBulk
          ? 'BULK_IMPORT'
          : 'CONTAINER_IMPORT'
        : booking.tradeDirection === 'EXPORT'
          ? isBulk
            ? 'BULK_EXPORT'
            : 'CONTAINER_EXPORT'
          : isBulk
            ? 'INTERCITY_BULK'
            : 'INTERCITY_CONTAINER';

    const lines: PriceLineItemDto[] = [];
    const usedRatesMap = new Map<string, Rate>();
    const wagonCount = await this.bookingsRepository.calculateWagonCount(booking.id);

    for (const container of evalInput.containers) {
      const rate = this.pickRate(liveRates, rateType, container.containerTypeId, 'USD');
      if (!rate) continue;

      usedRatesMap.set(rate.id, rate);
      const usdAmount = this.amountForRate(rate, container.quantity, wagonCount);
      const amount = isEtbBooking ? Math.round(usdAmount * usdToEtb) : usdAmount;
      const unitUsd = Number(rate.rateValue);
      const label = await this.containerTypeLabel(container.containerTypeId);
      lines.push({
        code: rateType,
        description: `${label} rail freight`,
        amount,
        unitAmount: isEtbBooking ? Math.round(unitUsd * usdToEtb) : unitUsd,
        unit: rate.rateUnit,
        quantity: this.effectiveUnitQuantity(rate.rateUnit, container.quantity, wagonCount),
        currency: paymentCurrency,
      });
    }

    if (lines.length === 0) {
      const fallback = liveRates.find(
        (r) => r.rateType === rateType && r.currency === 'USD' && r.status === 'LIVE',
      );
      if (fallback) {
        usedRatesMap.set(fallback.id, fallback);
        const bulkTons = Number(booking.cargoTotalWeightVgm ?? 0);
        const quantity =
          isBulk && fallback.rateUnit === 'PER_TON' ? Math.max(bulkTons, 0) : 1;
        const usdAmount = this.amountForRate(fallback, quantity, wagonCount);
        const amount = isEtbBooking ? Math.round(usdAmount * usdToEtb) : usdAmount;
        const unitUsd = Number(fallback.rateValue);
        lines.push({
          code: rateType,
          description: isBulk ? 'Bulk rail freight' : 'Container rail freight',
          amount,
          unitAmount: isEtbBooking ? Math.round(unitUsd * usdToEtb) : unitUsd,
          unit: fallback.rateUnit,
          quantity: this.effectiveUnitQuantity(fallback.rateUnit, quantity, wagonCount),
          currency: paymentCurrency,
        });
      }
    }

    return { lineItems: lines, usedRates: [...usedRatesMap.values()] };
  }

  /** Friendly container-type label for the per-unit card; degrades to "Container". */
  private async containerTypeLabel(containerTypeId: string): Promise<string> {
    try {
      const ct = await this.containerTypesService?.findById?.(containerTypeId);
      return ct?.label ?? 'Container';
    } catch {
      return 'Container';
    }
  }

  /** How many units a rate's total is divided into, by rate unit (for the per-unit card). */
  private effectiveUnitQuantity(
    rateUnit: string,
    quantity: number,
    wagonCount: number,
  ): number {
    switch (rateUnit) {
      case 'PER_WAGON':
        return wagonCount;
      case 'FLAT':
        return 1;
      case 'PER_CONTAINER':
      case 'PER_TON':
      default:
        return quantity;
    }
  }

  private pickRate(
    rates: Rate[],
    rateType: string,
    containerTypeId: string,
    currency: string,
  ): Rate | undefined {
    return (
      rates.find(
        (r) =>
          r.rateType === rateType &&
          r.currency === currency &&
          r.containerTypeId === containerTypeId,
      ) ??
      rates.find((r) => r.rateType === rateType && r.currency === currency && !r.containerTypeId)
    );
  }

  private amountForRate(rate: Rate, quantity: number, wagonCount: number): number {
    const value = Number(rate.rateValue);
    switch (rate.rateUnit) {
      case 'PER_CONTAINER':
        return value * quantity;
      case 'PER_WAGON':
        return value * wagonCount;
      case 'PER_TON':
        return value * quantity;
      case 'FLAT':
        return value;
      default:
        return value * quantity;
    }
  }

  private lineItemsSignature(items: PriceLineItemDto[]): string {
    return JSON.stringify(
      [...items]
        .map((item) => ({
          code: item.code,
          amount: item.amount,
          currency: item.currency,
        }))
        .sort((a, b) => a.code.localeCompare(b.code)),
    );
  }
}
