import { CIIBaseEncoder } from '../cii.encoder.js';
import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js';
import { ZUGFERD_PROFILE_IDS } from './zugferd.types.js';
import { CIIProfile } from '../cii.types.js';
import { DOMParser, XMLSerializer } from '../../../plugins.js';

/**
 * Encoder for ZUGFeRD invoice format
 */
export class ZUGFeRDEncoder extends CIIBaseEncoder {
  constructor() {
    super();
    // Set default profile to BASIC
    this.profile = CIIProfile.BASIC;
  }

  /**
   * Encodes a credit note into ZUGFeRD XML
   * @param creditNote Credit note to encode
   * @returns ZUGFeRD XML string
   */
  protected async encodeCreditNote(creditNote: TCreditNote): Promise<string> {
    // Create base XML
    const xmlDoc = this.createBaseXml();

    // Set document type code to credit note (381)
    this.setDocumentTypeCode(xmlDoc, '381');

    // Add common invoice data
    this.addCommonInvoiceData(xmlDoc, creditNote);

    // Serialize to string
    return new XMLSerializer().serializeToString(xmlDoc);
  }

  /**
   * Encodes a debit note (invoice) into ZUGFeRD XML
   * @param debitNote Debit note to encode
   * @returns ZUGFeRD XML string
   */
  protected async encodeDebitNote(debitNote: TDebitNote): Promise<string> {
    // Create base XML
    const xmlDoc = this.createBaseXml();

    // Set document type code to invoice (380)
    this.setDocumentTypeCode(xmlDoc, '380');

    // Add common invoice data
    this.addCommonInvoiceData(xmlDoc, debitNote);

    // Serialize to string
    return new XMLSerializer().serializeToString(xmlDoc);
  }

  /**
   * Creates a base ZUGFeRD XML document
   * @returns XML document with basic structure
   */
  private createBaseXml(): Document {
    // Create XML document from template
    const xmlString = this.createXmlRoot();
    const doc = new DOMParser().parseFromString(xmlString, 'application/xml');

    // Add ZUGFeRD profile
    this.addProfile(doc);

    return doc;
  }

  /**
   * Adds ZUGFeRD profile information to the XML document
   * @param doc XML document
   */
  private addProfile(doc: Document): void {
    // Get root element
    const root = doc.documentElement;

    // Create context element if it doesn't exist
    let contextElement = root.getElementsByTagName('rsm:ExchangedDocumentContext')[0];
    if (!contextElement) {
      contextElement = doc.createElement('rsm:ExchangedDocumentContext');
      root.appendChild(contextElement);
    }

    // Create guideline parameter element
    const guidelineElement = doc.createElement('ram:GuidelineSpecifiedDocumentContextParameter');
    contextElement.appendChild(guidelineElement);

    // Add ID element with profile
    const idElement = doc.createElement('ram:ID');

    // Set profile based on the selected profile
    let profileId = ZUGFERD_PROFILE_IDS.BASIC;
    if (this.profile === CIIProfile.COMFORT) {
      profileId = ZUGFERD_PROFILE_IDS.COMFORT;
    } else if (this.profile === CIIProfile.EXTENDED) {
      profileId = ZUGFERD_PROFILE_IDS.EXTENDED;
    }

    idElement.textContent = profileId;
    guidelineElement.appendChild(idElement);
  }

  /**
   * Sets the document type code in the XML document
   * @param doc XML document
   * @param typeCode Document type code (380 for invoice, 381 for credit note)
   */
  private setDocumentTypeCode(doc: Document, typeCode: string): void {
    // Get root element
    const root = doc.documentElement;

    // Create document element if it doesn't exist
    let documentElement = root.getElementsByTagName('rsm:ExchangedDocument')[0];
    if (!documentElement) {
      documentElement = doc.createElement('rsm:ExchangedDocument');
      root.appendChild(documentElement);
    }

    // Add type code element
    const typeCodeElement = doc.createElement('ram:TypeCode');
    typeCodeElement.textContent = typeCode;
    documentElement.appendChild(typeCodeElement);
  }

