import { BaseValidator } from './base.validator.js';
import { ValidationLevel } from '../interfaces.js';
import type { ValidationResult, ValidationError } from '../interfaces.js';
import * as xpath from 'xpath';
import { DOMParser } from 'xmldom';

/**
 * Validator for UBL (Universal Business Language) invoice format
 * Implements validation rules according to EN16931 and UBL 2.1 specification
 */
export class UBLValidator extends BaseValidator {
  // XML namespaces for UBL
  private static NS_INVOICE = 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2';
  private static NS_CAC = 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2';
  private static NS_CBC = 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2';
  
  // XML document for processing
  private xmlDoc: Document | null = null;
  
  // UBL profile or customization ID
  private customizationId: string = '';
  
  constructor(xml: string) {
    super(xml);
    
    try {
      // Parse XML document
      this.xmlDoc = new DOMParser().parseFromString(xml, 'application/xml');
      
      // Determine UBL customization ID (e.g. EN16931, XRechnung)
      this.detectCustomizationId();
    } catch (error) {
      this.addError('UBL-PARSE', `Failed to parse XML: ${error}`, '/');
    }
  }
  
  /**
   * Validates the UBL invoice against the specified level
   * @param level Validation level
   * @returns Validation result
   */
  public validate(level: ValidationLevel = ValidationLevel.SYNTAX): ValidationResult {
    // Reset errors
    this.errors = [];
    
    // Check if document was parsed successfully
    if (!this.xmlDoc) {
      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 XML against schema
   * @returns True if schema validation passed
   */
  protected validateSchema(): boolean {
    // Basic schema validation (simplified for now)
    if (!this.xmlDoc) return false;
    
    // Check for root element
    const root = this.xmlDoc.documentElement;
    if (!root || (root.nodeName !== 'Invoice' && root.nodeName !== 'CreditNote')) {
      this.addError('UBL-SCHEMA-1', 'Root element must be Invoice or CreditNote', '/');
      return false;
    }
    
    // Check for required namespaces
    if (!root.lookupNamespaceURI('cac') || !root.lookupNamespaceURI('cbc')) {
      this.addError('UBL-SCHEMA-2', 'Required namespaces cac and cbc must be declared', '/');
      return false;
    }
    
    return true;
  }
  
  /**
   * Validates structure of the XML document
   * @returns True if structure validation passed
   */
  private validateStructure(): boolean {
    if (!this.xmlDoc) return false;
    
    let valid = true;
    
    // Check for required main sections
    const sections = [
      'cbc:ID',
      'cbc:IssueDate',
      'cac:AccountingSupplierParty',
      'cac:AccountingCustomerParty',
      'cac:LegalMonetaryTotal'
    ];
    
    for (const section of sections) {
      if (!this.exists(`/${this.getRootNodeName()}/${section}`)) {
        this.addError('UBL-STRUCT-1', `Required section ${section} is missing`, `/${this.getRootNodeName()}`);
        valid = false;
      }
    }
    
    // Check for TaxTotal section
    if (this.exists(`/${this.getRootNodeName()}/cac:TaxTotal`)) {
      const taxSubsections = [
        'cbc:TaxAmount',
        'cac:TaxSubtotal'
      ];
      
      for (const subsection of taxSubsections) {
        if (!this.exists(`/${this.getRootNodeName()}/cac:TaxTotal/${subsection}`)) {
          this.addError('UBL-STRUCT-2', `Required subsection ${subsection} is missing`, 
            `/${this.getRootNodeName()}/cac:TaxTotal`);
          valid = false;
        }
      }
    }
    
    return valid;
  }
  
  /**
   * Validates business rules
   * @returns True if business rule validation passed
   */
  protected validateBusinessRules(): boolean {
    if (!this.xmlDoc) return false;
    
    let valid = true;
    
    // BR-16: Amount due for payment (BT-115) = Invoice total amount with VAT (BT-112) - Paid amount (BT-113)
    valid = this.validateAmounts() && valid;
    
    // BR-CO-3: Value added tax point date (BT-7) and Value added tax point date code (BT-8) are mutually exclusive
    valid = this.validateMutuallyExclusiveFields() && valid;
    
    // BR-S-1: An Invoice that contains a line where the VAT category code is "Standard rated" 
    // shall contain the Seller VAT Identifier or the Seller tax representative VAT identifier
    valid = this.validateSellerVatIdentifier() && valid;
    
    // XRechnung specific rules when customization ID matches
    if (this.isXRechnung()) {
      valid = this.validateXRechnungRules() && valid;
    }
    
    return valid;
  }
  
  /**
   * Gets the root node name (Invoice or CreditNote)
   * @returns Root node name
   */
  private getRootNodeName(): string {
    if (!this.xmlDoc || !this.xmlDoc.documentElement) return 'Invoice';
    return this.xmlDoc.documentElement.nodeName;
  }
  
  /**
   * Detects UBL customization ID from the XML
   */
  private detectCustomizationId(): void {
    if (!this.xmlDoc) return;
    
    // Look for customization ID
    const customizationNode = xpath.select1(
      `string(/${this.getRootNodeName()}/cbc:CustomizationID)`, 
      this.xmlDoc
    );
    
    if (customizationNode) {
      this.customizationId = customizationNode.toString();
    }
  }
  
  /**
   * Checks if invoice is an XRechnung
   * @returns True if XRechnung customization ID is present
   */
  private isXRechnung(): boolean {
    return this.customizationId.includes('xrechnung') || 
           this.customizationId.includes('XRechnung');
  }
  
  /**
   * Validates amount calculations in the invoice
   * @returns True if amount validation passed
   */
  private validateAmounts(): boolean {
    if (!this.xmlDoc) return false;
    
    try {
      // Extract amounts
      const totalAmount = this.getNumberValue(
        `/${this.getRootNodeName()}/cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount`
      );
      
      const paidAmount = this.getNumberValue(
        `/${this.getRootNodeName()}/cac:LegalMonetaryTotal/cbc:PrepaidAmount`
      ) || 0;
      
      const dueAmount = this.getNumberValue(
        `/${this.getRootNodeName()}/cac:LegalMonetaryTotal/cbc:PayableAmount`
      );
      
      // Calculate expected due amount
      const expectedDueAmount = totalAmount - paidAmount;
      
      // Compare with a small tolerance for rounding errors
      if (Math.abs(dueAmount - expectedDueAmount) > 0.01) {
        this.addError(
          'BR-16', 
          `Amount due for payment (${dueAmount}) must equal Invoice total amount with VAT (${totalAmount}) - Paid amount (${paidAmount})`,
          `/${this.getRootNodeName()}/cac:LegalMonetaryTotal`
        );
        return false;
      }
      
      return true;
    } catch (error) {
      this.addError('UBL-AMOUNT', `Error validating amounts: ${error}`, '/');
      return false;
    }
  }
  
  /**
   * Validates mutually exclusive fields
   * @returns True if validation passed
   */
  private validateMutuallyExclusiveFields(): boolean {
    if (!this.xmlDoc) return false;
    
    try {
      // Check for VAT point date and code (BR-CO-3)
      const vatPointDate = this.exists(`/${this.getRootNodeName()}/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:TaxPointDate`);
      const vatPointDateCode = this.exists(`/${this.getRootNodeName()}/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:TaxExemptionReasonCode`);
      
      if (vatPointDate && vatPointDateCode) {
        this.addError(
          'BR-CO-3', 
          'Value added tax point date and Value added tax point date code are mutually exclusive',
          `/${this.getRootNodeName()}/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory`
        );
        return false;
      }
      
      return true;
    } catch (error) {
      this.addError('UBL-MUTUAL', `Error validating mutually exclusive fields: ${error}`, '/');
      return false;
    }
  }
  
  /**
   * Validates seller VAT identifier requirements
   * @returns True if validation passed
   */
  private validateSellerVatIdentifier(): boolean {
    if (!this.xmlDoc) return false;
    
    try {
      // Check if there are any standard rated line items
      const standardRatedItems = this.exists(
        `/${this.getRootNodeName()}/cac:InvoiceLine/cac:Item/cac:ClassifiedTaxCategory/cbc:ID[text()="S"]`
      );
      
      if (standardRatedItems) {
        // Check for seller VAT identifier
        const sellerVatId = this.exists(`/${this.getRootNodeName()}/cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID`);
        const sellerTaxRepId = this.exists(`/${this.getRootNodeName()}/cac:TaxRepresentativeParty/cac:PartyTaxScheme/cbc:CompanyID`);
        
        if (!sellerVatId && !sellerTaxRepId) {
          this.addError(
            'BR-S-1', 
            'An Invoice with standard rated items must contain the Seller VAT Identifier or Tax representative VAT identifier',
            `/${this.getRootNodeName()}/cac:AccountingSupplierParty/cac:Party`
          );
          return false;
        }
      }
      
      return true;
    } catch (error) {
      this.addError('UBL-VAT', `Error validating seller VAT identifier: ${error}`, '/');
      return false;
    }
  }
  
  /**
   * Validates XRechnung specific rules
   * @returns True if validation passed
   */
  private validateXRechnungRules(): boolean {
    if (!this.xmlDoc) return false;
    
    let valid = true;
    
    try {
      // BR-DE-1: Buyer reference must be present for German VAT compliance
      if (!this.exists(`/${this.getRootNodeName()}/cbc:BuyerReference`)) {
        this.addError(
          'BR-DE-1', 
          'BuyerReference is mandatory for XRechnung',
          `/${this.getRootNodeName()}`
        );
        valid = false;
      }
      
      // BR-DE-15: Contact information must be present
      if (!this.exists(`/${this.getRootNodeName()}/cac:AccountingSupplierParty/cac:Party/cac:Contact`)) {
        this.addError(
          'BR-DE-15', 
          'Supplier contact information is mandatory for XRechnung',
          `/${this.getRootNodeName()}/cac:AccountingSupplierParty/cac:Party`
        );
        valid = false;
      }
      
      // BR-DE-16: Electronic address identifier scheme (e.g. PEPPOL) must be present
      if (!this.exists(`/${this.getRootNodeName()}/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID`) ||
          !this.exists(`/${this.getRootNodeName()}/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID/@schemeID`)) {
        this.addError(
          'BR-DE-16', 
          'Supplier electronic address with scheme identifier is mandatory for XRechnung',
          `/${this.getRootNodeName()}/cac:AccountingSupplierParty/cac:Party`
        );
        valid = false;
      }
      
      return valid;
    } catch (error) {
      this.addError('UBL-XRECHNUNG', `Error validating XRechnung rules: ${error}`, '/');
      return false;
    }
  }
  
  /**
   * Helper method to check if a node exists
   * @param xpathExpression XPath to check
   * @returns True if node exists
   */
  private exists(xpathExpression: string): boolean {
    if (!this.xmlDoc) return false;
    const nodes = xpath.select(xpathExpression, this.xmlDoc);
    // Handle different return types from xpath.select()
    if (Array.isArray(nodes)) {
      return nodes.length > 0;
    }
    return nodes ? true : false;
  }
  
  /**
   * Helper method to get a number value from XPath
   * @param xpathExpression XPath to get number from
   * @returns Number value or NaN if not found
   */
  private getNumberValue(xpathExpression: string): number {
    if (!this.xmlDoc) return NaN;
    const node = xpath.select1(`string(${xpathExpression})`, this.xmlDoc);
    return node ? parseFloat(node.toString()) : NaN;
  }
}