import { BaseRepository } from '@edr/api-common';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, In, IsNull, Repository, SelectQueryBuilder } from 'typeorm';

import { FileRecord } from '../files/entities/file.entity';
import { Contract } from './entities/contract.entity';
import { ContractApprovalStep } from './entities/contract-approval-step.entity';
import { ContractClearanceCycle } from './entities/contract-clearance-cycle.entity';
import {
  ContractDocReviewStatus,
  ContractDocumentReview,
} from './entities/contract-document-review.entity';
import { ContractRateSnapshot } from './entities/contract-rate-snapshot.entity';
import { ContractReviewNote, ContractReviewNoteType } from './entities/contract-review-note.entity';
import { ContractSignature, ContractSignerRole } from './entities/contract-signature.entity';
import { TERMINAL_CONTRACT_STATUSES } from './utils/contract-expiry.util';

export interface ContractListFilterOptions {
  statuses?: string[];
  status?: string;
  companyId?: string;
  companyProfileId?: string;
  contractKind?: string;
  serviceTypeId?: string;
  freightType?: string;
  tradeDirection?: string;
  paymentCurrency?: string;
  customsClearingEnabled?: boolean;
  /** true → only contracts with at least one uploaded clearance document. */
  hasClearanceDocuments?: boolean;
  createdFrom?: string;
  createdTo?: string;
}

@Injectable()
export class ContractsRepository extends BaseRepository<Contract> {
  constructor(
    @InjectRepository(Contract)
    repository: Repository<Contract>,
    private readonly dataSource: DataSource,
  ) {
    super(repository);
  }

  /** Find a contract by its human-readable reference number. */
  findByReference(reference: string): Promise<Contract | null> {
    return this.repository.findOne({ where: { reference } });
  }

  /**
   * Highest NNNNN sequence already issued for `CTR-<year>-…` references.
   * Includes soft-deleted contracts — their references still occupy the unique
   * index, so the next number must move past them. (A created-at count drifts
   * below the issued sequence after any delete and then collides forever.)
   */
  async maxReferenceSequence(year: number): Promise<number> {
    const row = await this.repository
      .createQueryBuilder('contract')
      .withDeleted()
      .select(
        "COALESCE(MAX(CAST(SUBSTRING(contract.reference FROM '[0-9]+$') AS int)), 0)",
        'max',
      )
      .where('contract.reference LIKE :prefix', { prefix: `CTR-${year}-%` })
      .getRawOne<{ max: string | number | null }>();
    return Number(row?.max ?? 0);
  }

  /**
   * Non-terminal contracts for the same company + service type, with routes
   * loaded — candidates for the duplicate-contract check on create(). Terminal
   * filtering happens in JS via isEffectivelyExpired (also covers the
   * date-passed-but-not-yet-cron-flipped case).
   */
  async findDuplicateCandidates(
    companyId: string,
    serviceTypeId: string,
  ): Promise<Contract[]> {
    return this.repository
      .createQueryBuilder('contract')
      .leftJoinAndSelect('contract.routes', 'routes')
      .where('contract.deleted_at IS NULL')
      .andWhere('contract.company_id = :companyId', { companyId })
      .andWhere('contract.service_type_id = :serviceTypeId', { serviceTypeId })
      .andWhere('contract.status NOT IN (:...terminal)', {
        terminal: TERMINAL_CONTRACT_STATUSES,
      })
      // A ONE_TIME contract allows a single booking, so once that booking
      // exists the contract is spent and can never carry another shipment.
      // Without this it kept blocking new requests on the same service type +
      // route until its validity lapsed — locking a customer out of a lane for
      // the rest of the term after one completed shipment.
      .andWhere(
        `(contract.contract_kind <> 'ONE_TIME' OR NOT EXISTS (
            SELECT 1 FROM freight.bookings b
             WHERE b.contract_id = contract.id AND b.deleted_at IS NULL
         ))`,
      )
      .getMany();
  }

