import { BadRequestException, Injectable } from '@nestjs/common';
import { ContractDocPhase } from '@edr/types';

import { ContractsRepository } from './contracts.repository';
import { ClearanceMilestoneService } from './clearance-milestone.service';
import { MilestoneMetadata } from './entities/clearance-milestone.entity';
import { splitMilestones } from './clearance-milestone.catalog';
import { Contract } from './entities/contract.entity';
import { ContractClearanceCycle } from './entities/contract-clearance-cycle.entity';
import { ClearanceMilestone } from './entities/clearance-milestone.entity';
import { BookingsRepository } from '../bookings/bookings.repository';
import { BookingLifecycleNotifierService } from '../bookings/booking-lifecycle-notifier.service';
import { Booking } from '../bookings/entities/booking.entity';
import type { ClearanceMetaState } from './clearance-workflow.types';
import { metaFromBooking } from './clearance-workflow.types';

export type ClearanceActorRole = 'CUSTOMER' | 'GL_ET' | 'GL_DJ' | 'OPERATIONS';

export interface ClearanceNextAction {
  actor: ClearanceActorRole;
  action: string;
  milestoneCode?: string | null;
  blockedReason?: string | null;
}

const IMPORT_BOUNDARY = 'DO_COLLECTED';
const EXPORT_BOUNDARY = 'EXPORT_RELEASED';

const IMPORT_DOC_UPLOADED = 'IMPORT_DOCS_UPLOADED';
const EXPORT_DOC_UPLOADED = 'EXPORT_DOCS_UPLOADED';

@Injectable()
export class ClearanceWorkflowService {
  constructor(
    private readonly contractsRepository: ContractsRepository,
    private readonly milestoneService: ClearanceMilestoneService,
    private readonly bookingsRepository: BookingsRepository,
    private readonly notifier: BookingLifecycleNotifierService,
  ) {}

  boundaryMilestone(tradeDirection: string): string {
    return tradeDirection === 'IMPORT' ? IMPORT_BOUNDARY : EXPORT_BOUNDARY;
  }

  // ── Contract scope (ONE_TIME) ─────────────────────────────────────────────

  async listMilestones(contractId: string): Promise<ClearanceMilestone[]> {
    return this.milestoneService.listForContract(contractId);
  }

  async listMilestonesForBooking(bookingId: string): Promise<ClearanceMilestone[]> {
    return this.milestoneService.listForBooking(bookingId);
  }

  async isBoundaryComplete(contractId: string, tradeDirection: string): Promise<boolean> {
    return this.isBoundaryCompleteForMilestones(
      await this.listMilestones(contractId),
      tradeDirection,
    );
  }

  async isBoundaryCompleteForBooking(
    bookingId: string,
    tradeDirection: string,
  ): Promise<boolean> {
    return this.isBoundaryCompleteForMilestones(
      await this.listMilestonesForBooking(bookingId),
      tradeDirection,
    );
  }

  private isBoundaryCompleteForMilestones(
    milestones: ClearanceMilestone[],
    tradeDirection: string,
  ): boolean {
    const code = this.boundaryMilestone(tradeDirection);
    const m = milestones.find((x) => x.milestoneCode === code);
    return m?.status === 'COMPLETED';
  }

  async assertBoundaryComplete(contract: Contract): Promise<void> {
    const ok = await this.isBoundaryComplete(contract.id, contract.tradeDirection);
    if (!ok) {
      throw new BadRequestException(
        `Pre-booking clearance is not complete — ${this.boundaryMilestone(contract.tradeDirection)} must be finished before booking.`,
      );
    }
  }

  async assertPriorComplete(
    contractId: string,
    tradeDirection: string,
    targetCode: string,
  ): Promise<void> {
    await this.assertPriorCompleteOnMilestones(
      await this.listMilestones(contractId),
      tradeDirection,
      targetCode,
    );
  }

  async assertPriorCompleteForBooking(
    bookingId: string,
    tradeDirection: string,
    targetCode: string,
  ): Promise<void> {
    await this.assertPriorCompleteOnMilestones(
      await this.listMilestonesForBooking(bookingId),
      tradeDirection,
      targetCode,
    );
  }

