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

import { business, finance } from './plugins.js';
import type { TInvoice } from './interfaces/common.js';
import { InvoiceFormat, ValidationLevel } from './interfaces/common.js';
import type { ValidationResult, ValidationError, XInvoiceOptions, IPdf, ExportFormat } from './interfaces/common.js';
// PDF-related imports are handled by the PDF utilities

// Import factories
import { DecoderFactory } from './formats/factories/decoder.factory.js';
import { EncoderFactory } from './formats/factories/encoder.factory.js';
import { ValidatorFactory } from './formats/factories/validator.factory.js';

// Import PDF utilities
import { PDFEmbedder } from './formats/pdf/pdf.embedder.js';
import { PDFExtractor } from './formats/pdf/pdf.extractor.js';

// Import format detector
import { FormatDetector } from './formats/utils/format.detector.js';

/**
 * Main class for working with electronic invoices.
 * Supports various invoice formats including Factur-X, ZUGFeRD, UBL, and XRechnung
 * Implements TInvoice interface for seamless integration with existing systems
 */
export class XInvoice {
  // TInvoice interface properties
  public id: string = '';
  public invoiceId: string = '';
  public invoiceType: 'creditnote' | 'debitnote' = 'debitnote';
  public versionInfo: business.TDocumentEnvelope<string, any>['versionInfo'] = {
    type: 'draft',
    version: '1.0.0'
  };
  public type: 'invoice' = 'invoice';
  public date = Date.now();
  public status: 'draft' | 'invoice' | 'paid' | 'refunded' = 'invoice';
  public subject: string = '';
  public from: business.TContact;
  public to: business.TContact;
  public incidenceId: string = '';
  public language: string = 'en';
  public legalContact?: business.TContact;
  public objectActions: any[] = [];
  public pdf: IPdf | null = null;
  public pdfAttachments: IPdf[] | null = null;
  public accentColor: string | null = null;
  public logoUrl: string | null = null;

  // Additional properties for invoice data
  public items: finance.TInvoiceItem[] = [];
  public dueInDays: number = 30;
  public reverseCharge: boolean = false;
  public currency: finance.TCurrency = 'EUR';
  public notes: string[] = [];
  public periodOfPerformance?: { from: number; to: number };
  public deliveryDate?: number;
  public buyerReference?: string;
  public electronicAddress?: { scheme: string; value: string };
  public paymentOptions?: finance.IPaymentOptionInfo;

  // XInvoice specific properties
  private xmlString: string = '';
  private detectedFormat: InvoiceFormat = InvoiceFormat.UNKNOWN;
  private validationErrors: ValidationError[] = [];
  private options: XInvoiceOptions = {
    validateOnLoad: false,
    validationLevel: ValidationLevel.SYNTAX
  };

  // PDF utilities
  private pdfEmbedder = new PDFEmbedder();
  private pdfExtractor = new PDFExtractor();

  /**
   * Creates a new XInvoice instance
   * @param options Configuration options
   */
  constructor(options?: XInvoiceOptions) {
    // Initialize empty contact objects
    this.from = this.createEmptyContact();
    this.to = this.createEmptyContact();

    // Apply options if provided
    if (options) {
      this.options = { ...this.options, ...options };
    }
  }

  /**
   * Creates an empty TContact object
   */
  private createEmptyContact(): business.TContact {
    return {
      name: '',
      type: 'company',
      description: '',
      address: {
        streetName: '',
        houseNumber: '0',
        city: '',
        country: '',
        postalCode: ''
      },
      status: 'active',
      foundedDate: {
        year: 2000,
        month: 1,
        day: 1
      },
      registrationDetails: {
        vatId: '',
        registrationId: '',
        registrationName: ''
      }
    };
  }

  /**
   * Creates a new XInvoice instance from XML
   * @param xmlString XML content
   * @param options Configuration options
   * @returns XInvoice instance
   */
  public static async fromXml(xmlString: string, options?: XInvoiceOptions): Promise<XInvoice> {
    const xinvoice = new XInvoice(options);

    // Load XML data
    await xinvoice.loadXml(xmlString);

    return xinvoice;
  }

