/**
 * Sales Invoice Application Service
 * Coordinates between domain logic and infrastructure
 * Uses dependency injection for all ports
 */

import {
  SalesInvoice,
  CreateSalesInvoiceRecord,
  SalesInvoicePaymentMethod,
} from '../domain/sales-invoice.model';
import {
  calculateSalesInvoiceTotals,
  validateSalesInvoice,
  calculatePaymentStatus,
  calculateAmountDue,
  canCancelInvoice,
  canEditInvoice,
} from '../domain/sales-invoice.domain';
import { SalesInvoiceRepository } from './sales-invoice-repository';
import { SalesAccountingPort } from './sales-accounting.port';
import { SalesSideEffectsPort } from './sales-side-effects.port';
import { SalesReferenceDataPort } from './sales-reference-data.port';
import { SalesIdGeneratorPort } from './sales-id-generator.port';

export type CreateSalesInvoiceInput = CreateSalesInvoiceRecord & {
  userId: string;
  userName: string;
  source?: string;
};

export type IssueSalesDebitNoteInput = {
  amount: number;
  reason: string;
  userId: string;
  userName: string;
};

export type UpdateSalesInvoiceInput = Partial<CreateSalesInvoiceRecord> & {
  userId: string;
  userName: string;
};

function roundTwo(value: number): number {
  return Math.round((Number(value || 0) + Number.EPSILON) * 100) / 100;
}

function resolveCoverageSplit(grandTotal: number, secondaryCustomerId?: string, secondaryCoveragePercentInput?: number) {
  const normalizedSecondaryCustomerId = String(secondaryCustomerId || '').trim();
  const secondaryCoveragePercentRaw = Number(secondaryCoveragePercentInput || 0);
  const secondaryCoveragePercent = normalizedSecondaryCustomerId
    ? Math.max(0, Math.min(100, secondaryCoveragePercentRaw))
    : 0;
  const primaryCoveragePercent = Math.max(0, roundTwo(100 - secondaryCoveragePercent));
  const secondaryCoverageAmount = roundTwo((Number(grandTotal || 0) * secondaryCoveragePercent) / 100);
  const primaryCoverageAmount = roundTwo(Number(grandTotal || 0) - secondaryCoverageAmount);

  return {
    secondaryCustomerId: normalizedSecondaryCustomerId || undefined,
    primaryCoveragePercent,
    secondaryCoveragePercent,
    primaryCoverageAmount,
    secondaryCoverageAmount,
  };
}

export class SalesInvoiceApplicationService {
  constructor(
    private repository: SalesInvoiceRepository,
    private accounting: SalesAccountingPort,
    private sideEffects: SalesSideEffectsPort,
    private referenceData: SalesReferenceDataPort,
    private idGenerator: SalesIdGeneratorPort
  ) {}

  /**
   * Static factory for default dependency injection
   */
  static createDefault(
    repository: SalesInvoiceRepository,
    accounting: SalesAccountingPort,
    sideEffects: SalesSideEffectsPort,
    referenceData: SalesReferenceDataPort,
    idGenerator: SalesIdGeneratorPort
  ): SalesInvoiceApplicationService {
    return new SalesInvoiceApplicationService(
      repository,
      accounting,
      sideEffects,
      referenceData,
      idGenerator
    );
  }

  private async hydrateItemsFromLinkedShipments(input: CreateSalesInvoiceInput): Promise<CreateSalesInvoiceInput> {
    const normalizedDocumentType = String(input.documentType || 'tax_invoice').trim();
    const linkedShipmentIds = Array.isArray(input.linkedShipmentIds)
      ? input.linkedShipmentIds.map((id) => String(id || '').trim()).filter(Boolean)
      : [];
    const hasManualItems = Array.isArray(input.items) && input.items.some((item) => Number(item?.quantity || 0) > 0);

    if (normalizedDocumentType === 'shipment' || hasManualItems || linkedShipmentIds.length === 0) {
      return input;
    }

    const linkedShipments = await Promise.all(linkedShipmentIds.map((id) => this.repository.getById(id)));
    const validShipments = linkedShipments.filter((entry): entry is SalesInvoice => Boolean(entry));
    if (validShipments.length === 0) {
      return input;
    }

    const shipmentItems = validShipments
      .filter((shipment) => String(shipment.customerId || '').trim() === String(input.customerId || '').trim())
      .flatMap((shipment) => (shipment.items || []).map((item) => {
        const qty = Number(item.quantity || 0);
        const price = Number(item.unitPrice || 0);
        const disc = Number(item.lineDiscountAmount || 0);
        return {
          materialId: item.materialId,
          name: item.name || item.materialId,
          quantity: qty,
          unitPrice: price,
          total: Number(item.total || 0) || (qty * price - disc),
          realEstateProjectId: item.realEstateProjectId,
          realEstateProjectName: item.realEstateProjectName,
          realEstateUnitId: item.realEstateUnitId,
          realEstateUnitName: item.realEstateUnitName,
          lineDiscountPercent: item.lineDiscountPercent,
          lineDiscountAmount: disc,
          taxRate: item.taxRate,
          warehouseId: item.warehouseId,
          shelfId: item.shelfId,
        };
      }))
      .filter((item) => Number(item.quantity || 0) > 0);

    if (shipmentItems.length === 0) {
      return input;
    }

    return {
      ...input,
      items: shipmentItems,
    };
  }

