import { Inject, Injectable, BadRequestException } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { BookingApprovalStep } from '../bookings/entities/booking-approval-step.entity';
import { BookingRateSnapshot } from '../bookings/entities/booking-rate-snapshot.entity';
import { Rate, RateTrigger } from './entities/rate.entity';
import {
  ICargoTypesRepository,
  CARGO_TYPES_REPOSITORY,
} from './interfaces/cargo-types.repository.interface';
import {
  IServiceTypesRepository,
  SERVICE_TYPES_REPOSITORY,
} from './interfaces/service-types.repository.interface';
import {
  IWeightLimitRulesRepository,
  WEIGHT_LIMIT_RULES_REPOSITORY,
} from './interfaces/weight-limit-rules.repository.interface';
import {
  IPriorityConfigsRepository,
  PRIORITY_CONFIGS_REPOSITORY,
} from './interfaces/priority-configs.repository.interface';
import {
  IRatesRepository,
  RATES_REPOSITORY,
} from './interfaces/rates.repository.interface';
import {
  IApprovalRulesRepository,
  APPROVAL_RULES_REPOSITORY,
} from './interfaces/approval-rules.repository.interface';
import {
  IShippingLinesRepository,
  SHIPPING_LINES_REPOSITORY,
} from './interfaces/shipping-lines.repository.interface';
import { DEFAULT_APPROVAL_RULE_ROWS } from './approval-rules.defaults';
import { GOVERNMENT_PRIORITY_BONUS } from './government-priority.constants';

export interface BookingContainerEvalInput {
  containerTypeId: string;
  quantity: number;
  vgmPerUnitTons: number;
  totalVgmTons: number;
  isReefer?: boolean;
  isOverweight?: boolean;
  overweightExcessTons?: number | null;
}

export interface BookingEvaluationInput {
  cargoTypeId?: string | null;
  freightType?: 'CONTAINER' | 'BULK';
  serviceTypeId: string;
  paymentCurrency: string;
  tradeDirection: string;
  isHazardous: boolean;
  /** Booking-level reefer flag; ORed with per-container reefer. */
  isReefer?: boolean;
  isGovernment?: boolean;
  allowConsolidation?: boolean;
  shippingLineId?: string | null;
  totalWagons: number;
  containers: BookingContainerEvalInput[];
}

export interface AppliedCargoModifier {
  /** The trigger-based rate that produced this surcharge line. */
  rateId: string;
  /** Stable display/audit code, derived from the rate's trigger + rateType. */
  surchargeCode: string;
  triggerValue: number | null;
  calculatedAmount: number;
  currency: string;
}

export interface ContainerWeightResult {
  containerTypeId: string;
  weightLimitRuleId: string | null;
  isOverweight: boolean;
  overweightExcessTons: number | null;
}

export interface RuleEvaluationResult {
  priorityScore: number;
  appliedModifiers: AppliedCargoModifier[];
  containerWeightResults: ContainerWeightResult[];
  warnings: string[];
  hardBlocked: string[];
  requiresDirectorApproval: boolean;
}

@Injectable()
export class RuleEngineService {
  constructor(
    @Inject(CARGO_TYPES_REPOSITORY)
    private readonly cargoTypesRepo: ICargoTypesRepository,
    @Inject(SERVICE_TYPES_REPOSITORY)
    private readonly serviceTypesRepo: IServiceTypesRepository,
    @Inject(WEIGHT_LIMIT_RULES_REPOSITORY)
    private readonly weightLimitRulesRepo: IWeightLimitRulesRepository,
    @Inject(PRIORITY_CONFIGS_REPOSITORY)
    private readonly priorityConfigsRepo: IPriorityConfigsRepository,
    @Inject(RATES_REPOSITORY)
    private readonly ratesRepo: IRatesRepository,
    @Inject(APPROVAL_RULES_REPOSITORY)
    private readonly approvalRulesRepo: IApprovalRulesRepository,
    @Inject(SHIPPING_LINES_REPOSITORY)
    private readonly shippingLinesRepo: IShippingLinesRepository,
    private readonly dataSource: DataSource,
  ) {}

