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

/**
 * Encoder for XRechnung (UBL) format
 * Extends the generic UBL encoder with XRechnung-specific customizations
 */
export class XRechnungEncoder extends UBLEncoder {
  /**
   * Encodes a credit note into XRechnung XML
   * @param creditNote Credit note to encode
   * @returns XRechnung XML string
   */
  protected async encodeCreditNote(creditNote: TCreditNote): Promise<string> {
    // First get the base UBL XML
    const baseXml = await super.encodeCreditNote(creditNote);
    
    // Parse and modify for XRechnung
    const doc = new DOMParser().parseFromString(baseXml, 'application/xml');
    this.applyXRechnungCustomizations(doc, creditNote as unknown as TInvoice);
    
    // Serialize back to string
    return new XMLSerializer().serializeToString(doc);
  }
  
  /**
   * Encodes a debit note (invoice) into XRechnung XML
   * @param debitNote Debit note to encode
   * @returns XRechnung XML string
   */
  protected async encodeDebitNote(debitNote: TDebitNote): Promise<string> {
    // First get the base UBL XML
    const baseXml = await super.encodeDebitNote(debitNote);
    
    // Parse and modify for XRechnung
    const doc = new DOMParser().parseFromString(baseXml, 'application/xml');
    this.applyXRechnungCustomizations(doc, debitNote as unknown as TInvoice);
    
    // Serialize back to string
    return new XMLSerializer().serializeToString(doc);
  }
  
  /**
   * Applies XRechnung-specific customizations to the document
   * @param doc XML document
   * @param invoice Invoice data
   */
  private applyXRechnungCustomizations(doc: Document, invoice: TInvoice): void {
    const root = doc.documentElement;
    
    // Extract metadata if available
    const metadata = (invoice as any).metadata?.extensions;
    
    // Update Customization ID to XRechnung 2.0
    const customizationId = root.getElementsByTagName('cbc:CustomizationID')[0];
    if (customizationId) {
      customizationId.textContent = 'urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.0';
    }
    
    // Add or update Buyer Reference (required for XRechnung)
    let buyerRef = root.getElementsByTagName('cbc:BuyerReference')[0];
    const buyerReferenceValue = (invoice as any).buyerReference || metadata?.businessReferences?.buyerReference || invoice.id;
    if (!buyerRef) {
      // Find where to insert it (after DocumentCurrencyCode)
      const currencyCode = root.getElementsByTagName('cbc:DocumentCurrencyCode')[0];
      if (currencyCode) {
        buyerRef = doc.createElement('cbc:BuyerReference');
        buyerRef.textContent = buyerReferenceValue;
        currencyCode.parentNode!.insertBefore(buyerRef, currencyCode.nextSibling);
      }
    } else if (!buyerRef.textContent || buyerRef.textContent.trim() === '') {
      buyerRef.textContent = buyerReferenceValue;
    }
    
    // Update payment terms to German
    const paymentTermsNotes = root.getElementsByTagName('cac:PaymentTerms');
    if (paymentTermsNotes.length > 0) {
      const noteElement = paymentTermsNotes[0].getElementsByTagName('cbc:Note')[0];
      if (noteElement && noteElement.textContent) {
        noteElement.textContent = `Zahlung innerhalb von ${invoice.dueInDays || 30} Tagen`;
      }
    }
    
    // Add electronic address for parties if available
    this.addElectronicAddressToParty(doc, 'cac:AccountingSupplierParty', invoice.from);
    this.addElectronicAddressToParty(doc, 'cac:AccountingCustomerParty', invoice.to);
    
    // Ensure payment reference is set
    const paymentMeans = root.getElementsByTagName('cac:PaymentMeans')[0];
    if (paymentMeans) {
      let paymentId = paymentMeans.getElementsByTagName('cbc:PaymentID')[0];
      if (!paymentId) {
        paymentId = doc.createElement('cbc:PaymentID');
        paymentId.textContent = invoice.id;
        paymentMeans.appendChild(paymentId);
      }
    }
    
    // Add country code handling for German addresses
    this.fixGermanCountryCodes(doc);
    
    // Preserve business references from metadata
    this.addBusinessReferences(doc, metadata?.businessReferences);
    
    // Preserve payment information from metadata
    this.enhancePaymentInformation(doc, metadata?.paymentInformation);
    
    // Preserve date information from metadata
    this.addDateInformation(doc, metadata?.dateInformation);
    
    // Enhance party information with contact details
    this.enhancePartyInformation(doc, invoice);
    
    // Enhance line items with metadata
    this.enhanceLineItems(doc, invoice);
  }
  