  /**
   * Create a new sales invoice
   */
  async create(input: CreateSalesInvoiceInput): Promise<SalesInvoice> {
    const normalizedInput = await this.hydrateItemsFromLinkedShipments(input);

    // Validate input
    const validation = validateSalesInvoice(normalizedInput);
    if (!validation.valid) {
      throw new Error(
        `Validation failed: ${validation.errors.join('; ')}`
      );
    }

    // Get customer details
    const customer = await this.referenceData.getCustomer(normalizedInput.customerId);
    if (!customer) {
      throw new Error(`Customer not found: ${normalizedInput.customerId}`);
    }

    const normalizedSecondaryCustomerId = String(normalizedInput.secondaryCustomerId || '').trim();
    if (normalizedSecondaryCustomerId && normalizedSecondaryCustomerId === String(normalizedInput.customerId || '').trim()) {
      throw new Error('Secondary customer must be different from primary customer');
    }

    let secondaryCustomerName: string | undefined;
    if (normalizedSecondaryCustomerId) {
      const secondaryCustomer = await this.referenceData.getCustomer(normalizedSecondaryCustomerId);
      if (!secondaryCustomer) {
        throw new Error(`Secondary customer not found: ${normalizedSecondaryCustomerId}`);
      }
      secondaryCustomerName = secondaryCustomer.name;
    }

    // Generate IDs
    const invoiceId = await this.idGenerator.generateInvoiceId();
    const invoiceNumber = normalizedInput.documentType === 'shipment'
      ? await this.idGenerator.generateShipmentNumber()
      : normalizedInput.source === 'store'
        ? await this.idGenerator.generateStoreInvoiceNumber()
        : await this.idGenerator.generateInvoiceNumber();

    // Calculate totals
    const totals = calculateSalesInvoiceTotals({
      items: normalizedInput.items,
      taxMethod: normalizedInput.taxMethod || 'tax-exempt',
      invoiceDiscountPercent: normalizedInput.invoiceDiscountPercent,
      invoiceDiscountAmount: normalizedInput.invoiceDiscountAmount,
    });

    // Calculate derived values
    const amountDue = calculateAmountDue(
      totals.grandTotal,
      normalizedInput.amountReceived
    );
    const coverageSplit = resolveCoverageSplit(
      totals.grandTotal,
      normalizedSecondaryCustomerId,
      normalizedInput.secondaryCoveragePercent
    );
    const paymentStatus = calculatePaymentStatus(
      totals.grandTotal,
      normalizedInput.amountReceived
    );

    const requestedPostingStatus = normalizedInput.postingStatus || 'saved';
    const requestedShippingStatus = input.shippingStatus || (requestedPostingStatus === 'posted' ? 'shipped' : 'pending');

    // Create invoice
    const invoice = await this.repository.create({
      id: invoiceId,
      invoiceNumber,
      customerId: normalizedInput.customerId,
      customerName: customer.name,
      secondaryCustomerId: coverageSplit.secondaryCustomerId,
      secondaryCustomerName,
      primaryCoveragePercent: coverageSplit.primaryCoveragePercent,
      secondaryCoveragePercent: coverageSplit.secondaryCoveragePercent,
      primaryCoverageAmount: coverageSplit.primaryCoverageAmount,
      secondaryCoverageAmount: coverageSplit.secondaryCoverageAmount,
      items: normalizedInput.items,
      date: normalizedInput.date,
      manualDocNumber: normalizedInput.manualDocNumber,
      salesRepId: normalizedInput.salesRepId,
      currencyCode: normalizedInput.currencyCode,
      exchangeRate: normalizedInput.exchangeRate,
      rateSource: normalizedInput.rateSource,
      invoiceDiscountPercent: normalizedInput.invoiceDiscountPercent,
      invoiceDiscountAmount:
        normalizedInput.invoiceDiscountAmount ||
        (normalizedInput.invoiceDiscountPercent
          ? totals.subTotal * (normalizedInput.invoiceDiscountPercent / 100)
          : 0),
      amountReceived: normalizedInput.amountReceived,
      paymentMethods: normalizedInput.paymentMethods,
      realEstateProjectId: normalizedInput.realEstateProjectId,
      realEstateProjectName: normalizedInput.realEstateProjectName,
      realEstateUnitId: normalizedInput.realEstateUnitId,
      realEstateUnitName: normalizedInput.realEstateUnitName,
      realEstateContractNumber: normalizedInput.realEstateContractNumber,
      realEstateContractDate: normalizedInput.realEstateContractDate,
      realEstateHandoverDate: normalizedInput.realEstateHandoverDate,
      realEstateContractValue: normalizedInput.realEstateContractValue,
      realEstateDownPayment: normalizedInput.realEstateDownPayment,
      realEstateDeferredAmount: normalizedInput.realEstateDeferredAmount,
      realEstateInstallmentMode: normalizedInput.realEstateInstallmentMode,
      realEstateInstallmentCount: normalizedInput.realEstateInstallmentCount,
      realEstateFirstDueDate: normalizedInput.realEstateFirstDueDate,
      realEstateInstallmentInterval: normalizedInput.realEstateInstallmentInterval,
      customerNotes: normalizedInput.customerNotes,
      internalNotes: normalizedInput.internalNotes,
      taxMethod: normalizedInput.taxMethod,
      taxExemptionReason: normalizedInput.taxExemptionReason,
      taxExemptionReference: normalizedInput.taxExemptionReference,
      subTotal: totals.subTotal,
      lineDiscountTotal: totals.lineDiscountTotal,
      totalDiscount:
        totals.lineDiscountTotal +
        (input.invoiceDiscountAmount ||
          (input.invoiceDiscountPercent
            ? totals.subTotal * (input.invoiceDiscountPercent / 100)
            : 0)),
      taxAmount: totals.taxAmount,
      grandTotal: totals.grandTotal,
      amountDue,
      paymentStatus,
      postingStatus: requestedPostingStatus,
      status: 'active',
      shippingStatus: requestedShippingStatus,
      documentType: normalizedInput.documentType,
      linkedShipmentIds: Array.isArray(normalizedInput.linkedShipmentIds) && normalizedInput.linkedShipmentIds.length > 0
        ? normalizedInput.linkedShipmentIds
        : undefined,
      createdAt: new Date().toISOString(),
    });

    // If this tax invoice links to shipments, mark each shipment with this invoice's ID
    if (
      normalizedInput.documentType !== 'shipment' &&
      Array.isArray(normalizedInput.linkedShipmentIds) &&
      normalizedInput.linkedShipmentIds.length > 0
    ) {
      for (const shipmentId of normalizedInput.linkedShipmentIds) {
        try {
          await this.repository.update(shipmentId, { taxInvoiceId: invoice.id });
        } catch (linkErr) {
          console.error(`[Sales] Failed to link shipment ${shipmentId} to tax invoice ${invoice.id}:`, linkErr);
        }
      }
    }

    if (requestedPostingStatus === 'posted' && input.documentType !== 'shipment') {
      try {
        await this.accounting.createInvoiceEntry({
          invoiceId: invoice.id,
          invoiceNumber: invoice.invoiceNumber,
          date: invoice.date,
          amount: invoice.grandTotal,
          baseCurrencyAmount: invoice.grandTotalBase,
          taxAmount: invoice.taxAmount,
          discountAmount: invoice.totalDiscount,
          exchangeRate: invoice.exchangeRate,
          description: `Sales invoice ${invoice.invoiceNumber}`,
          customerId: invoice.customerId,
          customerName: invoice.customerName,
          secondaryCustomerId: invoice.secondaryCustomerId,
          secondaryCustomerName: invoice.secondaryCustomerName,
          primaryCoveragePercent: invoice.primaryCoveragePercent,
          secondaryCoveragePercent: invoice.secondaryCoveragePercent,
          primaryCoverageAmount: invoice.primaryCoverageAmount,
          secondaryCoverageAmount: invoice.secondaryCoverageAmount,
          paymentMethods: invoice.paymentMethods,
          status: 'posted',
          createdBy: input.userName,
        });
      } catch (accountingError) {
        console.error('[Sales] Accounting journal creation failed (non-blocking):', accountingError);
      }
    }

    // Trigger side effects
    try {
      await this.sideEffects.onInvoiceCreated({
        invoiceId: invoice.id,
        invoiceNumber: invoice.invoiceNumber,
        customerName: customer.name,
        customerEmail: customer.email,
        amount: invoice.grandTotal,
      });
    } catch (error) {
      console.error('Side effect failed (non-blocking):', error);
    }

    // Add activity log
    try {
      await this.repository.addActivityLog(
        invoice.id,
        'created',
        `Invoice created by ${input.userName}`,
        input.userId,
        input.userName
      );

      if (requestedPostingStatus === 'posted') {
        await this.repository.addActivityLog(
          invoice.id,
          'posted',
          `Invoice posted by ${input.userName}`,
          input.userId,
          input.userName
        );
      }
    } catch (error) {
      console.error('Activity log failed (non-blocking):', error);
    }

    return invoice;
  }