  /**
   * Adds common invoice data to the XML document
   * @param doc XML document
   * @param invoice Invoice data
   */
  private addCommonInvoiceData(doc: Document, invoice: TInvoice): void {
    // Get root element
    const root = doc.documentElement;

    // Get document element or create it
    let documentElement = root.getElementsByTagName('rsm:ExchangedDocument')[0];
    if (!documentElement) {
      documentElement = doc.createElement('rsm:ExchangedDocument');
      root.appendChild(documentElement);
    }

    // Add ID element
    const idElement = doc.createElement('ram:ID');
    idElement.textContent = invoice.id;
    documentElement.appendChild(idElement);

    // Add issue date element
    const issueDateElement = doc.createElement('ram:IssueDateTime');
    const dateStringElement = doc.createElement('udt:DateTimeString');
    dateStringElement.setAttribute('format', '102'); // YYYYMMDD format
    dateStringElement.textContent = this.formatDateYYYYMMDD(invoice.date);
    issueDateElement.appendChild(dateStringElement);
    documentElement.appendChild(issueDateElement);

    // Add notes if available
    if (invoice.notes && invoice.notes.length > 0) {
      for (const note of invoice.notes) {
        const noteElement = doc.createElement('ram:IncludedNote');
        const contentElement = doc.createElement('ram:Content');
        contentElement.textContent = note;
        noteElement.appendChild(contentElement);
        documentElement.appendChild(noteElement);
      }
    }

    // Create transaction element if it doesn't exist
    let transactionElement = root.getElementsByTagName('rsm:SupplyChainTradeTransaction')[0];
    if (!transactionElement) {
      transactionElement = doc.createElement('rsm:SupplyChainTradeTransaction');
      root.appendChild(transactionElement);
    }

    // Add agreement section with seller and buyer
    this.addAgreementSection(doc, transactionElement, invoice);

    // Add delivery section
    this.addDeliverySection(doc, transactionElement, invoice);

    // Add settlement section with payment terms and totals
    this.addSettlementSection(doc, transactionElement, invoice);

    // Add line items
    this.addLineItems(doc, transactionElement, invoice);
  }

  /**
   * Adds agreement section with seller and buyer information
   * @param doc XML document
   * @param transactionElement Transaction element
   * @param invoice Invoice data
   */
  private addAgreementSection(doc: Document, transactionElement: Element, invoice: TInvoice): void {
    // Create agreement element
    const agreementElement = doc.createElement('ram:ApplicableHeaderTradeAgreement');
    transactionElement.appendChild(agreementElement);

    // Add buyer reference if available
    if (invoice.buyerReference) {
      const buyerRefElement = doc.createElement('ram:BuyerReference');
      buyerRefElement.textContent = invoice.buyerReference;
      agreementElement.appendChild(buyerRefElement);
    }

    // Add seller
    const sellerElement = doc.createElement('ram:SellerTradeParty');
    this.addPartyInfo(doc, sellerElement, invoice.from);
    
    // Add seller electronic address if available
    if (invoice.electronicAddress && invoice.from.type === 'company') {
      const contactElement = doc.createElement('ram:DefinedTradeContact');
      const uriElement = doc.createElement('ram:URIID');
      uriElement.setAttribute('schemeID', invoice.electronicAddress.scheme);
      uriElement.textContent = invoice.electronicAddress.value;
      contactElement.appendChild(uriElement);
      sellerElement.appendChild(contactElement);
    }
    
    agreementElement.appendChild(sellerElement);

    // Add buyer
    const buyerElement = doc.createElement('ram:BuyerTradeParty');
    this.addPartyInfo(doc, buyerElement, invoice.to);
    agreementElement.appendChild(buyerElement);
  }

