import * as plugins from './plugins.js';
import * as interfaces from './interfaces.js';
import {
  PDFDocument,
  PDFDict,
  PDFName,
  PDFRawStream,
  PDFArray,
  PDFString,
} from 'pdf-lib';
import { FacturXEncoder } from './formats/facturx.encoder.js';
import { XInvoiceEncoder } from './formats/xinvoice.encoder.js';
import { DecoderFactory } from './formats/decoder.factory.js';
import { BaseDecoder } from './formats/base.decoder.js';
import { ValidatorFactory } from './formats/validator.factory.js';
import { BaseValidator } from './formats/base.validator.js';

/**
 * Main class for working with electronic invoices.
 * Supports various invoice formats including Factur-X, ZUGFeRD, UBL, and XRechnung
 * Implements ILetter interface for seamless integration with existing systems
 */
export class XInvoice implements plugins.tsclass.business.ILetter {
  // ILetter interface properties
  public versionInfo: plugins.tsclass.business.ILetter['versionInfo'] = {
    type: 'draft',
    version: '1.0.0'
  };
  public type: plugins.tsclass.business.ILetter['type'] = 'invoice';
  public date = Date.now();
  public subject: plugins.tsclass.business.ILetter['subject'] = '';
  public from: plugins.tsclass.business.TContact;
  public to: plugins.tsclass.business.TContact;
  public content: {
    invoiceData: plugins.tsclass.finance.IInvoice;
    textData: null;
    timesheetData: null;
    contractData: null;
  };
  public needsCoverSheet: plugins.tsclass.business.ILetter['needsCoverSheet'] = false;
  public objectActions: plugins.tsclass.business.ILetter['objectActions'] = [];
  public pdf: plugins.tsclass.business.ILetter['pdf'] = null;
  public incidenceId: plugins.tsclass.business.ILetter['incidenceId'] = null;
  public language: plugins.tsclass.business.ILetter['language'] = null;
  public legalContact: plugins.tsclass.business.ILetter['legalContact'] = null;
  public logoUrl: plugins.tsclass.business.ILetter['logoUrl'] = null;
  public pdfAttachments: plugins.tsclass.business.ILetter['pdfAttachments'] = null;
  public accentColor: string | null = null;

  // XInvoice specific properties
  private xmlString: string = '';
  private encoderFacturX = new FacturXEncoder();
  private encoderXInvoice = new XInvoiceEncoder();
  private decoderInstance: BaseDecoder | null = null;
  private validatorInstance: BaseValidator | null = null;
  
  // Format of the invoice, if detected
  private detectedFormat: interfaces.InvoiceFormat = interfaces.InvoiceFormat.UNKNOWN;
  
  // Validation errors from last validation
  private validationErrors: interfaces.ValidationError[] = [];
  
  // Options for this XInvoice instance
  private options: interfaces.XInvoiceOptions = {
    validateOnLoad: false,
    validationLevel: interfaces.ValidationLevel.SYNTAX
  };
  
  /**
   * Creates a new XInvoice instance
   * @param options Configuration options
   */
  constructor(options?: interfaces.XInvoiceOptions) {
    // Initialize empty IContact objects
    this.from = this.createEmptyContact();
    this.to = this.createEmptyContact();
    
    // Initialize empty IInvoice
    this.content = {
      invoiceData: this.createEmptyInvoice(),
      textData: null,
      timesheetData: null,
      contractData: null
    };
    
    // Initialize with default options and override with provided options
    if (options) {
      this.options = { ...this.options, ...options };
    }
  }

  /**
   * Creates an empty IContact object
   */
  private createEmptyContact(): plugins.tsclass.business.TContact {
    return {
      name: '',
      type: 'company',
      description: '',
      address: {
        streetName: '',
        houseNumber: '0',
        city: '',
        country: '',
        postalCode: ''
      }
    };
  }

  /**
   * Creates an empty IInvoice object
   */
  private createEmptyInvoice(): plugins.tsclass.finance.IInvoice {
    return {
      id: '',
      status: null,
      type: 'debitnote',
      billedBy: this.createEmptyContact(),
      billedTo: this.createEmptyContact(),
      deliveryDate: Date.now(),
      dueInDays: 30,
      periodOfPerformance: null,
      printResult: null,
      currency: 'EUR' as plugins.tsclass.finance.TCurrency,
      notes: [],
      items: [],
      reverseCharge: false
    };
  }