  /**
   * Nightly expiry sweep: flips lapsed contracts to EXPIRED. Returns the
   * number of rows updated (for cron logging).
   */
  async expireLapsedContracts(): Promise<number> {
    const result = await this.repository
      .createQueryBuilder()
      .update(Contract)
      .set({ status: 'EXPIRED' })
      .where('deleted_at IS NULL')
      .andWhere('status NOT IN (:...terminal)', { terminal: TERMINAL_CONTRACT_STATUSES })
      .andWhere('contract_valid_until IS NOT NULL AND contract_valid_until < :now', {
        now: new Date(),
      })
      .execute();
    return result.affected ?? 0;
  }

  /**
   * Live contracts whose validity ends between `days` and `days + 1` days from
   * now — the slice the daily expiry-reminder cron warns about. The window is
   * rolling and exactly 24h wide, so consecutive daily runs tile it without
   * gaps or overlaps: each contract is picked up by exactly one run and the
   * customer is notified once, with no "already reminded" flag to store.
   */
  async findExpiringInDays(days: number): Promise<Contract[]> {
    const now = Date.now();
    return this.repository
      .createQueryBuilder('contract')
      .where('contract.deleted_at IS NULL')
      .andWhere('contract.status NOT IN (:...terminal)', {
        terminal: TERMINAL_CONTRACT_STATUSES,
      })
      .andWhere('contract.contract_valid_until >= :from', {
        from: new Date(now + days * 86_400_000),
      })
      .andWhere('contract.contract_valid_until < :to', {
        to: new Date(now + (days + 1) * 86_400_000),
      })
      .getMany();
  }

  /** Find a contract by ID with all child collections, service type, company and files. */
  async findByIdWithRelations(id: string): Promise<Contract | null> {
    if (!id) return null;

    const contract = await this.repository
      .createQueryBuilder('contract')
      .leftJoinAndSelect('contract.routes', 'routes')
      .leftJoinAndSelect('routes.originYard', 'routeOrigin')
      .leftJoinAndSelect('routes.destinationYard', 'routeDestination')
      .leftJoinAndSelect('contract.cargoScope', 'cargoScope')
      .leftJoinAndSelect('cargoScope.cargoType', 'cargoType')
      .leftJoinAndSelect('contract.rateSnapshots', 'rateSnapshots')
      .leftJoinAndSelect('contract.signatures', 'signatures')
      .leftJoinAndSelect('signatures.signatureFile', 'signatureFile')
      .leftJoinAndSelect('contract.approvalSteps', 'approvalSteps')
      .leftJoinAndSelect('contract.serviceType', 'serviceType')
      .leftJoinAndSelect('contract.company', 'company')
      .where('contract.id = :id', { id })
      .leftJoinAndMapMany(
        'contract.files',
        FileRecord,
        'file',
        // Superseded versions are soft-deleted, not dropped — keep them out of
        // the live file list (a manual join condition is not filtered for us).
        "file.resource_id = contract.id AND file.resource = 'contracts' AND file.deleted_at IS NULL",
      )
      .getOne();

    return contract ?? null;
  }

