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 { TriggerCondition } from './entities/surcharge-type.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 {
  IPriorityRulesRepository,
  PRIORITY_RULES_REPOSITORY,
} from './interfaces/priority-rules.repository.interface';
import {
  ISurchargeTypesRepository,
  SURCHARGE_TYPES_REPOSITORY,
} from './interfaces/surcharge-types.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';

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;
  allowConsolidation?: boolean;
  shippingLineId?: string | null;
  containers: BookingContainerEvalInput[];
}

export interface AppliedCargoModifier {
  surchargeTypeId: string;
  surchargeTypeCode: string;
  triggerValue: number | null;
  calculatedAmount: number;
  rateId: string;
  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_RULES_REPOSITORY)
    private readonly priorityRulesRepo: IPriorityRulesRepository,
    @Inject(SURCHARGE_TYPES_REPOSITORY)
    private readonly surchargeTypesRepo: ISurchargeTypesRepository,
    @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;
    }

    const priorityRules = await this.priorityRulesRepo.findAllActive();
    for (const rule of priorityRules) {
      if (
        rule.conditionCurrency === null ||
        rule.conditionCurrency === input.paymentCurrency
      ) {
        priorityScore += rule.score;
      }
    }

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

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

    const surchargeTypes = await this.surchargeTypesRepo.findAllActiveWithRate();
    const liveRates = await this.ratesRepo.findLiveRates();
    const rateById = new Map(liveRates.map((r) => [r.id, r]));

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

      const rate = st.rate ?? rateById.get(st.rateId);
      if (!rate) continue;

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

      if (st.triggerCondition === 'VGM_EXCEEDS_LIMIT') {
        triggerValue = containerWeightResults.reduce(
          (sum, r) => sum + (r.overweightExcessTons ?? 0),
          0,
        );
        if (rate.rateUnit === 'PER_TON') {
          calculatedAmount = triggerValue * Number(rate.rateValue);
        }
      }

      appliedModifiers.push({
        surchargeTypeId: st.id,
        surchargeTypeCode: st.code,
        triggerValue,
        calculatedAmount,
        rateId: rate.id,
        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 all LIVE rates into booking_rate_snapshot for a booking.
   */
  async snapshotLiveRates(bookingId: string): Promise<BookingRateSnapshot[]> {
    const liveRates = await this.ratesRepo.findLiveRates();
    const snapshotRepo = this.dataSource.getRepository(BookingRateSnapshot);
    const now = new Date();
    const snapshots: BookingRateSnapshot[] = [];

    for (const rate of liveRates) {
      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(
    condition: TriggerCondition,
    state: {
      isHazardous: boolean;
      hasReefer: boolean;
      hasOverweight: boolean;
      shippingLineMapped: boolean;
      allowConsolidation: boolean;
    },
  ): boolean {
    switch (condition) {
      case 'CARGO_FLAG_HAZARDOUS':
        return state.isHazardous;
      case 'CARGO_FLAG_REEFER':
        return state.hasReefer;
      case 'VGM_EXCEEDS_LIMIT':
        return state.hasOverweight;
      case 'SHIPPING_LINE_MAPPED':
        return state.shippingLineMapped;
      case 'CONSOLIDATION_ENABLED':
        return state.allowConsolidation;
      default:
        return false;
    }
  }
}
