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

import { RatesService } from '../rule-engine/services/rates.service';
import { ContainerTypesService } from '../rule-engine/services/container-types.service';
import { ExchangeService } from '@edr/api-common';
import { ContractsRepository } from './contracts.repository';
import { Contract } from './entities/contract.entity';

/** A single unit-rate line at contract phase — NO quantities, NO totals. */
export interface ContractUnitRateLineItem {
  code: string;
  label: string;
  unit: 'per_container' | 'per_wagon' | 'per_ton' | 'per_item' | 'per_km' | 'flat';
  unitPrice: number;
  containerSize?: string | null;
  conditionalOn?: string | null;
  cargoTypeCode?: string | null;
  /**
   * Customs clearance service fee — billed separately in advance (before the
   * clearance document step), never part of shipment booking totals.
   */
  isClearance?: boolean;
}

/** The contract `pricing_breakdown` shape (doc §9.1). */
export interface ContractPricingBreakdown {
  displayMode: 'UNIT_RATES';
  currency: string;
  lineItems: ContractUnitRateLineItem[];
  generatedAt: string;
}

/** Map a rate's storage unit to the contract-display unit. */
function toContractUnit(rateUnit: string): ContractUnitRateLineItem['unit'] {
  switch (rateUnit) {
    case 'PER_TON':
      return 'per_ton';
    case 'PER_ITEM':
      return 'per_item';
    case 'PER_KM':
      return 'per_km';
    case 'PER_WAGON':
      return 'per_wagon';
    case 'PER_CONTAINER':
      return 'per_container';
    default:
      return 'flat';
  }
}

@Injectable()
export class ContractPricingService {
  constructor(
    private readonly contractsRepository: ContractsRepository,
    private readonly ratesService: RatesService,
    private readonly containerTypesService: ContainerTypesService,
    private readonly exchangeService: ExchangeService,
  ) {}

  /** Base rail rate type for the contract's direction + freight. */
  private baseRateType(contract: Contract): string {
    const isBulk = contract.freightType === 'BULK';
    if (contract.tradeDirection === 'IMPORT') {
      return isBulk ? 'BULK_IMPORT' : 'CONTAINER_IMPORT';
    }
    if (contract.tradeDirection === 'EXPORT') {
      return isBulk ? 'BULK_EXPORT' : 'CONTAINER_EXPORT';
    }
    return isBulk ? 'INTERCITY_BULK' : 'INTERCITY_CONTAINER';
  }