  /**
   * Evaluate all rule engine rules against a booking snapshot.
   */
  async evaluate(input: BookingEvaluationInput): Promise<RuleEvaluationResult> {
    const warnings: string[] = [];
    const hardBlocked: string[] = [];
    const appliedModifiers: AppliedCargoModifier[] = [];
    const containerWeightResults: ContainerWeightResult[] = [];
    let priorityScore = 0;
    let requiresDirectorApproval = false;

    if (input.freightType === 'BULK') {
      requiresDirectorApproval = true;
    }

    if (input.cargoTypeId) {
      const cargoType = await this.cargoTypesRepo.findById(input.cargoTypeId);
      if (!cargoType) {
        hardBlocked.push(`Cargo type ${input.cargoTypeId} not found`);
      } else if (cargoType.requiresDirectorApproval) {
        requiresDirectorApproval = true;
      }
    }

    for (const container of input.containers) {
      const rules = await this.weightLimitRulesRepo.findActiveByContainerTypeId(
        container.containerTypeId,
        input.tradeDirection,
      );
      const rule = rules[0];
      let isOverweight = container.isOverweight ?? false;
      let excess = container.overweightExcessTons ?? null;

      if (rule) {
        const maxTotal = Number(rule.maxVgmTons) * container.quantity;
        const totalVgm = container.totalVgmTons;
        if (totalVgm > maxTotal) {
          isOverweight = true;
          excess = Math.max(0, totalVgm - maxTotal);
          warnings.push(
            `Container type ${container.containerTypeId} VGM ${totalVgm}t exceeds limit ${maxTotal}t`,
          );
        }
        containerWeightResults.push({
          containerTypeId: container.containerTypeId,
          weightLimitRuleId: rule.id,
          isOverweight,
          overweightExcessTons: excess,
        });
      } else {
        containerWeightResults.push({
          containerTypeId: container.containerTypeId,
          weightLimitRuleId: null,
          isOverweight,
          overweightExcessTons: excess,
        });
      }
    }

    const serviceType = await this.serviceTypesRepo.findById(input.serviceTypeId);
    if (serviceType) {
      priorityScore += serviceType.priorityBonusPoints;
    }

    // Additive priority blocks, each keyed on the booking's total wagon count:
    //  - WAGON    rules apply regardless of currency.
    //  - CURRENCY rules apply only when the payment currency matches.
    const priorityConfigs = await this.priorityConfigsRepo.findAllActive();
    const wagonsInRange = (cfg: { minWagonCount: number; maxWagonCount: number }) =>
      input.totalWagons >= cfg.minWagonCount &&
      input.totalWagons <= cfg.maxWagonCount;

    for (const cfg of priorityConfigs) {
      const applies =
        cfg.type === 'WAGON' ||
        (cfg.type === 'CURRENCY' && cfg.currency === input.paymentCurrency);
      if (applies && wagonsInRange(cfg)) {
        priorityScore += cfg.scorePoints;
      }
    }

    if (input.isGovernment) {
      priorityScore += GOVERNMENT_PRIORITY_BONUS;
    }

    let shippingLineMapped = false;
    if (input.shippingLineId) {
      const line = await this.shippingLinesRepo.findById(input.shippingLineId);
      shippingLineMapped = Boolean(line?.mappedToCode);
    }

    const hasReefer =
      input.isReefer === true || input.containers.some((c) => c.isReefer);
    const hasOverweight = containerWeightResults.some((r) => r.isOverweight);

    // Surcharges are now self-describing rates: any LIVE rate whose `trigger`
    // is not ALWAYS. Each fires independently and stacks on top of base freight
    // — hazard + reefer + overweight all add together, each with its own unit.
    //
    // A given surcharge identity (same trigger + rateType + unit + value +
    // scope) must contribute exactly ONE line. Duplicate LIVE rate rows — e.g.
    // from a non-idempotent seeder — would otherwise repeat the same surcharge
    // many times and inflate the total, so we collapse them to one row each.
    const liveRates = await this.ratesRepo.findLiveRates();
    const surchargeRates = this.dedupeRatesBySignature(
      liveRates.filter((r) => r.trigger && r.trigger !== 'ALWAYS'),
    );

    for (const rate of surchargeRates) {
      const triggered = this.matchesTrigger(rate.trigger, {
        isHazardous: input.isHazardous,
        hasReefer,
        hasOverweight,
        shippingLineMapped,
        allowConsolidation: input.allowConsolidation ?? false,
      });
      if (!triggered) continue;

      let triggerValue: number | null = null;
      let calculatedAmount = Number(rate.rateValue);

      // Per-ton surcharges (typically OVERWEIGHT) bill against the excess tons.
      if (rate.rateUnit === 'PER_TON' && rate.trigger === 'OVERWEIGHT') {
        triggerValue = containerWeightResults.reduce(
          (sum, r) => sum + (r.overweightExcessTons ?? 0),
          0,
        );
        calculatedAmount = triggerValue * Number(rate.rateValue);
      }

      // Safety guard: never include a surcharge with a non-positive amount (a
      // zero-rate or zero-trigger line would otherwise show as a confusing
      // "free" surcharge on the breakdown).
      if (!(calculatedAmount > 0)) continue;

      appliedModifiers.push({
        rateId: rate.id,
        surchargeCode: this.surchargeCode(rate),
        triggerValue,
        calculatedAmount,
        currency: rate.currency,
      });
    }

    return {
      priorityScore,
      appliedModifiers,
      containerWeightResults,
      warnings,
      hardBlocked,
      requiresDirectorApproval,
    };
  }

