/**
 * @fileoverview OrdoJS Dependency Analyzer - Tracks reactive variable usage and builds dependency graphs
 */

import {
    DirectiveType,
    ExpressionType,
    OptimizationError,
    OptimizationType,
    type AttributeNode,
    type ClientBlockNode,
    type ComponentAST,
    type ComponentNode,
    type ExpressionNode,
    type HTMLElementNode,
    type InterpolationNode,
    type MarkupBlockNode,
    type ReactiveVariableNode,
    type SourceRange
} from '../types/index.js';

/**
 * Dependency relationship between variables
 */
export interface Dependency {
  from: string;
  to: string;
  type: DependencyType;
  location: SourceRange;
}

/**
 * Types of dependencies
 */
export enum DependencyType {
  READ = 'READ',           // Variable is read
  WRITE = 'WRITE',         // Variable is written to
  COMPUTED = 'COMPUTED',   // Variable is used in computed expression
  EVENT = 'EVENT',         // Variable is used in event handler
  INTERPOLATION = 'INTERPOLATION' // Variable is used in template interpolation
}

/**
 * Dependency graph node
 */
export interface DependencyNode {
  name: string;
  variable: ReactiveVariableNode;
  dependencies: Set<string>;
  dependents: Set<string>;
  updateOrder: number;
  isCircular: boolean;
}

/**
 * Dependency graph structure
 */
export interface DependencyGraph {
  nodes: Map<string, DependencyNode>;
  edges: Dependency[];
  updateOrder: string[];
  circularDependencies: string[][];
}

/**
 * Update function metadata
 */
export interface UpdateFunction {
  variableName: string;
  targetElements: string[];
  updateType: UpdateType;
  code: string;
  dependencies: string[];
}

/**
 * Types of DOM updates
 */
export enum UpdateType {
  TEXT_CONTENT = 'TEXT_CONTENT',
  ATTRIBUTE = 'ATTRIBUTE',
  PROPERTY = 'PROPERTY',
  CLASS = 'CLASS',
  STYLE = 'STYLE',
  CONDITIONAL = 'CONDITIONAL',
  LIST = 'LIST'
}

/**
 * Dependency analyzer for reactive variables
 */
export class DependencyAnalyzer {
  private graph: DependencyGraph;
  private currentComponent: ComponentNode | null = null;
  private errors: OptimizationError[] = [];

  constructor() {
    this.graph = {
      nodes: new Map(),
      edges: [],
      updateOrder: [],
      circularDependencies: []
    };
  }

  /**
   * Analyze dependencies in a component AST
   */
  analyze(ast: ComponentAST): DependencyGraph {
    this.reset();
    this.currentComponent = ast.component;

    try {
      // Step 1: Build initial dependency nodes from reactive variables
      this.buildDependencyNodes(ast.component);

      // Step 2: Analyze expressions and markup for variable usage
      this.analyzeVariableUsage(ast.component);

      // Step 3: Detect circular dependencies
      this.detectCircularDependencies();

      // Step 4: Calculate update order
      this.calculateUpdateOrder();

      return this.graph;
    } catch (error) {
      if (error instanceof OptimizationError) {
        this.errors.push(error);
      }
      throw error;
    }
  }

  /**
   * Generate efficient update functions for reactive changes
   */
  generateUpdateFunctions(ast: ComponentAST): UpdateFunction[] {
    const graph = this.analyze(ast);
    const updateFunctions: UpdateFunction[] = [];

    // Generate update functions for each reactive variable
    for (const [varName, node] of graph.nodes) {
      const updateFunction = this.generateUpdateFunction(varName, node, ast.component);
      if (updateFunction) {
        updateFunctions.push(updateFunction);
      }
    }

    return updateFunctions;
  }

  /**
   * Get analysis errors
   */
  getErrors(): OptimizationError[] {
    return this.errors;
  }

  /**
   * Reset analyzer state
   */
  private reset(): void {
    this.graph = {
      nodes: new Map(),
      edges: [],
      updateOrder: [],
      circularDependencies: []
    };
    this.currentComponent = null;
    this.errors = [];
  }