  /** Paginated list with optional multi-status filter (API tab queues). */
  async findAllPaginated(
    options: ContractListFilterOptions & {
      page: number;
      pageSize: number;
      search?: string;
      sortBy?: string;
      sortOrder?: 'ASC' | 'DESC';
    },
  ): Promise<{
    items: Contract[];
    total: number;
    meta: {
      page: number;
      pageSize: number;
      total: number;
      totalPages: number;
      hasNextPage: boolean;
      hasPreviousPage: boolean;
    };
  }> {
    const page = options.page;
    const pageSize = options.pageSize;

    const qb = this.repository
      .createQueryBuilder('contract')
      .leftJoinAndSelect('contract.company', 'company')
      .leftJoinAndSelect('contract.serviceType', 'serviceType')
      .leftJoinAndSelect('contract.routes', 'routes')
      .leftJoinAndSelect('routes.originYard', 'routeOrigin')
      .leftJoinAndSelect('routes.destinationYard', 'routeDestination')
      .leftJoinAndSelect('contract.cargoScope', 'cargoScope')
      .where('contract.deleted_at IS NULL');

    this.applyListFilters(qb, options);

    // Free-text search across contract reference and customer (company) name.
    // Applied here (not in applyListFilters) because only this query joins the
    // `company` alias — the summary-metrics query builder does not.
    if (options.search) {
      qb.andWhere(
        '(contract.reference ILIKE :search OR company.name ILIKE :search)',
        { search: `%${options.search}%` },
      );
    }

    const sortField =
      options.sortBy === 'contractValidUntil'
        ? 'contract.contractValidUntil'
        : 'contract.createdAt';
    qb.orderBy(sortField, options.sortOrder ?? 'DESC');

    const [items, total] = await qb
      .skip((page - 1) * pageSize)
      .take(pageSize)
      .getManyAndCount();

    // Attach the generated contract PDF to each row so list/home can offer a
    // direct download. Loaded separately to keep pagination counts correct.
    await this.attachContractFiles(items);
    await this.attachClearancePhases(items);
    await this.attachRejectionNotes(items);

    const totalPages = pageSize > 0 ? Math.ceil(total / pageSize) : 0;
    return {
      items,
      total,
      meta: {
        page,
        pageSize,
        total,
        totalPages,
        hasNextPage: page < totalPages,
        hasPreviousPage: page > 1,
      },
    };
  }

  /**
   * Load contract-resource files for the given contracts and attach them to
   * `contract.files`. Kept separate from the paginated query so the one-to-many
   * join doesn't inflate the page count.
   */
  private async attachContractFiles(contracts: Contract[]): Promise<void> {
    if (contracts.length === 0) return;
    const ids = contracts.map((c) => c.id);
    const files = await this.dataSource.getRepository(FileRecord).find({
      where: { resource: 'contracts', resourceId: In(ids), deletedAt: IsNull() },
    });
    const byContract = new Map<string, FileRecord[]>();
    for (const file of files) {
      const list = byContract.get(file.resourceId) ?? [];
      list.push(file);
      byContract.set(file.resourceId, list);
    }
    for (const contract of contracts) {
      contract.files = byContract.get(contract.id) ?? [];
    }
  }

  /**
   * Attach each contract's persisted clearance phase (latest cycle's
   * current_phase) so list consumers can show step-accurate customer actions
   * ("Pay duty & upload slip" vs generic "Update clearance") without a
   * per-contract clearance-view request. One query per page, like
   * `attachContractFiles`.
   */
  private async attachClearancePhases(contracts: Contract[]): Promise<void> {
    if (contracts.length === 0) return;
    const ids = contracts.map((c) => c.id);
    const rows: Array<{
      contract_id: string;
      current_phase: string | null;
      booking_id: string | null;
      booking_status: string | null;
    }> = await this.dataSource.query(
      `SELECT DISTINCT ON (ccc.contract_id)
              ccc.contract_id, ccc.current_phase,
              b.id AS booking_id, b.status AS booking_status
         FROM freight.contract_clearance_cycles ccc
         LEFT JOIN freight.bookings b ON b.id = ccc.booking_id
        WHERE ccc.contract_id = ANY($1)
        ORDER BY ccc.contract_id, ccc.cycle_number DESC`,
      [ids],
    );
    const byContract = new Map(rows.map((r) => [r.contract_id, r]));
    for (const contract of contracts) {
      const row = byContract.get(contract.id);
      contract.clearancePhase = row?.current_phase ?? null;
      contract.latestCycleBookingId = row?.booking_id ?? null;
      contract.latestCycleBookingStatus = row?.booking_status ?? null;
    }
  }

