import { UBLBaseEncoder } from '../ubl.encoder.js';
import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js';
import { UBLDocumentType } from '../ubl.types.js';
import { DOMParser, XMLSerializer } from '../../../plugins.js';

/**
 * UBL Encoder implementation
 * Provides encoding functionality for UBL 2.1 invoice and credit note documents
 */
export class UBLEncoder extends UBLBaseEncoder {
  /**
   * Encodes a credit note into UBL XML
   * @param creditNote Credit note to encode
   * @returns UBL XML string
   */
  protected async encodeCreditNote(creditNote: TCreditNote): Promise<string> {
    // Create XML document from template
    const xmlString = this.createXmlRoot(UBLDocumentType.CREDIT_NOTE);
    const doc = new DOMParser().parseFromString(xmlString, 'application/xml');
    
    // Add common document elements
    this.addCommonElements(doc, creditNote as unknown as TInvoice, UBLDocumentType.CREDIT_NOTE);
    
    // Add credit note specific data
    this.addCreditNoteSpecificData(doc, creditNote);
    
    // Serialize to string
    return new XMLSerializer().serializeToString(doc);
  }
  
  /**
   * Encodes a debit note (invoice) into UBL XML
   * @param debitNote Debit note to encode
   * @returns UBL XML string
   */
  protected async encodeDebitNote(debitNote: TDebitNote): Promise<string> {
    // Create XML document from template
    const xmlString = this.createXmlRoot(UBLDocumentType.INVOICE);
    const doc = new DOMParser().parseFromString(xmlString, 'application/xml');
    
    // Add common document elements
    this.addCommonElements(doc, debitNote as unknown as TInvoice, UBLDocumentType.INVOICE);
    
    // Add invoice specific data
    this.addInvoiceSpecificData(doc, debitNote);
    
    // Serialize to string
    return new XMLSerializer().serializeToString(doc);
  }
  
  /**
   * Adds common document elements to both invoice and credit note
   * @param doc XML document
   * @param invoice Invoice or credit note data
   * @param documentType Document type (Invoice or CreditNote)
   */
  private addCommonElements(doc: Document, invoice: TInvoice, documentType: UBLDocumentType): void {
    const root = doc.documentElement;
    
    // UBL Version ID (2.1 is standard for EN16931)
    this.appendElement(doc, root, 'cbc:UBLVersionID', '2.1');
    
    // Customization ID - using generic UBL
    this.appendElement(doc, root, 'cbc:CustomizationID', 'urn:cen.eu:en16931:2017');
    
    // Profile ID - standard billing
    this.appendElement(doc, root, 'cbc:ProfileID', 'urn:fdc:peppol.eu:2017:poacc:billing:01:1.0');
    
    // ID
    this.appendElement(doc, root, 'cbc:ID', invoice.id);
    
    // Issue Date
    this.appendElement(doc, root, 'cbc:IssueDate', this.formatDate(invoice.date));
    
    // Due Date - ensure invoice.date is a valid timestamp
    const issueTimestamp = typeof invoice.date === 'number' ? invoice.date : Date.now();
    const dueDate = new Date(issueTimestamp);
    dueDate.setDate(dueDate.getDate() + (invoice.dueInDays || 30));
    this.appendElement(doc, root, 'cbc:DueDate', this.formatDate(dueDate.getTime()));
    
    // Document Type Code 
    const typeCode = documentType === UBLDocumentType.INVOICE ? '380' : '381';
    this.appendElement(doc, root, 'cbc:InvoiceTypeCode', typeCode);
    
    // Notes - include subject as first note if it exists
    const allNotes: string[] = [];
    if (invoice.subject && invoice.subject.trim()) {
      allNotes.push(invoice.subject);
    }
    if (invoice.notes && invoice.notes.length > 0) {
      allNotes.push(...invoice.notes);
    }
    
    // Write all notes
    for (const note of allNotes) {
      this.appendElement(doc, root, 'cbc:Note', note);
    }
    
    // Document Currency Code
    this.appendElement(doc, root, 'cbc:DocumentCurrencyCode', invoice.currency);
    
    // Add accounting supplier party (seller)
    this.addParty(doc, root, 'cac:AccountingSupplierParty', invoice.from);
    
    // Add accounting customer party (buyer)
    this.addParty(doc, root, 'cac:AccountingCustomerParty', invoice.to);
    
    // Add payment terms
    this.addPaymentTerms(doc, root, invoice);
    
    // Add tax summary
    this.addTaxTotal(doc, root, invoice);
    
    // Add monetary totals
    this.addLegalMonetaryTotal(doc, root, invoice);
    
    // Add line items
    this.addInvoiceLines(doc, root, invoice);
    
    // Preserve metadata if available
    this.preserveMetadata(doc, root, invoice);
  }
  
  /**
   * Adds credit note specific data to the document
   * @param doc XML document
   * @param creditNote Credit note data
   */
  private addCreditNoteSpecificData(doc: Document, creditNote: TCreditNote): void {
    // For now, there's no specific data to add for credit notes
    // If needed, additional credit note specific fields would be added here
  }
  
  /**
   * Adds invoice specific data to the document
   * @param doc XML document
   * @param invoice Invoice data
   */
  private addInvoiceSpecificData(doc: Document, invoice: TDebitNote): void {
    // For now, there's no specific data to add for invoices that's not already covered
    // If needed, additional invoice specific fields would be added here
  }
  