  /**
   * Adds party information to an element
   * @param doc XML document
   * @param partyElement Party element
   * @param party Party data
   */
  private addPartyInfo(doc: Document, partyElement: Element, party: any): void {
    // Add name
    const nameElement = doc.createElement('ram:Name');
    nameElement.textContent = party.name;
    partyElement.appendChild(nameElement);

    // Add postal address
    const addressElement = doc.createElement('ram:PostalTradeAddress');

    // Add address line 1 (street)
    if (party.address.streetName) {
      const line1Element = doc.createElement('ram:LineOne');
      line1Element.textContent = party.address.streetName;
      addressElement.appendChild(line1Element);
    }

    // Add address line 2 (house number) if present
    if (party.address.houseNumber && party.address.houseNumber !== '0') {
      const line2Element = doc.createElement('ram:LineTwo');
      line2Element.textContent = party.address.houseNumber;
      addressElement.appendChild(line2Element);
    }

    // Add postal code
    if (party.address.postalCode) {
      const postalCodeElement = doc.createElement('ram:PostcodeCode');
      postalCodeElement.textContent = party.address.postalCode;
      addressElement.appendChild(postalCodeElement);
    }

    // Add city
    if (party.address.city) {
      const cityElement = doc.createElement('ram:CityName');
      cityElement.textContent = party.address.city;
      addressElement.appendChild(cityElement);
    }

    // Add country
    if (party.address.country || party.address.countryCode) {
      const countryElement = doc.createElement('ram:CountryID');
      countryElement.textContent = party.address.countryCode || party.address.country;
      addressElement.appendChild(countryElement);
    }

    partyElement.appendChild(addressElement);

    // Add VAT ID if available
    if (party.registrationDetails && party.registrationDetails.vatId) {
      const taxRegistrationElement = doc.createElement('ram:SpecifiedTaxRegistration');
      const taxIdElement = doc.createElement('ram:ID');
      taxIdElement.setAttribute('schemeID', 'VA');
      taxIdElement.textContent = party.registrationDetails.vatId;
      taxRegistrationElement.appendChild(taxIdElement);
      partyElement.appendChild(taxRegistrationElement);
    }

    // Add registration ID if available
    if (party.registrationDetails && party.registrationDetails.registrationId) {
      const regRegistrationElement = doc.createElement('ram:SpecifiedTaxRegistration');
      const regIdElement = doc.createElement('ram:ID');
      regIdElement.setAttribute('schemeID', 'FC');
      regIdElement.textContent = party.registrationDetails.registrationId;
      regRegistrationElement.appendChild(regIdElement);
      partyElement.appendChild(regRegistrationElement);
    }
  }

  /**
   * Adds delivery section with delivery information
   * @param doc XML document
   * @param transactionElement Transaction element
   * @param invoice Invoice data
   */
  private addDeliverySection(doc: Document, transactionElement: Element, invoice: TInvoice): void {
    // Create delivery element
    const deliveryElement = doc.createElement('ram:ApplicableHeaderTradeDelivery');
    transactionElement.appendChild(deliveryElement);

    // Add delivery date if available
    if (invoice.deliveryDate) {
      const deliveryDateElement = doc.createElement('ram:ActualDeliverySupplyChainEvent');
      const occurrenceDateElement = doc.createElement('ram:OccurrenceDateTime');
      const dateStringElement = doc.createElement('udt:DateTimeString');
      dateStringElement.setAttribute('format', '102'); // YYYYMMDD format
      dateStringElement.textContent = this.formatDateYYYYMMDD(invoice.deliveryDate);
      occurrenceDateElement.appendChild(dateStringElement);
      deliveryDateElement.appendChild(occurrenceDateElement);
      deliveryElement.appendChild(deliveryDateElement);
    }

    // Add period of performance if available
    if (invoice.periodOfPerformance) {
      const periodElement = doc.createElement('ram:BillingSpecifiedPeriod');
      
      // Start date
      if (invoice.periodOfPerformance.from) {
        const startDateElement = doc.createElement('ram:StartDateTime');
        const startDateStringElement = doc.createElement('udt:DateTimeString');
        startDateStringElement.setAttribute('format', '102'); // YYYYMMDD format
        startDateStringElement.textContent = this.formatDateYYYYMMDD(invoice.periodOfPerformance.from);
        startDateElement.appendChild(startDateStringElement);
        periodElement.appendChild(startDateElement);
      }
      
      // End date
      if (invoice.periodOfPerformance.to) {
        const endDateElement = doc.createElement('ram:EndDateTime');
        const endDateStringElement = doc.createElement('udt:DateTimeString');
        endDateStringElement.setAttribute('format', '102'); // YYYYMMDD format
        endDateStringElement.textContent = this.formatDateYYYYMMDD(invoice.periodOfPerformance.to);
        endDateElement.appendChild(endDateStringElement);
        periodElement.appendChild(endDateElement);
      }
      
      deliveryElement.appendChild(periodElement);
    }
  }

