import { Injectable, NotFoundException } from '@nestjs/common';
import { hazardClassLabel } from '@edr/types';

import { ContractsRepository } from '../modules/contracts/contracts.repository';
import {
  Contract,
  ContractDocumentSnapshot,
} from '../modules/contracts/entities/contract.entity';
import { ContractRoute } from '../modules/contracts/entities/contract-route.entity';
import {
  ContractSignature,
  ContractSignerRole,
} from '../modules/contracts/entities/contract-signature.entity';
import { ContractPricingBreakdown } from '../modules/contracts/contract-pricing.service';
import { ContractTemplatesService } from '../modules/contract-templates/contract-templates.service';
import { ContractTemplateResolver } from './contract-template.resolver';
import { getTemplateMeta } from './contract-template.registry';
import {
  ContractDynamicTemplateView,
  ContractViewModel,
} from './contract-view-model.builder';
import { RateSchedule } from './contract-rate-schedule.builder';

/**
 * Signature row for the contract PDF. Mirrors the booking builder's
 * `ContractSignatureView` but widens `role` to the contract's signer roles
 * (CUSTOMER | STAFF | DIRECTOR | CEO).
 */
export interface ContractDocumentSignatureView {
  role: ContractSignerRole;
  signerDisplayName: string;
  signedAt: string;
  signatureImageUrl?: string | null;
  /** Company stamp/seal; rendered next to the signature when present. */
  stampImageUrl?: string | null;
}

/** A single unit-rate row on the contract PDF — price per unit, NO total. */
export interface ContractUnitRateRow {
  label: string;
  unitPrice: number;
  unit: string;
  currency: string;
}

/**
 * Pricing schedule for a Contract document: a unit-rate schedule (one price per
 * unit, e.g. "X ETB / container") with NO quantities and NO grand total. Shaped
 * to stay structurally compatible with the renderer's expectations of
 * {@link ContractViewModel.pricing} (it reads `currency`).
 */
export interface ContractUnitRateSchedule {
  displayMode: 'UNIT_RATES';
  unitRates: ContractUnitRateRow[];
  currency: string;
  equipmentReturn?: string;
  originLabel: string;
  destinationLabel: string;
}

/** Map a stored contract unit to a human PDF suffix ("/ container", "/ ton", …). */
function unitLabel(unit: string): string {
  switch (unit) {
    case 'per_container':
      return 'container';
    case 'per_ton':
      return 'ton';
    case 'per_item':
      return 'item';
    case 'per_km':
      return 'km';
    default:
      return 'unit';
  }
}

/**
 * Builds the contract PDF view-model from the {@link Contract} aggregate (the new
 * source of truth) — mirrors {@link ContractViewModelBuilder} but every field is
 * sourced from the contract, its routes, cargo scope and unit-rate breakdown.
 * The legacy booking-based builder remains untouched for the migration window.
 */
@Injectable()
export class ContractDocumentViewModelBuilder {
  constructor(
    private readonly contractsRepository: ContractsRepository,
    private readonly templateResolver: ContractTemplateResolver,
    private readonly contractTemplates: ContractTemplatesService,
  ) {}