  /**
   * Adds party information (supplier or customer)
   * @param doc XML document
   * @param parentElement Parent element
   * @param elementName Element name (AccountingSupplierParty or AccountingCustomerParty)
   * @param party Party data
   */
  private addParty(doc: Document, parentElement: Element, elementName: string, party: any): void {
    const partyElement = doc.createElement(elementName);
    parentElement.appendChild(partyElement);
    
    const partyNode = doc.createElement('cac:Party');
    partyElement.appendChild(partyNode);
    
    // Party name
    const partyNameNode = doc.createElement('cac:PartyName');
    partyNode.appendChild(partyNameNode);
    this.appendElement(doc, partyNameNode, 'cbc:Name', party.name);
    
    // Postal address
    const postalAddressNode = doc.createElement('cac:PostalAddress');
    partyNode.appendChild(postalAddressNode);
    
    if (party.address.streetName) {
      this.appendElement(doc, postalAddressNode, 'cbc:StreetName', party.address.streetName);
    }
    
    if (party.address.houseNumber && party.address.houseNumber !== '0') {
      this.appendElement(doc, postalAddressNode, 'cbc:BuildingNumber', party.address.houseNumber);
    }
    
    if (party.address.city) {
      this.appendElement(doc, postalAddressNode, 'cbc:CityName', party.address.city);
    }
    
    if (party.address.postalCode) {
      this.appendElement(doc, postalAddressNode, 'cbc:PostalZone', party.address.postalCode);
    }
    
    // Country
    if (party.address.country || party.address.countryCode) {
      const countryNode = doc.createElement('cac:Country');
      postalAddressNode.appendChild(countryNode);
      
      const countryCode = party.address.countryCode || this.getCountryCode(party.address.country);
      this.appendElement(doc, countryNode, 'cbc:IdentificationCode', countryCode);
      
      if (party.address.country) {
        this.appendElement(doc, countryNode, 'cbc:Name', party.address.country);
      }
    }
    
    // Party tax scheme (VAT ID)
    if (party.registrationDetails && party.registrationDetails.vatId) {
      const partyTaxSchemeNode = doc.createElement('cac:PartyTaxScheme');
      partyNode.appendChild(partyTaxSchemeNode);
      
      this.appendElement(doc, partyTaxSchemeNode, 'cbc:CompanyID', party.registrationDetails.vatId);
      
      const taxSchemeNode = doc.createElement('cac:TaxScheme');
      partyTaxSchemeNode.appendChild(taxSchemeNode);
      this.appendElement(doc, taxSchemeNode, 'cbc:ID', 'VAT');
    }
    
    // Party legal entity (registration information)
    if (party.registrationDetails) {
      const partyLegalEntityNode = doc.createElement('cac:PartyLegalEntity');
      partyNode.appendChild(partyLegalEntityNode);
      
      const registrationName = party.registrationDetails.registrationName || party.name;
      this.appendElement(doc, partyLegalEntityNode, 'cbc:RegistrationName', registrationName);
      
      if (party.registrationDetails.registrationId) {
        this.appendElement(doc, partyLegalEntityNode, 'cbc:CompanyID', party.registrationDetails.registrationId);
      }
    }
    
    // Contact information
    if (party.contactDetails) {
      const contactNode = doc.createElement('cac:Contact');
      partyNode.appendChild(contactNode);
      
      if (party.contactDetails.name) {
        this.appendElement(doc, contactNode, 'cbc:Name', party.contactDetails.name);
      }
      
      if (party.contactDetails.telephone) {
        this.appendElement(doc, contactNode, 'cbc:Telephone', party.contactDetails.telephone);
      }
      
      if (party.contactDetails.email) {
        this.appendElement(doc, contactNode, 'cbc:ElectronicMail', party.contactDetails.email);
      }
    }
  }
  
  /**
   * Adds payment terms information
   * @param doc XML document
   * @param parentElement Parent element
   * @param invoice Invoice data
   */
  private addPaymentTerms(doc: Document, parentElement: Element, invoice: TInvoice): void {
    const paymentTermsNode = doc.createElement('cac:PaymentTerms');
    parentElement.appendChild(paymentTermsNode);
    
    // Payment terms note
    this.appendElement(doc, paymentTermsNode, 'cbc:Note', `Due in ${invoice.dueInDays} days`);
    
    // Add payment means if available
    if (invoice.paymentOptions) {
      this.addPaymentMeans(doc, parentElement, invoice);
    }
  }
  