  /**
   * Static factory method to create XInvoice from XML string
   * @param xmlString XML content
   * @param options Configuration options
   * @returns XInvoice instance
   */
  public static async fromXml(xmlString: string, options?: interfaces.XInvoiceOptions): Promise<XInvoice> {
    const xinvoice = new XInvoice(options);
    
    // Load XML data
    await xinvoice.loadXml(xmlString);
    
    return xinvoice;
  }

  /**
   * Static factory method to create XInvoice from PDF buffer
   * @param pdfBuffer PDF buffer
   * @param options Configuration options
   * @returns XInvoice instance
   */
  public static async fromPdf(pdfBuffer: Uint8Array | Buffer, options?: interfaces.XInvoiceOptions): Promise<XInvoice> {
    const xinvoice = new XInvoice(options);
    
    // Load PDF data
    await xinvoice.loadPdf(pdfBuffer);
    
    return xinvoice;
  }

  /**
   * Loads XML data into this XInvoice instance
   * @param xmlString XML content
   * @param validate Whether to validate
   */
  public async loadXml(xmlString: string, validate: boolean = false): Promise<void> {
    // Basic XML validation - just check if it starts with <?xml
    if (!xmlString || !xmlString.trim().startsWith('<?xml')) {
      throw new Error('Invalid XML: Missing XML declaration');
    }
    
    // Store the XML string
    this.xmlString = xmlString;
    
    // Detect the format
    this.detectedFormat = this.determineFormat(xmlString);
    
    // Initialize the decoder with the XML string using the factory
    this.decoderInstance = DecoderFactory.createDecoder(xmlString);
    
    // Initialize the validator with the XML string using the factory
    this.validatorInstance = ValidatorFactory.createValidator(xmlString);
    
    // Validate the XML if requested or if validateOnLoad is true
    if (validate || this.options.validateOnLoad) {
      await this.validate(this.options.validationLevel);
    }
    
    // Parse XML to ILetter
    const letterData = await this.decoderInstance.getLetterData();
    
    // Copy letter data to this object
    this.copyLetterData(letterData);
  }

  /**
   * Loads PDF data into this XInvoice instance and extracts embedded XML if present
   * @param pdfBuffer PDF buffer
   */
  public async loadPdf(pdfBuffer: Uint8Array | Buffer): Promise<void> {
    this.pdf = Uint8Array.from(pdfBuffer);
    
    try {
      // Try to extract embedded XML
      const xmlContent = await this.extractXmlFromPdf();
      
      // If XML was found, load it
      if (xmlContent) {
        await this.loadXml(xmlContent);
      }
    } catch (error) {
      console.error('Error extracting or parsing embedded XML from PDF:', error);
      throw error;
    }
  }