  /**
   * Build initial dependency nodes from reactive variables
   */
  private buildDependencyNodes(component: ComponentNode): void {
    if (!component.clientBlock) {
      return;
    }

    // First pass: Create all nodes without analyzing dependencies
    for (const variable of component.clientBlock.reactiveVariables) {
      const node: DependencyNode = {
        name: variable.name,
        variable,
        dependencies: new Set(),
        dependents: new Set(),
        updateOrder: 0,
        isCircular: false
      };

      this.graph.nodes.set(variable.name, node);
    }

    // Create nodes for computed values
    for (const computed of component.clientBlock.computedValues) {
      const node: DependencyNode = {
        name: computed.name,
        variable: {
          type: 'ReactiveVariable',
          name: computed.name,
          initialValue: computed.expression,
          dataType: computed.dataType,
          isConst: true,
          range: computed.range
        },
        dependencies: new Set(),
        dependents: new Set(),
        updateOrder: 0,
        isCircular: false
      };

      this.graph.nodes.set(computed.name, node);
    }

    // Second pass: Analyze dependencies now that all nodes exist
    for (const variable of component.clientBlock.reactiveVariables) {
      this.analyzeExpressionDependencies(variable.initialValue, variable.name, DependencyType.COMPUTED);
    }

    for (const computed of component.clientBlock.computedValues) {
      this.analyzeExpressionDependencies(computed.expression, computed.name, DependencyType.COMPUTED);
    }
  }

  /**
   * Analyze variable usage throughout the component
   */
  private analyzeVariableUsage(component: ComponentNode): void {
    // Analyze markup block for interpolations and directives
    if (component.markupBlock) {
      this.analyzeMarkupBlock(component.markupBlock);
    }

    // Analyze client block for event handlers and functions
    if (component.clientBlock) {
      this.analyzeClientBlock(component.clientBlock);
    }
  }

  /**
   * Analyze markup block for variable dependencies
   */
  private analyzeMarkupBlock(markupBlock: MarkupBlockNode): void {
    // Analyze interpolations
    for (const interpolation of markupBlock.interpolations) {
      this.analyzeInterpolation(interpolation);
    }

    // Analyze HTML elements
    for (const element of markupBlock.elements) {
      this.analyzeHTMLElement(element);
    }
  }

  /**
   * Analyze HTML element for variable dependencies
   */
  private analyzeHTMLElement(element: HTMLElementNode): void {
    // Analyze attributes for directives and expressions
    for (const attr of element.attributes) {
      this.analyzeAttribute(attr);
    }

    // Recursively analyze children
    for (const child of element.children) {
      if (child.type === 'HTMLElement') {
        this.analyzeHTMLElement(child as HTMLElementNode);
      } else if (child.type === 'Interpolation') {
        this.analyzeInterpolation(child as InterpolationNode);
      }
    }
  }

  /**
   * Analyze attribute for variable dependencies
   */
  private analyzeAttribute(attr: AttributeNode): void {
    if (attr.isDirective && typeof attr.value !== 'string') {
      const expression = attr.value as ExpressionNode;

      if (attr.directiveType === DirectiveType.BIND) {
        // Two-way binding creates both read and write dependencies
        this.analyzeExpressionDependencies(expression, '', DependencyType.READ);
        this.analyzeExpressionDependencies(expression, '', DependencyType.WRITE);
      } else if (attr.directiveType === DirectiveType.ON) {
        // Event handlers create write dependencies
        this.analyzeExpressionDependencies(expression, '', DependencyType.EVENT);
      } else {
        // Other directives create read dependencies
        this.analyzeExpressionDependencies(expression, '', DependencyType.READ);
      }
    } else if (typeof attr.value !== 'string') {
      // Regular attribute with expression
      this.analyzeExpressionDependencies(attr.value as ExpressionNode, '', DependencyType.READ);
    }
  }

  /**
   * Analyze interpolation for variable dependencies
   */
  private analyzeInterpolation(interpolation: InterpolationNode): void {
    this.analyzeExpressionDependencies(interpolation.expression, '', DependencyType.INTERPOLATION);
  }