  /**
   * Adds payment means information
   * @param doc XML document
   * @param parentElement Parent element
   * @param invoice Invoice data
   */
  private addPaymentMeans(doc: Document, parentElement: Element, invoice: TInvoice): void {
    const paymentMeansNode = doc.createElement('cac:PaymentMeans');
    parentElement.appendChild(paymentMeansNode);
    
    // Payment means code - default to credit transfer
    this.appendElement(doc, paymentMeansNode, 'cbc:PaymentMeansCode', '30');
    
    // Payment due date - ensure invoice.date is a valid timestamp
    const issueTimestamp = typeof invoice.date === 'number' ? invoice.date : Date.now();
    const dueDate = new Date(issueTimestamp);
    dueDate.setDate(dueDate.getDate() + (invoice.dueInDays || 30));
    this.appendElement(doc, paymentMeansNode, 'cbc:PaymentDueDate', this.formatDate(dueDate.getTime()));
    
    // Add payment channel code if available
    if (invoice.paymentOptions.description) {
      this.appendElement(doc, paymentMeansNode, 'cbc:InstructionNote', invoice.paymentOptions.description);
    }
    
    // Add payment ID information if available - use invoice ID as payment reference
    this.appendElement(doc, paymentMeansNode, 'cbc:PaymentID', invoice.id);
    
    // Add bank account information if available
    if (invoice.paymentOptions.sepaConnection && invoice.paymentOptions.sepaConnection.iban) {
      const payeeFinancialAccountNode = doc.createElement('cac:PayeeFinancialAccount');
      paymentMeansNode.appendChild(payeeFinancialAccountNode);
      
      this.appendElement(doc, payeeFinancialAccountNode, 'cbc:ID', invoice.paymentOptions.sepaConnection.iban);
      
      // Add financial institution information if BIC is available
      if (invoice.paymentOptions.sepaConnection.bic) {
        const financialInstitutionNode = doc.createElement('cac:FinancialInstitutionBranch');
        payeeFinancialAccountNode.appendChild(financialInstitutionNode);
        
        this.appendElement(doc, financialInstitutionNode, 'cbc:ID', invoice.paymentOptions.sepaConnection.bic);
      }
    }
  }
  
  /**
   * Adds tax total information
   * @param doc XML document
   * @param parentElement Parent element
   * @param invoice Invoice data
   */
  private addTaxTotal(doc: Document, parentElement: Element, invoice: TInvoice): void {
    const taxTotalNode = doc.createElement('cac:TaxTotal');
    parentElement.appendChild(taxTotalNode);
    
    // Calculate total tax amount
    let totalTaxAmount = 0;
    const taxCategories = new Map<number, number>(); // Map of VAT rate to net amount
    
    // Calculate from items
    if (invoice.items) {
      for (const item of invoice.items) {
        const itemNetAmount = item.unitNetPrice * item.unitQuantity;
        const itemTaxAmount = itemNetAmount * (item.vatPercentage / 100);
        const vatRate = item.vatPercentage;
        
        totalTaxAmount += itemTaxAmount;
        
        // Aggregate by VAT rate
        const currentAmount = taxCategories.get(vatRate) || 0;
        taxCategories.set(vatRate, currentAmount + itemNetAmount);
      }
    }
    
    // Add total tax amount
    const taxAmountElement = doc.createElement('cbc:TaxAmount');
    taxAmountElement.setAttribute('currencyID', invoice.currency);
    taxAmountElement.textContent = totalTaxAmount.toFixed(2);
    taxTotalNode.appendChild(taxAmountElement);
    
    // Add tax subtotals
    for (const [rate, baseAmount] of taxCategories.entries()) {
      const taxSubtotalNode = doc.createElement('cac:TaxSubtotal');
      taxTotalNode.appendChild(taxSubtotalNode);
      
      // Taxable amount
      const taxableAmountElement = doc.createElement('cbc:TaxableAmount');
      taxableAmountElement.setAttribute('currencyID', invoice.currency);
      taxableAmountElement.textContent = baseAmount.toFixed(2);
      taxSubtotalNode.appendChild(taxableAmountElement);
      
      // Tax amount
      const taxAmount = baseAmount * (rate / 100);
      const subtotalTaxAmountElement = doc.createElement('cbc:TaxAmount');
      subtotalTaxAmountElement.setAttribute('currencyID', invoice.currency);
      subtotalTaxAmountElement.textContent = taxAmount.toFixed(2);
      taxSubtotalNode.appendChild(subtotalTaxAmountElement);
      
      // Tax category
      const taxCategoryNode = doc.createElement('cac:TaxCategory');
      taxSubtotalNode.appendChild(taxCategoryNode);
      
      // Determine tax category ID based on reverse charge
      const categoryId = invoice.reverseCharge ? 'AE' : 'S';
      this.appendElement(doc, taxCategoryNode, 'cbc:ID', categoryId);
      
      // Add percent with 2 decimal places
      this.appendElement(doc, taxCategoryNode, 'cbc:Percent', rate.toFixed(2));
      
      // Add tax exemption reason if reverse charge
      if (invoice.reverseCharge) {
        this.appendElement(doc, taxCategoryNode, 'cbc:TaxExemptionReasonCode', 'VATEX-EU-IC');
        this.appendElement(doc, taxCategoryNode, 'cbc:TaxExemptionReason', 'Reverse charge');
      }
      
      // Add tax scheme
      const taxSchemeNode = doc.createElement('cac:TaxScheme');
      taxCategoryNode.appendChild(taxSchemeNode);
      this.appendElement(doc, taxSchemeNode, 'cbc:ID', 'VAT');
    }
  }
  
