import type { PurchaseOrder, PurchaseOrderItem, PurchaseOrderLogEntry, ShipmentType } from '@/lib/types';
import { calculatePurchaseTotals, getPurchasePostingStatusLabel } from '../domain/purchase.domain';
import type { PurchaseCreateCommand } from '../domain/purchase.model';
import type { CreatePurchaseOrderRecord, PurchaseOrderRepository } from './purchase-order.repository';
import { createPurchaseOrderRepository } from '../infrastructure/purchase-order.repository.factory';
import type { LinkedShipment, PurchaseSideEffectsPort } from './purchase-side-effects.port';
import { createDefaultPurchaseSideEffectsPort } from '../infrastructure/default-purchase-side-effects.port';
import type { PurchaseReferenceDataPort } from './purchase-reference-data.port';
import { createDefaultPurchaseReferenceDataPort } from '../infrastructure/default-purchase-reference-data.port';
import type { PurchaseAccountingPort, PurchaseJournalPostingInput } from './purchase-accounting.port';
import { createDefaultPurchaseAccountingPort } from '../infrastructure/default-purchase-accounting.port';
import type { PurchaseIdGeneratorPort } from './purchase-id-generator.port';
import { createDefaultPurchaseIdGeneratorPort } from '../infrastructure/default-purchase-id-generator.port';

export type PurchaseJournalPayload = PurchaseJournalPostingInput;

export type CreatePurchaseOrderResult =
  | {
      success: true;
  orderId: string;
      orderNumber: string;
      stockReport: any[];
      postingStatus: 'draft' | 'saved' | 'posted';
      journalPayload?: PurchaseJournalPayload;
      revalidateInternalTransfers: boolean;
    }
  | {
      error: string;
      existing?: {
        orderNumber: string;
        date: string;
        grandTotal: number;
        amountDue: number;
      };
    };

function createPurchaseOrderLogEntry(
  action: PurchaseOrderLogEntry['action'],
  message: string,
  by: string | undefined,
  referenceData: PurchaseReferenceDataPort
): PurchaseOrderLogEntry {
  return {
    id: referenceData.nextPrefixedId('po-log'),
    at: new Date().toISOString(),
    action,
    message,
    by,
  };
}

export class PurchaseApplicationService {
  constructor(
    private readonly repository: PurchaseOrderRepository,
    private readonly sideEffects: PurchaseSideEffectsPort,
    private readonly referenceData: PurchaseReferenceDataPort = createDefaultPurchaseReferenceDataPort(),
    private readonly accounting: PurchaseAccountingPort = createDefaultPurchaseAccountingPort(),
    private readonly idGenerator: PurchaseIdGeneratorPort = createDefaultPurchaseIdGeneratorPort()
  ) {}

  static createDefault(): PurchaseApplicationService {
    return new PurchaseApplicationService(
      createPurchaseOrderRepository(),
      createDefaultPurchaseSideEffectsPort(),
      createDefaultPurchaseReferenceDataPort(),
      createDefaultPurchaseAccountingPort(),
      createDefaultPurchaseIdGeneratorPort()
    );
  }

  private async validateSupplierAndDuplicates(command: PurchaseCreateCommand): Promise<
    { supplierId: string; supplierName: string } | { error: string; existing?: any }
  > {
    let supplier = await this.repository.findSupplierById(command.supplierId);
    if (!supplier) {
      const customerMatch = await this.repository.findCustomerById(command.supplierId);
      if (customerMatch) {
        supplier = await this.repository.createSupplierFromCustomer(customerMatch, command.supplierId);
      }
    }

    if (!supplier) {
      return { error: 'Supplier not found.' };
    }

    const normalizedInvoice = String(command.supplierInvoiceNumber || '').trim().toLowerCase();
    if (normalizedInvoice) {
      const existingOrders = await this.repository.listPurchaseOrders();
      const duplicate = existingOrders.find(
        (order) =>
          order.supplierId === command.supplierId &&
          String(order.supplierInvoiceNumber || '').trim().toLowerCase() === normalizedInvoice
      );

      if (duplicate && !command.allowDuplicateInvoice) {
        return {
          error: 'DUPLICATE_SUPPLIER_INVOICE',
          existing: {
            orderNumber: duplicate.orderNumber,
            date: duplicate.date,
            grandTotal: duplicate.grandTotal,
            amountDue: duplicate.amountDue,
          },
        };
      }
    }

    return {
      supplierId: command.supplierId,
      supplierName: command.supplierName,
    };
  }

