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

/**
 * A class to convert a given ILetter with invoice data
 * into an XInvoice/XRechnung compliant XML (based on UBL).
 * 
 * XRechnung is the German implementation of the European standard EN16931 
 * for electronic invoices to the German public sector.
 */
export class XInvoiceEncoder {

  constructor() {}

  /**
   * Creates an XInvoice compliant XML based on the provided letter data.
   */
  public createXInvoiceXml(letterArg: plugins.tsclass.business.ILetter): string {
    // Use SmartXml for XML creation
    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;

    // Create the XML document
    const doc = smartxmlInstance
      .create({ version: '1.0', encoding: 'UTF-8' })
      .ele('Invoice', {
        'xmlns': 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2',
        'xmlns:cac': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2',
        'xmlns:cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2',
        'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance'
      });

    // UBL Version ID
    doc.ele('cbc:UBLVersionID').txt('2.1').up();
    
    // CustomizationID for XRechnung
    doc.ele('cbc:CustomizationID').txt('urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.0').up();
    
    // ID - Invoice number
    doc.ele('cbc:ID').txt(invoice.id).up();
    
    // Issue date
    const issueDate = new Date(letterArg.date);
    const issueDateStr = `${issueDate.getFullYear()}-${String(issueDate.getMonth() + 1).padStart(2, '0')}-${String(issueDate.getDate()).padStart(2, '0')}`;
    doc.ele('cbc:IssueDate').txt(issueDateStr).up();
    
    // Due date
    const dueDate = new Date(letterArg.date);
    dueDate.setDate(dueDate.getDate() + invoice.dueInDays);
    const dueDateStr = `${dueDate.getFullYear()}-${String(dueDate.getMonth() + 1).padStart(2, '0')}-${String(dueDate.getDate()).padStart(2, '0')}`;
    doc.ele('cbc:DueDate').txt(dueDateStr).up();
    
    // Invoice type code
    const invoiceTypeCode = invoice.type === 'creditnote' ? '381' : '380';
    doc.ele('cbc:InvoiceTypeCode').txt(invoiceTypeCode).up();
    
    // Note - optional invoice note
    if (invoice.notes && invoice.notes.length > 0) {
      doc.ele('cbc:Note').txt(invoice.notes[0]).up();
    }
    
    // Document currency code
    doc.ele('cbc:DocumentCurrencyCode').txt(invoice.currency).up();
    
    // Tax currency code - same as document currency in this case
    doc.ele('cbc:TaxCurrencyCode').txt(invoice.currency).up();
    
    // Accounting supplier party (seller)
    const supplierParty = doc.ele('cac:AccountingSupplierParty');
    const supplierPartyDetails = supplierParty.ele('cac:Party');
    
    // Seller VAT ID
    if (billedBy.vatId) {
      const partyTaxScheme = supplierPartyDetails.ele('cac:PartyTaxScheme');
      partyTaxScheme.ele('cbc:CompanyID').txt(billedBy.vatId).up();
      partyTaxScheme.ele('cac:TaxScheme')
        .ele('cbc:ID').txt('VAT').up()
      .up();
    }
    
    // Seller name
    supplierPartyDetails.ele('cac:PartyName')
      .ele('cbc:Name').txt(billedBy.name).up()
    .up();
    
    // Seller postal address
    const supplierAddress = supplierPartyDetails.ele('cac:PostalAddress');
    supplierAddress.ele('cbc:StreetName').txt(billedBy.address.streetName).up();
    if (billedBy.address.houseNumber) {
      supplierAddress.ele('cbc:BuildingNumber').txt(billedBy.address.houseNumber).up();
    }
    supplierAddress.ele('cbc:CityName').txt(billedBy.address.city).up();
    supplierAddress.ele('cbc:PostalZone').txt(billedBy.address.postalCode).up();
    supplierAddress.ele('cac:Country')
      .ele('cbc:IdentificationCode').txt(billedBy.address.country || 'DE').up()
    .up();
    
    // Seller contact
    const supplierContact = supplierPartyDetails.ele('cac:Contact');
    if (billedBy.email) {
      supplierContact.ele('cbc:ElectronicMail').txt(billedBy.email).up();
    }
    if (billedBy.phone) {
      supplierContact.ele('cbc:Telephone').txt(billedBy.phone).up();
    }
    
    supplierParty.up(); // Close AccountingSupplierParty
    
    // Accounting customer party (buyer)
    const customerParty = doc.ele('cac:AccountingCustomerParty');
    const customerPartyDetails = customerParty.ele('cac:Party');
    
    // Buyer VAT ID
    if (billedTo.vatId) {
      const partyTaxScheme = customerPartyDetails.ele('cac:PartyTaxScheme');
      partyTaxScheme.ele('cbc:CompanyID').txt(billedTo.vatId).up();
      partyTaxScheme.ele('cac:TaxScheme')
        .ele('cbc:ID').txt('VAT').up()
      .up();
    }
    
    // Buyer name
    customerPartyDetails.ele('cac:PartyName')
      .ele('cbc:Name').txt(billedTo.name).up()
    .up();
    
    // Buyer postal address
    const customerAddress = customerPartyDetails.ele('cac:PostalAddress');
    customerAddress.ele('cbc:StreetName').txt(billedTo.address.streetName).up();
    if (billedTo.address.houseNumber) {
      customerAddress.ele('cbc:BuildingNumber').txt(billedTo.address.houseNumber).up();
    }
    customerAddress.ele('cbc:CityName').txt(billedTo.address.city).up();
    customerAddress.ele('cbc:PostalZone').txt(billedTo.address.postalCode).up();
    customerAddress.ele('cac:Country')
      .ele('cbc:IdentificationCode').txt(billedTo.address.country || 'DE').up()
    .up();
    
    // Buyer contact
    if (billedTo.email || billedTo.phone) {
      const customerContact = customerPartyDetails.ele('cac:Contact');
      if (billedTo.email) {
        customerContact.ele('cbc:ElectronicMail').txt(billedTo.email).up();
      }
      if (billedTo.phone) {
        customerContact.ele('cbc:Telephone').txt(billedTo.phone).up();
      }
    }
    
    customerParty.up(); // Close AccountingCustomerParty
    
    // Payment means
    if (billedBy.sepaConnection) {
      const paymentMeans = doc.ele('cac:PaymentMeans');
      paymentMeans.ele('cbc:PaymentMeansCode').txt('58').up(); // 58 = SEPA credit transfer
      paymentMeans.ele('cbc:PaymentID').txt(invoice.id).up();
      
      // IBAN
      if (billedBy.sepaConnection.iban) {
        const payeeAccount = paymentMeans.ele('cac:PayeeFinancialAccount');
        payeeAccount.ele('cbc:ID').txt(billedBy.sepaConnection.iban).up();
        
        // BIC
        if (billedBy.sepaConnection.bic) {
          payeeAccount.ele('cac:FinancialInstitutionBranch')
            .ele('cbc:ID').txt(billedBy.sepaConnection.bic).up()
          .up();
        }
      }
    }
    
    // Payment terms
    const paymentTerms = doc.ele('cac:PaymentTerms');
    paymentTerms.ele('cbc:Note').txt(`Payment due in ${invoice.dueInDays} days`).up();
    
    // Tax summary
    // Group items by VAT rate
    const vatRates: { [rate: number]: plugins.tsclass.finance.IInvoiceItem[] } = {};
    
    // Collect items by VAT rate
    invoice.items.forEach(item => {
      if (!vatRates[item.vatPercentage]) {
        vatRates[item.vatPercentage] = [];
      }
      vatRates[item.vatPercentage].push(item);
    });
    
    // Calculate tax subtotals for each rate
    Object.entries(vatRates).forEach(([rate, items]) => {
      const taxRate = parseFloat(rate);
      
      // Calculate base amount for this rate
      let taxableAmount = 0;
      items.forEach(item => {
        taxableAmount += item.unitNetPrice * item.unitQuantity;
      });
      
      // Calculate tax amount
      const taxAmount = taxableAmount * (taxRate / 100);
      
      // Create tax subtotal
      const taxSubtotal = doc.ele('cac:TaxTotal')
        .ele('cbc:TaxAmount').txt(taxAmount.toFixed(2))
          .att('currencyID', invoice.currency)
        .up();
        
      taxSubtotal.ele('cac:TaxSubtotal')
        .ele('cbc:TaxableAmount')
          .txt(taxableAmount.toFixed(2))
          .att('currencyID', invoice.currency)
        .up()
        .ele('cbc:TaxAmount')
          .txt(taxAmount.toFixed(2))
          .att('currencyID', invoice.currency)
        .up()
        .ele('cac:TaxCategory')
          .ele('cbc:ID').txt('S').up() // Standard rate
          .ele('cbc:Percent').txt(taxRate.toFixed(2)).up()
          .ele('cac:TaxScheme')
            .ele('cbc:ID').txt('VAT').up()
          .up()
        .up()
      .up();
    });
    
    // Calculate invoice totals
    let lineExtensionAmount = 0;
    let taxExclusiveAmount = 0;
    let taxInclusiveAmount = 0;
    let totalVat = 0;
    
    // Sum all items
    invoice.items.forEach(item => {
      const net = item.unitNetPrice * item.unitQuantity;
      const vat = net * (item.vatPercentage / 100);
      
      lineExtensionAmount += net;
      taxExclusiveAmount += net;
      totalVat += vat;
    });
    
    taxInclusiveAmount = taxExclusiveAmount + totalVat;
    
    // Legal monetary total
    const legalMonetaryTotal = doc.ele('cac:LegalMonetaryTotal');
    legalMonetaryTotal.ele('cbc:LineExtensionAmount')
      .txt(lineExtensionAmount.toFixed(2))
      .att('currencyID', invoice.currency)
    .up();
    
    legalMonetaryTotal.ele('cbc:TaxExclusiveAmount')
      .txt(taxExclusiveAmount.toFixed(2))
      .att('currencyID', invoice.currency)
    .up();
    
    legalMonetaryTotal.ele('cbc:TaxInclusiveAmount')
      .txt(taxInclusiveAmount.toFixed(2))
      .att('currencyID', invoice.currency)
    .up();
    
    legalMonetaryTotal.ele('cbc:PayableAmount')
      .txt(taxInclusiveAmount.toFixed(2))
      .att('currencyID', invoice.currency)
    .up();
    
    // Invoice lines
    invoice.items.forEach((item, index) => {
      const invoiceLine = doc.ele('cac:InvoiceLine');
      invoiceLine.ele('cbc:ID').txt((index + 1).toString()).up();
      
      // Quantity
      invoiceLine.ele('cbc:InvoicedQuantity')
        .txt(item.unitQuantity.toString())
        .att('unitCode', this.mapUnitType(item.unitType))
      .up();
      
      // Line extension amount (net)
      const lineAmount = item.unitNetPrice * item.unitQuantity;
      invoiceLine.ele('cbc:LineExtensionAmount')
        .txt(lineAmount.toFixed(2))
        .att('currencyID', invoice.currency)
      .up();
      
      // Item details
      const itemEle = invoiceLine.ele('cac:Item');
      itemEle.ele('cbc:Description').txt(item.name).up();
      itemEle.ele('cbc:Name').txt(item.name).up();
      
      // Classified tax category
      itemEle.ele('cac:ClassifiedTaxCategory')
        .ele('cbc:ID').txt('S').up() // Standard rate
        .ele('cbc:Percent').txt(item.vatPercentage.toFixed(2)).up()
        .ele('cac:TaxScheme')
          .ele('cbc:ID').txt('VAT').up()
        .up()
      .up();
      
      // Price
      invoiceLine.ele('cac:Price')
        .ele('cbc:PriceAmount')
          .txt(item.unitNetPrice.toFixed(2))
          .att('currencyID', invoice.currency)
        .up()
      .up();
    });
    
    // Return the formatted XML
    return doc.end({ prettyPrint: true });
  }
  
  /**
   * Helper: Map your custom 'unitType' to an ISO code.
   */
  private mapUnitType(unitType: string): string {
    switch (unitType.toLowerCase()) {
      case 'hour':
      case 'hours':
        return 'HUR';
      case 'day':
      case 'days':
        return 'DAY';
      case 'piece':
      case 'pieces':
        return 'C62';
      default:
        return 'C62'; // fallback for unknown unit types
    }
  }
}