  /**
   * Adds electronic address to party if not already present
   * @param doc XML document
   * @param partyType Party type selector
   * @param party Party data
   */
  private addElectronicAddressToParty(doc: Document, partyType: string, party: any): void {
    const partyContainer = doc.getElementsByTagName(partyType)[0];
    if (!partyContainer) return;
    
    const partyElement = partyContainer.getElementsByTagName('cac:Party')[0];
    if (!partyElement) return;
    
    // Check if electronic address already exists
    const existingEndpoint = partyElement.getElementsByTagName('cbc:EndpointID')[0];
    if (!existingEndpoint && party.electronicAddress) {
      // Add electronic address at the beginning of party element
      const endpointNode = doc.createElement('cbc:EndpointID');
      endpointNode.setAttribute('schemeID', party.electronicAddress.scheme || '0204');
      endpointNode.textContent = party.electronicAddress.value;
      
      // Insert as first child of party element
      if (partyElement.firstChild) {
        partyElement.insertBefore(endpointNode, partyElement.firstChild);
      } else {
        partyElement.appendChild(endpointNode);
      }
    }
    
    // Add GLN (Global Location Number) if available
    if (party.gln && !existingEndpoint) {
      const endpointNode = doc.createElement('cbc:EndpointID');
      endpointNode.setAttribute('schemeID', '0088'); // GLN scheme ID
      endpointNode.textContent = party.gln;
      
      // Insert as first child of party element
      if (partyElement.firstChild) {
        partyElement.insertBefore(endpointNode, partyElement.firstChild);
      } else {
        partyElement.appendChild(endpointNode);
      }
    }
    
    // Add PartyIdentification for additional identifiers
    if (party.additionalIdentifiers) {
      for (const identifier of party.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 = partyElement.getElementsByTagName('cbc:EndpointID')[0];
        if (endpoint && endpoint.nextSibling) {
          partyElement.insertBefore(partyId, endpoint.nextSibling);
        } else if (partyElement.firstChild) {
          partyElement.insertBefore(partyId, partyElement.firstChild);
        } else {
          partyElement.appendChild(partyId);
        }
      }
    }
    
    // Add company registration number to PartyLegalEntity
    if (party.registrationDetails?.registrationId) {
      let legalEntity = partyElement.getElementsByTagName('cac:PartyLegalEntity')[0];
      if (!legalEntity) {
        legalEntity = doc.createElement('cac:PartyLegalEntity');
        // Insert after PostalAddress
        const postalAddress = partyElement.getElementsByTagName('cac:PostalAddress')[0];
        if (postalAddress && postalAddress.nextSibling) {
          partyElement.insertBefore(legalEntity, postalAddress.nextSibling);
        } else {
          partyElement.appendChild(legalEntity);
        }
      }
      
      // Add registration name if not present
      if (!legalEntity.getElementsByTagName('cbc:RegistrationName')[0]) {
        const regName = doc.createElement('cbc:RegistrationName');
        regName.textContent = party.registrationDetails.registrationName || party.name;
        legalEntity.appendChild(regName);
      }
      
      // Add company ID if not present
      if (!legalEntity.getElementsByTagName('cbc:CompanyID')[0]) {
        const companyId = doc.createElement('cbc:CompanyID');
        companyId.textContent = party.registrationDetails.registrationId;
        legalEntity.appendChild(companyId);
      }
    }
  }
  
  /**
   * Fixes German country codes in the document
   * @param doc XML document
   */
  private fixGermanCountryCodes(doc: Document): void {
    const countryNodes = doc.getElementsByTagName('cbc:IdentificationCode');
    for (let i = 0; i < countryNodes.length; i++) {
      const node = countryNodes[i];
      if (node.textContent) {
        const text = node.textContent.toLowerCase();
        if (text === 'germany' || text === 'deutschland' || text === 'de') {
          node.textContent = 'DE';
        } else if (text.length > 2) {
          // Try to use first 2 characters as country code
          node.textContent = text.substring(0, 2).toUpperCase();
        }
      }
    }
  }

  /**
   * Adds business references from metadata to the document
   * @param doc XML document
   * @param businessReferences Business references from metadata
   */
  private addBusinessReferences(doc: Document, businessReferences?: any): void {
    if (!businessReferences) return;
    
    const root = doc.documentElement;
    
    // 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 DocumentCurrencyCode
      const currencyCode = root.getElementsByTagName('cbc:DocumentCurrencyCode')[0];
      if (currencyCode && currencyCode.parentNode) {
        currencyCode.parentNode.insertBefore(orderRef, currencyCode.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
   * @param doc XML document
   * @param paymentInfo Payment information from metadata
   */
  private enhancePaymentInformation(doc: Document, paymentInfo?: any): void {
    if (!paymentInfo) return;
    
    const root = doc.documentElement;
    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
   * @param doc XML document
   * @param dateInfo Date information from metadata
   */
  private addDateInformation(doc: Document, dateInfo?: any): void {
    if (!dateInfo) return;
    
    const root = doc.documentElement;
    
    // 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 enhancePartyInformation(doc: Document, invoice: TInvoice): void {
    // Enhance supplier party
    this.addContactToParty(doc, 'cac:AccountingSupplierParty', (invoice.from as any)?.metadata?.contactInformation);
    
    // Enhance customer party
    this.addContactToParty(doc, 'cac:AccountingCustomerParty', (invoice.to as any)?.metadata?.contactInformation);
  }

  /**
   * Adds contact information to a party
   * @param doc XML document
   * @param partySelector Party selector
   * @param contactInfo Contact information from metadata
   */
  private addContactToParty(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
   * @param doc XML document
   * @param invoice Invoice data
   */
  private enhanceLineItems(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;
      
      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 (itemMetadata.buyerItemID && !itemElement.getElementsByTagName('cac:SellersItemIdentification')[0]) {
        const sellerId = doc.createElement('cac:SellersItemIdentification');
        const id = doc.createElement('cbc:ID');
        id.textContent = item.articleNumber || itemMetadata.buyerItemID;
        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);
        }
      }
    }
  }
}