  /**
   * Adds legal monetary total information
   * @param doc XML document
   * @param parentElement Parent element
   * @param invoice Invoice data
   */
  private addLegalMonetaryTotal(doc: Document, parentElement: Element, invoice: TInvoice): void {
    const legalMonetaryTotalNode = doc.createElement('cac:LegalMonetaryTotal');
    parentElement.appendChild(legalMonetaryTotalNode);
    
    // Calculate totals
    let totalNetAmount = 0;
    let totalTaxAmount = 0;
    
    // Calculate from items
    if (invoice.items) {
      for (const item of invoice.items) {
        const itemNetAmount = item.unitNetPrice * item.unitQuantity;
        const itemTaxAmount = itemNetAmount * (item.vatPercentage / 100);
        
        totalNetAmount += itemNetAmount;
        totalTaxAmount += itemTaxAmount;
      }
    }
    
    const totalGrossAmount = totalNetAmount + totalTaxAmount;
    
    // Line extension amount (sum of line net amounts)
    const lineExtensionAmountElement = doc.createElement('cbc:LineExtensionAmount');
    lineExtensionAmountElement.setAttribute('currencyID', invoice.currency);
    lineExtensionAmountElement.textContent = totalNetAmount.toFixed(2);
    legalMonetaryTotalNode.appendChild(lineExtensionAmountElement);
    
    // Tax exclusive amount
    const taxExclusiveAmountElement = doc.createElement('cbc:TaxExclusiveAmount');
    taxExclusiveAmountElement.setAttribute('currencyID', invoice.currency);
    taxExclusiveAmountElement.textContent = totalNetAmount.toFixed(2);
    legalMonetaryTotalNode.appendChild(taxExclusiveAmountElement);
    
    // Tax inclusive amount
    const taxInclusiveAmountElement = doc.createElement('cbc:TaxInclusiveAmount');
    taxInclusiveAmountElement.setAttribute('currencyID', invoice.currency);
    taxInclusiveAmountElement.textContent = totalGrossAmount.toFixed(2);
    legalMonetaryTotalNode.appendChild(taxInclusiveAmountElement);
    
    // Payable amount
    const payableAmountElement = doc.createElement('cbc:PayableAmount');
    payableAmountElement.setAttribute('currencyID', invoice.currency);
    payableAmountElement.textContent = totalGrossAmount.toFixed(2);
    legalMonetaryTotalNode.appendChild(payableAmountElement);
  }
  
  /**
   * Adds invoice lines
   * @param doc XML document
   * @param parentElement Parent element
   * @param invoice Invoice data
   */
  private addInvoiceLines(doc: Document, parentElement: Element, invoice: TInvoice): void {
    if (!invoice.items) return;
    
    for (const item of invoice.items) {
      const invoiceLineNode = doc.createElement('cac:InvoiceLine');
      parentElement.appendChild(invoiceLineNode);
      
      // ID
      this.appendElement(doc, invoiceLineNode, 'cbc:ID', item.position.toString());
      
      // Invoiced quantity
      const quantityElement = doc.createElement('cbc:InvoicedQuantity');
      quantityElement.setAttribute('unitCode', item.unitType);
      quantityElement.textContent = item.unitQuantity.toString();
      invoiceLineNode.appendChild(quantityElement);
      
      // Line extension amount (line net amount)
      const itemNetAmount = item.unitNetPrice * item.unitQuantity;
      const lineExtensionAmountElement = doc.createElement('cbc:LineExtensionAmount');
      lineExtensionAmountElement.setAttribute('currencyID', invoice.currency);
      lineExtensionAmountElement.textContent = itemNetAmount.toFixed(2);
      invoiceLineNode.appendChild(lineExtensionAmountElement);
      
      // Item information
      const itemNode = doc.createElement('cac:Item');
      invoiceLineNode.appendChild(itemNode);
      
      // Description - use description field if available, otherwise use name
      const description = (item as any).description || item.name;
      this.appendElement(doc, itemNode, 'cbc:Description', description);
      this.appendElement(doc, itemNode, 'cbc:Name', item.name);
      
      // Seller's item identification
      if (item.articleNumber) {
        const sellersItemIdentificationNode = doc.createElement('cac:SellersItemIdentification');
        itemNode.appendChild(sellersItemIdentificationNode);
        this.appendElement(doc, sellersItemIdentificationNode, 'cbc:ID', item.articleNumber);
      }
      
      // Item tax information
      const classifiedTaxCategoryNode = doc.createElement('cac:ClassifiedTaxCategory');
      itemNode.appendChild(classifiedTaxCategoryNode);
      
      // Determine tax category ID based on reverse charge
      const categoryId = invoice.reverseCharge ? 'AE' : 'S';
      this.appendElement(doc, classifiedTaxCategoryNode, 'cbc:ID', categoryId);
      
      // Tax percent with 2 decimal places
      this.appendElement(doc, classifiedTaxCategoryNode, 'cbc:Percent', item.vatPercentage.toFixed(2));
      
      // Tax scheme
      const taxSchemeNode = doc.createElement('cac:TaxScheme');
      classifiedTaxCategoryNode.appendChild(taxSchemeNode);
      this.appendElement(doc, taxSchemeNode, 'cbc:ID', 'VAT');
      
      // Price information
      const priceNode = doc.createElement('cac:Price');
      invoiceLineNode.appendChild(priceNode);
      
      // Price amount
      const priceAmountElement = doc.createElement('cbc:PriceAmount');
      priceAmountElement.setAttribute('currencyID', invoice.currency);
      priceAmountElement.textContent = item.unitNetPrice.toFixed(2);
      priceNode.appendChild(priceAmountElement);
    }
  }
  
  /**
   * Helper method to append a simple element with text content
   * @param doc XML document
   * @param parentElement Parent element
   * @param elementName Element name
   * @param textContent Text content
   */
  private appendElement(doc: Document, parentElement: Element, elementName: string, textContent: string): void {
    const element = doc.createElement(elementName);
    element.textContent = textContent;
    parentElement.appendChild(element);
  }
  