  /**
   * Build the unit-rate breakdown from live rates. Emits per-unit prices only
   * (one per container size, conditional hazard/reefer surcharges, and bulk
   * commodity rate) — NO totals or quantities (doc §9.1).
   */
  async buildBreakdown(contract: Contract): Promise<ContractPricingBreakdown> {
    const liveRates = await this.ratesService.findLiveRates();
    const currency = contract.paymentCurrency;
    const isEtb = currency === 'ETB';
    const usdToEtb = isEtb ? await this.exchangeService.getRate('USD', 'ETB') : 1;
    const convert = (usd: number): number => (isEtb ? Math.round(usd * usdToEtb) : usd);

    const lineItems: ContractUnitRateLineItem[] = [];
    const baseType = this.baseRateType(contract);

    if (contract.freightType === 'CONTAINER') {
      const sizes = (contract.cargoScope ?? [])
        .map((c) => c.containerSize)
        .filter((s): s is string => !!s);
      const { items: containerTypes } = await this.containerTypesService.findAll({
        isActive: true,
        pageSize: 100,
      });
      for (const size of sizes) {
        const sizeFt = size === '40ft' ? 40 : 20;
        const matchedTypes = containerTypes.filter((ct) => ct.sizeFt === sizeFt);
        const matchedIds = new Set(matchedTypes.map((ct) => ct.id));
        const rate =
          liveRates.find(
            (r) =>
              r.rateType === baseType &&
              r.currency === 'USD' &&
              r.containerTypeId &&
              matchedIds.has(r.containerTypeId),
          ) ??
          liveRates.find(
            (r) => r.rateType === baseType && r.currency === 'USD' && !r.containerTypeId,
          );
        if (!rate) continue;
        lineItems.push({
          code: `CONTAINER_${size.toUpperCase()}`,
          label: `${size} container`,
          unit: toContractUnit(rate.rateUnit),
          unitPrice: convert(Number(rate.rateValue)),
          containerSize: size,
        });
      }
    } else {
      const cargoScope = (contract.cargoScope ?? []).find((c) => c.cargoTypeId);
      // Freeze the rate for the contract's own commodity when one is configured
      // — a per-item machinery rate and a per-ton wheat rate live side by side.
      const bulkRates = liveRates.filter(
        (r) => r.rateType === baseType && r.currency === 'USD',
      );
      const bulkRate =
        (cargoScope?.cargoTypeId
          ? bulkRates.find((r) => r.cargoTypeId === cargoScope.cargoTypeId)
          : undefined) ??
        bulkRates.find((r) => !r.cargoTypeId) ??
        bulkRates[0] ??
        null;
      if (bulkRate) {
        lineItems.push({
          code: 'BULK_FREIGHT',
          label: cargoScope?.cargoType?.cargoTypeName ?? 'Bulk cargo',
          unit: toContractUnit(bulkRate.rateUnit),
          unitPrice: convert(Number(bulkRate.rateValue)),
          cargoTypeCode: cargoScope?.cargoType?.code ?? null,
        });
      }
    }

    // First / last mile trucking unit rates — shown when the contract carries
    // that leg. Per-unit prices only; the actual amount (× km / containers /
    // tons / flat) is computed at booking time.
    if (contract.firstMilePickupAddress) {
      const fm = liveRates.find(
        (r) => r.rateType === 'FIRST_MILE' && r.currency === 'USD',
      );
      if (fm && Number(fm.rateValue) > 0) {
        lineItems.push({
          code: 'FIRST_MILE',
          label: 'First mile (pick-up)',
          unit: toContractUnit(fm.rateUnit),
          unitPrice: convert(Number(fm.rateValue)),
        });
      }
    }
    if (contract.lastMileDeliveryAddress) {
      const lm = liveRates.find(
        (r) => r.rateType === 'LAST_MILE' && r.currency === 'USD',
      );
      if (lm && Number(lm.rateValue) > 0) {
        lineItems.push({
          code: 'LAST_MILE',
          label: 'Last mile (delivery)',
          unit: toContractUnit(lm.rateUnit),
          unitPrice: convert(Number(lm.rateValue)),
        });
      }
    }

    // Conditional surcharges — shown only when the contract toggles them on AND
    // the rate has a non-zero value (a 0 rate means "no surcharge").
    if (contract.isHazardous) {
      const hazard = liveRates.find(
        (r) => r.rateType === 'HAZARD_SURCHARGE' && r.currency === 'USD',
      );
      if (hazard && Number(hazard.rateValue) > 0) {
        lineItems.push({
          code: 'HAZARD_SURCHARGE',
          label: 'Hazardous surcharge',
          unit: toContractUnit(hazard.rateUnit),
          unitPrice: convert(Number(hazard.rateValue)),
          conditionalOn: 'is_hazardous',
        });
      }
    }
    if (contract.isReefer) {
      const reefer = liveRates.find(
        (r) => r.rateType === 'REEFER_SURCHARGE' && r.currency === 'USD',
      );
      if (reefer && Number(reefer.rateValue) > 0) {
        lineItems.push({
          code: 'REEFER_SURCHARGE',
          label: 'Reefer surcharge',
          unit: toContractUnit(reefer.rateUnit),
          unitPrice: convert(Number(reefer.rateValue)),
          conditionalOn: 'is_reefer',
        });
      }
    }
    // Lashing / cargo securing — BULK only, shown when the contract's commodity
    // needs lashing (cargoType.hasLashing). The commodity-scoped rate for the
    // contract's direction wins over the commodity-wide catch-all; billed at
    // booking on the live rate (per ton / per wagon), this line is display.
    if (contract.freightType === 'BULK') {
      const scope = (contract.cargoScope ?? []).find((c) => c.cargoTypeId);
      if (scope?.cargoType?.hasLashing) {
        const onDirection = liveRates.filter(
          (r) =>
            r.trigger === 'LASHING' &&
            r.currency === 'USD' &&
            !r.containerTypeId &&
            r.tradeDirection === contract.tradeDirection,
        );
        const lashing =
          onDirection.find((r) => r.cargoTypeId === scope.cargoTypeId) ??
          onDirection.find((r) => !r.cargoTypeId);
        if (lashing && Number(lashing.rateValue) > 0) {
          lineItems.push({
            code: 'LASHING',
            label: `Lashing / cargo securing (${scope.cargoType.cargoTypeName})`,
            unit: toContractUnit(lashing.rateUnit),
            unitPrice: convert(Number(lashing.rateValue)),
            cargoTypeCode: scope.cargoType.code ?? null,
            conditionalOn: 'has_lashing',
          });
        }
      }
    }

    // Empty-container return service — container contracts only, toggled on the
    // contract like hazard/reefer. Billed at booking per WITH_RETURN container.
    if (
      contract.freightType === 'CONTAINER' &&
      contract.equipmentReturn === 'WITH_RETURN'
    ) {
      // Return is sold per direction + route + container type (import-only) —
      // one display line per contract size that has a configured rate. A size
      // with no rate shows nothing here and hard-blocks at booking time.
      // ponytail: bookings bill the live route rate, not a frozen snapshot.
      const route = [...(contract.routes ?? [])].sort(
        (a, b) => a.sortOrder - b.sortOrder,
      )[0];
      const onLeg = route
        ? liveRates.filter(
            (r) =>
              r.rateType === 'RETURN_SURCHARGE' &&
              r.currency === 'USD' &&
              r.tradeDirection === contract.tradeDirection &&
              r.originYardId === route.originYardId &&
              r.destinationYardId === route.destinationYardId,
          )
        : [];
      if (onLeg.length > 0) {
        const sizes = (contract.cargoScope ?? [])
          .map((c) => c.containerSize)
          .filter((s): s is string => !!s);
        const { items: containerTypes } = await this.containerTypesService.findAll({
          isActive: true,
          pageSize: 100,
        });
        for (const size of sizes) {
          const sizeFt = size === '40ft' ? 40 : 20;
          const matchedIds = new Set(
            containerTypes.filter((ct) => ct.sizeFt === sizeFt).map((ct) => ct.id),
          );
          const rate =
            onLeg.find((r) => r.containerTypeId && matchedIds.has(r.containerTypeId)) ??
            onLeg.find((r) => !r.containerTypeId);
          if (!rate || Number(rate.rateValue) <= 0) continue;
          lineItems.push({
            code: 'RETURN_SURCHARGE',
            label: `Empty container return (${size})`,
            unit: toContractUnit(rate.rateUnit),
            unitPrice: convert(Number(rate.rateValue)),
            containerSize: size,
            conditionalOn: 'with_return',
          });
        }
      }
    }

    // Customs clearance service fee (Path B) — billed on the booking invoice
    // together with the freight. Sold per direction + route + cargo kind:
    // container contracts freeze one fee line per contract size (each size's
    // own container-type rate), bulk contracts freeze the route's bulk fee.
    // A customs contract may not proceed without the fee(s) configured.
    if (contract.customsClearingEnabled) {
      // Strict, no route-less fallback.
      // ponytail: multi-route contracts bill the first lane's fee; per-lane fees need per-route snapshots.
      const route = [...(contract.routes ?? [])].sort(
        (a, b) => a.sortOrder - b.sortOrder,
      )[0];
      const onLeg = route
        ? liveRates.filter(
            (r) =>
              r.rateType === 'CUSTOMS_CLEARANCE' &&
              r.currency === 'USD' &&
              r.tradeDirection === contract.tradeDirection &&
              r.originYardId === route.originYardId &&
              r.destinationYardId === route.destinationYardId,
          )
        : [];
      if (contract.freightType === 'CONTAINER') {
        const sizes = (contract.cargoScope ?? [])
          .map((c) => c.containerSize)
          .filter((s): s is string => !!s);
        const { items: containerTypes } = await this.containerTypesService.findAll({
          isActive: true,
          pageSize: 100,
        });
        for (const size of sizes) {
          const sizeFt = size === '40ft' ? 40 : 20;
          const matchedIds = new Set(
            containerTypes.filter((ct) => ct.sizeFt === sizeFt).map((ct) => ct.id),
          );
          const rate = onLeg.find(
            (r) => r.containerTypeId && matchedIds.has(r.containerTypeId),
          );
          if (!rate || Number(rate.rateValue) <= 0) {
            throw new UnprocessableEntityException(
              `No customs clearance service fee is configured for ${size} containers on this direction and route. Ask the rates team to set a live CUSTOMS_CLEARANCE rate for this container type and origin → destination.`,
            );
          }
          lineItems.push({
            // Distinct code per size so the frozen snapshots don't collide —
            // booking pricing looks each size up by CUSTOMS_CLEARANCE_<FT>FT.
            code: `CUSTOMS_CLEARANCE_${sizeFt}FT`,
            label: `Customs clearance service (${size})`,
            unit: toContractUnit(rate.rateUnit),
            unitPrice: convert(Number(rate.rateValue)),
            containerSize: size,
            isClearance: true,
          });
        }
      } else {
        // Bulk fee — the rate scoped to the contract's commodity wins; a
        // commodity-less rate (legacy) is the catch-all fallback.
        const scope = (contract.cargoScope ?? []).find((c) => c.cargoTypeId);
        const rate =
          (scope?.cargoTypeId
            ? onLeg.find(
                (r) => !r.containerTypeId && r.cargoTypeId === scope.cargoTypeId,
              )
            : undefined) ?? onLeg.find((r) => !r.containerTypeId && !r.cargoTypeId);
        if (!rate || Number(rate.rateValue) <= 0) {
          throw new UnprocessableEntityException(
            'No bulk customs clearance service fee is configured for this cargo type on this direction and route. Ask the rates team to set a live bulk CUSTOMS_CLEARANCE rate for this commodity and origin → destination.',
          );
        }
        lineItems.push({
          code: 'CUSTOMS_CLEARANCE',
          label: `Customs clearance service (${scope?.cargoType?.cargoTypeName ?? 'bulk'})`,
          unit: toContractUnit(rate.rateUnit),
          unitPrice: convert(Number(rate.rateValue)),
          cargoTypeCode: scope?.cargoType?.code ?? null,
          isClearance: true,
        });
      }
    }

    return {
      displayMode: 'UNIT_RATES',
      currency,
      lineItems,
      generatedAt: new Date().toISOString(),
    };
  }