  /**
   * Analyze client block for variable dependencies
   */
  private analyzeClientBlock(clientBlock: ClientBlockNode): void {
    // Analyze event handlers
    for (const handler of clientBlock.eventHandlers) {
      if (handler.handler && typeof handler.handler !== 'string') {
        if (Array.isArray(handler.handler)) {
          // Statement array - would need statement analysis
          // For now, skip complex statement analysis
        } else {
          this.analyzeExpressionDependencies(handler.handler as ExpressionNode, '', DependencyType.EVENT);
        }
      }
    }

    // Analyze functions
    for (const func of clientBlock.functions) {
      // Analyze function body statements
      for (const statement of func.body) {
        if (statement.expression) {
          this.analyzeExpressionDependencies(statement.expression, '', DependencyType.READ);
        }
      }
    }
  }

  /**
   * Analyze expression for variable dependencies
   */
  private analyzeExpressionDependencies(
    expr: ExpressionNode,
    targetVariable: string,
    dependencyType: DependencyType
  ): void {
    switch (expr.expressionType) {
      case ExpressionType.IDENTIFIER:
        if (expr.identifier && this.graph.nodes.has(expr.identifier)) {
          // For computed dependencies, the target depends on the identifier
          // So we add dependency from identifier to target
          this.addDependency(expr.identifier, targetVariable, dependencyType, expr.range);
        }
        break;

      case ExpressionType.BINARY:
        if (expr.left) {
          this.analyzeExpressionDependencies(expr.left, targetVariable, dependencyType);
        }
        if (expr.right) {
          this.analyzeExpressionDependencies(expr.right, targetVariable, dependencyType);
        }
        break;

      case ExpressionType.UNARY:
        if (expr.right) {
          this.analyzeExpressionDependencies(expr.right, targetVariable, dependencyType);
        }
        break;

      case ExpressionType.CALL:
        if (expr.callee) {
          this.analyzeExpressionDependencies(expr.callee, targetVariable, dependencyType);
        }
        if (expr.arguments) {
          for (const arg of expr.arguments) {
            this.analyzeExpressionDependencies(arg, targetVariable, dependencyType);
          }
        }
        break;

      case ExpressionType.MEMBER:
        if (expr.object) {
          this.analyzeExpressionDependencies(expr.object, targetVariable, dependencyType);
        }
        if (expr.property) {
          this.analyzeExpressionDependencies(expr.property, targetVariable, dependencyType);
        }
        break;

      case ExpressionType.ASSIGNMENT:
        if (expr.left) {
          // Left side is being written to
          this.analyzeExpressionDependencies(expr.left, targetVariable, DependencyType.WRITE);
        }
        if (expr.right) {
          // Right side is being read from
          this.analyzeExpressionDependencies(expr.right, targetVariable, DependencyType.READ);
        }
        break;

      case ExpressionType.CONDITIONAL:
        // Analyze all parts of conditional expression
        if (expr.left) { // condition
          this.analyzeExpressionDependencies(expr.left, targetVariable, dependencyType);
        }
        if (expr.right) { // true/false branches would be in a different structure
          this.analyzeExpressionDependencies(expr.right, targetVariable, dependencyType);
        }
        break;

      case ExpressionType.ARRAY:
        if (expr.arguments) {
          for (const element of expr.arguments) {
            this.analyzeExpressionDependencies(element, targetVariable, dependencyType);
          }
        }
        break;

      case ExpressionType.OBJECT:
        // Object expressions would need property analysis
        // For now, skip complex object analysis
        break;

      case ExpressionType.LITERAL:
        // Literals don't create dependencies
        break;
    }
  }

  /**
   * Add a dependency relationship
   */
  private addDependency(
    from: string,
    to: string,
    type: DependencyType,
    location: SourceRange
  ): void {
    const dependency: Dependency = { from, to, type, location };
    this.graph.edges.push(dependency);

    // Update dependency graph nodes
    if (to && this.graph.nodes.has(to)) {
      const toNode = this.graph.nodes.get(to)!;
      toNode.dependencies.add(from);
    }

    if (this.graph.nodes.has(from)) {
      const fromNode = this.graph.nodes.get(from)!;
      if (to) {
        fromNode.dependents.add(to);
      }
    }
  }