  /**
   * Adds settlement section with payment terms and totals
   * @param doc XML document
   * @param transactionElement Transaction element
   * @param invoice Invoice data
   */
  private addSettlementSection(doc: Document, transactionElement: Element, invoice: TInvoice): void {
    // Create settlement element
    const settlementElement = doc.createElement('ram:ApplicableHeaderTradeSettlement');
    transactionElement.appendChild(settlementElement);

    // Add currency
    const currencyElement = doc.createElement('ram:InvoiceCurrencyCode');
    currencyElement.textContent = invoice.currency;
    settlementElement.appendChild(currencyElement);

    // Add payment terms
    const paymentTermsElement = doc.createElement('ram:SpecifiedTradePaymentTerms');

    // Add payment instructions if available
    if (invoice.paymentOptions) {
      // Add payment instructions as description - this is generic enough to work with any payment type
      const descriptionElement = doc.createElement('ram:Description');
      descriptionElement.textContent = `Due in ${invoice.dueInDays} days. ${invoice.paymentOptions.description || ''}`;
      paymentTermsElement.appendChild(descriptionElement);
    }

    // Add due date
    const dueDateElement = doc.createElement('ram:DueDateDateTime');
    const dateStringElement = doc.createElement('udt:DateTimeString');
    dateStringElement.setAttribute('format', '102'); // YYYYMMDD format

    // Calculate due date
    const dueDate = new Date(invoice.date);
    dueDate.setDate(dueDate.getDate() + invoice.dueInDays);

    dateStringElement.textContent = this.formatDateYYYYMMDD(dueDate.getTime());
    dueDateElement.appendChild(dateStringElement);
    paymentTermsElement.appendChild(dueDateElement);

    settlementElement.appendChild(paymentTermsElement);

    // Add payment means if available (using a generic approach)
    if (invoice.paymentOptions) {
      const paymentMeansElement = doc.createElement('ram:SpecifiedTradeSettlementPaymentMeans');
      
      // Payment type code (58 for SEPA transfer as default)
      const typeCodeElement = doc.createElement('ram:TypeCode');
      typeCodeElement.textContent = '58';
      paymentMeansElement.appendChild(typeCodeElement);
      
      // Description (optional)
      if (invoice.paymentOptions.description) {
        const infoElement = doc.createElement('ram:Information');
        infoElement.textContent = invoice.paymentOptions.description;
        paymentMeansElement.appendChild(infoElement);
      }
      
      // If payment details are available in a standard format
      if (invoice.paymentOptions.sepaConnection.iban) {
        // Payee account
        const payeeAccountElement = doc.createElement('ram:PayeePartyCreditorFinancialAccount');
        const ibanElement = doc.createElement('ram:IBANID');
        ibanElement.textContent = invoice.paymentOptions.sepaConnection.iban;
        payeeAccountElement.appendChild(ibanElement);
        paymentMeansElement.appendChild(payeeAccountElement);
        
        // Payee financial institution if BIC available
        if (invoice.paymentOptions.sepaConnection.bic) {
          const institutionElement = doc.createElement('ram:PayeeSpecifiedCreditorFinancialInstitution');
          const bicElement = doc.createElement('ram:BICID');
          bicElement.textContent = invoice.paymentOptions.sepaConnection.bic;
          institutionElement.appendChild(bicElement);
          paymentMeansElement.appendChild(institutionElement);
        }
      }
      
      settlementElement.appendChild(paymentMeansElement);
    }

    // Add tax details
    this.addTaxDetails(doc, settlementElement, invoice);

    // Add totals
    this.addMonetarySummation(doc, settlementElement, invoice);
  }

