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

import { ContainerType } from '../rule-engine/entities/container-type.entity';
import { BookingApprovalStep } from './entities/booking-approval-step.entity';
import { BookingCargoModifier } from './entities/booking-cargo-modifier.entity';
import { BookingContainer } from './entities/booking-container.entity';
import { BookingRateSnapshot } from './entities/booking-rate-snapshot.entity';
import { BookingReviewNote, ReviewNoteType } from './entities/booking-review-note.entity';
import { Booking } from './entities/booking.entity';
import {
  BookingContractSignature,
  ContractSignerRole,
} from './entities/booking-contract-signature.entity';
import { FileRecord } from '../files/entities/file.entity';
import { ContainerWeightResult } from '../rule-engine/rule-engine.service';

export interface BookingListFilterOptions {
  statuses?: string[];
  status?: string;
  companyId?: string;
  contractType?: string;
  serviceTypeId?: string;
  cargoTypeId?: string;
  freightType?: string;
  tradeDirection?: string;
  paymentCurrency?: string;
  allowConsolidation?: boolean;
  consolidationPaired?: string;
}

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

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

  /** Count bookings created in a specific year. */
  async countByYear(year: number): Promise<number> {
    const startDate = new Date(year, 0, 1);
    const endDate = new Date(year + 1, 0, 1);

    return this.repository
      .createQueryBuilder('booking')
      .where('booking.created_at >= :startDate', { startDate })
      .andWhere('booking.created_at < :endDate', { endDate })
      .getCount();
  }

  /** Find a booking by reference with files and relations. */
  async findByReferenceWithFiles(reference: string): Promise<Booking | null> {
    return this.findByIdWithFiles(
      (
        await this.repository.findOne({ where: { reference }, select: ['id'] })
      )?.id ?? '',
    );
  }

  /** Find a booking by ID with files, containers, and config relations. */
  async findByIdWithFiles(id: string): Promise<Booking | null> {
    if (!id) return null;

    const booking = await this.repository
      .createQueryBuilder('booking')
      .leftJoinAndSelect('booking.bookingContainers', 'bc')
      .leftJoinAndSelect('bc.containerType', 'ct')
      .leftJoinAndSelect('booking.company', 'company')
      // .leftJoinAndSelect('booking.customer', 'customer')
      .leftJoinAndSelect('booking.train', 'train')
      .leftJoinAndSelect('booking.serviceType', 'st')
      .leftJoinAndSelect('booking.cargoType', 'cargo')
      .leftJoinAndSelect('booking.originYard', 'oy')
      .leftJoinAndSelect('booking.destinationYard', 'dy')
      .leftJoinAndSelect('booking.shippingLine', 'sl')
      .leftJoinAndSelect('booking.approvalSteps', 'steps')
      .leftJoinAndSelect('booking.rateSnapshots', 'snapshots')
      .leftJoinAndSelect('booking.cargoModifiers', 'modifiers')
      .leftJoinAndSelect('booking.reviewNotes', 'reviewNotes')
      .where('booking.id = :id', { id })
      .leftJoinAndMapMany(
        'booking.files',
        FileRecord,
        'file',
        "file.resource_id = booking.id AND file.resource = 'bookings'",
      )
      .getOne();

    return booking ?? null;
  }

  /** Persist booking container rows with weight rule results. */
  async createContainers(
    bookingId: string,
    containers: Array<{
      containerTypeId: string;
      quantity: number;
      vgmPerUnitTons: number;
      weightResult: ContainerWeightResult;
    }>,
  ): Promise<BookingContainer[]> {
    const containerRepo = this.dataSource.getRepository(BookingContainer);
    const typeRepo = this.dataSource.getRepository(ContainerType);
    const saved: BookingContainer[] = [];

    for (const item of containers) {
      const ct = await typeRepo.findOne({ where: { id: item.containerTypeId } });
      const wagonsPerUnit = ct ? Number(ct.wagonsPerUnit) : 1;
      const totalVgm = item.quantity * item.vgmPerUnitTons;
      const wagonsRequired = Math.ceil(item.quantity * wagonsPerUnit);

      const row = containerRepo.create({
        bookingId,
        containerTypeId: item.containerTypeId,
        quantity: item.quantity,
        vgmPerUnitTons: item.vgmPerUnitTons,
        totalVgmTons: totalVgm,
        wagonsRequired,
        weightLimitRuleId: item.weightResult.weightLimitRuleId,
        isOverweight: item.weightResult.isOverweight,
        overweightExcessTons: item.weightResult.overweightExcessTons,
      });
      saved.push(await containerRepo.save(row));
    }

    return saved;
  }

  /** SQL aggregate wagon count for a booking. */
  async calculateWagonCount(bookingId: string): Promise<number> {
    const result = await this.dataSource
      .createQueryBuilder()
      .select('CEILING(SUM(bc.quantity * ct.wagons_per_unit))', 'total')
      .from(BookingContainer, 'bc')
      .innerJoin(ContainerType, 'ct', 'ct.id = bc.container_type_id')
      .where('bc.booking_id = :bookingId', { bookingId })
      .getRawOne<{ total: string }>();

    return Number(result?.total ?? 0);
  }

  /**
   * Find another booking whose container quantity complements this one to fill whole wagon(s)
   * (same route, same container type, partial wagon on both sides).
   */
  async findComplementaryConsolidationPartner(
    booking: Booking,
    slot: {
      containerTypeId: string;
      quantity: number;
      containersPerWagon: number;
    },
  ): Promise<Booking | null> {
    const { containerTypeId, quantity, containersPerWagon: perWagon } = slot;

    return this.repository
      .createQueryBuilder('b')
      .innerJoinAndSelect('b.bookingContainers', 'bc')
      .innerJoin('bc.containerType', 'ct')
      .where('b.id != :bookingId', { bookingId: booking.id })
      .andWhere('b.allowConsolidation = true')
      .andWhere('b.consolidationPartnerId IS NULL')
      .andWhere('b.status IN (:...statuses)', {
        statuses: ['DRAFT', 'PENDING_CONSOLIDATION'],
      })
      .andWhere('b.originYardId = :originYardId', {
        originYardId: booking.originYardId,
      })
      .andWhere('b.destinationYardId = :destinationYardId', {
        destinationYardId: booking.destinationYardId,
      })
      .andWhere('b.tradeDirection = :tradeDirection', {
        tradeDirection: booking.tradeDirection,
      })
      .andWhere('bc.containerTypeId = :containerTypeId', { containerTypeId })
      .andWhere('(bc.quantity % :perWagon) > 0', { perWagon })
      .andWhere('((:quantity + bc.quantity) % :perWagon) = 0', {
        quantity,
        perWagon,
      })
      .orderBy('b.createdAt', 'ASC')
      .getOne();
  }

  /** Try each partial-wagon line until a complementary partner booking is found. */
  async findConsolidationPartner(
    booking: Booking,
    slots: Array<{
      containerTypeId: string;
      quantity: number;
      containersPerWagon: number;
    }>,
  ): Promise<Booking | null> {
    for (const slot of slots) {
      const partner = await this.findComplementaryConsolidationPartner(booking, slot);
      if (partner) return partner;
    }
    return null;
  }

  /** Pair two bookings for consolidation. */
  async pairConsolidation(bookingId: string, partnerId: string): Promise<void> {
    await this.repository.update(bookingId, {
      consolidationPartnerId: partnerId,
      status: 'CONSOLIDATED',
    } as never);
    await this.repository.update(partnerId, {
      consolidationPartnerId: bookingId,
      status: 'CONSOLIDATED',
    } as never);
  }

  /** Un-pair a consolidation. */
  async unpairConsolidation(bookingId: string, partnerId: string): Promise<void> {
    await this.repository.update(bookingId, {
      consolidationPartnerId: null,
      status: 'PENDING_CONSOLIDATION',
    } as never);
    await this.repository.update(partnerId, {
      consolidationPartnerId: null,
      status: 'PENDING_CONSOLIDATION',
    } as never);
  }

  /** Delete all containers for a booking (used on draft update). */
  async deleteContainers(bookingId: string): Promise<void> {
    await this.dataSource.getRepository(BookingContainer).delete({ bookingId });
  }

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

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

  /** Get pending approval step for a role (must match next in sequence). */
  async findPendingApprovalStep(
    bookingId: string,
    requiredRole: string,
  ): Promise<BookingApprovalStep | null> {
    const next = await this.findNextPendingApprovalStep(bookingId);
    if (!next || next.requiredRole !== requiredRole) return null;
    return next;
  }

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

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

  /** Persist cargo modifiers linked to rate snapshots. */
  async createCargoModifiers(
    rows: Array<{
      bookingId: string;
      surchargeTypeId: string;
      triggerValue: number | null;
      calculatedAmount: number;
      rateSnapshotId: string;
    }>,
  ): Promise<BookingCargoModifier[]> {
    const repo = this.dataSource.getRepository(BookingCargoModifier);
    const saved: BookingCargoModifier[] = [];
    for (const row of rows) {
      saved.push(await repo.save(repo.create(row)));
    }
    return saved;
  }

  /** Find rate snapshot by rate id for a booking. */
  async findRateSnapshotByRateId(
    bookingId: string,
    rateId: string,
  ): Promise<BookingRateSnapshot | null> {
    return this.dataSource.getRepository(BookingRateSnapshot).findOne({
      where: { bookingId, rateId },
    });
  }

  async createReviewNote(
    bookingId: string,
    note: string,
    type: ReviewNoteType,
    authorId?: string,
  ): Promise<BookingReviewNote> {
    const repo = this.dataSource.getRepository(BookingReviewNote);
    return repo.save(
      repo.create({ bookingId, note, type, authorId: authorId ?? null }),
    );
  }

  async findLatestReviewNote(
    bookingId: string,
    type?: ReviewNoteType,
  ): Promise<BookingReviewNote | null> {
    const repo = this.dataSource.getRepository(BookingReviewNote);
    return repo.findOne({
      where: type ? { bookingId, type } : { bookingId },
      order: { createdAt: 'DESC' },
    });
  }

  async clearPricingArtifacts(bookingId: string): Promise<void> {
    await this.dataSource.getRepository(BookingCargoModifier).delete({ bookingId });
    await this.dataSource.getRepository(BookingRateSnapshot).delete({ bookingId });
  }

  /** Queue listing with optional bulk exclusion for LINE_STAFF. */
  async findQueue(options: {
    status: string | string[];
    page?: number;
    pageSize?: number;
    excludeBulk?: boolean;
    sortBy?: string;
    sortOrder?: 'ASC' | 'DESC';
  }): Promise<{ items: Booking[]; total: number }> {
    const page = options.page ?? 1;
    const pageSize = options.pageSize ?? 20;
    const statuses = Array.isArray(options.status) ? options.status : [options.status];

    const qb = this.repository
      .createQueryBuilder('booking')
      .leftJoinAndSelect('booking.company', 'company')
      .leftJoinAndSelect('booking.originYard', 'originYard')
      .leftJoinAndSelect('booking.destinationYard', 'destinationYard')
      .leftJoinAndSelect('booking.cargoType', 'cargo')
      .leftJoinAndSelect('booking.serviceType', 'serviceType')
      .leftJoinAndSelect('booking.approvalSteps', 'approvalSteps')
      .where('booking.status IN (:...statuses)', { statuses });

    if (options.excludeBulk) {
      qb.andWhere("booking.freight_type = 'CONTAINER'");
    }

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

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

    return { items, total };
  }

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

    const qb = this.repository
      .createQueryBuilder('booking')
      .leftJoinAndSelect('booking.company', 'company')
      .leftJoinAndSelect('booking.originYard', 'originYard')
      .leftJoinAndSelect('booking.destinationYard', 'destinationYard')
      .leftJoinAndSelect('booking.serviceType', 'serviceType')
      .leftJoinAndSelect('booking.approvalSteps', 'approvalSteps')
      .where('booking.deleted_at IS NULL');

    this.applyListFilters(qb, options);

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

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

    return { items, total };
  }

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

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

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

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

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

    const urgent = await baseQb()
      .andWhere('booking.priority_score >= :urgentPriorityThreshold', {
        urgentPriorityThreshold: options.urgentPriorityThreshold,
      })
      .getCount();

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

    return { inQueue, onThisPage, needsAction, urgent };
  }

  private applyListFilters(
    qb: SelectQueryBuilder<Booking>,
    options: BookingListFilterOptions,
  ): void {
    if (options.statuses?.length) {
      qb.andWhere('booking.status IN (:...statuses)', {
        statuses: options.statuses,
      });
    } else if (options.status) {
      qb.andWhere('booking.status = :status', { status: options.status });
    }

    if (options.companyId) {
      qb.andWhere('booking.company_id = :companyId', {
        companyId: options.companyId,
      });
    }
    if (options.contractType) {
      qb.andWhere('booking.contract_type = :contractType', {
        contractType: options.contractType,
      });
    }
    if (options.serviceTypeId) {
      qb.andWhere('booking.service_type_id = :serviceTypeId', {
        serviceTypeId: options.serviceTypeId,
      });
    }
    if (options.cargoTypeId) {
      qb.andWhere('booking.cargo_type_id = :cargoTypeId', {
        cargoTypeId: options.cargoTypeId,
      });
    }
    if (options.freightType) {
      qb.andWhere('booking.freight_type = :freightType', {
        freightType: options.freightType,
      });
    }
    if (options.tradeDirection) {
      qb.andWhere('booking.trade_direction = :tradeDirection', {
        tradeDirection: options.tradeDirection,
      });
    }
    if (options.paymentCurrency) {
      qb.andWhere('booking.payment_currency = :paymentCurrency', {
        paymentCurrency: options.paymentCurrency,
      });
    }
    if (options.allowConsolidation !== undefined) {
      qb.andWhere('booking.allow_consolidation = :allowConsolidation', {
        allowConsolidation: options.allowConsolidation,
      });
    }
    if (options.consolidationPaired === 'true') {
      qb.andWhere('booking.consolidation_partner_id IS NOT NULL');
    } else if (options.consolidationPaired === 'false') {
      qb.andWhere('booking.consolidation_partner_id IS NULL');
    }
  }

  async findAndCountFiltered(where: FindOptionsWhere<Booking>, options: {
    skip: number;
    take: number;
    order: Record<string, 'ASC' | 'DESC'>;
  }): Promise<[Booking[], number]> {
    return this.repository.findAndCount({
      where,
      skip: options.skip,
      take: options.take,
      order: options.order,
    });
  }

  findContractSignatures(bookingId: string): Promise<BookingContractSignature[]> {
    return this.dataSource.getRepository(BookingContractSignature).find({
      where: { bookingId },
      relations: ['signatureFile'],
      order: { signedAt: 'ASC' },
    });
  }

  findContractSignature(
    bookingId: string,
    role: ContractSignerRole,
  ): Promise<BookingContractSignature | null> {
    return this.dataSource.getRepository(BookingContractSignature).findOne({
      where: { bookingId, signerRole: role },
      relations: ['signatureFile'],
    });
  }

  async saveContractSignature(
    data: Partial<BookingContractSignature>,
  ): Promise<BookingContractSignature> {
    const repo = this.dataSource.getRepository(BookingContractSignature);
    const existing = await repo.findOne({
      where: {
        bookingId: data.bookingId!,
        signerRole: data.signerRole!,
      },
    });
    if (existing) {
      Object.assign(existing, data);
      return repo.save(existing);
    }
    return repo.save(repo.create(data));
  }
}