  /**
   * Get invoice by ID
   */
  async getById(id: string): Promise<SalesInvoice | null> {
    return this.repository.getById(id);
  }

  /**
   * Update an existing invoice (recalculates totals)
   */
  async updateInvoice(id: string, input: UpdateSalesInvoiceInput): Promise<SalesInvoice> {
    const existing = await this.repository.getById(id);
    if (!existing) {
      throw new Error(`Invoice not found: ${id}`);
    }

    const editCheck = canEditInvoice(existing);
    if (!editCheck.canEdit) {
      throw new Error(editCheck.reason || 'Cannot edit this invoice');
    }

    const rawItems = input.items || existing.items;
    // Map to full SalesInvoiceItem shape (name + total required by domain model)
    const items: SalesInvoice['items'] = rawItems.map((item) => ({
      materialId: item.materialId,
      name: (item as SalesInvoice['items'][number]).name ?? item.materialId,
      realEstateProjectId: (item as SalesInvoice['items'][number]).realEstateProjectId,
      realEstateProjectName: (item as SalesInvoice['items'][number]).realEstateProjectName,
      realEstateUnitId: (item as SalesInvoice['items'][number]).realEstateUnitId,
      realEstateUnitName: (item as SalesInvoice['items'][number]).realEstateUnitName,
      quantity: item.quantity,
      unitPrice: item.unitPrice,
      total: (item as SalesInvoice['items'][number]).total
        ?? (item.quantity * item.unitPrice),
      lineDiscountPercent: item.lineDiscountPercent,
      lineDiscountAmount: item.lineDiscountAmount,
      taxRate: item.taxRate,
      warehouseId: item.warehouseId,
      shelfId: item.shelfId,
    }));
    const taxMethod = input.taxMethod || existing.taxMethod || 'tax-exempt';

    const totals = calculateSalesInvoiceTotals({
      items,
      taxMethod,
      invoiceDiscountPercent: input.invoiceDiscountPercent,
      invoiceDiscountAmount: input.invoiceDiscountAmount,
    });

    const invoiceDiscountAmt =
      input.invoiceDiscountAmount ||
      (input.invoiceDiscountPercent
        ? totals.subTotal * (input.invoiceDiscountPercent / 100)
        : existing.invoiceDiscountAmount || 0);

    const amountReceived = input.amountReceived ?? existing.amountReceived;
    const amountDue = calculateAmountDue(totals.grandTotal, amountReceived);
    const resolvedPrimaryCustomerId = String(input.customerId || existing.customerId || '').trim();
    const resolvedSecondaryCustomerId = String(
      input.secondaryCustomerId !== undefined ? input.secondaryCustomerId : existing.secondaryCustomerId
    ).trim();
    if (resolvedSecondaryCustomerId && resolvedSecondaryCustomerId === resolvedPrimaryCustomerId) {
      throw new Error('Secondary customer must be different from primary customer');
    }

    let resolvedSecondaryCustomerName = existing.secondaryCustomerName;
    if (resolvedSecondaryCustomerId) {
      const secondaryCustomer = await this.referenceData.getCustomer(resolvedSecondaryCustomerId);
      if (!secondaryCustomer) {
        throw new Error(`Secondary customer not found: ${resolvedSecondaryCustomerId}`);
      }
      resolvedSecondaryCustomerName = secondaryCustomer.name;
    } else {
      resolvedSecondaryCustomerName = undefined;
    }

    const coverageSplit = resolveCoverageSplit(
      totals.grandTotal,
      resolvedSecondaryCustomerId,
      input.secondaryCoveragePercent !== undefined ? input.secondaryCoveragePercent : existing.secondaryCoveragePercent
    );
    const paymentStatus = calculatePaymentStatus(totals.grandTotal, amountReceived);
    const resolvedPostingStatus = input.postingStatus ?? existing.postingStatus ?? 'saved';
    const resolvedShippingStatus = input.shippingStatus ?? existing.shippingStatus ?? (resolvedPostingStatus === 'posted' ? 'shipped' : 'pending');

    const updatePatch = {
      customerId: input.customerId || existing.customerId,
      secondaryCustomerId: coverageSplit.secondaryCustomerId,
      secondaryCustomerName: resolvedSecondaryCustomerName,
      primaryCoveragePercent: coverageSplit.primaryCoveragePercent,
      secondaryCoveragePercent: coverageSplit.secondaryCoveragePercent,
      primaryCoverageAmount: coverageSplit.primaryCoverageAmount,
      secondaryCoverageAmount: coverageSplit.secondaryCoverageAmount,
      date: input.date || existing.date,
      manualDocNumber: input.manualDocNumber !== undefined ? input.manualDocNumber : existing.manualDocNumber,
      items,
      salesRepId: input.salesRepId !== undefined ? input.salesRepId : existing.salesRepId,
      currencyCode: input.currencyCode || existing.currencyCode,
      exchangeRate: input.exchangeRate ?? existing.exchangeRate,
      rateSource: input.rateSource || existing.rateSource,
      invoiceDiscountPercent: input.invoiceDiscountPercent ?? existing.invoiceDiscountPercent,
      invoiceDiscountAmount: invoiceDiscountAmt,
      amountReceived,
      paymentMethods: input.paymentMethods || existing.paymentMethods,
      realEstateProjectId: input.realEstateProjectId !== undefined ? input.realEstateProjectId : existing.realEstateProjectId,
      realEstateProjectName: input.realEstateProjectName !== undefined ? input.realEstateProjectName : existing.realEstateProjectName,
      realEstateUnitId: input.realEstateUnitId !== undefined ? input.realEstateUnitId : existing.realEstateUnitId,
      realEstateUnitName: input.realEstateUnitName !== undefined ? input.realEstateUnitName : existing.realEstateUnitName,
      realEstateContractNumber: input.realEstateContractNumber !== undefined ? input.realEstateContractNumber : existing.realEstateContractNumber,
      realEstateContractDate: input.realEstateContractDate !== undefined ? input.realEstateContractDate : existing.realEstateContractDate,
      realEstateHandoverDate: input.realEstateHandoverDate !== undefined ? input.realEstateHandoverDate : existing.realEstateHandoverDate,
      realEstateContractValue: input.realEstateContractValue !== undefined ? input.realEstateContractValue : existing.realEstateContractValue,
      realEstateDownPayment: input.realEstateDownPayment !== undefined ? input.realEstateDownPayment : existing.realEstateDownPayment,
      realEstateDeferredAmount: input.realEstateDeferredAmount !== undefined ? input.realEstateDeferredAmount : existing.realEstateDeferredAmount,
      realEstateInstallmentMode: input.realEstateInstallmentMode !== undefined ? input.realEstateInstallmentMode : existing.realEstateInstallmentMode,
      realEstateInstallmentCount: input.realEstateInstallmentCount !== undefined ? input.realEstateInstallmentCount : existing.realEstateInstallmentCount,
      realEstateFirstDueDate: input.realEstateFirstDueDate !== undefined ? input.realEstateFirstDueDate : existing.realEstateFirstDueDate,
      realEstateInstallmentInterval: input.realEstateInstallmentInterval !== undefined ? input.realEstateInstallmentInterval : existing.realEstateInstallmentInterval,
      customerNotes: input.customerNotes !== undefined ? input.customerNotes : existing.customerNotes,
      internalNotes: input.internalNotes !== undefined ? input.internalNotes : existing.internalNotes,
      taxMethod,
      taxExemptionReason: input.taxExemptionReason !== undefined ? input.taxExemptionReason : existing.taxExemptionReason,
      taxExemptionReference: input.taxExemptionReference !== undefined ? input.taxExemptionReference : existing.taxExemptionReference,
      subTotal: totals.subTotal,
      lineDiscountTotal: totals.lineDiscountTotal,
      totalDiscount: totals.lineDiscountTotal + invoiceDiscountAmt,
      taxAmount: totals.taxAmount,
      grandTotal: totals.grandTotal,
      amountDue,
      paymentStatus,
      postingStatus: resolvedPostingStatus,
      shippingStatus: resolvedShippingStatus,
    };

    const updated = await this.repository.update(id, updatePatch);

    const trackedKeys: Array<{ key: keyof typeof updatePatch; label: string }> = [
      { key: 'customerId', label: 'العميل' },
      { key: 'secondaryCustomerId', label: 'العميل الثانوي' },
      { key: 'primaryCoveragePercent', label: 'نسبة العميل الرئيسي' },
      { key: 'secondaryCoveragePercent', label: 'نسبة العميل الثانوي' },
      { key: 'date', label: 'التاريخ' },
      { key: 'manualDocNumber', label: 'الرقم المرجعي' },
      { key: 'items', label: 'الأصناف' },
      { key: 'salesRepId', label: 'مندوب المبيعات' },
      { key: 'currencyCode', label: 'العملة' },
      { key: 'exchangeRate', label: 'سعر الصرف' },
      { key: 'invoiceDiscountPercent', label: 'خصم %' },
      { key: 'invoiceDiscountAmount', label: 'مبلغ الخصم' },
      { key: 'amountReceived', label: 'المبلغ المقبوض' },
      { key: 'paymentMethods', label: 'طرق الدفع' },
      { key: 'taxMethod', label: 'طريقة الضريبة' },
      { key: 'postingStatus', label: 'حالة الحفظ' },
      { key: 'shippingStatus', label: 'حالة الشحن' },
      { key: 'customerNotes', label: 'ملاحظات العميل' },
      { key: 'internalNotes', label: 'ملاحظات داخلية' },
    ];

    const summarizeItems = (value: unknown) => {
      if (!Array.isArray(value) || value.length === 0) return 'لا توجد أصناف';
      return value
        .slice(0, 3)
        .map((item: any, index: number) => {
          const qty = Number(item?.quantity || 0);
          const price = Number(item?.unitPrice || 0);
          const total = Number(item?.total ?? qty * price);
          const name = String(item?.name || item?.materialId || `#${index + 1}`);
          return `${index + 1}) ${name} | ك:${qty} | س:${price.toFixed(2)} | إ:${total.toFixed(2)}`;
        })
        .join(' ; ');
    };

    const summarizePaymentMethods = (value: unknown) => {
      if (!Array.isArray(value) || value.length === 0) return 'لا توجد دفعات';
      return value
        .slice(0, 4)
        .map((method: any, index: number) => {
          const label = String(method?.method || `method-${index + 1}`);
          const amount = Number(method?.amount || 0).toFixed(2);
          return `${label}:${amount}`;
        })
        .join(' ; ');
    };

    const summarize = (key: string, value: unknown) => {
      if (key === 'items') {
        return summarizeItems(value);
      }
      if (key === 'paymentMethods') {
        return summarizePaymentMethods(value);
      }
      if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || value == null) {
        return value;
      }
      return JSON.stringify(value);
    };