  /**
   * Creates a new XInvoice instance from PDF
   * @param pdfBuffer PDF buffer
   * @param options Configuration options
   * @returns XInvoice instance
   */
  public static async fromPdf(pdfBuffer: Uint8Array | Buffer, options?: XInvoiceOptions): Promise<XInvoice> {
    const xinvoice = new XInvoice(options);

    // Load PDF data
    await xinvoice.loadPdf(pdfBuffer);

    return xinvoice;
  }

  /**
   * Loads XML data into the XInvoice instance
   * @param xmlString XML content
   * @param validate Whether to validate the XML
   * @returns This instance for chaining
   */
  public async loadXml(xmlString: string, validate: boolean = false): Promise<XInvoice> {
    this.xmlString = xmlString;

    // Detect format
    this.detectedFormat = FormatDetector.detectFormat(xmlString);

    try {
      // Initialize the decoder with the XML string using the factory
      const decoder = DecoderFactory.createDecoder(xmlString);

      // Decode the XML into a TInvoice object
      const invoice = await decoder.decode();

      // Copy data from the decoded invoice
      this.copyInvoiceData(invoice);

      // Validate the XML if requested or if validateOnLoad is true
      if (validate || this.options.validateOnLoad) {
        await this.validate(this.options.validationLevel);
      }
    } catch (error) {
      console.error('Error loading XML:', error);
      throw error;
    }

    return this;
  }

  /**
   * Loads PDF data into the XInvoice instance
   * @param pdfBuffer PDF buffer
   * @param validate Whether to validate the extracted XML
   * @returns This instance for chaining
   */
  public async loadPdf(pdfBuffer: Uint8Array | Buffer, validate: boolean = false): Promise<XInvoice> {
    try {
      // Extract XML from PDF using the consolidated extractor
      const extractResult = await this.pdfExtractor.extractXml(pdfBuffer);
  
      // Store the PDF buffer
      this.pdf = {
        name: 'invoice.pdf',
        id: `invoice-${Date.now()}`,
        metadata: {
          textExtraction: '',
          format: extractResult.success ? extractResult.format?.toString() : undefined
        },
        buffer: pdfBuffer instanceof Buffer ? new Uint8Array(pdfBuffer) : pdfBuffer
      };
  
      // Handle extraction result
      if (!extractResult.success || !extractResult.xml) {
        const errorMessage = extractResult.error ? extractResult.error.message : 'Unknown error extracting XML from PDF';
        console.warn('XML extraction failed:', errorMessage);
        throw new Error(`No XML found in PDF: ${errorMessage}`);
      }
  
      // Load the extracted XML
      await this.loadXml(extractResult.xml, validate);
  
      // Store the detected format
      this.detectedFormat = extractResult.format || InvoiceFormat.UNKNOWN;
  
      return this;
    } catch (error) {
      console.error('Error loading PDF:', error);
      throw error;
    }
  }  

  /**
   * Copies data from a TInvoice object
   * @param invoice Source invoice data
   */
  private copyInvoiceData(invoice: TInvoice): void {
    // Copy basic properties
    this.id = invoice.id;
    this.invoiceId = invoice.invoiceId || invoice.id;
    this.invoiceType = invoice.invoiceType;
    this.versionInfo = { ...invoice.versionInfo };
    this.type = invoice.type;
    this.date = invoice.date;
    this.status = invoice.status;
    this.subject = invoice.subject;
    this.from = { ...invoice.from };
    this.to = { ...invoice.to };
    this.incidenceId = invoice.incidenceId;
    this.language = invoice.language;
    this.legalContact = invoice.legalContact ? { ...invoice.legalContact } : undefined;
    this.objectActions = [...invoice.objectActions];
    this.pdf = invoice.pdf;
    this.pdfAttachments = invoice.pdfAttachments;

    // Copy invoice-specific properties
    if (invoice.items) this.items = [...invoice.items];
    if (invoice.dueInDays) this.dueInDays = invoice.dueInDays;
    if (invoice.reverseCharge !== undefined) this.reverseCharge = invoice.reverseCharge;
    if (invoice.currency) this.currency = invoice.currency;
    if (invoice.notes) this.notes = [...invoice.notes];
    if (invoice.periodOfPerformance) this.periodOfPerformance = { ...invoice.periodOfPerformance };
    if (invoice.deliveryDate) this.deliveryDate = invoice.deliveryDate;
    if (invoice.buyerReference) this.buyerReference = invoice.buyerReference;
    if (invoice.electronicAddress) this.electronicAddress = { ...invoice.electronicAddress };
    if (invoice.paymentOptions) this.paymentOptions = { ...invoice.paymentOptions };
  }