  private async validatePaymentMethods(command: PurchaseCreateCommand): Promise<{ ok: true } | { error: string }> {
    const paymentMethodsList = Array.isArray(command.paymentMethods) ? command.paymentMethods : [];
    const totalPaymentAmount = paymentMethodsList.reduce((sum, method) => sum + Number(method.amount || 0), 0);

    if (Math.abs(totalPaymentAmount - command.amountPaid) > 0.01) {
      return { error: 'Payment methods total must equal amount paid.' };
    }

    for (const method of paymentMethodsList) {
      if (method.method === 'check' && method.checkSource === 'incoming') {
        const selectedIncomingCheckIds = Array.isArray(method.incomingCheckIds) ? method.incomingCheckIds : [];
        if (selectedIncomingCheckIds.length === 0) {
          return { error: 'At least one incoming check must be selected for incoming check payment method.' };
        }

        const incomingChecks = await this.referenceData.getIncomingChecks();
        const selectedIncomingChecks = incomingChecks.filter((entry) => selectedIncomingCheckIds.includes(entry.id));
        if (selectedIncomingChecks.length !== selectedIncomingCheckIds.length) {
          return { error: 'One or more selected incoming checks were not found.' };
        }

        const blockedCheck = selectedIncomingChecks.find(
          (entry) => entry.status !== 'pending' || (entry.endorsementStatus && entry.endorsementStatus !== 'none')
        );
        if (blockedCheck) {
          return { error: `Incoming check ${blockedCheck.checkNumber} is not available for payment.` };
        }

        const methodCurrency = method.currencyCode || command.currencyCode;
        let totalIncomingInMethodCurrency = 0;
        for (const entry of selectedIncomingChecks) {
          const checkCurrency = entry.currency || command.baseCurrencyCode;
          const checkAmount = Number(entry.amount || 0);
          totalIncomingInMethodCurrency += checkCurrency === methodCurrency
            ? checkAmount
            : await this.referenceData.convertCurrency(checkAmount, checkCurrency, methodCurrency);
        }

        if (Math.abs(totalIncomingInMethodCurrency - Number(method.amount || 0)) > 0.01) {
          return { error: 'Total selected incoming checks must equal payment method amount.' };
        }
      }
    }

    return { ok: true };
  }

