import { BaseDecoder } from '../base/base.decoder.js';
import type { TInvoice, TCreditNote, TDebitNote } from '../../interfaces/common.js';
import { UBLDocumentType, UBL_NAMESPACES } from './ubl.types.js';
import { DOMParser, xpath } from '../../plugins.js';

/**
 * Base decoder for UBL-based invoice formats
 */
export abstract class UBLBaseDecoder extends BaseDecoder {
  protected doc: Document;
  protected namespaces: Record<string, string>;
  protected select: xpath.XPathSelect;

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

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

    // Set up namespaces for XPath queries
    this.namespaces = {
      cbc: UBL_NAMESPACES.CBC,
      cac: UBL_NAMESPACES.CAC
    };

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

  /**
   * Decodes UBL XML into a TInvoice object
   * @returns Promise resolving to a TInvoice object
   */
  public async decode(): Promise<TInvoice> {
    // Determine document type
    const documentType = this.getDocumentType();

    if (documentType === UBLDocumentType.CREDIT_NOTE) {
      return this.decodeCreditNote();
    } else {
      return this.decodeDebitNote();
    }
  }

  /**
   * Gets the UBL document type
   * @returns UBL document type
   */
  protected getDocumentType(): UBLDocumentType {
    const rootName = this.doc.documentElement.nodeName;

    if (rootName === UBLDocumentType.CREDIT_NOTE) {
      return UBLDocumentType.CREDIT_NOTE;
    } else {
      return UBLDocumentType.INVOICE;
    }
  }

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

  /**
   * Decodes a UBL 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;
  }
}