  async build(
    contractId: string,
  ): Promise<{ contract: Contract; view: ContractViewModel }> {
    const contract = await this.contractsRepository.findByIdWithRelations(contractId);
    if (!contract) {
      throw new NotFoundException(`Contract ${contractId} not found`);
    }

    const templateKey =
      contract.contractTemplateKey ?? this.templateResolver.resolve(this.toResolverInput(contract));
    let template = getTemplateMeta(templateKey);

    // The document articles come, in order of preference, from:
    //  1. this contract's frozen snapshot (staff accepted / edited it) — the
    //     shared six templates are never consulted for these contracts;
    //  2. the admin-editable DB template matching the direction/freight pair;
    //  3. the code-defined generic layout (handled below when none of the above).
    const snapshot = contract.documentSnapshot as ContractDocumentSnapshot | null;
    let dynamicTemplate: ContractDynamicTemplateView | undefined;
    if (snapshot && (snapshot.articles?.length ?? 0) > 0) {
      dynamicTemplate = {
        code: snapshot.code ?? 'CONTRACT',
        name: snapshot.name ?? template.title,
        documentTitle: snapshot.documentTitle ?? '',
        whereasClauses: snapshot.whereasClauses ?? [],
        articles: snapshot.articles,
      };
    } else {
      const dynamicSource = await this.contractTemplates.findActiveForContract(
        contract.tradeDirection,
        contract.freightType,
      );
      dynamicTemplate = dynamicSource
        ? {
            code: dynamicSource.code,
            name: dynamicSource.name,
            documentTitle: dynamicSource.documentTitle,
            whereasClauses: dynamicSource.whereasClauses ?? [],
            articles: dynamicSource.articles ?? [],
          }
        : undefined;
    }
    if (dynamicTemplate) {
      template = {
        ...template,
        title: dynamicTemplate.name,
        templateFile: 'edr-dynamic.hbs',
      };
    }

    const pricing = this.buildPricing(contract);
    const rateSchedule = this.buildRateSchedule(pricing);
    const signatures = await this.loadSignatures(contractId);

    const hasCustomer = signatures.some((s) => s.role === 'CUSTOMER');
    const hasStaff = signatures.some((s) => s.role === 'STAFF');
    // Signed before company stamps were required — the customer has to sign
    // again to attach one, otherwise EDR can never counter-sign the contract.
    const customerStampMissing = signatures.some(
      (s) => s.role === 'CUSTOMER' && !s.stampImageUrl,
    );
    const hasContractFile = Boolean(
      contract.files?.some((f) => f.code === 'contract'),
    );

    const view: ContractViewModel = {
      bookingId: contract.id,
      reference: contract.reference,
      status: contract.status,
      templateKey,
      template,
      contractDate: new Date().toLocaleDateString('en-GB', {
        day: 'numeric',
        month: 'long',
        year: 'numeric',
      }),
      contractYear: new Date().getFullYear(),
      client: {
        companyName: contract.company?.name ?? 'Client',
        companyAddress: this.valueOrDash(contract.company?.address),
        companyLocation: this.valueOrDash(contract.company?.country),
        phone: this.valueOrDash(contract.company?.phone),
        email: this.valueOrDash(contract.company?.email),
        tinNumber: this.valueOrDash(contract.company?.tin),
        vatNumber: this.valueOrDash(contract.company?.vatNumber),
        fanNumber: this.valueOrDash(contract.company?.fanNumber),
        businessLicense: this.valueOrDash(
          contract.company?.companyProfiles?.[0]?.businessLicense,
        ),
      },
      provider: {
        name: 'Ethio-Djibouti Standard Gauge Railway Share Company',
        address: 'Addis Ababa, Ethiopia',
        phone: '+251 11 872 0000',
        email: 'info@edr.gov.et',
        tinNumber: '—',
      },
      schedule: this.buildSchedule(contract),
      pricing: pricing as unknown as ContractViewModel['pricing'],
      rateSchedule,
      // Cast: contract signers (CUSTOMER|STAFF|DIRECTOR|CEO) widen the booking
      // view-model's narrower CUSTOMER|STAFF role union.
      signatures: signatures as unknown as ContractViewModel['signatures'],
      canSignCustomer:
        (contract.status === 'CONTRACT_READY' && !hasCustomer) ||
        (contract.status === 'SIGNED_CUSTOMER' && customerStampMissing),
      canSignStaff:
        contract.status === 'SIGNED_CUSTOMER' && hasCustomer && !hasStaff,
      hasContractDocument: hasContractFile,
      hasCustomerSignature: hasCustomer,
      hasStaffSignature: hasStaff,
      dynamicTemplate,
    };

    return { contract, view };
  }

  private async loadSignatures(
    contractId: string,
  ): Promise<ContractDocumentSignatureView[]> {
    const rows = await this.contractsRepository.findSignatures(contractId);
    return rows.map((s) => this.toSignatureView(s));
  }

  toSignatureView(row: ContractSignature): ContractDocumentSignatureView {
    return {
      role: row.role,
      signerDisplayName: row.signerDisplayName,
      signedAt: this.formatDate(row.signedAt),
      signatureImageUrl: row.signatureFile?.url ?? null,
      stampImageUrl: row.stampFile?.url ?? null,
    };
  }

  /** Unit-rate schedule from the contract's frozen pricing breakdown — NO totals. */
  private buildPricing(contract: Contract): ContractUnitRateSchedule {
    const breakdown = contract.pricingBreakdown as ContractPricingBreakdown | null;
    const currency = breakdown?.currency ?? contract.paymentCurrency;
    const lineItems = breakdown?.lineItems ?? [];
    const firstRoute = this.firstRoute(contract);

    return {
      displayMode: 'UNIT_RATES',
      unitRates: lineItems.map((line) => ({
        label: line.label,
        unitPrice: line.unitPrice,
        unit: unitLabel(line.unit),
        currency,
      })),
      currency,
      equipmentReturn: contract.equipmentReturn ?? '—',
      originLabel: this.yardLabel(firstRoute?.originYard),
      destinationLabel: this.yardLabel(firstRoute?.destinationYard),
    };
  }