  private async assertPriorCompleteOnMilestones(
    milestones: ClearanceMilestone[],
    tradeDirection: string,
    targetCode: string,
  ): Promise<void> {
    const { preBooking } = splitMilestones(tradeDirection);
    const byCode = new Map(milestones.map((m) => [m.milestoneCode, m]));
    const targetIdx = preBooking.findIndex((d) => d.code === targetCode);
    if (targetIdx < 0) return;

    for (let i = 0; i < targetIdx; i++) {

      const code = preBooking[i]!.code;
      const m = byCode.get(code);
      if (!m) continue;
      if (m.status === 'SKIPPED') continue;
      if (m.status !== 'COMPLETED') {
        throw new BadRequestException(
          `Complete "${preBooking[i]!.label}" before proceeding.`,
        );
      }
    }
  }

  async skipMilestones(contractId: string, codes: string[]): Promise<void> {
    for (const code of codes) {
      await this.milestoneService.skipForContract(contractId, code);
    }
  }

  async skipMilestonesForBooking(bookingId: string, codes: string[]): Promise<void> {
    for (const code of codes) {
      await this.milestoneService.skipForBooking(bookingId, code);
    }
  }

  async completeMilestone(
    contractId: string,
    code: string,
    userId?: string,
    metadata?: MilestoneMetadata,
  ): Promise<ClearanceMilestone> {
    if (metadata && Object.keys(metadata).length > 0) {
      return this.milestoneService.completeWithMetadataForContract(
        contractId,
        code,
        metadata,
        userId,
      );
    }
    return this.milestoneService.completeForContract(contractId, code, userId);
  }

  async completeMilestoneForBooking(
    bookingId: string,
    code: string,
    userId?: string,
    metadata?: MilestoneMetadata,
  ): Promise<ClearanceMilestone> {
    if (metadata && Object.keys(metadata).length > 0) {
      return this.milestoneService.completeWithMetadataForBooking(
        bookingId,
        code,
        metadata,
        userId,
      );
    }
    return this.milestoneService.completeForBooking(bookingId, code, userId);
  }

  async onCustomerDocsUploaded(contractId: string, tradeDirection: string): Promise<void> {
    const uploaded =
      tradeDirection === 'IMPORT' ? IMPORT_DOC_UPLOADED : EXPORT_DOC_UPLOADED;
    await this.completeMilestone(contractId, uploaded);
    await this.completeMilestone(contractId, 'PENDING_DOCUMENT_REVIEW');
  }

  async onCustomerDocsUploadedForBooking(
    bookingId: string,
    tradeDirection: string,
  ): Promise<void> {
    const uploaded =
      tradeDirection === 'IMPORT' ? IMPORT_DOC_UPLOADED : EXPORT_DOC_UPLOADED;
    await this.completeMilestoneForBooking(bookingId, uploaded);
    await this.completeMilestoneForBooking(bookingId, 'PENDING_DOCUMENT_REVIEW');
  }

  async onAllDocsApproved(contractId: string): Promise<void> {
    await this.completeMilestone(contractId, 'DOCUMENTS_APPROVED');
  }

  async onAllDocsApprovedForBooking(bookingId: string): Promise<void> {
    await this.completeMilestoneForBooking(bookingId, 'DOCUMENTS_APPROVED');
  }

  /** Customer doc queried or re-uploaded — document approval milestone must reopen. */
  async onDocumentReviewReopened(contractId: string): Promise<void> {
    await this.milestoneService.reopenForContract(contractId, 'DOCUMENTS_APPROVED');
  }

  async onDocumentReviewReopenedForBooking(bookingId: string): Promise<void> {
    await this.milestoneService.reopenForBooking(bookingId, 'DOCUMENTS_APPROVED');
  }

  async onDeclarationUploaded(contractId: string, userId?: string): Promise<void> {
    await this.completeMilestone(contractId, 'UNDER_CUSTOMS_CLEARANCE');
    await this.completeMilestone(contractId, 'DECLARED', userId);
  }

  async onDeclarationUploadedForBooking(bookingId: string, userId?: string): Promise<void> {
    await this.completeMilestoneForBooking(bookingId, 'UNDER_CUSTOMS_CLEARANCE');
    await this.completeMilestoneForBooking(bookingId, 'DECLARED', userId);
  }