  private async persistOrder(
    command: PurchaseCreateCommand,
    actor: { id: string; name?: string },
    totals: ReturnType<typeof calculatePurchaseTotals>,
    settings: Record<string, unknown>
  ): Promise<{ order: PurchaseOrder; fxRate: number; amountPaidBase: number; amountDueBase: number; taxAmountBase: number; netPurchasesBase: number }> {
    const fxRate = Number(command.exchangeRate || 1);
    const safeFxRate = Number.isFinite(fxRate) && fxRate > 0 ? fxRate : 1;
    const netPurchasesBase = totals.discountedSubTotal / safeFxRate;
    const taxAmountBase = totals.taxAmount / safeFxRate;
    const grandTotalBase = totals.grandTotal / safeFxRate;
    const amountPaidBase = command.amountPaid / safeFxRate;
    const amountDueBase = totals.amountDue / safeFxRate;

    if (command.postingStatus === 'posted') {
      await this.repository.addSupplierBalance(command.supplierId, amountDueBase);
    }

    const activityLog: PurchaseOrderLogEntry[] = [
      createPurchaseOrderLogEntry(
        'created',
        `تم إنشاء الفاتورة بحالة ${getPurchasePostingStatusLabel(command.postingStatus)}.`,
        actor.name || actor.id,
        this.referenceData
      ),
    ];

    if (String(command.notes || '').trim()) {
      activityLog.push(
        createPurchaseOrderLogEntry('notes', 'تمت إضافة ملاحظات عند إنشاء الفاتورة.', actor.name || actor.id, this.referenceData)
      );
    }

    const orderDate = new Date().toISOString();
    this.referenceData.assertNotLockedByPeriod(
      orderDate,
      String(settings.periodCloseDate || '') || undefined,
      String(settings.inventoryCloseDate || '') || undefined
    );

    const paymentMethodsList = Array.isArray(command.paymentMethods) ? command.paymentMethods : [];

    const generatedPurchaseOrderId = this.idGenerator.generatePurchaseOrderId();

    const toCreate: CreatePurchaseOrderRecord = {
      id: generatedPurchaseOrderId,
      date: orderDate,
      currencyCode: command.currencyCode,
      baseCurrencyCode: command.baseCurrencyCode || await this.referenceData.getDefaultBaseCurrencyCode(),
      exchangeRate: fxRate,
      rateDate: command.rateDate || new Date().toISOString(),
      rateSource: command.rateSource || 'manual',
      isAutoRate: command.isAutoRate ?? true,
      supplierId: command.supplierId,
      supplierName: command.supplierName,
      notes: String(command.notes || '').trim() || undefined,
      activityLog,
      documents: command.documents,
      supplierInvoiceNumber: command.supplierInvoiceNumber || undefined,
      supplierInvoiceDate: command.supplierInvoiceDate || undefined,
      warehouseId: command.warehouseId || undefined,
      realEstateLinkMode: command.realEstateLinkMode || 'none',
      realEstateProjectId: command.realEstateProjectId || undefined,
      realEstateProjectName: command.realEstateProjectName || undefined,
      realEstateScope: command.realEstateScope || undefined,
      realEstateFloorLabel: command.realEstateFloorLabel || undefined,
      realEstatePartLabel: command.realEstatePartLabel || undefined,
      realEstateAllocationMethod: command.realEstateAllocationMethod || 'area',
      items: totals.processedItems,
      subTotal: totals.subTotal,
      invoiceDiscountPercent: command.invoiceDiscountPercent || undefined,
      invoiceDiscountAmount: totals.invoiceDiscountAmount > 0 ? totals.invoiceDiscountAmount : undefined,
      lineDiscountTotal: totals.lineDiscountTotal > 0 ? totals.lineDiscountTotal : undefined,
      totalDiscount: totals.totalDiscount > 0 ? totals.totalDiscount : undefined,
      taxMethod: command.taxMethod,
      taxAmount: totals.taxAmount,
      grandTotal: totals.grandTotal,
      amountPaid: command.amountPaid,
      paymentMethods: paymentMethodsList.map((method) => ({
        id: method.id || `${Date.now()}-${Math.random()}`,
        method: method.method,
        amount: method.amount,
        currencyCode: method.currencyCode || command.currencyCode,
        checkSource: method.checkSource,
        incomingCheckIds: method.incomingCheckIds,
        checkNumber: method.checkNumber,
        checkDate: method.checkDate,
        bankId: method.bankId,
        bankName: method.bankName,
        reference: method.reference,
      })),
      amountDue: totals.amountDue,
      postingStatus: command.postingStatus,
      documentType: command.documentType || 'tax_invoice',
      linkedShipmentIds: Array.isArray(command.linkedShipmentIds) && command.linkedShipmentIds.length > 0 ? command.linkedShipmentIds : undefined,
      grandTotalBase,
      amountPaidBase,
      amountDueBase,
      createdBy: actor.name || 'admin',
    };

    const order = await this.repository.createPurchaseOrder(toCreate);
    return { order, fxRate, amountPaidBase, amountDueBase, taxAmountBase, netPurchasesBase };
  }

  private async executePostedSideEffects(
    command: PurchaseCreateCommand,
    order: PurchaseOrder,
    processedItems: PurchaseOrderItem[],
    settings: Record<string, unknown>,
    actor: { id: string; name?: string }
  ): Promise<{ linkedShipment: LinkedShipment; error?: string }> {
    // Manual-first workflow: do not auto-create shipment/inventory/transfer side effects.
    // These are created explicitly from manual actions in the view screen.
    const shouldApplyInventory = false;

    if (!shouldApplyInventory) {
      return { linkedShipment: null };
    }

    const paymentMethodsList = Array.isArray(command.paymentMethods) ? command.paymentMethods : [];
    const endorsed = await this.sideEffects.endorseIncomingChecksForSupplier({
      paymentMethods: paymentMethodsList,
      supplierId: command.supplierId,
      supplierName: command.supplierName,
      orderNumber: order.orderNumber,
    });
    if (!endorsed) {
      return { linkedShipment: null, error: 'Failed to endorse incoming check to supplier.' };
    }

    const linkedShipment = await this.sideEffects.applyInventoryForPostedOrder({
      order: { id: order.id, date: order.date },
      items: processedItems,
      periodCloseDate: String(settings.periodCloseDate || '') || undefined,
      inventoryCloseDate: String(settings.inventoryCloseDate || '') || undefined,
    });

    return { linkedShipment };
  }