  /**
   * Ensure ITMLS default approval chains exist (container + bulk). Idempotent.
   */
  async ensureDefaultApprovalRules(): Promise<void> {
    for (const flag of [false, true] as const) {
      const existing = await this.approvalRulesRepo.findChainForCargo(flag);
      if (existing.length > 0) continue;

      const rows = DEFAULT_APPROVAL_RULE_ROWS.filter(
        (r) => r.requiresDirectorApproval === flag,
      );
      for (const row of rows) {
        await this.approvalRulesRepo.create({
          requiresDirectorApproval: row.requiresDirectorApproval,
          stepOrder: row.stepOrder,
          requiredRole: row.requiredRole,
          actionLabel: row.actionLabel,
          blocksRole: row.blocksRole,
        });
      }
    }
  }

  /**
   * Instantiate booking_approval_step rows from approval_rules by freight type.
   */
  async instantiateApprovalSteps(
    bookingId: string,
    options: {
      freightType: 'CONTAINER' | 'BULK';
      cargoTypeId?: string | null;
    },
  ): Promise<BookingApprovalStep[]> {
    await this.ensureDefaultApprovalRules();

    let requiresDirectorApproval = options.freightType === 'BULK';

    if (options.cargoTypeId) {
      const cargoType = await this.cargoTypesRepo.findById(options.cargoTypeId);
      if (!cargoType) {
        throw new BadRequestException(`Cargo type ${options.cargoTypeId} not found`);
      }
      if (cargoType.requiresDirectorApproval) {
        requiresDirectorApproval = true;
      }
    }

    const chain = await this.approvalRulesRepo.findChainForCargo(
      requiresDirectorApproval,
    );

    if (chain.length === 0) {
      throw new BadRequestException(
        `Approval chain could not be loaded for requiresDirectorApproval=${requiresDirectorApproval}.`,
      );
    }

    const stepRepo = this.dataSource.getRepository(BookingApprovalStep);
    const steps: BookingApprovalStep[] = [];

    for (const rule of chain) {
      const step = stepRepo.create({
        bookingId,
        approvalRuleId: rule.id,
        stepOrder: rule.stepOrder,
        requiredRole: rule.requiredRole,
        blocksRole: rule.blocksRole ?? null,
        status: 'PENDING',
      });
      steps.push(await stepRepo.save(step));
    }

    return steps;
  }