  /**
   * Extracts XML from PDF
   * @returns XML content or null if not found
   */
  private async extractXmlFromPdf(): Promise<string> {
    if (!this.pdf) {
      throw new Error('No PDF data available');
    }
    
    try {
      const pdfDoc = await PDFDocument.load(this.pdf);

      // Get the document's metadata dictionary
      const namesDictObj = pdfDoc.catalog.lookup(PDFName.of('Names'));
      if (!(namesDictObj instanceof PDFDict)) {
        throw new Error('No Names dictionary found in PDF! This PDF does not contain embedded files.');
      }

      const embeddedFilesDictObj = namesDictObj.lookup(PDFName.of('EmbeddedFiles'));
      if (!(embeddedFilesDictObj instanceof PDFDict)) {
        throw new Error('No EmbeddedFiles dictionary found! This PDF does not contain embedded files.');
      }

      const filesSpecObj = embeddedFilesDictObj.lookup(PDFName.of('Names'));
      if (!(filesSpecObj instanceof PDFArray)) {
        throw new Error('No files specified in EmbeddedFiles dictionary!');
      }

      // Try to find an XML file in the embedded files
      let xmlFile: PDFRawStream | undefined;
      let xmlFileName: string | undefined;

      for (let i = 0; i < filesSpecObj.size(); i += 2) {
        const fileNameObj = filesSpecObj.lookup(i);
        const fileSpecObj = filesSpecObj.lookup(i + 1);

        if (!(fileNameObj instanceof PDFString)) {
          continue;
        }
        if (!(fileSpecObj instanceof PDFDict)) {
          continue;
        }

        // Get the filename as string
        const fileName = fileNameObj.toString();
        
        // Check if it's an XML file (checking both extension and known standard filenames)
        if (fileName.toLowerCase().includes('.xml') || 
            fileName.toLowerCase().includes('factur-x') ||
            fileName.toLowerCase().includes('zugferd') ||
            fileName.toLowerCase().includes('xrechnung')) {
            
          const efDictObj = fileSpecObj.lookup(PDFName.of('EF'));
          if (!(efDictObj instanceof PDFDict)) {
            continue;
          }

          const maybeStream = efDictObj.lookup(PDFName.of('F'));
          if (maybeStream instanceof PDFRawStream) {
            // Found an XML file - save it
            xmlFile = maybeStream;
            xmlFileName = fileName;
            break;
          }
        }
      }

      // If no XML file was found, throw an error
      if (!xmlFile) {
        throw new Error('No embedded XML file found in the PDF!');
      }

      // Decompress and decode the XML content
      const xmlCompressedBytes = xmlFile.getContents().buffer;
      const xmlBytes = plugins.pako.inflate(xmlCompressedBytes);
      const xmlContent = new TextDecoder('utf-8').decode(xmlBytes);

      console.log(`Successfully extracted ${this.determineFormat(xmlContent)} XML from PDF file. File name: ${xmlFileName}`);
      
      return xmlContent;
    } catch (error) {
      console.error('Error extracting or parsing embedded XML from PDF:', error);
      throw error;
    }
  }

  /**
   * Copies data from another ILetter object
   * @param letter Source letter data
   */
  private copyLetterData(letter: plugins.tsclass.business.ILetter): void {
    this.versionInfo = { ...letter.versionInfo };
    this.type = letter.type;
    this.date = letter.date;
    this.subject = letter.subject;
    this.from = { ...letter.from };
    this.to = { ...letter.to };
    this.content = {
      invoiceData: letter.content.invoiceData ? { ...letter.content.invoiceData } : this.createEmptyInvoice(),
      textData: letter.content.textData,
      timesheetData: letter.content.timesheetData,
      contractData: letter.content.contractData
    };
    this.needsCoverSheet = letter.needsCoverSheet;
    this.objectActions = [...letter.objectActions];
    this.incidenceId = letter.incidenceId;
    this.language = letter.language;
    this.legalContact = letter.legalContact;
    this.logoUrl = letter.logoUrl;
    this.pdfAttachments = letter.pdfAttachments;
    this.accentColor = letter.accentColor;
  }

  /**
   * Validates the XML against the appropriate validation rules
   * @param level Validation level (syntax, semantic, business)
   * @returns Validation result
   */
  public async validate(level: interfaces.ValidationLevel = interfaces.ValidationLevel.SYNTAX): Promise<interfaces.ValidationResult> {
    if (!this.xmlString) {
      throw new Error('No XML to validate');
    }
    
    if (!this.validatorInstance) {
      // Initialize the validator with the XML string if not already done
      this.validatorInstance = ValidatorFactory.createValidator(this.xmlString);
    }
    
    // Run validation
    const result = this.validatorInstance.validate(level);
    
    // Store validation errors
    this.validationErrors = result.errors;
    
    return result;
  }
  
  /**
   * Checks if the document is valid based on the last validation
   * @returns True if the document is valid
   */
  public isValid(): boolean {
    if (!this.validatorInstance) {
      return false;
    }
    
    return this.validatorInstance.isValid();
  }
  
  /**
   * Gets validation errors from the last validation
   * @returns Array of validation errors
   */
  public getValidationErrors(): interfaces.ValidationError[] {
    return this.validationErrors;
  }

  /**
   * Exports the invoice to XML format
   * @param format Target format (e.g., 'facturx', 'xrechnung')
   * @returns XML string in the specified format
   */
  public async exportXml(format: string = 'facturx'): Promise<string> {
    format = format.toLowerCase();
    
    // Generate XML based on format
    switch (format) {
      case 'facturx':
      case 'zugferd':
        return this.encoderFacturX.createFacturXXml(this);
        
      case 'xrechnung':
      case 'ubl':
        return this.encoderXInvoice.createXInvoiceXml(this);
        
      default:
        // Default to Factur-X
        return this.encoderFacturX.createFacturXXml(this);
    }
  }