  /**
   * Helper method to get country code from country name
   * Simple implementation that assumes the country name is already a code
   * @param countryName Country name
   * @returns Country code (2-letter ISO code)
   */
  private getCountryCode(countryName: string): string {
    // In a real implementation, this would map country names to ISO codes
    // For now, just return the first 2 characters or "XX" as fallback
    if (!countryName) return 'XX';
    return countryName.length >= 2 ? countryName.substring(0, 2).toUpperCase() : 'XX';
  }

  /**
   * Preserves metadata from invoice to enhance UBL XML output
   * @param doc XML document
   * @param root Root element
   * @param invoice Invoice data
   */
  private preserveMetadata(doc: Document, root: Element, invoice: TInvoice): void {
    // Extract metadata if available
    const metadata = (invoice as any).metadata?.extensions;
    if (!metadata) return;
    
    // Preserve business references
    this.addBusinessReferencesToUBL(doc, root, metadata.businessReferences);
    
    // Preserve payment information
    this.enhancePaymentInformationUBL(doc, root, metadata.paymentInformation);
    
    // Preserve date information
    this.addDateInformationUBL(doc, root, metadata.dateInformation);
    
    // Enhance party information with contact details
    this.enhancePartyInformationUBL(doc, invoice);
    
    // Enhance line items with metadata
    this.enhanceLineItemsUBL(doc, invoice);
  }

  /**
   * Adds business references from metadata to UBL document
   * @param doc XML document
   * @param root Root element
   * @param businessReferences Business references from metadata
   */
  private addBusinessReferencesToUBL(doc: Document, root: Element, businessReferences?: any): void {
    if (!businessReferences) return;
    
    // Add BuyerReference
    if (businessReferences.buyerReference && !root.getElementsByTagName('cbc:BuyerReference')[0]) {
      const buyerRef = doc.createElement('cbc:BuyerReference');
      buyerRef.textContent = businessReferences.buyerReference;
      
      // Insert after DocumentCurrencyCode
      const currencyCode = root.getElementsByTagName('cbc:DocumentCurrencyCode')[0];
      if (currencyCode && currencyCode.parentNode) {
        currencyCode.parentNode.insertBefore(buyerRef, currencyCode.nextSibling);
      }
    }
    
    // Add OrderReference
    if (businessReferences.orderReference && !root.getElementsByTagName('cac:OrderReference')[0]) {
      const orderRef = doc.createElement('cac:OrderReference');
      const orderId = doc.createElement('cbc:ID');
      orderId.textContent = businessReferences.orderReference;
      orderRef.appendChild(orderId);
      
      // Insert after BuyerReference or DocumentCurrencyCode
      const buyerRef = root.getElementsByTagName('cbc:BuyerReference')[0];
      const currencyCode = root.getElementsByTagName('cbc:DocumentCurrencyCode')[0];
      const insertAfter = buyerRef || currencyCode;
      if (insertAfter && insertAfter.parentNode) {
        insertAfter.parentNode.insertBefore(orderRef, insertAfter.nextSibling);
      }
    }
    
    // Add ContractDocumentReference
    if (businessReferences.contractReference && !root.getElementsByTagName('cac:ContractDocumentReference')[0]) {
      const contractRef = doc.createElement('cac:ContractDocumentReference');
      const contractId = doc.createElement('cbc:ID');
      contractId.textContent = businessReferences.contractReference;
      contractRef.appendChild(contractId);
      
      // Insert after OrderReference or DocumentCurrencyCode
      const orderRef = root.getElementsByTagName('cac:OrderReference')[0];
      const insertAfter = orderRef || root.getElementsByTagName('cbc:DocumentCurrencyCode')[0];
      if (insertAfter && insertAfter.parentNode) {
        insertAfter.parentNode.insertBefore(contractRef, insertAfter.nextSibling);
      }
    }
    
    // Add ProjectReference
    if (businessReferences.projectReference && !root.getElementsByTagName('cac:ProjectReference')[0]) {
      const projectRef = doc.createElement('cac:ProjectReference');
      const projectId = doc.createElement('cbc:ID');
      projectId.textContent = businessReferences.projectReference;
      projectRef.appendChild(projectId);
      
      // Insert after ContractDocumentReference or other refs
      const contractRef = root.getElementsByTagName('cac:ContractDocumentReference')[0];
      const orderRef = root.getElementsByTagName('cac:OrderReference')[0];
      const insertAfter = contractRef || orderRef || root.getElementsByTagName('cbc:DocumentCurrencyCode')[0];
      if (insertAfter && insertAfter.parentNode) {
        insertAfter.parentNode.insertBefore(projectRef, insertAfter.nextSibling);
      }
    }
  }

