import * as plugins from './plugins.js';

/**
 * A class to convert a given ILetter with invoice data
 * into a minimal Factur-X / ZUGFeRD / EN16931-style XML.
 */
export class ZugferdXmlEncoder {

  constructor() {}

  public createZugferdXml(letterArg: plugins.tsclass.business.ILetter): string {
    // 1) Get your "SmartXml" or "xmlbuilder2" instance
    const smartxmlInstance = new plugins.smartxml.SmartXml();

    if (!letterArg?.content?.invoiceData) {
      throw new Error('Letter does not contain invoice data.');
    }

    const invoice: plugins.tsclass.finance.IInvoice = letterArg.content.invoiceData;
    const billedBy: plugins.tsclass.business.IContact = invoice.billedBy;
    const billedTo: plugins.tsclass.business.IContact = invoice.billedTo;

    // 2) Start building the document
    const doc = smartxmlInstance
      .create({ version: '1.0', encoding: 'UTF-8' })
      .ele('rsm:CrossIndustryInvoice', {
        'xmlns:rsm': 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100',
        'xmlns:udt': 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100',
        'xmlns:qdt': 'urn:un:unece:uncefact:data:standard:QualifiedDataType:100',
        'xmlns:ram': 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'
      });

    // 3) Exchanged Document Context
    doc.ele('rsm:ExchangedDocumentContext')
      .ele('ram:TestIndicator')
        .ele('udt:Indicator')
          .txt(this.isDraft(letterArg) ? 'true' : 'false')
        .up()
      .up()
    .up(); // </rsm:ExchangedDocumentContext>

    // 4) Exchanged Document (Invoice Header Info)
    const exchangedDoc = doc.ele('rsm:ExchangedDocument');
    exchangedDoc.ele('ram:ID').txt(invoice.id).up();
    exchangedDoc
      .ele('ram:TypeCode')
      // Usually: '380' = commercial invoice, '381' = credit note
      .txt(invoice.type === 'creditnote' ? '381' : '380')
      .up();
    exchangedDoc
      .ele('ram:IssueDateTime')
        .ele('udt:DateTimeString', { format: '102' })
          // Format 'YYYYMMDD' or 'YYYY-MM-DD'? Depending on standard
          .txt(this.formatDate(letterArg.date))
        .up()
      .up();
    exchangedDoc.up(); // </rsm:ExchangedDocument>

    // 5) Supply Chain Trade Transaction
    const supplyChainEle = doc.ele('rsm:SupplyChainTradeTransaction');

    // 5.1) Included Supply Chain Trade Line Items
    invoice.items.forEach((item) => {
      const lineItemEle = supplyChainEle.ele('ram:IncludedSupplyChainTradeLineItem');

      lineItemEle.ele('ram:SpecifiedTradeProduct')
        .ele('ram:Name')
        .txt(item.name)
        .up()
      .up(); // </ram:SpecifiedTradeProduct>

      lineItemEle.ele('ram:SpecifiedLineTradeAgreement')
        .ele('ram:GrossPriceProductTradePrice')
          .ele('ram:ChargeAmount')
          .txt(item.unitNetPrice.toFixed(2))
          .up()
        .up()
      .up(); // </ram:SpecifiedLineTradeAgreement>

      lineItemEle.ele('ram:SpecifiedLineTradeDelivery')
        .ele('ram:BilledQuantity', {
          '@unitCode': this.mapUnitType(item.unitType)
        })
        .txt(item.unitQuantity.toString())
        .up()
      .up(); // </ram:SpecifiedLineTradeDelivery>

      lineItemEle.ele('ram:SpecifiedLineTradeSettlement')
        .ele('ram:ApplicableTradeTax')
          .ele('ram:RateApplicablePercent')
          .txt(item.vatPercentage.toFixed(2))
          .up()
        .up()
        .ele('ram:SpecifiedTradeSettlementLineMonetarySummation')
          .ele('ram:LineTotalAmount')
          .txt(
            (
              item.unitQuantity *
              item.unitNetPrice *
              (1 + item.vatPercentage / 100)
            ).toFixed(2)
          )
          .up()
        .up()
      .up(); // </ram:SpecifiedLineTradeSettlement>
    });

    // 5.2) Applicable Header Trade Agreement
    const headerTradeAgreementEle = supplyChainEle.ele('ram:ApplicableHeaderTradeAgreement');
    // Seller
    const sellerPartyEle = headerTradeAgreementEle.ele('ram:SellerTradeParty');
    sellerPartyEle.ele('ram:Name').txt(billedBy.name).up();
    // Example: If it's a company, put company name, etc.
    const sellerAddressEle = sellerPartyEle.ele('ram:PostalTradeAddress');
    sellerAddressEle.ele('ram:PostcodeCode').txt(billedBy.address.postalCode).up();
    sellerAddressEle.ele('ram:LineOne').txt(billedBy.address.streetName).up();
    sellerAddressEle.ele('ram:CityName').txt(billedBy.address.city).up();
    // Typically you'd include 'ram:CountryID' with ISO2 code, e.g. "DE"
    sellerAddressEle.up(); // </ram:PostalTradeAddress>
    sellerPartyEle.up(); // </ram:SellerTradeParty>

    // Buyer
    const buyerPartyEle = headerTradeAgreementEle.ele('ram:BuyerTradeParty');
    buyerPartyEle.ele('ram:Name').txt(billedTo.name).up();
    const buyerAddressEle = buyerPartyEle.ele('ram:PostalTradeAddress');
    buyerAddressEle.ele('ram:PostcodeCode').txt(billedTo.address.postalCode).up();
    buyerAddressEle.ele('ram:LineOne').txt(billedTo.address.streetName).up();
    buyerAddressEle.ele('ram:CityName').txt(billedTo.address.city).up();
    buyerAddressEle.up(); // </ram:PostalTradeAddress>
    buyerPartyEle.up(); // </ram:BuyerTradeParty>
    headerTradeAgreementEle.up(); // </ram:ApplicableHeaderTradeAgreement>

    // 5.3) Applicable Header Trade Delivery
    const headerTradeDeliveryEle = supplyChainEle.ele('ram:ApplicableHeaderTradeDelivery');
    const actualDeliveryEle = headerTradeDeliveryEle.ele('ram:ActualDeliverySupplyChainEvent');
    const occurrenceEle = actualDeliveryEle.ele('ram:OccurrenceDateTime')
      .ele('udt:DateTimeString', { format: '102' });

    const deliveryDate = invoice.deliveryDate || letterArg.date;
    occurrenceEle.txt(this.formatDate(deliveryDate)).up();
    actualDeliveryEle.up(); // </ram:ActualDeliverySupplyChainEvent>
    headerTradeDeliveryEle.up(); // </ram:ApplicableHeaderTradeDelivery>

    // 5.4) Applicable Header Trade Settlement
    const headerTradeSettlementEle = supplyChainEle.ele('ram:ApplicableHeaderTradeSettlement');
    // Tax currency code, doc currency code, etc.
    headerTradeSettlementEle.ele('ram:InvoiceCurrencyCode').txt(invoice.currency).up();

    // Example single tax breakdown
    const tradeTaxEle = headerTradeSettlementEle.ele('ram:ApplicableTradeTax');
    tradeTaxEle.ele('ram:TypeCode').txt('VAT').up();
    tradeTaxEle.ele('ram:CalculatedAmount').txt(this.sumAllVat(invoice).toFixed(2)).up();
    tradeTaxEle
      .ele('ram:RateApplicablePercent')
      .txt(this.extractMainVatRate(invoice.items).toFixed(2))
      .up();
    tradeTaxEle.up(); // </ram:ApplicableTradeTax>

    // Payment Terms
    const paymentTermsEle = headerTradeSettlementEle.ele('ram:SpecifiedTradePaymentTerms');
    paymentTermsEle.ele('ram:Description').txt(`Payment due in ${invoice.dueInDays} days.`).up();
    paymentTermsEle.up(); // </ram:SpecifiedTradePaymentTerms>

    // Monetary Summation
    const monetarySummationEle = headerTradeSettlementEle.ele('ram:SpecifiedTradeSettlementHeaderMonetarySummation');
    monetarySummationEle
      .ele('ram:LineTotalAmount')
      .txt(this.calcLineTotalNet(invoice).toFixed(2))
      .up();
    monetarySummationEle
      .ele('ram:TaxTotalAmount')
      .txt(this.sumAllVat(invoice).toFixed(2))
      .up();
    monetarySummationEle
      .ele('ram:GrandTotalAmount')
      .txt(this.calcGrandTotal(invoice).toFixed(2))
      .up();
    monetarySummationEle.up(); // </ram:SpecifiedTradeSettlementHeaderMonetarySummation>
    headerTradeSettlementEle.up(); // </ram:ApplicableHeaderTradeSettlement>

    supplyChainEle.up(); // </rsm:SupplyChainTradeTransaction>
    doc.up(); // </rsm:CrossIndustryInvoice>

    // 6) Return the final XML string
    return doc.end({ prettyPrint: true });
  }