  /**
   * Exports the invoice to PDF format with embedded XML
   * @param format Target format (e.g., 'facturx', 'zugferd')
   * @returns PDF buffer with embedded XML
   */
  public async exportPdf(format: string = 'facturx'): Promise<Uint8Array> {
    format = format.toLowerCase();
    
    if (!this.pdf) {
      throw new Error('No PDF data available. Use loadPdf() first or set the pdf property.');
    }
    
    try {
      // Generate XML based on format
      const xmlContent = await this.exportXml(format);
      
      // Load the PDF
      const pdfDoc = await PDFDocument.load(this.pdf);

      // Convert the XML string to a Uint8Array
      const xmlBuffer = new TextEncoder().encode(xmlContent);
      
      // Determine attachment filename based on format
      let filename = 'invoice.xml';
      let description = 'XML Invoice';
      
      switch (format) {
        case 'facturx':
          filename = 'factur-x.xml';
          description = 'Factur-X XML Invoice';
          break;
        case 'zugferd':
          filename = 'zugferd.xml';
          description = 'ZUGFeRD XML Invoice';
          break;
        case 'xrechnung':
          filename = 'xrechnung.xml';
          description = 'XRechnung XML Invoice';
          break;
        case 'ubl':
          filename = 'ubl.xml';
          description = 'UBL XML Invoice';
          break;
      }

      // Make sure filename is lowercase (as required by documentation)
      filename = filename.toLowerCase();

      // Use pdf-lib's .attach() to embed the XML
      pdfDoc.attach(xmlBuffer, filename, {
        mimeType: 'application/xml',
        description: description,
      });

      // Save the modified PDF
      const modifiedPdfBytes = await pdfDoc.save();
      
      // Update the pdf property
      this.pdf = modifiedPdfBytes;
      
      return modifiedPdfBytes;
    } catch (error) {
      console.error('Error embedding XML into PDF:', error);
      throw error;
    }
  }

  /**
   * Gets the invoice format as an enum value
   * @returns InvoiceFormat enum value
   */
  public getFormat(): interfaces.InvoiceFormat {
    return this.detectedFormat;
  }
  
  /**
   * Checks if the invoice is in a specific format
   * @param format Format to check
   * @returns True if the invoice is in the specified format
   */
  public isFormat(format: interfaces.InvoiceFormat): boolean {
    return this.detectedFormat === format;
  }
  
  /**
   * Determines the format of an XML document and returns the format enum
   * @param xmlContent XML content as string
   * @returns InvoiceFormat enum value
   */
  private determineFormat(xmlContent: string): interfaces.InvoiceFormat {
    if (!xmlContent) {
      return interfaces.InvoiceFormat.UNKNOWN;
    }
    
    // Check for ZUGFeRD/CII/Factur-X
    if (xmlContent.includes('CrossIndustryInvoice') || 
        xmlContent.includes('rsm:') || 
        xmlContent.includes('ram:')) {
      
      // Check for specific profiles
      if (xmlContent.includes('factur-x') || xmlContent.includes('Factur-X')) {
        return interfaces.InvoiceFormat.FACTURX;
      }
      if (xmlContent.includes('zugferd') || xmlContent.includes('ZUGFeRD')) {
        return interfaces.InvoiceFormat.ZUGFERD;
      }
      
      return interfaces.InvoiceFormat.CII;
    }
    
    // Check for UBL
    if (xmlContent.includes('<Invoice') || 
        xmlContent.includes('ubl:Invoice') || 
        xmlContent.includes('oasis:names:specification:ubl')) {
      
      // Check for XRechnung
      if (xmlContent.includes('xrechnung') || xmlContent.includes('XRechnung')) {
        return interfaces.InvoiceFormat.XRECHNUNG;
      }
      
      return interfaces.InvoiceFormat.UBL;
    }
    
    // Check for FatturaPA
    if (xmlContent.includes('FatturaElettronica') || 
        xmlContent.includes('fatturapa.gov.it')) {
      return interfaces.InvoiceFormat.FATTURAPA;
    }
    
    // For unknown formats, return unknown
    return interfaces.InvoiceFormat.UNKNOWN;
  }
}