  /**
   * Detect circular dependencies using depth-first search
   */
  private detectCircularDependencies(): void {
    const visited = new Set<string>();
    const recursionStack = new Set<string>();
    const currentPath: string[] = [];

    for (const [nodeName] of this.graph.nodes) {
      if (!visited.has(nodeName)) {
        this.dfsCircularDetection(nodeName, visited, recursionStack, currentPath);
      }
    }
  }

  /**
   * Depth-first search for circular dependency detection
   */
  private dfsCircularDetection(
    nodeName: string,
    visited: Set<string>,
    recursionStack: Set<string>,
    currentPath: string[]
  ): void {
    visited.add(nodeName);
    recursionStack.add(nodeName);
    currentPath.push(nodeName);

    const node = this.graph.nodes.get(nodeName);
    if (!node) return;

    // Follow the dependency chain - if A depends on B, we go from A to B
    for (const dependency of node.dependencies) {
      if (!visited.has(dependency)) {
        this.dfsCircularDetection(dependency, visited, recursionStack, currentPath);
      } else if (recursionStack.has(dependency)) {
        // Found circular dependency
        const cycleStart = currentPath.indexOf(dependency);
        const cycle = currentPath.slice(cycleStart).concat([dependency]);
        this.graph.circularDependencies.push(cycle);

        // Mark all nodes in cycle as circular
        for (const cycleName of cycle) {
          const cycleNode = this.graph.nodes.get(cycleName);
          if (cycleNode) {
            cycleNode.isCircular = true;
          }
        }

        // Create warning for circular dependency
        const currentNode = this.graph.nodes.get(nodeName);
        if (currentNode) {
          this.errors.push(new OptimizationError(
            `Circular dependency detected: ${cycle.join(' -> ')}`,
            currentNode.variable.range,
            OptimizationType.DEPENDENCY_OPTIMIZATION
          ));
        }
      }
    }

    recursionStack.delete(nodeName);
    currentPath.pop();
  }

  /**
   * Calculate optimal update order using topological sort
   */
  private calculateUpdateOrder(): void {
    const inDegree = new Map<string, number>();
    const queue: string[] = [];

    // Initialize in-degree count
    for (const [nodeName] of this.graph.nodes) {
      inDegree.set(nodeName, 0);
    }

    // Calculate in-degrees - count how many dependencies each node has
    for (const [nodeName, node] of this.graph.nodes) {
      inDegree.set(nodeName, node.dependencies.size);
    }

    // Find nodes with no dependencies (in-degree = 0)
    for (const [nodeName, degree] of inDegree) {
      if (degree === 0) {
        queue.push(nodeName);
      }
    }

    const updateOrder: string[] = [];
    let orderIndex = 0;

    // Process nodes in topological order
    while (queue.length > 0) {
      const nodeName = queue.shift()!;
      updateOrder.push(nodeName);

      const node = this.graph.nodes.get(nodeName);
      if (node) {
        node.updateOrder = orderIndex++;

        // Reduce in-degree of dependent nodes
        for (const dependent of node.dependents) {
          const currentInDegree = inDegree.get(dependent) || 0;
          const newInDegree = currentInDegree - 1;
          inDegree.set(dependent, newInDegree);

          if (newInDegree === 0) {
            queue.push(dependent);
          }
        }
      }
    }

    // Handle circular dependencies by adding them at the end
    for (const [nodeName, node] of this.graph.nodes) {
      if (!updateOrder.includes(nodeName)) {
        updateOrder.push(nodeName);
        node.updateOrder = orderIndex++;
      }
    }

    this.graph.updateOrder = updateOrder;
  }