    const changedFields = trackedKeys
      .map(({ key, label }) => {
        const before = (existing as any)[key];
        const after = (updated as any)[key];
        if (JSON.stringify(before) === JSON.stringify(after)) return null;
        return {
          key: String(key),
          label,
          before: summarize(String(key), before),
          after: summarize(String(key), after),
        };
      })
      .filter(Boolean);

    const undoPatch: Partial<CreateSalesInvoiceRecord> = {
      customerId: existing.customerId,
      date: existing.date,
      manualDocNumber: existing.manualDocNumber,
      items: existing.items.map((item) => ({
        materialId: item.materialId,
        quantity: item.quantity,
        unitPrice: item.unitPrice,
        realEstateProjectId: item.realEstateProjectId,
        realEstateProjectName: item.realEstateProjectName,
        realEstateUnitId: item.realEstateUnitId,
        realEstateUnitName: item.realEstateUnitName,
        lineDiscountPercent: item.lineDiscountPercent,
        lineDiscountAmount: item.lineDiscountAmount,
        taxRate: item.taxRate,
        warehouseId: item.warehouseId,
        shelfId: item.shelfId,
      })),
      salesRepId: existing.salesRepId,
      currencyCode: existing.currencyCode,
      exchangeRate: existing.exchangeRate,
      rateSource: existing.rateSource,
      invoiceDiscountPercent: existing.invoiceDiscountPercent,
      invoiceDiscountAmount: existing.invoiceDiscountAmount,
      amountReceived: existing.amountReceived,
      paymentMethods: existing.paymentMethods,
      realEstateProjectId: existing.realEstateProjectId,
      realEstateProjectName: existing.realEstateProjectName,
      realEstateUnitId: existing.realEstateUnitId,
      realEstateUnitName: existing.realEstateUnitName,
      realEstateContractNumber: existing.realEstateContractNumber,
      realEstateContractDate: existing.realEstateContractDate,
      realEstateHandoverDate: existing.realEstateHandoverDate,
      realEstateContractValue: existing.realEstateContractValue,
      realEstateDownPayment: existing.realEstateDownPayment,
      realEstateDeferredAmount: existing.realEstateDeferredAmount,
      realEstateInstallmentMode: existing.realEstateInstallmentMode,
      realEstateInstallmentCount: existing.realEstateInstallmentCount,
      realEstateFirstDueDate: existing.realEstateFirstDueDate,
      realEstateInstallmentInterval: existing.realEstateInstallmentInterval,
      customerNotes: existing.customerNotes,
      internalNotes: existing.internalNotes,
      taxMethod: existing.taxMethod,
      taxExemptionReason: existing.taxExemptionReason,
      taxExemptionReference: existing.taxExemptionReference,
      postingStatus: existing.postingStatus,
      shippingStatus: existing.shippingStatus,
      documentType: existing.documentType,
      linkedShipmentIds: (existing as any).linkedShipmentIds,
    };