  /**
   * Attach the latest REJECTION review-note body to each REJECTED contract so
   * list consumers (portal rows, backoffice queues) can show why without a
   * per-contract detail fetch. One query per page, like `attachContractFiles`.
   */
  private async attachRejectionNotes(contracts: Contract[]): Promise<void> {
    const rejected = contracts.filter((c) => c.status === 'REJECTED');
    if (rejected.length === 0) return;
    const ids = rejected.map((c) => c.id);
    const rows: Array<{ contract_id: string; body: string }> =
      await this.dataSource.query(
        `SELECT DISTINCT ON (contract_id) contract_id, body
           FROM freight.contract_review_notes
          WHERE contract_id = ANY($1)
            AND note_type = 'REJECTION'
            AND deleted_at IS NULL
          ORDER BY contract_id, created_at DESC`,
        [ids],
      );
    const byContract = new Map(rows.map((r) => [r.contract_id, r.body]));
    for (const contract of rejected) {
      contract.latestRejectionNote = byContract.get(contract.id) ?? null;
    }
  }

  async getStatusCounts(): Promise<Record<string, number>> {
    const rows = await this.repository
      .createQueryBuilder('contract')
      .select('contract.status', 'status')
      .addSelect('COUNT(*)::int', 'count')
      .where('contract.deleted_at IS NULL')
      .groupBy('contract.status')
      .getRawMany<{ status: string; count: string }>();

    return Object.fromEntries(rows.map((row) => [row.status, Number(row.count)]));
  }

  async getListSummaryMetrics(
    options: ContractListFilterOptions & {
      page: number;
      pageSize: number;
      needsActionStatuses: readonly string[];
    },
  ): Promise<{ inQueue: number; onThisPage: number; needsAction: number }> {
    const baseQb = () => {
      const qb = this.repository
        .createQueryBuilder('contract')
        .where('contract.deleted_at IS NULL');
      this.applyListFilters(qb, options);
      return qb;
    };

    const inQueue = await baseQb().getCount();

    const needsAction = await baseQb()
      .andWhere('contract.status IN (:...needsActionStatuses)', {
        needsActionStatuses: [...options.needsActionStatuses],
      })
      .getCount();

    const offset = (options.page - 1) * options.pageSize;
    const onThisPage = Math.min(options.pageSize, Math.max(0, inQueue - offset));

    return { inQueue, onThisPage, needsAction };
  }

  private applyListFilters(
    qb: SelectQueryBuilder<Contract>,
    options: ContractListFilterOptions,
  ): void {
    if (options.statuses?.length) {
      qb.andWhere('contract.status IN (:...statuses)', { statuses: options.statuses });
    } else if (options.status) {
      qb.andWhere('contract.status = :status', { status: options.status });
    }
    if (options.companyId) {
      qb.andWhere('contract.company_id = :companyId', { companyId: options.companyId });
    }
    if (options.companyProfileId) {
      qb.andWhere('contract.company_profile_id = :companyProfileId', {
        companyProfileId: options.companyProfileId,
      });
    }
    if (options.contractKind) {
      qb.andWhere('contract.contract_kind = :contractKind', {
        contractKind: options.contractKind,
      });
    }
    if (options.customsClearingEnabled !== undefined) {
      qb.andWhere('contract.customs_clearing_enabled = :customsClearingEnabled', {
        customsClearingEnabled: options.customsClearingEnabled,
      });
    }
    if (options.hasClearanceDocuments) {
      qb.andWhere(
        'EXISTS (SELECT 1 FROM freight.contract_document_review cdr ' +
          'WHERE cdr.contract_id = contract.id AND cdr.deleted_at IS NULL)',
      );
    }
    if (options.serviceTypeId) {
      qb.andWhere('contract.service_type_id = :serviceTypeId', {
        serviceTypeId: options.serviceTypeId,
      });
    }
    if (options.freightType) {
      qb.andWhere('contract.freight_type = :freightType', {
        freightType: options.freightType,
      });
    }
    if (options.tradeDirection) {
      qb.andWhere('contract.trade_direction = :tradeDirection', {
        tradeDirection: options.tradeDirection,
      });
    }
    if (options.paymentCurrency) {
      qb.andWhere('contract.payment_currency = :paymentCurrency', {
        paymentCurrency: options.paymentCurrency,
      });
    }
    if (options.createdFrom) {
      qb.andWhere('contract.created_at >= :createdFrom', {
        createdFrom: options.createdFrom,
      });
    }
    if (options.createdTo) {
      qb.andWhere('contract.created_at <= :createdTo', { createdTo: options.createdTo });
    }
  }