  async onDutySkipped(contractId: string): Promise<void> {
    await this.skipMilestones(contractId, ['DUTY_TAXES_ADVISED', 'DUTY_TAX_PAID']);
  }

  async onDutySkippedForBooking(bookingId: string): Promise<void> {
    await this.skipMilestonesForBooking(bookingId, ['DUTY_TAXES_ADVISED', 'DUTY_TAX_PAID']);
  }

  async onExportReleased(contractId: string, userId?: string): Promise<void> {
    await this.completeMilestone(contractId, 'EXPORT_RELEASED', userId);
    await this.markReadyForBooking(contractId);
  }

  async onExportReleasedForBooking(bookingId: string, userId?: string): Promise<void> {
    await this.completeMilestoneForBooking(bookingId, 'EXPORT_RELEASED', userId);
    await this.markReadyForOperation(bookingId);
  }

  async markReadyForBooking(contractId: string): Promise<void> {
    const cycle = await this.contractsRepository.currentCycle(contractId);
    await this.contractsRepository.update(contractId, {
      status: 'CLEARANCE_READY_FOR_BOOKING',
      clearanceStatus: 'CLEARANCE_READY_FOR_BOOKING',
    } as never);
    if (cycle) {
      await this.contractsRepository.setCycleStatus(cycle.id, 'CLEARANCE_READY_FOR_BOOKING', {
        clearanceReadyAt: new Date(),
        currentPhase: ContractDocPhase.GlEtPostClearance,
      });
      await this.contractsRepository.updateCycle(cycle.id, {
        currentPhase: ContractDocPhase.GlEtPostClearance,
      });
    }
  }

  /** GENERAL per-booking: boundary complete → customer may proceed to operations. */
  async markReadyForOperation(bookingId: string): Promise<void> {
    await this.bookingsRepository.update(bookingId, {
      status: 'CLEARANCE_READY',
      clearanceCurrentPhase: ContractDocPhase.GlEtPostClearance,
    } as never);
    // Tell the customer clearance is done and operation can be requested. Load
    // failure only skips the notice — the status change above already committed.
    try {
      const booking = await this.bookingsRepository.findByIdWithFiles(bookingId);
      if (booking) this.notifier.clearanceReady(booking);
    } catch {
      /* notification is best-effort */
    }
  }

  resolvePhase(
    contract: Contract,
    cycle: ContractClearanceCycle | null,
    milestones: ClearanceMilestone[],
  ): ContractDocPhase {
    const meta: ClearanceMetaState = {
      dutyRequired: cycle?.dutyRequired ?? null,
      vesselDepartureDate: cycle?.vesselDepartureDate ?? null,
      roHoldReason: cycle?.roHoldReason ?? null,
      currentPhase: cycle?.currentPhase ?? null,
    };
    return this.resolvePhaseFromMeta(contract.tradeDirection, meta, milestones);
  }

  resolvePhaseForBooking(
    booking: Booking,
    milestones: ClearanceMilestone[],
  ): ContractDocPhase {
    return this.resolvePhaseFromMeta(
      booking.tradeDirection ?? 'IMPORT',
      metaFromBooking(booking),
      milestones,
    );
  }

  private resolvePhaseFromMeta(
    tradeDirection: string,
    meta: ClearanceMetaState,
    milestones: ClearanceMilestone[],
  ): ContractDocPhase {
    if (meta.currentPhase) {
      return meta.currentPhase as ContractDocPhase;
    }
    return this.inferPhaseFromMeta(tradeDirection, meta, milestones);
  }

  inferPhase(
    contract: Contract,
    cycle: ContractClearanceCycle | null,
    milestones: ClearanceMilestone[],
  ): ContractDocPhase {
    return this.inferPhaseFromMeta(
      contract.tradeDirection,
      {
        dutyRequired: cycle?.dutyRequired ?? null,
        roHoldReason: cycle?.roHoldReason ?? null,
      },
      milestones,
    );
  }

  inferPhaseForBooking(booking: Booking, milestones: ClearanceMilestone[]): ContractDocPhase {
    return this.inferPhaseFromMeta(
      booking.tradeDirection ?? 'IMPORT',
      metaFromBooking(booking),
      milestones,
    );
  }