    if (resolvedPostingStatus === 'posted') {
      try {
        await this.accounting.createInvoiceEntry({
          invoiceId: updated.id,
          invoiceNumber: updated.invoiceNumber,
          date: updated.date,
          amount: updated.grandTotal,
          baseCurrencyAmount: updated.grandTotalBase,
          taxAmount: updated.taxAmount,
          discountAmount: updated.totalDiscount,
          exchangeRate: updated.exchangeRate,
          description: `Sales invoice ${updated.invoiceNumber}`,
          customerId: updated.customerId,
          customerName: updated.customerName,
          secondaryCustomerId: updated.secondaryCustomerId,
          secondaryCustomerName: updated.secondaryCustomerName,
          primaryCoveragePercent: updated.primaryCoveragePercent,
          secondaryCoveragePercent: updated.secondaryCoveragePercent,
          primaryCoverageAmount: updated.primaryCoverageAmount,
          secondaryCoverageAmount: updated.secondaryCoverageAmount,
          paymentMethods: updated.paymentMethods,
          status: 'posted',
          createdBy: input.userName,
        });
      } catch (accountingError) {
        console.error('[Sales] Accounting journal update failed (non-blocking):', accountingError);
      }
    }

    try {
      await this.repository.addActivityLog(
        id,
        'updated',
        `Invoice updated by ${input.userName}`,
        input.userId,
        input.userName,
        {
          changedFields,
          undoPatch,
          changeCount: changedFields.length,
        }
      );
    } catch (error) {
      console.error('Activity log failed (non-blocking):', error);
    }