  // ── Approval steps ─────────────────────────────────────────────────────────

  /** Lowest-order pending approval step (sequential enforcement). */
  async findNextPendingApprovalStep(
    contractId: string,
  ): Promise<ContractApprovalStep | null> {
    return this.dataSource.getRepository(ContractApprovalStep).findOne({
      where: { contractId, status: 'PENDING' },
      order: { stepOrder: 'ASC' },
    });
  }

  async findApprovalStepById(
    contractId: string,
    stepId: string,
  ): Promise<ContractApprovalStep | null> {
    return this.dataSource.getRepository(ContractApprovalStep).findOne({
      where: { contractId, id: stepId },
    });
  }

  /** Mark an approval step complete. */
  async completeApprovalStep(
    stepId: string,
    actorId: string,
    status: 'APPROVED' | 'REJECTED',
    note?: string,
  ): Promise<void> {
    await this.dataSource.getRepository(ContractApprovalStep).update(stepId, {
      status,
      actedByStaffId: actorId,
      actedAt: new Date(),
      note,
    });
  }

  /**
   * Send-back reset: every step at or after `fromStepOrder` returns to PENDING
   * with its actor/verdict cleared, so the chain re-runs from that stage. The
   * send-back reason lives in the review-note trail, not on the wiped steps.
   */
  async resetApprovalStepsFrom(
    contractId: string,
    fromStepOrder: number,
  ): Promise<void> {
    await this.dataSource
      .getRepository(ContractApprovalStep)
      .createQueryBuilder()
      .update()
      .set({ status: 'PENDING', actedByStaffId: null, actedAt: null, note: null })
      .where('contract_id = :contractId', { contractId })
      .andWhere('step_order >= :fromStepOrder', { fromStepOrder })
      .execute();
  }

  /** Check if all approval steps are approved. */
  async allApprovalStepsComplete(contractId: string): Promise<boolean> {
    const pending = await this.dataSource.getRepository(ContractApprovalStep).count({
      where: { contractId, status: 'PENDING' },
    });
    return pending === 0;
  }

  /** Persist a contract approval step (instantiated at staff accept). */
  async createApprovalStep(
    data: Partial<ContractApprovalStep>,
  ): Promise<ContractApprovalStep> {
    const repo = this.dataSource.getRepository(ContractApprovalStep);
    return repo.save(repo.create(data));
  }

  // ── Signatures ──────────────────────────────────────────────────────────────

  findSignatures(contractId: string): Promise<ContractSignature[]> {
    return this.dataSource.getRepository(ContractSignature).find({
      where: { contractId },
      relations: ['signatureFile', 'stampFile'],
      order: { signedAt: 'ASC' },
    });
  }

  findSignature(
    contractId: string,
    role: ContractSignerRole,
  ): Promise<ContractSignature | null> {
    return this.dataSource.getRepository(ContractSignature).findOne({
      where: { contractId, role },
      relations: ['signatureFile', 'stampFile'],
    });
  }

  async saveSignature(data: Partial<ContractSignature>): Promise<ContractSignature> {
    const repo = this.dataSource.getRepository(ContractSignature);
    const existing = await repo.findOne({
      where: { contractId: data.contractId!, role: data.role! },
    });
    if (existing) {
      Object.assign(existing, data);
      return repo.save(existing);
    }
    return repo.save(repo.create(data));
  }