  /**
   * Generate update function for a specific variable
   */
  private generateUpdateFunction(
    varName: string,
    node: DependencyNode,
    component: ComponentNode
  ): UpdateFunction | null {
    const targetElements: string[] = [];
    const dependencies: string[] = Array.from(node.dependencies);

    // Find all elements that depend on this variable
    const updateType = this.determineUpdateType(varName, component);

    // Generate update code based on update type
    let code = '';
    switch (updateType) {
      case UpdateType.TEXT_CONTENT:
        code = this.generateTextContentUpdate(varName);
        break;
      case UpdateType.ATTRIBUTE:
        code = this.generateAttributeUpdate(varName);
        break;
      case UpdateType.PROPERTY:
        code = this.generatePropertyUpdate(varName);
        break;
      case UpdateType.CLASS:
        code = this.generateClassUpdate(varName);
        break;
      case UpdateType.STYLE:
        code = this.generateStyleUpdate(varName);
        break;
      default:
        code = this.generateGenericUpdate(varName);
    }

    return {
      variableName: varName,
      targetElements,
      updateType,
      code,
      dependencies
    };
  }

  /**
   * Determine the type of update needed for a variable
   */
  private determineUpdateType(varName: string, component: ComponentNode): UpdateType {
    // Analyze how the variable is used in the markup
    // For now, default to text content updates
    return UpdateType.TEXT_CONTENT;
  }

  /**
   * Generate text content update code
   */
  private generateTextContentUpdate(varName: string): string {
    return `
      const ${varName}Elements = document.querySelectorAll('[data-bind-${varName}]');
      ${varName}Elements.forEach(el => {
        if (el.getAttribute('data-bind-type-${varName}') === 'text') {
          el.textContent = component.state.${varName};
        }
      });
    `.trim();
  }

  /**
   * Generate attribute update code
   */
  private generateAttributeUpdate(varName: string): string {
    return `
      const ${varName}Elements = document.querySelectorAll('[data-bind-${varName}]');
      ${varName}Elements.forEach(el => {
        const attrName = el.getAttribute('data-bind-attr-${varName}');
        if (attrName) {
          el.setAttribute(attrName, component.state.${varName});
        }
      });
    `.trim();
  }

  /**
   * Generate property update code
   */
  private generatePropertyUpdate(varName: string): string {
    return `
      const ${varName}Elements = document.querySelectorAll('[data-bind-${varName}]');
      ${varName}Elements.forEach(el => {
        const propName = el.getAttribute('data-bind-prop-${varName}');
        if (propName && propName in el) {
          el[propName] = component.state.${varName};
        }
      });
    `.trim();
  }

  /**
   * Generate class update code
   */
  private generateClassUpdate(varName: string): string {
    return `
      const ${varName}Elements = document.querySelectorAll('[data-bind-${varName}]');
      ${varName}Elements.forEach(el => {
        const className = el.getAttribute('data-bind-class-${varName}');
        if (className) {
          el.classList.toggle(className, !!component.state.${varName});
        }
      });
    `.trim();
  }

  /**
   * Generate style update code
   */
  private generateStyleUpdate(varName: string): string {
    return `
      const ${varName}Elements = document.querySelectorAll('[data-bind-${varName}]');
      ${varName}Elements.forEach(el => {
        const styleProp = el.getAttribute('data-bind-style-${varName}');
        if (styleProp) {
          el.style[styleProp] = component.state.${varName};
        }
      });
    `.trim();
  }

  /**
   * Generate generic update code
   */
  private generateGenericUpdate(varName: string): string {
    return `
      const ${varName}Elements = document.querySelectorAll('[data-bind-${varName}]');
      ${varName}Elements.forEach(el => {
        const bindType = el.getAttribute('data-bind-type-${varName}');
        const value = component.state.${varName};

        switch (bindType) {
          case 'text':
            el.textContent = value;
            break;
          case 'attr':
            const attrName = el.getAttribute('data-bind-attr-${varName}');
            if (attrName) el.setAttribute(attrName, value);
            break;
          case 'prop':
            const propName = el.getAttribute('data-bind-prop-${varName}');
            if (propName && propName in el) el[propName] = value;
            break;
          case 'class':
            const className = el.getAttribute('data-bind-class-${varName}');
            if (className) el.classList.toggle(className, !!value);
            break;
          case 'style':
            const styleProp = el.getAttribute('data-bind-style-${varName}');
            if (styleProp) el.style[styleProp] = value;
            break;
        }
      });
    `.trim();
  }
}