  /** Generate (and persist) the unit-rate breakdown for a contract. */
  async generatePrice(contractId: string): Promise<ContractPricingBreakdown> {
    const contract = await this.contractsRepository.findByIdWithRelations(contractId);
    if (!contract) {
      throw new Error(`Contract ${contractId} not found`);
    }
    const breakdown = await this.buildBreakdown(contract);
    await this.contractsRepository.update(contractId, {
      pricingBreakdown: breakdown as never,
      pricingDisplayMode: 'UNIT_RATES',
    } as never);
    return breakdown;
  }

  /**
   * Freeze the contract's unit rates into contract_rate_snapshots (one row per
   * rate line) at submit time. The booking later computes totals from these.
   */
  async freezeRateSnapshots(contractId: string): Promise<void> {
    const contract = await this.contractsRepository.findByIdWithRelations(contractId);
    if (!contract) return;
    const breakdown =
      (contract.pricingBreakdown as ContractPricingBreakdown | null) ??
      (await this.buildBreakdown(contract));

    await this.contractsRepository.clearRateSnapshots(contractId);
    for (const line of breakdown.lineItems) {
      await this.contractsRepository.createRateSnapshot({
        contractId,
        rateCode: line.code,
        description: line.label,
        unitPrice: line.unitPrice,
        unitOfMeasure: line.unit,
        currency: breakdown.currency,
        containerSize: line.containerSize ?? null,
        isSurcharge: !!line.conditionalOn,
        conditionalOn: line.conditionalOn ?? null,
        isClearance: !!line.isClearance,
      });
    }
  }
}