  private getInventorySideEffectItems(
    command: PurchaseCreateCommand,
    processedItems: PurchaseOrderItem[],
    linkedShipmentOrders: PurchaseOrder[]
  ): PurchaseOrderItem[] {
    if (command.documentType === 'shipment') {
      return processedItems;
    }

    const linkedIds = Array.isArray(command.linkedShipmentIds) ? command.linkedShipmentIds.filter(Boolean) : [];
    if (linkedIds.length === 0) {
      return processedItems;
    }

    const linkedSet = new Set(linkedIds);
    const linkedShipments = linkedShipmentOrders.filter((order) =>
      linkedSet.has(order.id) && order.documentType === 'shipment'
    );
    if (linkedShipments.length === 0) {
      return processedItems;
    }

    const coveredQtyByMaterial = new Map<string, number>();
    linkedShipments.forEach((shipment) => {
      (shipment.items || []).forEach((item) => {
        const materialId = String(item.materialId || '').trim();
        const quantity = Number(item.quantity || 0);
        if (!materialId || !Number.isFinite(quantity) || quantity <= 0) return;
        coveredQtyByMaterial.set(materialId, (coveredQtyByMaterial.get(materialId) || 0) + quantity);
      });
    });

    if (coveredQtyByMaterial.size === 0) {
      return processedItems;
    }

    return processedItems
      .map((item) => {
        const materialId = String(item.materialId || '').trim();
        const quantity = Number(item.quantity || 0);
        if (!materialId || !Number.isFinite(quantity) || quantity <= 0) {
          return null;
        }

        const covered = Number(coveredQtyByMaterial.get(materialId) || 0);
        if (covered <= 0) {
          return item;
        }

        if (covered >= quantity) {
          coveredQtyByMaterial.set(materialId, covered - quantity);
          return null;
        }

        coveredQtyByMaterial.set(materialId, 0);
        return {
          ...item,
          quantity: quantity - covered,
        };
      })
      .filter((item): item is PurchaseOrderItem => Boolean(item));
  }

  async listOrders(): Promise<PurchaseOrder[]> {
    return this.repository.listPurchaseOrders();
  }

