import { BaseDecoder } from '../base/base.decoder.js';
import type { TInvoice, TCreditNote, TDebitNote } from '../../interfaces/common.js';
import { CII_NAMESPACES, CIIProfile } from './cii.types.js';
import { DOMParser, xpath } from '../../plugins.js';

/**
 * Base decoder for CII-based invoice formats
 */
export abstract class CIIBaseDecoder extends BaseDecoder {
  protected doc: Document;
  protected namespaces: Record<string, string>;
  protected select: xpath.XPathSelect;
  protected profile: CIIProfile = CIIProfile.EN16931;

  constructor(xml: string) {
    super(xml);

    // Parse XML document
    this.doc = new DOMParser().parseFromString(xml, 'application/xml');

    // Set up namespaces for XPath queries
    this.namespaces = {
      rsm: CII_NAMESPACES.RSM,
      ram: CII_NAMESPACES.RAM,
      udt: CII_NAMESPACES.UDT
    };

    // Create XPath selector with namespaces
    this.select = xpath.useNamespaces(this.namespaces);

    // Detect profile
    this.detectProfile();
  }

  /**
   * Decodes CII XML into a TInvoice object
   * @returns Promise resolving to a TInvoice object
   */
  public async decode(): Promise<TInvoice> {
    // Determine if it's a credit note or debit note based on type code
    const typeCode = this.getText('//ram:TypeCode');

    if (typeCode === '381') { // Credit note type code
      return this.decodeCreditNote();
    } else {
      return this.decodeDebitNote();
    }
  }

  /**
   * Detects the CII profile from the XML
   */
  protected detectProfile(): void {
    // Look for profile identifier
    const profileNode = this.select(
      'string(//rsm:ExchangedDocumentContext/ram:GuidelineSpecifiedDocumentContextParameter/ram:ID)',
      this.doc
    );

    if (profileNode) {
      const profileText = profileNode.toString();

      if (profileText.includes('BASIC')) {
        this.profile = CIIProfile.BASIC;
      } else if (profileText.includes('EN16931')) {
        this.profile = CIIProfile.EN16931;
      } else if (profileText.includes('EXTENDED')) {
        this.profile = CIIProfile.EXTENDED;
      } else if (profileText.includes('MINIMUM')) {
        this.profile = CIIProfile.MINIMUM;
      } else if (profileText.includes('COMFORT')) {
        this.profile = CIIProfile.COMFORT;
      }
    }
  }

  /**
   * Decodes a CII credit note
   * @returns Promise resolving to a TCreditNote object
   */
  protected abstract decodeCreditNote(): Promise<TCreditNote>;

  /**
   * Decodes a CII debit note (invoice)
   * @returns Promise resolving to a TDebitNote object
   */
  protected abstract decodeDebitNote(): Promise<TDebitNote>;

  /**
   * Gets a text value from an XPath expression
   * @param xpath XPath expression
   * @param context Optional context node
   * @returns Text value or empty string if not found
   */
  protected getText(xpathExpr: string, context?: Node): string {
    const node = this.select(xpathExpr, context || this.doc)[0];
    return node ? (node.textContent || '') : '';
  }

  /**
   * Gets a number value from an XPath expression
   * @param xpath XPath expression
   * @param context Optional context node
   * @returns Number value or 0 if not found or not a number
   */
  protected getNumber(xpathExpr: string, context?: Node): number {
    const text = this.getText(xpathExpr, context);
    const num = parseFloat(text);
    return isNaN(num) ? 0 : num;
  }

  /**
   * Gets a date value from an XPath expression
   * @param xpath XPath expression
   * @param context Optional context node
   * @returns Date timestamp or current time if not found or invalid
   */
  protected getDate(xpathExpr: string, context?: Node): number {
    const text = this.getText(xpathExpr, context);
    if (!text) return Date.now();

    const date = new Date(text);
    return isNaN(date.getTime()) ? Date.now() : date.getTime();
  }

  /**
   * Checks if a node exists
   * @param xpath XPath expression
   * @param context Optional context node
   * @returns True if node exists
   */
  protected exists(xpathExpr: string, context?: Node): boolean {
    const nodes = this.select(xpathExpr, context || this.doc);
    if (Array.isArray(nodes)) {
      return nodes.length > 0;
    }
    return false;
  }
}