import {
  BadRequestException,
  forwardRef,
  Inject,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { Readable } from 'stream';

import { ContractPdfService } from '../../contracts/contract-pdf.service';
import { ContractRendererService } from '../../contracts/contract-renderer.service';
import { getTemplateMeta } from '../../contracts/contract-template.registry';
import { ContractTemplateResolver } from '../../contracts/contract-template.resolver';
import { ContractViewModelBuilder } from '../../contracts/contract-view-model.builder';
import { MinioService } from '../minio/minio.service';
import { FilesService } from '../files/files.service';
import { FileRecord } from '../files/entities/file.entity';
import { BookingsRepository } from './bookings.repository';
import { Booking } from './entities/booking.entity';
import { assertBookingStatus } from './booking-status.util';
import { clearanceSettingCode } from './clearance.util';
import { ContractViewDto } from './dto/contract-view.dto';
import { SignContractDto } from './dto/sign-contract.dto';
import { ContractSignerRole } from './entities/booking-contract-signature.entity';

/**
 * Default ordering window (months) for a general contract activated on
 * counter-sign. Mirrors GeneralContractService.DEFAULT_CONTRACT_PERIOD_MONTHS;
 * defined locally to avoid a circular module dependency on booking-orders.
 */
const DEFAULT_CONTRACT_PERIOD_MONTHS = 3;
import { BookingBatchService } from '../train-scheduling/booking-batch.service';
import { SignaturesService } from '../signatures/signatures.service';

@Injectable()
export class BookingContractService {
  private readonly logger = new Logger(BookingContractService.name);

  constructor(
    private readonly bookingsRepository: BookingsRepository,
    private readonly filesService: FilesService,
    private readonly minioService: MinioService,
    private readonly templateResolver: ContractTemplateResolver,
    private readonly viewModelBuilder: ContractViewModelBuilder,
    private readonly renderer: ContractRendererService,
    private readonly pdfService: ContractPdfService,
    @Inject(forwardRef(() => BookingBatchService))
    private readonly bookingBatchService: BookingBatchService,
    private readonly signaturesService: SignaturesService,
  ) {}

  buildContractSummary(booking: Booking): string {
    const direction =
      booking.tradeDirection === 'IMPORT'
        ? 'Import'
        : booking.tradeDirection === 'EXPORT'
          ? 'Export'
          : booking.tradeDirection;

    const cargo = booking.cargoType;
    const isBulk = booking.freightType === 'BULK';

    let cargoLabel: string;
    if (isBulk) {
      cargoLabel = `Bulk (${booking.cargoFreeText || cargo?.cargoTypeName || 'Commodity'})`;
    } else {
      const lines =
        booking.bookingContainers?.map((bc) => {
          const label = bc.containerType?.label ?? bc.containerType?.code ?? 'Container';
          return `${bc.quantity}× ${label}`;
        }) ?? [];
      cargoLabel =
        lines.length > 0
          ? `Container (${lines.join(', ')})`
          : 'Container (Standard)';
    }

    return `Operation: ${direction} | Cargo Type: ${cargoLabel}`;
  }

  async getSummary(bookingId: string): Promise<{ summary: string }> {
    const booking = await this.requireBooking(bookingId);
    const summary = booking.contractSummary ?? this.buildContractSummary(booking);
    return { summary };
  }

  async getContractView(
    bookingId: string,
    viewerUserId?: string,
  ): Promise<ContractViewDto> {
    const { view } = await this.viewModelBuilder.build(bookingId);
    await this.inlineSignatureImages(view.signatures);
    const html = this.renderer.render(view);
    const savedSignature = viewerUserId
      ? ((await this.signaturesService.getForUser(viewerUserId)) ?? undefined)
      : undefined;
    return {
      bookingId: view.bookingId,
      reference: view.reference,
      status: view.status,
      templateKey: view.templateKey,
      title: view.template.title,
      html,
      canSignCustomer: view.canSignCustomer,
      canSignStaff: view.canSignStaff,
      hasContractDocument: view.hasContractDocument,
      signatures: view.signatures,
      savedSignature,
      pricingSchedule: view.pricing as unknown as Record<string, unknown>,
    };
  }

  async generateContract(bookingId: string): Promise<Booking> {
    const booking = await this.requireBooking(bookingId);
    assertBookingStatus(booking, ['APPROVED']);

    const templateKey = this.templateResolver.resolve(booking);
    const summary = this.buildContractSummary(booking);

    // PDF rendering (Puppeteer/Chromium) is best-effort and must NOT block the contract
    // from becoming ready — the document is (re)rendered lazily on view/download.
    try {
      await this.upsertContractPdf(bookingId, booking.reference, templateKey);
    } catch (err) {
      this.logger.warn(
        `Contract PDF deferred for ${booking.reference}: ${err}. It will render on view/download once Chromium is available.`,
      );
    }

    const now = new Date();
    const updated = await this.bookingsRepository.update(bookingId, {
      status: 'CONTRACT_READY',
      contractSummary: summary,
      contractTemplateKey: templateKey,
      contractGeneratedAt: now,
    } as never);
    return updated!;
  }

  async streamContract(bookingId: string) {
    const booking = await this.requireBooking(bookingId);
    const templateKey =
      booking.contractTemplateKey ?? this.templateResolver.resolve(booking);
    const record = await this.upsertContractPdf(
      bookingId,
      booking.reference,
      templateKey,
    );
    return this.filesService.streamById(record.id);
  }

  async signContract(
    bookingId: string,
    dto: SignContractDto,
    options: { signerUserId?: string; ipAddress?: string },
  ): Promise<Booking> {
    const booking = await this.requireBooking(bookingId);
    const role = dto.role as ContractSignerRole;

    if (role === 'CUSTOMER') {
      assertBookingStatus(booking, ['CONTRACT_READY']);
      const existing = await this.bookingsRepository.findContractSignature(
        bookingId,
        'CUSTOMER',
      );
      if (existing) {
        throw new BadRequestException('Customer has already signed this contract');
      }
    } else {
      assertBookingStatus(booking, ['SIGNED_CUSTOMER']);
      const existing = await this.bookingsRepository.findContractSignature(
        bookingId,
        'STAFF',
      );
      if (existing) {
        throw new BadRequestException('Staff has already signed this contract');
      }
    }

    const buffer = this.decodeSignatureImage(dto.signatureImageBase64);
    const sigFile: Express.Multer.File = {
      fieldname: `signature_${role.toLowerCase()}`,
      originalname: `signature-${role.toLowerCase()}-${booking.reference}.png`,
      encoding: '7bit',
      mimetype: 'image/png',
      size: buffer.length,
      buffer,
      stream: Readable.from(buffer),
      destination: '',
      filename: '',
      path: '',
    };

    const fileRecord = await this.filesService.upsertByCode({
      resourceId: bookingId,
      resource: 'bookings',
      code: role === 'CUSTOMER' ? 'signature_customer' : 'signature_staff',
      file: sigFile,
    });

    const now = new Date();
    await this.bookingsRepository.saveContractSignature({
      bookingId,
      signerRole: role,
      signerUserId: options.signerUserId ?? null,
      signerDisplayName: dto.signerDisplayName,
      signedAt: now,
      signatureFileId: fileRecord.id,
      consentText: dto.consentText ?? null,
      ipAddress: options.ipAddress ?? null,
    });

    // Persist the just-used signature to the signer's reusable profile so they
    // don't have to redraw it on the next contract. Best-effort: a failure here
    // must never block contract execution.
    if (options.signerUserId) {
      try {
        await this.signaturesService.upsertForUser({
          userId: options.signerUserId,
          signerDisplayName: dto.signerDisplayName,
          signatureImageBase64: dto.signatureImageBase64,
        });
      } catch (err) {
        this.logger.warn(
          `Could not save reusable signature for user ${options.signerUserId}: ${err}`,
        );
      }
    }

    const updates: Record<string, unknown> = {};

    // Whether a document-clearance gate applies (IMPORT/EXPORT bookings). When it
    // does, the counter-signed booking goes to AWAITING_DOCUMENTS for the customer
    // to upload clearance documents instead of straight into the batch pipeline.
    const includesCustoms = booking.serviceType?.includesCustoms ?? false;
    const clearanceCode = clearanceSettingCode(
      booking.tradeDirection,
      booking.freightType,
      includesCustoms,
    );

    const isGeneralContract = booking.bookingType === 'GENERAL_CONTRACT';

    if (role === 'CUSTOMER') {
      updates.status = 'SIGNED_CUSTOMER';
      updates.customerSignedAt = now;
    } else if (isGeneralContract) {
      // A general contract is NOT paid up front — each drawdown order is priced
      // and paid on its own. So on counter-sign it becomes ACTIVE directly and
      // opens its ordering window; orders spawn their own priced child bookings.
      const expiresAt = new Date(now);
      expiresAt.setMonth(expiresAt.getMonth() + DEFAULT_CONTRACT_PERIOD_MONTHS);
      updates.fullyExecutedAt = now;
      updates.marketingApprovedAt = now;
      updates.marketingApprovedById = options.signerUserId ?? null;
      updates.lockedAt = now;
      updates.status = 'CONTRACT_ACTIVE';
      updates.expiresAt = expiresAt;
    } else {
      updates.fullyExecutedAt = now;
      updates.marketingApprovedAt = now;
      updates.marketingApprovedById = options.signerUserId ?? null;
      updates.lockedAt = now;
      updates.status = clearanceCode ? 'AWAITING_DOCUMENTS' : 'FULLY_EXECUTED';
    }

    const updated = await this.bookingsRepository.update(bookingId, updates as never);
    // Only the non-clearance (legacy/domestic) path enters the batch pipeline now;
    // clearance bookings enter operations after the GL document gate.
    if (role === 'STAFF' && !clearanceCode && updated?.trainScheduleId) {
      this.bookingBatchService.enqueueScheduleProcessing(updated.trainScheduleId);
    }
    try {
      await this.upsertContractPdf(
        bookingId,
        booking.reference,
        booking.contractTemplateKey ?? this.templateResolver.resolve(booking),
      );
    } catch (err) {
      this.logger.warn(
        `Signed-contract PDF deferred for ${booking.reference}: ${err}. It will render on view/download.`,
      );
    }
    return updated!;
  }

  async getSignatures(bookingId: string) {
    const rows = await this.bookingsRepository.findContractSignatures(bookingId);
    const views = rows.map((r) => this.viewModelBuilder.toSignatureView(r));
    await this.inlineSignatureImages(views);
    return { signatures: views };
  }

  private async upsertContractPdf(
    bookingId: string,
    reference: string,
    templateKey: string,
  ): Promise<FileRecord> {
    const { view } = await this.viewModelBuilder.build(bookingId);
    view.templateKey = templateKey;
    view.template = getTemplateMeta(templateKey);
    await this.inlineSignatureImages(view.signatures);

    const html = this.renderer.render(view);
    const pdfBuffer = await this.pdfService.htmlToPdfBuffer(html);
    const file: Express.Multer.File = {
      fieldname: 'contract',
      originalname: `contract-${reference}.pdf`,
      encoding: '7bit',
      mimetype: 'application/pdf',
      size: pdfBuffer.length,
      buffer: pdfBuffer,
      stream: Readable.from(pdfBuffer),
      destination: '',
      filename: '',
      path: '',
    };

    return this.filesService.upsertByCode({
      resourceId: bookingId,
      resource: 'bookings',
      code: 'contract',
      file,
    });
  }

  private async inlineSignatureImages(
    signatures: Array<{ signatureImageUrl?: string | null }>,
  ): Promise<void> {
    for (const sig of signatures) {
      if (!sig.signatureImageUrl) continue;
      try {
        if (sig.signatureImageUrl.startsWith('data:')) continue;
        const objectName = this.minioService.getObjectNameFromUrl(
          sig.signatureImageUrl,
        );
        const stream = await this.minioService.getFileStream(objectName);
        const buffer = await this.streamToBuffer(stream);
        sig.signatureImageUrl = `data:image/png;base64,${buffer.toString(
          'base64',
        )}`;
      } catch {
        /* keep original url */
      }
    }
  }

  private streamToBuffer(stream: Readable): Promise<Buffer> {
    return new Promise((resolve, reject) => {
      const chunks: Buffer[] = [];
      stream.on('data', (chunk: Buffer | string) => {
        chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
      });
      stream.on('error', reject);
      stream.on('end', () => resolve(Buffer.concat(chunks)));
    });
  }

  private decodeSignatureImage(base64: string): Buffer {
    const raw = base64.includes(',') ? base64.split(',')[1]! : base64;
    return Buffer.from(raw, 'base64');
  }

  private async requireBooking(id: string): Promise<Booking> {
    const booking = await this.bookingsRepository.findByIdWithFiles(id);
    if (!booking) throw new NotFoundException(`Booking ${id} not found`);
    return booking;
  }
}