  /**
   * Validates the XML against the appropriate format rules
   * @param level Validation level (syntax, semantic, business)
   * @returns Validation result
   */
  public async validate(level: ValidationLevel = ValidationLevel.SYNTAX): Promise<ValidationResult> {
    if (!this.xmlString) {
      throw new Error('No XML to validate');
    }

    try {
      // Initialize the validator with the XML string
      const validator = ValidatorFactory.createValidator(this.xmlString);

      // Run validation
      const result = validator.validate(level);

      // Store validation errors
      this.validationErrors = result.errors;

      return result;
    } catch (error) {
      console.error('Error validating XML:', error);
      const errorResult: ValidationResult = {
        valid: false,
        errors: [{
          code: 'VAL-ERROR',
          message: `Validation error: ${error instanceof Error ? error.message : String(error)}`
        }],
        level
      };
      this.validationErrors = errorResult.errors;
      return errorResult;
    }
  }

  /**
   * Checks if the invoice is valid
   * @returns True if no validation errors were found
   */
  public isValid(): boolean {
    return this.validationErrors.length === 0;
  }

  /**
   * Gets validation errors from the last validation
   * @returns Array of validation errors
   */
  public getValidationErrors(): ValidationError[] {
    return this.validationErrors;
  }

  /**
   * Exports the invoice as XML in the specified format
   * @param format Target format (e.g., 'facturx', 'xrechnung')
   * @returns XML string in the specified format
   */
  public async exportXml(format: ExportFormat = 'facturx'): Promise<string> {
    // Create encoder for the specified format
    const encoder = EncoderFactory.createEncoder(format);

    // Generate XML
    return await encoder.encode(this as unknown as TInvoice);
  }

  /**
   * Exports the invoice as a PDF with embedded XML
   * @param format Target format (e.g., 'facturx', 'zugferd', 'xrechnung', 'ubl')
   * @returns PDF object with embedded XML
   */
  public async exportPdf(format: ExportFormat = 'facturx'): Promise<IPdf> {
    if (!this.pdf) {
      throw new Error('No PDF data available. Use loadPdf() first or set the pdf property.');
    }

    // Generate XML in the specified format
    const xmlContent = await this.exportXml(format);

    // Determine filename based on format
    let filename = 'invoice.xml';
    let description = 'XML Invoice';

    switch (format.toLowerCase()) {
      case 'facturx':
        filename = 'factur-x.xml';
        description = 'Factur-X XML Invoice';
        break;
      case 'zugferd':
        filename = 'zugferd-invoice.xml';
        description = 'ZUGFeRD XML Invoice';
        break;
      case 'xrechnung':
        filename = 'xrechnung.xml';
        description = 'XRechnung XML Invoice';
        break;
      case 'ubl':
        filename = 'ubl-invoice.xml';
        description = 'UBL XML Invoice';
        break;
    }

    // Embed XML into PDF
    const result = await this.pdfEmbedder.createPdfWithXml(
      this.pdf.buffer,
      xmlContent,
      filename,
      description,
      this.pdf.name,
      this.pdf.id
    );

    // Handle potential errors
    if (!result.success || !result.pdf) {
      const errorMessage = result.error ? result.error.message : 'Unknown error embedding XML into PDF';
      console.error('Error exporting PDF:', errorMessage);
      throw new Error(`Failed to export PDF: ${errorMessage}`);
    }

    return result.pdf;
  }

  /**
   * Gets the raw XML content
   * @returns XML string
   */
  public getXml(): string {
    return this.xmlString;
  }

  /**
   * Gets the invoice format as an enum value
   * @returns InvoiceFormat enum value
   */
  public getFormat(): InvoiceFormat {
    return this.detectedFormat;
  }

  /**
   * Checks if the invoice is in the specified format
   * @param format Format to check
   * @returns True if the invoice is in the specified format
   */
  public isFormat(format: InvoiceFormat): boolean {
    return this.detectedFormat === format;
  }
}