  private inferPhaseFromMeta(
    tradeDirection: string,
    meta: ClearanceMetaState,
    milestones: ClearanceMilestone[],
  ): ContractDocPhase {
    const byCode = new Map(milestones.map((m) => [m.milestoneCode, m]));
    const isDone = (code: string) =>
      byCode.get(code)?.status === 'COMPLETED' || byCode.get(code)?.status === 'SKIPPED';

    const docUploaded =
      tradeDirection === 'IMPORT'
        ? isDone(IMPORT_DOC_UPLOADED)
        : isDone(EXPORT_DOC_UPLOADED);

    if (!docUploaded) return ContractDocPhase.CustomerIntake;
    if (!isDone('DOCUMENTS_APPROVED')) return ContractDocPhase.GlEtReview;

    if (tradeDirection === 'EXPORT') {
      if (!isDone('RELEASE_ORDER_SECURED')) {
        return ContractDocPhase.GlDjCollection;
      }
      if (!isDone('DECLARED')) return ContractDocPhase.GlEtOutput;
      if (!isDone(EXPORT_BOUNDARY)) return ContractDocPhase.GlEtPostClearance;
      return ContractDocPhase.GlEtPostClearance;
    }

    if (!isDone('DECLARED')) return ContractDocPhase.GlEtOutput;
    if (meta.dutyRequired === true && !isDone('DUTY_TAX_PAID')) {
      return ContractDocPhase.CustomerDuty;
    }
    if (!isDone('TRANSIT_PERMIT_UPLOADED')) return ContractDocPhase.GlEtPostClearance;
    if (!meta.preClearanceFinalizedAt) return ContractDocPhase.GlEtPostClearance;
    if (!isDone(IMPORT_BOUNDARY)) return ContractDocPhase.GlDjCollection;
    return ContractDocPhase.GlEtPostClearance;
  }

  computeNextAction(
    contract: Contract,
    cycle: ContractClearanceCycle | null,
    milestones: ClearanceMilestone[],
  ): ClearanceNextAction | null {
    return this.computeNextActionFromMeta(
      contract.tradeDirection,
      {
        dutyRequired: cycle?.dutyRequired ?? null,
        roHoldReason: cycle?.roHoldReason ?? null,
        preClearanceFinalizedAt: cycle?.preClearanceFinalizedAt ?? null,
      },
      milestones,
      'contract',
    );
  }

  computeNextActionForBooking(
    booking: Booking,
    milestones: ClearanceMilestone[],
  ): ClearanceNextAction | null {
    return this.computeNextActionFromMeta(
      booking.tradeDirection ?? 'IMPORT',
      metaFromBooking(booking),
      milestones,
      'booking',
    );
  }