  async createPurchaseOrder(command: PurchaseCreateCommand, actor: { id: string; name?: string }): Promise<CreatePurchaseOrderResult> {
    const settings = await this.referenceData.getSettings();

    const itemGroups = await this.referenceData.getItemGroupsForTaxCalc();
    const itemGroupMap = new Map(itemGroups.map((entry) => [entry.id, entry]));
    const defaultTaxRate = command.taxMethod === 'line-level' ? Number(settings.defaultLineTaxRate || 0) : 0;

    const totals = calculatePurchaseTotals(
      command,
      (materialId: string) => this.referenceData.getTaxRateForMaterial(materialId, itemGroupMap, defaultTaxRate)
    );

    const supplierValidation = await this.validateSupplierAndDuplicates(command);
    if ('error' in supplierValidation) {
      return supplierValidation;
    }

    const paymentValidation = await this.validatePaymentMethods(command);
    if ('error' in paymentValidation) {
      return paymentValidation;
    }

    const persisted = await this.persistOrder(command, actor, totals, settings as Record<string, unknown>);
    const { order, fxRate, amountPaidBase, taxAmountBase, netPurchasesBase } = persisted;

    const linkedShipmentOrders = Array.isArray(command.linkedShipmentIds) && command.linkedShipmentIds.length > 0
      ? await this.repository.listPurchaseOrders()
      : [];
    const inventorySideEffectItems = this.getInventorySideEffectItems(
      command,
      totals.processedItems,
      linkedShipmentOrders
    );

    const sideEffectsResult = await this.executePostedSideEffects(
      command,
      order,
      inventorySideEffectItems,
      settings as Record<string, unknown>,
      actor
    );
    if (sideEffectsResult.error) {
      return { error: sideEffectsResult.error };
    }

    if (command.postingStatus !== 'draft') {
      await this.sideEffects.syncMaterialPricesFromPurchase({
        processedItems: totals.processedItems,
        taxMethod: command.taxMethod,
        calculatedTaxAmount: totals.taxAmount,
        discountedSubTotal: totals.discountedSubTotal,
        currencyCode: command.currencyCode,
      });
    }

    // If this is a tax invoice that links shipments, mark each linked shipment with this invoice's ID
    if (command.documentType !== 'shipment' && Array.isArray(command.linkedShipmentIds) && command.linkedShipmentIds.length > 0) {
      const allOrders = linkedShipmentOrders.length > 0
        ? linkedShipmentOrders
        : await this.repository.listPurchaseOrders();
      for (const shipmentId of command.linkedShipmentIds) {
        const shipmentOrder = allOrders.find((o) => o.id === shipmentId);
        if (shipmentOrder && !shipmentOrder.taxInvoiceId) {
          await this.repository.updatePurchaseOrder(shipmentId, { taxInvoiceId: order.id });
        }
      }
    }

    let stockReport: any[] = [];
    // Shipment receipts: inventory effect + draft GRNI journal entry (when posted)
    // Tax invoices: settle GRNI for linked shipments + post final journal entry
    const isShipmentDoc = command.documentType === 'shipment';
    const journalPayload =
      command.postingStatus === 'posted' && !isShipmentDoc
        ? {
            orderNumber: order.orderNumber,
            supplierId: command.supplierId,
            supplierName: command.supplierName,
            totalAmountBase: netPurchasesBase,
            taxAmountBase,
            discountAmountBase: totals.totalDiscount / (Number.isFinite(fxRate) && fxRate > 0 ? fxRate : 1),
            amountPaidBase,
            currencyCode: command.currencyCode,
            createdBy: actor.name || 'admin',
            opts: {
              invoiceId: order.id,
              shipmentId: sideEffectsResult.linkedShipment?.id,
              shipmentType: sideEffectsResult.linkedShipment?.shipmentType,
              paymentMethods: command.paymentMethods,
              fxRate,
            },
          }
        : undefined;

    if (command.postingStatus === 'posted' && !isShipmentDoc) {
      const materialIds = Array.from(new Set(inventorySideEffectItems.map((item) => item.materialId)));
      stockReport = await this.referenceData.getStockLocationsByMaterial(materialIds);

      // Settle any GRNI draft entries for linked shipments before posting final invoice
      if (Array.isArray(command.linkedShipmentIds) && command.linkedShipmentIds.length > 0) {
        await this.accounting.settleGrniEntries(
          command.linkedShipmentIds,
          order.orderNumber,
          actor.name || 'admin',
        );
      }

      if (journalPayload) {
        await this.accounting.postPurchaseOrderJournalEntry(journalPayload);
      }
    } else if (isShipmentDoc && (command.postingStatus === 'saved' || command.postingStatus === 'posted')) {
      // Shipment: report stock locations
      const materialIds = Array.from(new Set(inventorySideEffectItems.map((item) => item.materialId)));
      stockReport = await this.referenceData.getStockLocationsByMaterial(materialIds);

      // Shipment (saved/posted): create or refresh draft GRNI entry for review
      if (netPurchasesBase > 0) {
        await this.accounting.postShipmentGrniEntry({
          shipmentOrderNumber: order.orderNumber,
          shipmentId: order.id,
          supplierId: command.supplierId,
          supplierName: command.supplierName,
          totalAmountBase: netPurchasesBase,
          createdBy: actor.name || 'admin',
        });
      }
    }

    return {
      success: true,
      orderId: order.id,
      orderNumber: order.orderNumber,
      stockReport,
      postingStatus: command.postingStatus,
      journalPayload,
      revalidateInternalTransfers: command.postingStatus === 'posted',
    };
  }
}