    return updated;
  }

  /**
   * Get invoice by invoice number
   */
  async getByInvoiceNumber(invoiceNumber: string): Promise<SalesInvoice | null> {
    return this.repository.getByInvoiceNumber(invoiceNumber);
  }

  /**
   * List invoices with filtering
   */
  async list(options?: {
    skip?: number;
    take?: number;
    status?: string;
    documentType?: 'shipment' | 'tax_invoice';
    customerId?: string;
    search?: string;
    excludeShipments?: boolean;
  }) {
    return this.repository.list(options);
  }

  /**
   * Lock invoice for editing
   */
  async lock(id: string, userId: string, userName: string): Promise<SalesInvoice> {
    const invoice = await this.repository.getById(id);
    if (!invoice) {
      throw new Error(`Invoice not found: ${id}`);
    }

    const editCheck = canEditInvoice(invoice);
    if (!editCheck.canEdit) {
      throw new Error(editCheck.reason || 'Cannot edit this invoice');
    }

    return this.repository.lock(id, userId, userName);
  }

  /**
   * Unlock invoice
   */
  async unlock(id: string): Promise<SalesInvoice> {
    return this.repository.unlock(id);
  }

  /**
   * Cancel invoice
   */
  async cancel(
    id: string,
    reason: string,
    userId: string,
    userName: string
  ): Promise<SalesInvoice> {
    const invoice = await this.repository.getById(id);
    if (!invoice) {
      throw new Error(`Invoice not found: ${id}`);
    }

    const cancelCheck = canCancelInvoice(invoice, 0); // TODO: check linked documents
    if (!cancelCheck.canCancel) {
      throw new Error(cancelCheck.reason || 'Cannot cancel this invoice');
    }

    const cancelled = await this.repository.cancel(id, reason, userId, userName);

    await this.accounting.reverseInvoice(cancelled.id, cancelled.invoiceNumber);

    // Trigger side effects
    try {
      const customer = await this.referenceData.getCustomer(cancelled.customerId);
      await this.sideEffects.onInvoiceCancelled({
        invoiceId: cancelled.id,
        invoiceNumber: cancelled.invoiceNumber,
        customerName: customer?.name || cancelled.customerName,
        amount: cancelled.grandTotal,
        reason,
      });
    } catch (error) {
      console.error('Side effect failed (non-blocking):', error);
    }

    return cancelled;
  }

  /**
   * Mark invoice as shipped
   */
  async markAsShipped(
    id: string,
    shipmentId: string,
    shipmentDate: string,
    userId: string,
    userName: string
  ): Promise<SalesInvoice> {
    const invoice = await this.repository.getById(id);
    if (!invoice) {
      throw new Error(`Invoice not found: ${id}`);
    }

    const shipped = await this.repository.markAsShipped(
      id,
      shipmentId,
      shipmentDate,
      userId,
      userName
    );

    // Trigger side effects
    try {
      const customer = await this.referenceData.getCustomer(shipped.customerId);
      await this.sideEffects.onInvoiceShipped({
        invoiceId: shipped.id,
        invoiceNumber: shipped.invoiceNumber,
        customerName: customer?.name || shipped.customerName,
        customerEmail: customer?.email,
        amount: shipped.grandTotal,
      });
    } catch (error) {
      console.error('Side effect failed (non-blocking):', error);
    }

    return shipped;
  }

  /**
   * Record payment received
   */
  async recordPayment(
    id: string,
    amount: number,
    method: SalesInvoicePaymentMethod,
    userId: string,
    userName: string
  ): Promise<SalesInvoice> {
    const invoice = await this.repository.getById(id);
    if (!invoice) {
      throw new Error(`Invoice not found: ${id}`);
    }

    if (amount <= 0) {
      throw new Error('Payment amount must be greater than 0');
    }

    const payment = await this.repository.recordPayment(
      id,
      amount,
      method,
      userId
    );

    // Trigger side effects
    try {
      const customer = await this.referenceData.getCustomer(payment.customerId);
      await this.sideEffects.onPaymentReceived({
        invoiceId: payment.id,
        invoiceNumber: payment.invoiceNumber,
        customerName: customer?.name || payment.customerName,
        amount: payment.grandTotal,
        amountReceived: amount,
      });
    } catch (error) {
      console.error('Side effect failed (non-blocking):', error);
    }

    return payment;
  }

  /**
   * Reverse (undo) a previously applied payment amount from an invoice.
   * Used when cancelling/updating posted payment receipts.
   */
  async reversePayment(
    id: string,
    amount: number,
    userId: string,
    userName: string,
    note?: string
  ): Promise<SalesInvoice> {
    const invoice = await this.repository.getById(id);
    if (!invoice) {
      throw new Error(`Invoice not found: ${id}`);
    }

    if (amount <= 0) {
      throw new Error('Reversal amount must be greater than 0');
    }

    const currentReceived = Number(invoice.amountReceived || 0);
    if (amount - currentReceived > 0.01) {
      throw new Error(`Reversal amount exceeds received amount for invoice ${invoice.invoiceNumber}`);
    }

    const newAmountReceived = Math.max(0, currentReceived - amount);
    const newAmountDue = calculateAmountDue(invoice.grandTotal, newAmountReceived);
    const newPaymentStatus = calculatePaymentStatus(invoice.grandTotal, newAmountReceived);

    const updated = await this.repository.update(id, {
      amountReceived: newAmountReceived,
      amountDue: newAmountDue,
      paymentStatus: newPaymentStatus,
    });

    try {
      await this.repository.addActivityLog(
        id,
        'updated',
        note || `Payment reversed (${amount.toFixed(2)}) by ${userName}`,
        userId,
        userName
      );
    } catch (error) {
      console.error('Activity log failed (non-blocking):', error);
    }

    return updated;
  }

  /**
   * Issue debit note against an existing invoice
   */
  async issueDebitNote(
    id: string,
    input: IssueSalesDebitNoteInput
  ): Promise<SalesInvoice> {
    const invoice = await this.repository.getById(id);
    if (!invoice) {
      throw new Error(`Invoice not found: ${id}`);
    }

    if (invoice.status === 'cancelled') {
      throw new Error('Cannot issue debit note for cancelled invoice');
    }

    if (input.amount <= 0) {
      throw new Error('Debit note amount must be greater than 0');
    }

    if (input.amount - Number(invoice.grandTotal || 0) > 0.01) {
      throw new Error(`Debit note amount exceeds invoice total (${invoice.invoiceNumber})`);
    }

    const nextGrandTotal = roundTwo(Math.max(0, Number(invoice.grandTotal || 0) - input.amount));
    const nextAmountDue = calculateAmountDue(nextGrandTotal, invoice.amountReceived);
    const nextPaymentStatus = calculatePaymentStatus(nextGrandTotal, invoice.amountReceived);
    const nextSecondaryCoveragePercent = Math.max(0, Math.min(100, Number(invoice.secondaryCoveragePercent || 0)));
    const hasSecondaryCoverage = Boolean(String(invoice.secondaryCustomerId || '').trim()) && nextSecondaryCoveragePercent > 0;
    const nextSecondaryCoverageAmount = hasSecondaryCoverage
      ? roundTwo((nextGrandTotal * nextSecondaryCoveragePercent) / 100)
      : 0;
    const nextPrimaryCoverageAmount = roundTwo(nextGrandTotal - nextSecondaryCoverageAmount);

    const notesLine = `[DEBIT_NOTE ${new Date().toISOString()}] -${input.amount.toFixed(2)} | ${input.reason}`;
    const nextInternalNotes = invoice.internalNotes
      ? `${invoice.internalNotes}\n${notesLine}`
      : notesLine;

    const updated = await this.repository.update(id, {
      grandTotal: nextGrandTotal,
      amountDue: nextAmountDue,
      paymentStatus: nextPaymentStatus,
      primaryCoverageAmount: nextPrimaryCoverageAmount,
      secondaryCoverageAmount: hasSecondaryCoverage ? nextSecondaryCoverageAmount : undefined,
      internalNotes: nextInternalNotes,
    });

    if ((invoice.postingStatus || 'saved') === 'posted') {
      try {
        await this.accounting.createInvoiceEntry({
          invoiceId: updated.id,
          invoiceNumber: updated.invoiceNumber,
          date: updated.date,
          amount: updated.grandTotal,
          baseCurrencyAmount: Number(updated.grandTotalBase || 0) > 0
            ? updated.grandTotalBase
            : roundTwo(Number(updated.grandTotal || 0) * (Number(updated.exchangeRate || 1) || 1)),
          taxAmount: updated.taxAmount,
          discountAmount: updated.totalDiscount,
          exchangeRate: updated.exchangeRate,
          description: `Sales invoice ${updated.invoiceNumber}`,
          customerId: updated.customerId,
          customerName: updated.customerName,
          secondaryCustomerId: updated.secondaryCustomerId,
          secondaryCustomerName: updated.secondaryCustomerName,
          primaryCoveragePercent: updated.primaryCoveragePercent,
          secondaryCoveragePercent: updated.secondaryCoveragePercent,
          primaryCoverageAmount: updated.primaryCoverageAmount,
          secondaryCoverageAmount: updated.secondaryCoverageAmount,
          paymentMethods: updated.paymentMethods,
          status: 'posted',
          createdBy: input.userName,
        });
      } catch (accountingError) {
        console.error('[Sales] Accounting journal update after debit note failed (non-blocking):', accountingError);
      }
    }

    await this.repository.addActivityLog(
      id,
      'updated',
      `Debit note issued (${input.amount.toFixed(2)}): ${input.reason}`,
      input.userId,
      input.userName
    );

    return updated;
  }
}
