import { parse } from './parser/DSLParser.js';
import { RuleGenerator } from './generator/RuleGenerator.js';
import { validateDslText } from './validation/DSLValidation.js';

/**
 * DSL Compiler - Main integration layer
 * Compiles DSL text into rule configurations for the zanzibar-graph system
 */
export class DSLCompiler {
  constructor(arbiter) {
    this.arbiter = arbiter;
    this.parser = parse;
    this.generator = new RuleGenerator(arbiter);
    this.compiledPrograms = new Map();
  }

  /**
   * Compile DSL text into rule configurations
   * @param {string} dslText - DSL text to compile
   * @param {string} programName - Optional name for the program
   * @returns {Object} Compilation result
   */
  compile(dslText, programName = 'default') {
    try {
      const validation = validateDslText(dslText);
      if (!validation.success) {
        return {
          success: false,
          errors: validation.errors,
          warnings: validation.warnings,
          program: null,
          generatedRules: new Map()
        };
      }

      const program = validation.program;
      
      // Convert plain AST to ProgramNode structure
      const programNode = {
        definitions: program.body.filter(s => s.type === 'Definition'),
        facts: program.body.filter(s => s.type === 'Fact'),
        evidence: program.body.filter(s => s.type === 'Evidence'),
        measures: program.body.filter(s => s.type === 'Measure'),
        validate: () => ({ isValid: true, errors: [], warnings: [] })
      };

      // Generate rules from AST
      const generationResult = this.generator.generateRules(programNode);
      
      if (!generationResult.success) {
        return {
          success: false,
          errors: generationResult.errors,
          warnings: [],
          program: programNode,
          generatedRules: new Map()
        };
      }

      // Store compiled program
      this.compiledPrograms.set(programName, {
        program: programNode,
        generatedRules: this.generator.getGeneratedRules(),
        dependencyIndex: this.generator.getDependencyIndex(),
        compiledAt: new Date()
      });

      return {
        success: true,
        errors: validation.errors,
        warnings: validation.warnings,
        program: programNode,
        generatedRules: this.generator.getGeneratedRules(),
        dependencyIndex: this.generator.getDependencyIndex(),
        generatedCount: generationResult.generatedCount
      };
    } catch (error) {
      return {
        success: false,
        errors: [`Compilation error: ${error.message}`],
        warnings: [],
        program: null,
        generatedRules: new Map()
      };
    }
  }

  /**
   * Compile multiple DSL programs
   * @param {Object} programs - Map of program names to DSL text
   * @returns {Object} Compilation result for all programs
   */
  compileMultiple(programs) {
    const results = {};
    let overallSuccess = true;
    const allErrors = [];
    const allWarnings = [];

    for (const [name, dslText] of Object.entries(programs)) {
      const result = this.compile(dslText, name);
      results[name] = result;
      
      if (!result.success) {
        overallSuccess = false;
      }
      
      allErrors.push(...result.errors.map(err => `${name}: ${err}`));
      allWarnings.push(...result.warnings.map(warn => `${name}: ${warn}`));
    }

    return {
      success: overallSuccess,
      errors: allErrors,
      warnings: allWarnings,
      results: results
    };
  }

  /**
   * Get compiled program by name
   * @param {string} programName - Name of the program
   * @returns {Object|null} Compiled program or null
   */
  getCompiledProgram(programName) {
    return this.compiledPrograms.get(programName) || null;
  }

  /**
   * Get all compiled programs
   * @returns {Map} Map of all compiled programs
   */
  getAllCompiledPrograms() {
    return this.compiledPrograms;
  }

  /**
   * Remove compiled program
   * @param {string} programName - Name of the program to remove
   * @returns {boolean} True if removed successfully
   */
  removeCompiledProgram(programName) {
    return this.compiledPrograms.delete(programName);
  }

  /**
   * Clear all compiled programs
   */
  clearCompiledPrograms() {
    this.compiledPrograms.clear();
  }

  /**
   * Get compilation statistics
   * @returns {Object} Compilation statistics
   */
  getCompilationStats() {
    const stats = {
      totalPrograms: this.compiledPrograms.size,
      totalRules: 0,
      programs: {}
    };

    this.compiledPrograms.forEach((program, name) => {
      const programStats = {
        name: name,
        compiledAt: program.compiledAt,
        ruleCount: program.generatedRules.size,
        definitions: program.program.definitions.length,
        facts: program.program.facts.length,
        evidence: program.program.evidence.length,
        measures: program.program.measures.length
      };
      
      stats.programs[name] = programStats;
      stats.totalRules += program.generatedRules.size;
    });

    return stats;
  }