  /**
   * Enhances payment information from metadata in UBL document
   * @param doc XML document
   * @param root Root element
   * @param paymentInfo Payment information from metadata
   */
  private enhancePaymentInformationUBL(doc: Document, root: Element, paymentInfo?: any): void {
    if (!paymentInfo) return;
    
    let paymentMeans = root.getElementsByTagName('cac:PaymentMeans')[0];
    
    // Create PaymentMeans if it doesn't exist
    if (!paymentMeans) {
      paymentMeans = doc.createElement('cac:PaymentMeans');
      // Insert before TaxTotal
      const taxTotal = root.getElementsByTagName('cac:TaxTotal')[0];
      if (taxTotal && taxTotal.parentNode) {
        taxTotal.parentNode.insertBefore(paymentMeans, taxTotal);
      }
    }
    
    // Add PaymentMeansCode
    if (paymentInfo.paymentMeansCode && !paymentMeans.getElementsByTagName('cbc:PaymentMeansCode')[0]) {
      const meansCode = doc.createElement('cbc:PaymentMeansCode');
      meansCode.textContent = paymentInfo.paymentMeansCode;
      paymentMeans.appendChild(meansCode);
    }
    
    // Add PaymentID
    if (paymentInfo.paymentID && !paymentMeans.getElementsByTagName('cbc:PaymentID')[0]) {
      const paymentId = doc.createElement('cbc:PaymentID');
      paymentId.textContent = paymentInfo.paymentID;
      paymentMeans.appendChild(paymentId);
    }
    
    // Add PaymentDueDate
    if (paymentInfo.paymentDueDate && !paymentMeans.getElementsByTagName('cbc:PaymentDueDate')[0]) {
      const dueDate = doc.createElement('cbc:PaymentDueDate');
      dueDate.textContent = paymentInfo.paymentDueDate;
      paymentMeans.appendChild(dueDate);
    }
    
    // Add IBAN and BIC
    if (paymentInfo.iban || paymentInfo.bic) {
      let payeeAccount = paymentMeans.getElementsByTagName('cac:PayeeFinancialAccount')[0];
      if (!payeeAccount) {
        payeeAccount = doc.createElement('cac:PayeeFinancialAccount');
        paymentMeans.appendChild(payeeAccount);
      }
      
      // Add IBAN
      if (paymentInfo.iban && !payeeAccount.getElementsByTagName('cbc:ID')[0]) {
        const iban = doc.createElement('cbc:ID');
        iban.textContent = paymentInfo.iban;
        payeeAccount.appendChild(iban);
      }
      
      // Add account name (must come after ID but before FinancialInstitutionBranch)
      if (paymentInfo.accountName && !payeeAccount.getElementsByTagName('cbc:Name')[0]) {
        const accountName = doc.createElement('cbc:Name');
        accountName.textContent = paymentInfo.accountName;
        // Insert after ID but before FinancialInstitutionBranch
        const id = payeeAccount.getElementsByTagName('cbc:ID')[0];
        const finInstBranch = payeeAccount.getElementsByTagName('cac:FinancialInstitutionBranch')[0];
        if (finInstBranch) {
          payeeAccount.insertBefore(accountName, finInstBranch);
        } else if (id && id.nextSibling) {
          payeeAccount.insertBefore(accountName, id.nextSibling);
        } else {
          payeeAccount.appendChild(accountName);
        }
      }
      
      // Add BIC and bank name
      if (paymentInfo.bic || paymentInfo.bankName) {
        let finInstBranch = payeeAccount.getElementsByTagName('cac:FinancialInstitutionBranch')[0];
        if (!finInstBranch) {
          finInstBranch = doc.createElement('cac:FinancialInstitutionBranch');
          payeeAccount.appendChild(finInstBranch);
        }
        
        // Add BIC as branch ID
        if (paymentInfo.bic && !finInstBranch.getElementsByTagName('cbc:ID')[0]) {
          const bicElement = doc.createElement('cbc:ID');
          bicElement.textContent = paymentInfo.bic;
          finInstBranch.appendChild(bicElement);
        }
        
        // Add bank name
        if (paymentInfo.bankName && !finInstBranch.getElementsByTagName('cbc:Name')[0]) {
          const bankNameElement = doc.createElement('cbc:Name');
          bankNameElement.textContent = paymentInfo.bankName;
          finInstBranch.appendChild(bankNameElement);
        }
      }
    }
    
    // Add payment terms with discount if available
    if (paymentInfo.paymentTermsNote && paymentInfo.paymentTermsNote.includes('early payment')) {
      let paymentTerms = root.getElementsByTagName('cac:PaymentTerms')[0];
      if (!paymentTerms) {
        paymentTerms = doc.createElement('cac:PaymentTerms');
        // Insert before PaymentMeans
        const paymentMeans = root.getElementsByTagName('cac:PaymentMeans')[0];
        if (paymentMeans && paymentMeans.parentNode) {
          paymentMeans.parentNode.insertBefore(paymentTerms, paymentMeans);
        }
      }
      
      // Update or add note
      let note = paymentTerms.getElementsByTagName('cbc:Note')[0];
      if (!note) {
        note = doc.createElement('cbc:Note');
        paymentTerms.appendChild(note);
      }
      note.textContent = paymentInfo.paymentTermsNote;
      
      // Add discount percent if available
      if (paymentInfo.discountPercent && !paymentTerms.getElementsByTagName('cbc:SettlementDiscountPercent')[0]) {
        const discountElement = doc.createElement('cbc:SettlementDiscountPercent');
        discountElement.textContent = paymentInfo.discountPercent;
        paymentTerms.appendChild(discountElement);
      }
    }
  }

