import { CIIBaseEncoder } from '../cii.encoder.js';
import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js';
import { FACTURX_PROFILE_IDS } from './facturx.types.js';
import { DOMParser, XMLSerializer } from 'xmldom';

/**
 * Encoder for Factur-X invoice format
 */
export class FacturXEncoder extends CIIBaseEncoder {
  /**
   * Encodes a TCreditNote object into Factur-X XML
   * @param creditNote TCreditNote object to encode
   * @returns Factur-X 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 TDebitNote object into Factur-X XML
   * @param debitNote TDebitNote object to encode
   * @returns Factur-X 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 Factur-X 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 Factur-X profile
    this.addProfile(doc);

    return doc;
  }

  /**
   * Adds Factur-X 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 = FACTURX_PROFILE_IDS.EN16931;
    if (this.profile === 'BASIC') {
      profileId = FACTURX_PROFILE_IDS.BASIC;
    } else if (this.profile === 'MINIMUM') {
      profileId = FACTURX_PROFILE_IDS.MINIMUM;
    }

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

    // 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 seller
    const sellerElement = doc.createElement('ram:SellerTradeParty');
    this.addPartyInfo(doc, sellerElement, invoice.from);
    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)
    const line1Element = doc.createElement('ram:LineOne');
    line1Element.textContent = party.address.streetName;
    addressElement.appendChild(line1Element);

    // Add address line 2 (house number)
    const line2Element = doc.createElement('ram:LineTwo');
    line2Element.textContent = party.address.houseNumber;
    addressElement.appendChild(line2Element);

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

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

    // Add country
    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);
    }
  }

  /**
   * 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 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 totals
    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 = '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}`;
  }
}
