import { BaseValidator } from '../base/base.validator.js';
import { ValidationLevel } from '../../interfaces/common.js';
import type { ValidationResult } from '../../interfaces/common.js';
import { CII_NAMESPACES, CIIProfile } from './cii.types.js';
import { DOMParser } from 'xmldom';
import * as xpath from 'xpath';

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

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

    try {
      // 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();
    } catch (error) {
      this.addError('CII-PARSE', `Failed to parse XML: ${error}`, '/');
    }
  }

  /**
   * Validates CII XML against the specified level of validation
   * @param level Validation level
   * @returns Result of validation
   */
  public validate(level: ValidationLevel = ValidationLevel.SYNTAX): ValidationResult {
    // Reset errors
    this.errors = [];

    // Check if document was parsed successfully
    if (!this.doc) {
      return {
        valid: false,
        errors: this.errors,
        level: level
      };
    }

    // Perform validation based on level
    let valid = true;

    if (level === ValidationLevel.SYNTAX) {
      valid = this.validateSchema();
    } else if (level === ValidationLevel.SEMANTIC) {
      valid = this.validateSchema() && this.validateStructure();
    } else if (level === ValidationLevel.BUSINESS) {
      valid = this.validateSchema() &&
              this.validateStructure() &&
              this.validateBusinessRules();
    }

    return {
      valid,
      errors: this.errors,
      level
    };
  }

  /**
   * Validates CII XML against schema
   * @returns True if schema validation passed
   */
  protected validateSchema(): boolean {
    // Basic schema validation (simplified for now)
    if (!this.doc) return false;

    // Check for root element
    const root = this.doc.documentElement;
    if (!root || root.nodeName !== 'rsm:CrossIndustryInvoice') {
      this.addError('CII-SCHEMA-1', 'Root element must be rsm:CrossIndustryInvoice', '/');
      return false;
    }

    // Check for required namespaces
    if (!root.lookupNamespaceURI('rsm') || !root.lookupNamespaceURI('ram')) {
      this.addError('CII-SCHEMA-2', 'Required namespaces rsm and ram must be declared', '/');
      return false;
    }

    return true;
  }

  /**
   * Validates structure of the CII XML document
   * @returns True if structure validation passed
   */
  protected abstract validateStructure(): boolean;

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

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

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