  /**
   * A rate schedule for the contract PDF, sourced from the contract's own frozen
   * unit rates (its agreed lane prices) rather than the global rate config — a
   * signed contract must show the prices it was signed on. Rendered as freight
   * lanes labelled with the contract's primary origin → destination route.
   */
  private buildRateSchedule(pricing: ContractUnitRateSchedule): RateSchedule {
    const route = `${pricing.originLabel} → ${pricing.destinationLabel}`;
    const freightLanes = pricing.unitRates.map((line) => ({
      route,
      cargo: line.label,
      currency: line.currency,
      amount: this.formatAmount(line.unitPrice),
      unit: line.unit.startsWith('per ') ? line.unit : `per ${line.unit}`,
    }));

    return {
      freightLanes,
      additionalServices: [],
      surcharges: [],
      isEmpty: freightLanes.length === 0,
      currencyLabel: pricing.currency,
    };
  }

  private formatAmount(value: number | string): string {
    const num = Number(value);
    if (!Number.isFinite(num)) return String(value);
    return num.toLocaleString('en-US', {
      minimumFractionDigits: 0,
      maximumFractionDigits: 2,
    });
  }

  private buildSchedule(contract: Contract): ContractViewModel['schedule'] {
    const firstRoute = this.firstRoute(contract);
    const cargoScope = (contract.cargoScope ?? [])[0];
    const cargoName =
      cargoScope?.cargoType?.cargoTypeName ||
      cargoScope?.cargoFreeText ||
      (cargoScope?.containerSize
        ? `${cargoScope.containerSize} container`
        : 'Container cargo');

    return {
      originLabel: this.yardLabel(firstRoute?.originYard),
      destinationLabel: this.yardLabel(firstRoute?.destinationYard),
      tradeDirection: this.valueOrDash(contract.tradeDirection),
      freightType: this.valueOrDash(contract.freightType),
      serviceType: this.valueOrDash(
        contract.serviceType?.serviceName ?? contract.serviceType?.code,
      ),
      // Estimated shipment date was removed from the contract wizard; the
      // binding scheduled date is set per-booking, not on the contract.
      scheduledDate: this.formatDate(null),
      contractType: this.valueOrDash(contract.contractType),
      cargoDescription: this.valueOrDash(cargoName),
      totalWeightVgm: '—',
      equipmentReturn: this.valueOrDash(contract.equipmentReturn),
      // A hazardous contract names the declared class + UN number on the
      // schedule — the flag alone is not a dangerous-goods declaration.
      hazardousLabel: contract.isHazardous
        ? [
            hazardClassLabel(contract.hazardClass) ?? 'Yes',
            contract.unNumber ? `UN ${contract.unNumber}` : null,
          ]
            .filter(Boolean)
            .join(' · ')
        : 'No',
      firstMilePickupAddress: this.valueOrDash(contract.firstMilePickupAddress),
      lastMileDeliveryAddress: this.valueOrDash(contract.lastMileDeliveryAddress),
    };
  }

  /** The contract's primary route (lowest sortOrder), used for origin/destination labels. */
  private firstRoute(contract: Contract): ContractRoute | undefined {
    const routes = [...(contract.routes ?? [])].sort(
      (a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0),
    );
    return routes[0];
  }

  /**
   * The template resolver reads a Booking; a contract carries equivalent fields
   * under a different shape (cargoType lives on cargoScope). Build a minimal,
   * structurally-compatible adapter rather than widening the resolver signature.
   */
  private toResolverInput(
    contract: Contract,
  ): Parameters<ContractTemplateResolver['resolve']>[0] {
    const cargoType = (contract.cargoScope ?? []).find((c) => c.cargoType)?.cargoType;
    return {
      tradeDirection: contract.tradeDirection,
      freightType: contract.freightType,
      paymentCurrency: contract.paymentCurrency,
      cargoType: cargoType ?? undefined,
      serviceType: contract.serviceType,
    } as Parameters<ContractTemplateResolver['resolve']>[0];
  }

  private yardLabel(yard?: { label?: string; code?: string } | null): string {
    return this.valueOrDash(yard?.label ?? yard?.code);
  }

  private formatDate(value?: Date | string | null): string {
    if (!value) return '—';
    const date = value instanceof Date ? value : new Date(value);
    if (Number.isNaN(date.getTime())) return '—';
    return date.toLocaleDateString('en-GB', {
      day: 'numeric',
      month: 'long',
      year: 'numeric',
    });
  }

  private valueOrDash(value?: string | number | null): string {
    if (value === undefined || value === null || value === '') return '—';
    return String(value);
  }
}

// Re-export for callers that want the role union without importing the entity.
export type { ContractSignerRole };