  private computeNextActionFromMeta(
    tradeDirection: string,
    meta: ClearanceMetaState,
    milestones: ClearanceMilestone[],
    terminalScope: 'contract' | 'booking',
  ): ClearanceNextAction | null {
    if (meta.roHoldReason) {
      return {
        actor: 'GL_DJ',
        action: 'Re-upload Release Order or request port amendment',
        milestoneCode: 'RELEASE_ORDER_SECURED',
        blockedReason: meta.roHoldReason,
      };
    }

    const byCode = new Map(milestones.map((m) => [m.milestoneCode, m]));
    const pending = (code: string) => {
      const m = byCode.get(code);
      return m && m.status === 'PENDING';
    };
    const isDone = (code: string) => {
      const m = byCode.get(code);
      return m?.status === 'COMPLETED' || m?.status === 'SKIPPED';
    };

    const docCode =
      tradeDirection === 'IMPORT' ? IMPORT_DOC_UPLOADED : EXPORT_DOC_UPLOADED;

    if (pending(docCode) || !isDone(docCode)) {
      return {
        actor: 'CUSTOMER',
        action: 'Upload clearance documents',
        milestoneCode: docCode,
      };
    }

    if (!isDone('DOCUMENTS_APPROVED')) {
      return {
        actor: 'GL_ET',
        action: 'Review and approve customer documents',
        milestoneCode: 'DOCUMENTS_APPROVED',
      };
    }

    const terminalAction =
      terminalScope === 'contract'
        ? 'Create shipment booking'
        : 'Proceed to request operation';

    if (tradeDirection === 'EXPORT') {
      if (!isDone('RELEASE_ORDER_SECURED')) {
        return {
          actor: 'GL_DJ',
          action: 'Upload Release Order and vessel departure date',
          milestoneCode: 'RELEASE_ORDER_SECURED',
        };
      }
      if (!isDone('DECLARED')) {
        return {
          actor: 'GL_ET',
          action: 'Upload customs declaration documents',
          milestoneCode: 'DECLARED',
        };
      }
      if (!isDone(EXPORT_BOUNDARY)) {
        return {
          actor: 'GL_ET',
          action: 'Confirm export release',
          milestoneCode: EXPORT_BOUNDARY,
        };
      }
      if (terminalScope === 'booking') {
        if (!isDone('FREIGHT_PAYMENT_SETTLED')) {
          return {
            actor: 'CUSTOMER',
            action: 'Pay freight charges',
            milestoneCode: 'FREIGHT_PAYMENT_SETTLED',
          };
        }
        if (!isDone('WAGON_ALLOCATED')) {
          return {
            actor: 'OPERATIONS',
            action: 'Allocate wagon',
            milestoneCode: 'WAGON_ALLOCATED',
          };
        }
        if (!isDone('EXPORT_TRANSPORT_ISSUED')) {
          return {
            actor: 'GL_ET',
            action: 'Upload transit permit',
            milestoneCode: 'EXPORT_TRANSPORT_ISSUED',
          };
        }
        return null;
      }
      return {
        actor: terminalScope === 'contract' ? 'GL_ET' : 'CUSTOMER',
        action: terminalAction,
        milestoneCode: EXPORT_BOUNDARY,
      };
    }

    if (!isDone('DECLARED')) {
      return {
        actor: 'GL_ET',
        action: 'Upload customs declaration documents',
        milestoneCode: 'DECLARED',
      };
    }

    if (meta.dutyRequired === null || meta.dutyRequired === undefined) {
      return {
        actor: 'GL_ET',
        action: 'Set whether duty/tax applies',
        milestoneCode: 'DUTY_TAXES_ADVISED',
      };
    }

    if (meta.dutyRequired && !isDone('DUTY_TAX_PAID')) {
      if (!isDone('DUTY_TAXES_ADVISED')) {
        return {
          actor: 'GL_ET',
          action: 'Advise duty and tax amount',
          milestoneCode: 'DUTY_TAXES_ADVISED',
        };
      }
      return {
        actor: 'CUSTOMER',
        action: 'Upload duty/tax payment slip',
        milestoneCode: 'DUTY_TAX_PAID',
      };
    }

    if (!isDone('TRANSIT_PERMIT_UPLOADED')) {
      return {
        actor: 'GL_ET',
        action: 'Upload transit permit screenshot',
        milestoneCode: 'TRANSIT_PERMIT_UPLOADED',
      };
    }

    if (!meta.preClearanceFinalizedAt) {
      return {
        actor: 'GL_ET',
        action: 'Finalize pre-clearance',
        milestoneCode: 'TRANSIT_PERMIT_UPLOADED',
      };
    }

    if (!isDone(IMPORT_BOUNDARY)) {
      return {
        actor: 'GL_DJ',
        action: 'Upload Delivery Order',
        milestoneCode: IMPORT_BOUNDARY,
      };
    }

    return {
      actor: terminalScope === 'contract' ? 'GL_ET' : 'CUSTOMER',
      action: terminalAction,
      milestoneCode: IMPORT_BOUNDARY,
    };
  }

  etPendingMilestoneCodes(milestones: ClearanceMilestone[]): string | null {
    const pending = milestones.find((m) => m.status === 'PENDING' && m.ownerRegion === 'ET');
    return pending?.milestoneCode ?? null;
  }

  djPendingMilestoneCodes(milestones: ClearanceMilestone[]): string | null {
    const pending = milestones.find((m) => m.status === 'PENDING' && m.ownerRegion === 'DJ');
    return pending?.milestoneCode ?? null;
  }
}