  // ── Review notes ──────────────────────────────────────────────────────────────

  async createReviewNote(
    contractId: string,
    body: string,
    noteType: ContractReviewNoteType,
    authorUserId?: string,
    authorRole?: string,
  ): Promise<ContractReviewNote> {
    const repo = this.dataSource.getRepository(ContractReviewNote);
    return repo.save(
      repo.create({
        contractId,
        body,
        noteType,
        authorUserId: authorUserId ?? null,
        authorRole: authorRole ?? null,
      }),
    );
  }

  /** Review notes of one type, newest first — the duty advice/dispute rounds. */
  async findReviewNotes(
    contractId: string,
    noteType: ContractReviewNoteType,
  ): Promise<ContractReviewNote[]> {
    return this.dataSource.getRepository(ContractReviewNote).find({
      where: { contractId, noteType },
      order: { createdAt: 'DESC' },
    });
  }

  async findLatestReviewNote(
    contractId: string,
    noteType?: ContractReviewNoteType,
  ): Promise<ContractReviewNote | null> {
    const repo = this.dataSource.getRepository(ContractReviewNote);
    return repo.findOne({
      where: noteType ? { contractId, noteType } : { contractId },
      order: { createdAt: 'DESC' },
    });
  }

  // ── Pre-booking clearance document reviews ────────────────────────────────────

  findDocumentReviews(
    contractId: string,
    cycleId?: string | null,
  ): Promise<ContractDocumentReview[]> {
    return this.dataSource.getRepository(ContractDocumentReview).find({
      where:
        cycleId !== undefined
          ? { contractId, clearanceCycleId: cycleId === null ? IsNull() : cycleId }
          : { contractId },
      order: { createdAt: 'ASC' },
    });
  }

  /**
   * Upsert a document-review row to PENDING for a freshly uploaded file. Resets
   * any prior QUERIED/APPROVED state so the GL re-reviews the new upload. Keyed
   * on (contractId, clearanceCycleId, settingCode, fileKey).
   */
  async upsertDocumentReviewPending(input: {
    contractId: string;
    clearanceCycleId?: string | null;
    settingCode: string;
    fileKey: string;
    fileRecordId: string;
    uploadedByRole?: 'CUSTOMER' | 'GL_ET' | 'GL_DJ';
  }): Promise<void> {
    const repo = this.dataSource.getRepository(ContractDocumentReview);
    const cycleId = input.clearanceCycleId ?? null;
    const existing = await repo.findOne({
      where: {
        contractId: input.contractId,
        clearanceCycleId: cycleId === null ? IsNull() : cycleId,
        settingCode: input.settingCode,
        fileKey: input.fileKey,
      },
    });
    if (existing) {
      await repo.update(existing.id, {
        fileRecordId: input.fileRecordId,
        status: 'PENDING',
        note: null,
        reviewedByStaffId: null,
        reviewedAt: null,
      });
      return;
    }
    await repo.save(
      repo.create({
        contractId: input.contractId,
        clearanceCycleId: cycleId,
        settingCode: input.settingCode,
        fileKey: input.fileKey,
        fileRecordId: input.fileRecordId,
        status: 'PENDING',
        uploadedByRole: input.uploadedByRole ?? 'CUSTOMER',
      }),
    );
  }