  /**
   * Snapshot only the rates used in a booking's final price.
   */
  async snapshotRates(
    bookingId: string,
    rates: Array<{
      id: string;
      rateType: string;
      rateValue: number;
      rateUnit: string;
      currency: string;
    }>,
  ): Promise<BookingRateSnapshot[]> {
    const snapshotRepo = this.dataSource.getRepository(BookingRateSnapshot);
    const now = new Date();
    const seen = new Set<string>();
    const snapshots: BookingRateSnapshot[] = [];

    for (const rate of rates) {
      if (seen.has(rate.id)) continue;
      seen.add(rate.id);

      const snapshot = snapshotRepo.create({
        bookingId,
        rateId: rate.id,
        rateType: rate.rateType,
        rateValue: rate.rateValue,
        rateUnit: rate.rateUnit,
        currency: rate.currency,
        snapshottedAt: now,
      });
      snapshots.push(await snapshotRepo.save(snapshot));
    }

    return snapshots;
  }

  /** Guard helper — throws BadRequestException if hardBlocked is non-empty. */
  assertNoHardBlocks(result: RuleEvaluationResult): void {
    if (result.hardBlocked.length > 0) {
      throw new BadRequestException(result.hardBlocked.join('; '));
    }
  }

  private matchesTrigger(
    trigger: RateTrigger,
    state: {
      isHazardous: boolean;
      hasReefer: boolean;
      hasOverweight: boolean;
      shippingLineMapped: boolean;
      allowConsolidation: boolean;
    },
  ): boolean {
    // Coerce defensively: a flag may arrive as the string "true"/"false" (e.g.
    // from multipart form-data) and a non-empty "false" string is truthy.
    const truthy = (v: unknown): boolean => v === true || v === 'true';
    switch (trigger) {
      case 'HAZARDOUS':
        return truthy(state.isHazardous);
      case 'REEFER':
        return truthy(state.hasReefer);
      case 'OVERWEIGHT':
        return truthy(state.hasOverweight);
      case 'SHIPPING_LINE':
        return truthy(state.shippingLineMapped);
      case 'CONSOLIDATION':
        return truthy(state.allowConsolidation);
      // CANCELLATION / DEMURRAGE / PIL_EXTRA_FEE are contextual charges applied
      // explicitly elsewhere (not auto-triggered by a booking's cargo flags).
      default:
        return false;
    }
  }

  /** Stable surcharge code for display + audit, derived from the rate. */
  private surchargeCode(rate: Rate): string {
    return rate.rateType ?? rate.trigger;
  }

  /**
   * Collapse rates that describe the same charge to a single representative.
   *
   * Two rates are "the same" when they would produce an identical price line:
   * same trigger, rateType, unit, value, currency, and scoping (container /
   * cargo type). Duplicate rows (e.g. a seeder run more than once) therefore
   * stack into one line instead of repeating — keeping the breakdown clean and
   * the total correct. The first row of each signature is kept so an existing
   * rateId is preserved for snapshotting.
   */
  private dedupeRatesBySignature(rates: Rate[]): Rate[] {
    const seen = new Set<string>();
    const result: Rate[] = [];
    for (const rate of rates) {
      const signature = [
        rate.trigger,
        rate.rateType,
        rate.rateUnit,
        Number(rate.rateValue),
        rate.currency,
        rate.containerTypeId ?? '',
        rate.cargoTypeId ?? '',
      ].join('|');
      if (seen.has(signature)) continue;
      seen.add(signature);
      result.push(rate);
    }
    return result;
  }
}