  /**
   * Adds date information from metadata to UBL document
   * @param doc XML document
   * @param root Root element
   * @param dateInfo Date information from metadata
   */
  private addDateInformationUBL(doc: Document, root: Element, dateInfo?: any): void {
    if (!dateInfo) return;
    
    // Add InvoicePeriod
    if ((dateInfo.periodStart || dateInfo.periodEnd) && !root.getElementsByTagName('cac:InvoicePeriod')[0]) {
      const invoicePeriod = doc.createElement('cac:InvoicePeriod');
      
      if (dateInfo.periodStart) {
        const startDate = doc.createElement('cbc:StartDate');
        startDate.textContent = dateInfo.periodStart;
        invoicePeriod.appendChild(startDate);
      }
      
      if (dateInfo.periodEnd) {
        const endDate = doc.createElement('cbc:EndDate');
        endDate.textContent = dateInfo.periodEnd;
        invoicePeriod.appendChild(endDate);
      }
      
      // Insert after business references or DocumentCurrencyCode
      const projectRef = root.getElementsByTagName('cac:ProjectReference')[0];
      const contractRef = root.getElementsByTagName('cac:ContractDocumentReference')[0];
      const orderRef = root.getElementsByTagName('cac:OrderReference')[0];
      const insertAfter = projectRef || contractRef || orderRef || root.getElementsByTagName('cbc:DocumentCurrencyCode')[0];
      if (insertAfter && insertAfter.parentNode) {
        insertAfter.parentNode.insertBefore(invoicePeriod, insertAfter.nextSibling);
      }
    }
    
    // Add Delivery with ActualDeliveryDate
    if (dateInfo.deliveryDate && !root.getElementsByTagName('cac:Delivery')[0]) {
      const delivery = doc.createElement('cac:Delivery');
      const deliveryDate = doc.createElement('cbc:ActualDeliveryDate');
      deliveryDate.textContent = dateInfo.deliveryDate;
      delivery.appendChild(deliveryDate);
      
      // Insert before PaymentMeans
      const paymentMeans = root.getElementsByTagName('cac:PaymentMeans')[0];
      if (paymentMeans && paymentMeans.parentNode) {
        paymentMeans.parentNode.insertBefore(delivery, paymentMeans);
      }
    }
  }

  /**
   * Enhances party information with contact details from metadata
   * @param doc XML document
   * @param invoice Invoice data
   */
  private enhancePartyInformationUBL(doc: Document, invoice: TInvoice): void {
    // Enhance supplier party
    this.enhancePartyUBL(doc, 'cac:AccountingSupplierParty', invoice.from);
    
    // Enhance customer party
    this.enhancePartyUBL(doc, 'cac:AccountingCustomerParty', invoice.to);
  }
  
  /**
   * Enhances a party with GLN, additional identifiers, and contact info
   * @param doc XML document
   * @param partySelector Party selector
   * @param partyData Party data
   */
  private enhancePartyUBL(doc: Document, partySelector: string, partyData: any): void {
    if (!partyData) return;
    
    const partyContainer = doc.getElementsByTagName(partySelector)[0];
    if (!partyContainer) return;
    
    const party = partyContainer.getElementsByTagName('cac:Party')[0];
    if (!party) return;
    
    // Add GLN if available
    if (partyData.gln && !party.getElementsByTagName('cbc:EndpointID')[0]) {
      const endpointNode = doc.createElement('cbc:EndpointID');
      endpointNode.setAttribute('schemeID', '0088'); // GLN scheme ID
      endpointNode.textContent = partyData.gln;
      
      // Insert as first child
      if (party.firstChild) {
        party.insertBefore(endpointNode, party.firstChild);
      } else {
        party.appendChild(endpointNode);
      }
    }
    
    // Add additional identifiers
    if (partyData.additionalIdentifiers && Array.isArray(partyData.additionalIdentifiers)) {
      for (const identifier of partyData.additionalIdentifiers) {
        const partyId = doc.createElement('cac:PartyIdentification');
        const id = doc.createElement('cbc:ID');
        if (identifier.scheme) {
          id.setAttribute('schemeID', identifier.scheme);
        }
        id.textContent = identifier.value;
        partyId.appendChild(id);
        
        // Insert after EndpointID or at beginning
        const endpoint = party.getElementsByTagName('cbc:EndpointID')[0];
        if (endpoint && endpoint.nextSibling) {
          party.insertBefore(partyId, endpoint.nextSibling);
        } else if (party.firstChild) {
          party.insertBefore(partyId, party.firstChild);
        } else {
          party.appendChild(partyId);
        }
      }
    }
    
    // Add contact information from metadata if not already present
    const contactInfo = partyData.metadata?.contactInformation;
    if (contactInfo) {
      this.addContactToPartyUBL(doc, partySelector, contactInfo);
    }
  }

  /**
   * Adds contact information to a party in UBL document
   * @param doc XML document
   * @param partySelector Party selector
   * @param contactInfo Contact information from metadata
   */
  private addContactToPartyUBL(doc: Document, partySelector: string, contactInfo?: any): void {
    if (!contactInfo) return;
    
    const partyContainer = doc.getElementsByTagName(partySelector)[0];
    if (!partyContainer) return;
    
    const party = partyContainer.getElementsByTagName('cac:Party')[0];
    if (!party) return;
    
    // Check if Contact already exists
    let contact = party.getElementsByTagName('cac:Contact')[0];
    if (!contact && (contactInfo.name || contactInfo.phone || contactInfo.email)) {
      contact = doc.createElement('cac:Contact');
      
      // Insert after PartyName
      const partyName = party.getElementsByTagName('cac:PartyName')[0];
      if (partyName && partyName.parentNode) {
        partyName.parentNode.insertBefore(contact, partyName.nextSibling);
      } else {
        party.appendChild(contact);
      }
    }
    
    if (contact) {
      // Add contact name
      if (contactInfo.name && !contact.getElementsByTagName('cbc:Name')[0]) {
        const name = doc.createElement('cbc:Name');
        name.textContent = contactInfo.name;
        contact.appendChild(name);
      }
      
      // Add telephone
      if (contactInfo.phone && !contact.getElementsByTagName('cbc:Telephone')[0]) {
        const phone = doc.createElement('cbc:Telephone');
        phone.textContent = contactInfo.phone;
        contact.appendChild(phone);
      }
      
      // Add email
      if (contactInfo.email && !contact.getElementsByTagName('cbc:ElectronicMail')[0]) {
        const email = doc.createElement('cbc:ElectronicMail');
        email.textContent = contactInfo.email;
        contact.appendChild(email);
      }
    }
  }