  /** GL marks a document APPROVED or QUERIED (with an optional note). */
  async setDocumentReviewStatus(input: {
    contractId: string;
    clearanceCycleId?: string | null;
    settingCode: string;
    fileKey: string;
    status: ContractDocReviewStatus;
    staffId: string;
    note?: string;
  }): Promise<void> {
    const repo = this.dataSource.getRepository(ContractDocumentReview);
    const cycleId = input.clearanceCycleId ?? null;
    const existing = await repo.findOne({
      where: {
        contractId: input.contractId,
        clearanceCycleId: cycleId === null ? IsNull() : cycleId,
        settingCode: input.settingCode,
        fileKey: input.fileKey,
      },
    });
    const patch = {
      status: input.status,
      note: input.note ?? null,
      reviewedByStaffId: input.staffId,
      reviewedAt: new Date(),
    };
    if (existing) {
      await repo.update(existing.id, patch);
      return;
    }
    await repo.save(
      repo.create({
        contractId: input.contractId,
        clearanceCycleId: cycleId,
        settingCode: input.settingCode,
        fileKey: input.fileKey,
        ...patch,
      }),
    );
  }

  // ── Clearance cycles ──────────────────────────────────────────────────────────

  /** The current (latest, non-completed) clearance cycle for a contract. */
  async currentCycle(contractId: string): Promise<ContractClearanceCycle | null> {
    return this.dataSource.getRepository(ContractClearanceCycle).findOne({
      where: { contractId },
      order: { cycleNumber: 'DESC' },
    });
  }

  /** Open a new clearance cycle (incrementing cycle_number). */
  async openCycle(
    contractId: string,
    cycleNumber: number,
  ): Promise<ContractClearanceCycle> {
    const repo = this.dataSource.getRepository(ContractClearanceCycle);
    return repo.save(
      repo.create({
        contractId,
        cycleNumber,
        status: 'AWAITING_DOCUMENTS',
      }),
    );
  }

  async setCycleStatus(
    cycleId: string,
    status: string,
    fields: Partial<
      Pick<
        ContractClearanceCycle,
        | 'bookingId'
        | 'clearanceReadyAt'
        | 'completedAt'
        | 'dutyRequired'
        | 'vesselDepartureDate'
        | 'roAmendmentRequestedAt'
        | 'roHoldReason'
        | 'currentPhase'
      >
    > = {},
  ): Promise<void> {
    await this.dataSource
      .getRepository(ContractClearanceCycle)
      .update(cycleId, { status, ...fields } as never);
  }

  async updateCycle(
    cycleId: string,
    fields: Partial<
      Pick<
        ContractClearanceCycle,
        | 'dutyRequired'
        | 'vesselDepartureDate'
        | 'vesselArrivalDate'
        | 'doCollectedDate'
        | 'roAmendmentRequestedAt'
        | 'roHoldReason'
        | 'currentPhase'
        | 'status'
        | 'preClearanceFinalizedAt'
        | 'completedAt'
        | 'transitAssigneeRequestedAt'
        | 'transitAssigneeRequestedByUserId'
        | 'transitAssigneeRequestNote'
        | 'transitAssigneeName'
        | 'transitAssigneeAssignedAt'
        | 'transitAssigneeAssignedByUserId'
      >
    >,
  ): Promise<void> {
    await this.dataSource.getRepository(ContractClearanceCycle).update(cycleId, fields as never);
  }

  /** Link the GL-created booking to a clearance cycle. */
  async linkBooking(cycleId: string, bookingId: string): Promise<void> {
    await this.dataSource
      .getRepository(ContractClearanceCycle)
      .update(cycleId, { bookingId });
  }

  // ── Rate snapshots ──────────────────────────────────────────────────────────

  async createRateSnapshot(
    data: Partial<ContractRateSnapshot>,
  ): Promise<ContractRateSnapshot> {
    const repo = this.dataSource.getRepository(ContractRateSnapshot);
    return repo.save(repo.create(data));
  }

  async clearRateSnapshots(contractId: string): Promise<void> {
    await this.dataSource.getRepository(ContractRateSnapshot).delete({ contractId });
  }

  findRateSnapshots(contractId: string): Promise<ContractRateSnapshot[]> {
    return this.dataSource.getRepository(ContractRateSnapshot).find({
      where: { contractId },
      order: { createdAt: 'ASC' },
    });
  }
}