  /**
   * Adds tax details to the settlement section
   * @param doc XML document
   * @param settlementElement Settlement element
   * @param invoice Invoice data
   */
  private addTaxDetails(doc: Document, settlementElement: Element, invoice: TInvoice): void {
    // Calculate tax categories and totals
    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 vatRate = item.vatPercentage;
        
        const currentAmount = taxCategories.get(vatRate) || 0;
        taxCategories.set(vatRate, currentAmount + itemNetAmount);
      }
    }

    // Add each tax category
    for (const [rate, baseAmount] of taxCategories.entries()) {
      const taxElement = doc.createElement('ram:ApplicableTradeTax');
      
      // Calculate tax amount
      const taxAmount = baseAmount * (rate / 100);
      
      // Add calculated amount
      const calculatedAmountElement = doc.createElement('ram:CalculatedAmount');
      calculatedAmountElement.textContent = taxAmount.toFixed(2);
      taxElement.appendChild(calculatedAmountElement);
      
      // Add type code (VAT)
      const typeCodeElement = doc.createElement('ram:TypeCode');
      typeCodeElement.textContent = 'VAT';
      taxElement.appendChild(typeCodeElement);
      
      // Add basis amount
      const basisAmountElement = doc.createElement('ram:BasisAmount');
      basisAmountElement.textContent = baseAmount.toFixed(2);
      taxElement.appendChild(basisAmountElement);
      
      // Add category code
      const categoryCodeElement = doc.createElement('ram:CategoryCode');
      categoryCodeElement.textContent = invoice.reverseCharge ? 'AE' : 'S';
      taxElement.appendChild(categoryCodeElement);
      
      // Add rate
      const rateElement = doc.createElement('ram:RateApplicablePercent');
      rateElement.textContent = rate.toString();
      taxElement.appendChild(rateElement);
      
      settlementElement.appendChild(taxElement);
    }
  }

  /**
   * Adds monetary summation to the settlement section
   * @param doc XML document
   * @param settlementElement Settlement element
   * @param invoice Invoice data
   */
  private addMonetarySummation(doc: Document, settlementElement: Element, invoice: TInvoice): void {
    const monetarySummationElement = doc.createElement('ram:SpecifiedTradeSettlementHeaderMonetarySummation');

    // 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;

    // Add line total amount
    const lineTotalElement = doc.createElement('ram:LineTotalAmount');
    lineTotalElement.textContent = totalNetAmount.toFixed(2);
    monetarySummationElement.appendChild(lineTotalElement);

    // Add tax total amount
    const taxTotalElement = doc.createElement('ram:TaxTotalAmount');
    taxTotalElement.textContent = totalTaxAmount.toFixed(2);
    taxTotalElement.setAttribute('currencyID', invoice.currency);
    monetarySummationElement.appendChild(taxTotalElement);

    // Add grand total amount
    const grandTotalElement = doc.createElement('ram:GrandTotalAmount');
    grandTotalElement.textContent = totalGrossAmount.toFixed(2);
    monetarySummationElement.appendChild(grandTotalElement);

    // Add due payable amount
    const duePayableElement = doc.createElement('ram:DuePayableAmount');
    duePayableElement.textContent = totalGrossAmount.toFixed(2);
    monetarySummationElement.appendChild(duePayableElement);

    settlementElement.appendChild(monetarySummationElement);
  }

  /**
   * Adds line items to the XML document
   * @param doc XML document
   * @param transactionElement Transaction element
   * @param invoice Invoice data
   */
  private addLineItems(doc: Document, transactionElement: Element, invoice: TInvoice): void {
    // Add each line item
    if (invoice.items) {
      for (const item of invoice.items) {
        // Create line item element
        const lineItemElement = doc.createElement('ram:IncludedSupplyChainTradeLineItem');

        // Add line ID
        const lineIdElement = doc.createElement('ram:AssociatedDocumentLineDocument');
        const lineIdValueElement = doc.createElement('ram:LineID');
        lineIdValueElement.textContent = item.position.toString();
        lineIdElement.appendChild(lineIdValueElement);
        lineItemElement.appendChild(lineIdElement);

        // Add product information
        const productElement = doc.createElement('ram:SpecifiedTradeProduct');

        // Add name
        const nameElement = doc.createElement('ram:Name');
        nameElement.textContent = item.name;
        productElement.appendChild(nameElement);

        // Add article number if available
        if (item.articleNumber) {
          const articleNumberElement = doc.createElement('ram:SellerAssignedID');
          articleNumberElement.textContent = item.articleNumber;
          productElement.appendChild(articleNumberElement);
        }

        lineItemElement.appendChild(productElement);

        // Add agreement information (price)
        const agreementElement = doc.createElement('ram:SpecifiedLineTradeAgreement');
        const priceElement = doc.createElement('ram:NetPriceProductTradePrice');
        const chargeAmountElement = doc.createElement('ram:ChargeAmount');
        chargeAmountElement.textContent = item.unitNetPrice.toFixed(2);
        priceElement.appendChild(chargeAmountElement);
        agreementElement.appendChild(priceElement);
        lineItemElement.appendChild(agreementElement);

        // Add delivery information (quantity)
        const deliveryElement = doc.createElement('ram:SpecifiedLineTradeDelivery');
        const quantityElement = doc.createElement('ram:BilledQuantity');
        quantityElement.textContent = item.unitQuantity.toString();
        quantityElement.setAttribute('unitCode', item.unitType);
        deliveryElement.appendChild(quantityElement);
        lineItemElement.appendChild(deliveryElement);

        // Add settlement information (tax)
        const settlementElement = doc.createElement('ram:SpecifiedLineTradeSettlement');

        // Add tax information
        const taxElement = doc.createElement('ram:ApplicableTradeTax');

        // Add tax type code
        const taxTypeCodeElement = doc.createElement('ram:TypeCode');
        taxTypeCodeElement.textContent = 'VAT';
        taxElement.appendChild(taxTypeCodeElement);

        // Add tax category code
        const taxCategoryCodeElement = doc.createElement('ram:CategoryCode');
        taxCategoryCodeElement.textContent = invoice.reverseCharge ? 'AE' : 'S';
        taxElement.appendChild(taxCategoryCodeElement);

        // Add tax rate
        const taxRateElement = doc.createElement('ram:RateApplicablePercent');
        taxRateElement.textContent = item.vatPercentage.toString();
        taxElement.appendChild(taxRateElement);

        settlementElement.appendChild(taxElement);

        // Add monetary summation
        const monetarySummationElement = doc.createElement('ram:SpecifiedLineTradeSettlementMonetarySummation');

        // Calculate item total
        const itemNetAmount = item.unitNetPrice * item.unitQuantity;

        // Add line total amount
        const lineTotalElement = doc.createElement('ram:LineTotalAmount');
        lineTotalElement.textContent = itemNetAmount.toFixed(2);
        monetarySummationElement.appendChild(lineTotalElement);

        settlementElement.appendChild(monetarySummationElement);

        lineItemElement.appendChild(settlementElement);

        // Add line item to transaction
        transactionElement.appendChild(lineItemElement);
      }
    }
  }

  /**
   * Formats a date as YYYYMMDD
   * @param timestamp Timestamp to format
   * @returns Formatted date string
   */
  private formatDateYYYYMMDD(timestamp: number): string {
    const date = new Date(timestamp);
    const year = date.getFullYear();
    const month = (date.getMonth() + 1).toString().padStart(2, '0');
    const day = date.getDate().toString().padStart(2, '0');
    return `${year}${month}${day}`;
  }
}