  /**
   * Helper: Determine if the letter is in draft or final.
   */
  private isDraft(letterArg: plugins.tsclass.business.ILetter): boolean {
    return letterArg.versionInfo?.type === 'draft';
  }

  /**
   * Helper: Format date to certain patterns (very minimal example).
   * e.g. 'yyyyMMdd' => '20231231'
   */
  private formatDate(timestampMs: number): string {
    const date = new Date(timestampMs);
    const yyyy = date.getFullYear();
    const mm = String(date.getMonth() + 1).padStart(2, '0');
    const dd = String(date.getDate()).padStart(2, '0');
    return `${yyyy}${mm}${dd}`;
  }

  /**
   * Helper: Map your custom 'unitType' to an ISO code or similar.
   */
  private mapUnitType(unitType: string): string {
    switch (unitType.toLowerCase()) {
      case 'hour':
        return 'HUR';
      case 'piece':
        return 'C62';
      default:
        return 'C62'; // fallback
    }
  }

  /**
   * Example: Sum all VAT amounts from items.
   */
  private sumAllVat(invoice: plugins.tsclass.finance.IInvoice): number {
    return invoice.items.reduce((acc, item) => {
      const net = item.unitNetPrice * item.unitQuantity;
      const vat = net * (item.vatPercentage / 100);
      return acc + vat;
    }, 0);
  }

  /**
   * Example: Extract main (or highest) VAT rate from items as representative.
   * In reality, you might list multiple 'ApplicableTradeTax' blocks by group.
   */
  private extractMainVatRate(items: plugins.tsclass.finance.IInvoiceItem[]): number {
    let max = 0;
    items.forEach((item) => {
      if (item.vatPercentage > max) max = item.vatPercentage;
    });
    return max;
  }

  /**
   * Example: Sum net amounts (without VAT).
   */
  private calcLineTotalNet(invoice: plugins.tsclass.finance.IInvoice): number {
    return invoice.items.reduce((acc, item) => {
      const net = item.unitNetPrice * item.unitQuantity;
      return acc + net;
    }, 0);
  }

  /**
   * Example: net + VAT = grand total
   */
  private calcGrandTotal(invoice: plugins.tsclass.finance.IInvoice): number {
    const net = this.calcLineTotalNet(invoice);
    const vat = this.sumAllVat(invoice);
    return net + vat;
  }
}