  /**
   * Validate DSL text without compiling
   * @param {string} dslText - DSL text to validate
   * @returns {Object} Validation result
   */
  validate(dslText) {
    const validation = validateDslText(dslText);
    return {
      success: validation.success,
      errors: validation.errors,
      warnings: validation.warnings,
      program: validation.program
    };
  }

  /**
   * Get parser errors from last parse
   * @returns {string[]} Array of parser errors
   */
  getParserErrors() {
    return this.parser.getErrors();
  }

  /**
   * Get generator errors from last generation
   * @returns {string[]} Array of generator errors
   */
  getGeneratorErrors() {
    return this.generator.getErrors();
  }

  /**
   * Check if a relation is configured
   * @param {string} relation - Relation name to check
   * @returns {boolean} True if relation is configured
   */
  isRelationConfigured(relation) {
    return this.arbiter.relationConfigs.has(relation);
  }

  /**
   * Get relation configuration
   * @param {string} relation - Relation name
   * @returns {Object|null} Relation configuration or null
   */
  getRelationConfig(relation) {
    return this.arbiter.relationConfigs.get(relation) || null;
  }

  /**
   * Get all configured relations
   * @returns {Map} Map of all relation configurations
   */
  getAllRelationConfigs() {
    return this.arbiter.relationConfigs;
  }

  /**
   * Export compiled program to JSON
   * @param {string} programName - Name of the program to export
   * @returns {string|null} JSON string or null if program not found
   */
  exportProgram(programName) {
    const program = this.getCompiledProgram(programName);
    if (!program) {
      return null;
    }

    return JSON.stringify({
      name: programName,
      compiledAt: program.compiledAt,
      program: this.serializeProgram(program.program),
      generatedRules: Array.from(program.generatedRules.entries())
    }, null, 2);
  }

  /**
   * Import compiled program from JSON
   * @param {string} jsonString - JSON string to import
   * @returns {boolean} True if imported successfully
   */
  importProgram(jsonString) {
    try {
      const data = JSON.parse(jsonString);
      const program = this.deserializeProgram(data.program);
      
      this.compiledPrograms.set(data.name, {
        program: program,
        generatedRules: new Map(data.generatedRules),
        compiledAt: new Date(data.compiledAt)
      });

      // Apply rules to arbiter
      data.generatedRules.forEach(([relation, config]) => {
        this.arbiter.setRelationConfig(relation, config);
      });

      return true;
    } catch (error) {
      return false;
    }
  }

  /**
   * Serialize program to plain object
   * @param {ProgramNode} program - Program to serialize
   * @returns {Object} Serialized program
   */
  serializeProgram(program) {
    // This is a simplified serialization - in a real implementation,
    // you'd want to properly serialize all node types
    return {
      type: 'Program',
      definitions: program.definitions.map(def => ({
        type: 'Definition',
        name: def.name,
        definitionType: def.definitionType,
        fields: def.fields.map(field => ({
          type: 'Field',
          name: field.name,
          type: field.type,
          isArray: field.isArray,
          isOptional: field.isOptional
        }))
      })),
      facts: program.facts.map(fact => ({
        type: 'Fact',
        name: fact.name,
        parameters: fact.parameters.map(param => ({
          type: 'Parameter',
          name: param.name,
          type: param.type,
          isArray: param.isArray
        }))
      })),
      evidence: program.evidence.map(ev => ({
        type: 'Evidence',
        name: ev.name,
        parameters: ev.parameters.map(param => ({
          type: 'Parameter',
          name: param.name,
          type: param.type,
          isArray: param.isArray
        })),
        returnType: ev.returnType
      })),
      measures: program.measures.map(measure => ({
        type: 'Measure',
        name: measure.name,
        parameters: measure.parameters.map(param => ({
          type: 'Parameter',
          name: param.name,
          type: param.type,
          isArray: param.isArray
        })),
        returnType: measure.returnType
      }))
    };
  }

  /**
   * Deserialize program from plain object
   * @param {Object} data - Serialized program data
   * @returns {ProgramNode} Deserialized program
   */
  deserializeProgram(data) {
    // This is a simplified deserialization - in a real implementation,
    // you'd want to properly deserialize all node types
    const program = new ProgramNode();
    
    // Note: This is a basic implementation. In practice, you'd need
    // to properly reconstruct all the AST nodes from the serialized data
    
    return program;
  }
}