  /**
   * Enhances line items with metadata in UBL document
   * @param doc XML document
   * @param invoice Invoice data
   */
  private enhanceLineItemsUBL(doc: Document, invoice: TInvoice): void {
    const invoiceLines = doc.getElementsByTagName('cac:InvoiceLine');
    
    for (let i = 0; i < invoiceLines.length && i < invoice.items.length; i++) {
      const line = invoiceLines[i];
      const item = invoice.items[i];
      const itemMetadata = (item as any).metadata;
      
      if (!itemMetadata) continue;
      
      // Add OrderLineReference if available
      if (itemMetadata.orderLineReference && !line.getElementsByTagName('cac:OrderLineReference')[0]) {
        const orderLineRef = doc.createElement('cac:OrderLineReference');
        const lineId = doc.createElement('cbc:LineID');
        lineId.textContent = itemMetadata.orderLineReferenceId || '1';
        orderLineRef.appendChild(lineId);
        
        if (itemMetadata.orderLineReference) {
          const orderRef = doc.createElement('cac:OrderReference');
          const orderId = doc.createElement('cbc:ID');
          orderId.textContent = itemMetadata.orderLineReference;
          orderRef.appendChild(orderId);
          orderLineRef.appendChild(orderRef);
        }
        
        // Insert after ID
        const invoiceLineId = line.getElementsByTagName('cbc:ID')[0];
        if (invoiceLineId && invoiceLineId.nextSibling) {
          line.insertBefore(orderLineRef, invoiceLineId.nextSibling);
        } else {
          // Insert before InvoicedQuantity
          const quantity = line.getElementsByTagName('cbc:InvoicedQuantity')[0];
          if (quantity) {
            line.insertBefore(orderLineRef, quantity);
          }
        }
      }
      
      const itemElement = line.getElementsByTagName('cac:Item')[0];
      if (!itemElement) continue;
      
      // Add item description
      if (itemMetadata.description && !itemElement.getElementsByTagName('cbc:Description')[0]) {
        const desc = doc.createElement('cbc:Description');
        desc.textContent = itemMetadata.description;
        // Insert before Name
        const name = itemElement.getElementsByTagName('cbc:Name')[0];
        if (name && name.parentNode) {
          name.parentNode.insertBefore(desc, name);
        } else {
          itemElement.appendChild(desc);
        }
      }
      
      // Add SellersItemIdentification
      if (item.articleNumber && !itemElement.getElementsByTagName('cac:SellersItemIdentification')[0]) {
        const sellerId = doc.createElement('cac:SellersItemIdentification');
        const id = doc.createElement('cbc:ID');
        id.textContent = item.articleNumber;
        sellerId.appendChild(id);
        itemElement.appendChild(sellerId);
      }
      
      // Add BuyersItemIdentification
      if (itemMetadata.buyerItemID && !itemElement.getElementsByTagName('cac:BuyersItemIdentification')[0]) {
        const buyerId = doc.createElement('cac:BuyersItemIdentification');
        const id = doc.createElement('cbc:ID');
        id.textContent = itemMetadata.buyerItemID;
        buyerId.appendChild(id);
        itemElement.appendChild(buyerId);
      }
      
      // Add StandardItemIdentification
      if (itemMetadata.standardItemID && !itemElement.getElementsByTagName('cac:StandardItemIdentification')[0]) {
        const standardId = doc.createElement('cac:StandardItemIdentification');
        const id = doc.createElement('cbc:ID');
        id.textContent = itemMetadata.standardItemID;
        standardId.appendChild(id);
        itemElement.appendChild(standardId);
      }
      
      // Add CommodityClassification
      if (itemMetadata.commodityClassification && !itemElement.getElementsByTagName('cac:CommodityClassification')[0]) {
        const classification = doc.createElement('cac:CommodityClassification');
        const code = doc.createElement('cbc:ItemClassificationCode');
        code.textContent = itemMetadata.commodityClassification;
        classification.appendChild(code);
        itemElement.appendChild(classification);
      }
      
      // Add additional item properties
      if (itemMetadata.additionalProperties) {
        for (const [propName, propValue] of Object.entries(itemMetadata.additionalProperties)) {
          const additionalProp = doc.createElement('cac:AdditionalItemProperty');
          
          const nameElement = doc.createElement('cbc:Name');
          nameElement.textContent = propName;
          additionalProp.appendChild(nameElement);
          
          const valueElement = doc.createElement('cbc:Value');
          valueElement.textContent = propValue as string;
          additionalProp.appendChild(valueElement);
          
          itemElement.appendChild(additionalProp);
        }
      }
    }
  }
}