/**
 * @fileoverview OrdoJS Code Generator - Transforms AST into JavaScript and HTML
 */

import {
    type CompilationTarget,
    type ComponentAST,
    type ExpressionNode,
    type GeneratedCode,
    type HTMLElementNode,
    type InterpolationNode,
    type MarkupBlockNode,
    type ReactiveVariableNode,
    type SourceMap,
    type StatementNode,
    type TextNode
} from '../types/index.js';
import { RPCGenerator, type RPCGeneratorOptions } from './rpc-generator.js';

/**
 * Code generator options
 */
interface CodeGeneratorOptions {
  minify: boolean;
  sourceMaps: boolean;
  target: 'development' | 'production';
  compilationTarget?: CompilationTarget;
  rpcOptions?: RPCGeneratorOptions;
}

/**
 * Default code generator options
 */
const DEFAULT_OPTIONS: CodeGeneratorOptions = {
  minify: false,
  sourceMaps: true,
  target: 'development',
  compilationTarget: 'client'
};

/**
 * OrdoJS Code Generator
 * Transforms AST into JavaScript and HTML output
 */
export class OrdoJSCodeGenerator {
  private options: CodeGeneratorOptions;
  private componentId: string;
  private reactiveVars: Map<string, ReactiveVariableNode>;
  private eventHandlers: Map<string, string>;
  private indentLevel: number = 0;
  private sourceMap: SourceMap;
  private rpcGenerator: RPCGenerator;

  constructor(options: Partial<CodeGeneratorOptions> = {}) {
    this.options = { ...DEFAULT_OPTIONS, ...options };
    this.componentId = '';
    this.reactiveVars = new Map();
    this.eventHandlers = new Map();
    this.sourceMap = {
      version: 3,
      sources: [],
      names: [],
      mappings: '',
      sourcesContent: [],
    };
    this.rpcGenerator = new RPCGenerator(options.rpcOptions);
  }

  /**
   * Generate client-side JavaScript code from AST
   */
  generateClientCode(ast: ComponentAST): string {
    this.componentId = `ordojs_${ast.component.name}_${Date.now().toString(36)}`;
    this.reactiveVars.clear();
    this.eventHandlers.clear();
    this.indentLevel = 0;

    // Extract reactive variables from client block
    if (ast.component.clientBlock) {
      for (const variable of ast.component.clientBlock.reactiveVariables) {
        this.reactiveVars.set(variable.name, variable);
      }
    }

    const lines: string[] = [];

    // Generate component wrapper function
    lines.push(`// OrdoJS component: ${ast.component.name}`);
    lines.push(`function ${ast.component.name}(props = {}) {`);
    this.indentLevel++;

    // Generate component initialization
    lines.push(this.indent(`const component = {};`));
    lines.push(this.indent(`component.id = "${this.componentId}";`));
    lines.push(this.indent(`component.props = props;`));
    lines.push(this.indent(`component.state = {};`));
    lines.push(this.indent(`component.refs = {};`));
    lines.push(this.indent(`component.update = function() { updateDOM(component); };`));
    lines.push('');

    // Generate reactive state initialization
    if (ast.component.clientBlock) {
      lines.push(this.indent(`// Initialize reactive state`));
      for (const variable of ast.component.clientBlock.reactiveVariables) {
        const initialValueCode = this.generateExpression(variable.initialValue);
        lines.push(this.indent(`let ${variable.name} = ${initialValueCode};`));
        lines.push(this.indent(`Object.defineProperty(component.state, "${variable.name}", {`));
        this.indentLevel++;
        lines.push(this.indent(`get: () => ${variable.name},`));
        lines.push(this.indent(`set: (value) => {`));
        this.indentLevel++;
        lines.push(this.indent(`${variable.name} = value;`));
        lines.push(this.indent(`component.update();`));
        this.indentLevel--;
        lines.push(this.indent(`},`));
        lines.push(this.indent(`enumerable: true`));
        this.indentLevel--;
        lines.push(this.indent(`});`));
      }
      lines.push('');
    }

    // Generate client-side functions
    if (ast.component.clientBlock && ast.component.clientBlock.functions.length > 0) {
      lines.push(this.indent(`// Client-side functions`));
      for (const func of ast.component.clientBlock.functions) {
        lines.push(this.indent(`function ${func.name}(${this.generateFunctionParameters(func.parameters)}) {`));
        this.indentLevel++;

        // Generate function body
        for (const statement of func.body) {
          lines.push(this.indent(this.generateStatement(statement)));
        }

        this.indentLevel--;
        lines.push(this.indent(`}`));
        lines.push('');
      }
    }

    // Generate server function stubs for RPC calls
    if (ast.component.serverBlock) {
      const rpcStubs = this.rpcGenerator.generateRPCStubs(ast);
      if (rpcStubs.length > 0) {
        lines.push(this.indent(`// Server function stubs for RPC calls`));
        lines.push(this.indent(`component.server = component.server || {};`));

        for (const stub of rpcStubs) {
          // Extract the function body from the stub code
          const stubCode = stub.clientCode;
          const functionBodyStart = stubCode.indexOf('{') + 1;
          const functionBodyEnd = stubCode.lastIndexOf('}');
          const functionBody = stubCode.substring(functionBodyStart, functionBodyEnd).trim();

          // Add the function to the component.server object
          lines.push(this.indent(`component.server.${stub.functionName} = async function(${this.generateFunctionParameters(stub.metadata.parameters)}) {`));
          this.indentLevel++;

          // Split the function body into lines and add proper indentation
          const bodyLines = functionBody.split('\n');
          for (const line of bodyLines) {
            if (line.trim()) {
              lines.push(this.indent(line.trim()));
            }
          }

          this.indentLevel--;
          lines.push(this.indent(`};`));
          lines.push('');
        }
      }
    }

    // Generate DOM creation code
    lines.push(this.indent(`// Create DOM elements`));
    lines.push(this.indent(`function createDOM() {`));
    this.indentLevel++;

    if (ast.component.markupBlock) {
      const domCreationCode = this.generateMarkupBlockCode(ast.component.markupBlock);
      lines.push(this.indent(`const rootElement = ${domCreationCode};`));
      lines.push(this.indent(`return rootElement;`));
    } else {
      lines.push(this.indent(`return document.createElement("div");`));
    }

    this.indentLevel--;
    lines.push(this.indent(`}`));
    lines.push('');

    // Generate DOM update function
    lines.push(this.indent(`// Update DOM elements`));
    lines.push(this.indent(`function updateDOM(component) {`));
    this.indentLevel++;
    lines.push(this.indent(`// Update text nodes and attributes based on state`));

    // Generate update code for each reactive variable
    for (const [name, variable] of this.reactiveVars.entries()) {
      lines.push(this.indent(`// Update elements that depend on ${name}`));
      lines.push(this.indent(`const ${name}Value = component.state.${name};`));
      lines.push(this.indent(`const ${name}Elements = document.querySelectorAll('[data-bind-${name}]');`));
      lines.push(this.indent(`${name}Elements.forEach(el => {`));
      this.indentLevel++;
      lines.push(this.indent(`const bindType = el.getAttribute('data-bind-type-${name}');`));
      lines.push(this.indent(`if (bindType === 'text') {`));
      this.indentLevel++;
      lines.push(this.indent(`el.textContent = ${name}Value;`));
      this.indentLevel--;
      lines.push(this.indent(`} else if (bindType === 'attr') {`));
      this.indentLevel++;
      lines.push(this.indent(`const attrName = el.getAttribute('data-bind-attr-${name}');`));
      lines.push(this.indent(`if (attrName) el.setAttribute(attrName, ${name}Value);`));
      this.indentLevel--;
      lines.push(this.indent(`}`));
      this.indentLevel--;
      lines.push(this.indent(`});`));
    }

    this.indentLevel--;
    lines.push(this.indent(`}`));
    lines.push('');

    // Generate event handler setup
    if (this.eventHandlers.size > 0) {
      lines.push(this.indent(`// Set up event handlers`));
      lines.push(this.indent(`function setupEventHandlers(rootElement) {`));
      this.indentLevel++;

      for (const [selector, handlerCode] of this.eventHandlers.entries()) {
        const [elementId, eventName] = selector.split(':');
        lines.push(this.indent(`const ${elementId}El = rootElement.querySelector('#${elementId}');`));
        lines.push(this.indent(`if (${elementId}El) {`));
        this.indentLevel++;
        lines.push(this.indent(`${elementId}El.addEventListener('${eventName}', (event) => {`));
        this.indentLevel++;
        lines.push(this.indent(`${handlerCode}`));
        lines.push(this.indent(`component.update();`));
        this.indentLevel--;
        lines.push(this.indent(`});`));
        this.indentLevel--;
        lines.push(this.indent(`}`));
      }

      this.indentLevel--;
      lines.push(this.indent(`}`));
      lines.push('');
    }

    // Generate mount and unmount methods
    lines.push(this.indent(`// Component lifecycle methods`));
    lines.push(this.indent(`component.mount = function(container) {`));
    this.indentLevel++;
    lines.push(this.indent(`const rootElement = createDOM();`));
    if (this.eventHandlers.size > 0) {
      lines.push(this.indent(`setupEventHandlers(rootElement);`));
    }
    lines.push(this.indent(`container.appendChild(rootElement);`));
    lines.push(this.indent(`component.rootElement = rootElement;`));
    lines.push(this.indent(`return component;`));
    this.indentLevel--;
    lines.push(this.indent(`};`));
    lines.push('');

    lines.push(this.indent(`component.unmount = function() {`));
    this.indentLevel++;
    lines.push(this.indent(`if (component.rootElement && component.rootElement.parentNode) {`));
    this.indentLevel++;
    lines.push(this.indent(`component.rootElement.parentNode.removeChild(component.rootElement);`));
    this.indentLevel--;
    lines.push(this.indent(`}`));
    this.indentLevel--;
    lines.push(this.indent(`};`));
    lines.push('');

    // Return the component object
    lines.push(this.indent(`return component;`));
    this.indentLevel--;
    lines.push(`}`);

    return lines.join('\n');
  }

  /**
   * Generate server-side JavaScript code from AST
   */
  generateServerCode(ast: ComponentAST): string {
    if (!ast.component.serverBlock) {
      return '// No server block defined for this component';
    }

    this.indentLevel = 0;
    const lines: string[] = [];

    // Generate server module wrapper
    lines.push(`// OrdoJS server component: ${ast.component.name}`);
    lines.push(`const ${ast.component.name}Server = (function() {`);
    this.indentLevel++;

    // Generate server module object
    lines.push(this.indent(`const serverModule = {};`));
    lines.push('');

    // Generate server functions
    if (ast.component.serverBlock.functions.length > 0) {
      lines.push(this.indent(`// Server functions`));
      for (const func of ast.component.serverBlock.functions) {
        const isPublic = func.isPublic ? 'public ' : '';
        lines.push(this.indent(`${isPublic}async function ${func.name}(${this.generateFunctionParameters(func.parameters)}) {`));
        this.indentLevel++;

        // Generate function body
        for (const statement of func.body) {
          lines.push(this.indent(this.generateStatement(statement)));
        }

        this.indentLevel--;
        lines.push(this.indent(`}`));
        lines.push('');

        // Export public functions
        if (func.isPublic) {
          lines.push(this.indent(`serverModule.${func.name} = ${func.name};`));
        }
      }
    }

    // Return the server module
    lines.push(this.indent(`return serverModule;`));
    this.indentLevel--;
    lines.push(`})();`);
    lines.push('');
    lines.push(`module.exports = ${ast.component.name}Server;`);

    return lines.join('\n');
  }

  /**
   * Generate HTML template for server-side rendering
   */
  generateHTML(ast: ComponentAST, initialProps: Record<string, any> = {}): string {
    this.componentId = `ordojs_${ast.component.name}_${Date.now().toString(36)}`;

    let html = '';

    if (ast.component.markupBlock) {
      html = this.generateMarkupBlockHTML(ast.component.markupBlock);
    }

    // Add component props as data attributes for hydration
    let propsAttributes = '';
    if (ast.component.props.length > 0 || Object.keys(initialProps).length > 0) {
      // Create a props object with default values from prop definitions
      const defaultProps: Record<string, any> = {};

      // Add default values from prop definitions
      for (const prop of ast.component.props) {
        if (prop.defaultValue && prop.defaultValue.expressionType === 'LITERAL') {
          defaultProps[prop.name] = prop.defaultValue.value;
        }
      }

      // Merge with initial props
      const mergedProps = { ...defaultProps, ...initialProps };

      // Add as data attribute for hydration
      propsAttributes = ` data-props="${this.escapeString(JSON.stringify(mergedProps))}"`;
    }

    // Add hydration markers and component metadata
    html = `<div
  data-ordojs-component="${ast.component.name}"
  data-component-id="${this.componentId}"${propsAttributes}
  data-ordojs-version="1.0"
>${html}</div>`;

    return html;
  }

  /**
   * Generate complete code bundle
   */
  generate(ast: ComponentAST): GeneratedCode {
    const clientCode = this.generateClientCode(ast);
    const serverCode = this.generateServerCode(ast);
    const html = this.generateHTML(ast);

    return {
      client: clientCode,
      server: serverCode,
      html,
      sourceMap: this.sourceMap
    };
  }

  /**
   * Generate code for markup block
   */
  private generateMarkupBlockCode(markupBlock: MarkupBlockNode): string {
    // For simplicity, we'll assume there's a single root element
    if (markupBlock.elements.length === 0) {
      return 'document.createElement("div")';
    }

    // Generate code for the first element
    return this.generateElementCode(markupBlock.elements[0]);
  }

  /**
   * Generate code for HTML element
   */
  private generateElementCode(element: HTMLElementNode): string {
    const elementId = `el_${this.generateUniqueId()}`;
    const lines: string[] = [];

    // Create element
    lines.push(`(() => {`);
    lines.push(`  const ${elementId} = document.createElement("${element.tagName}");`);

    // Add attributes
    for (const attr of element.attributes) {
      if (attr.isDirective) {
        if (attr.directiveType === 'BIND') {
          // Handle bind directive
          const bindName = attr.name.substring(5); // Remove 'bind:' prefix
          const varName = typeof attr.value === 'string' ? attr.value : this.generateExpression(attr.value);

          lines.push(`  ${elementId}.setAttribute("data-bind-${bindName}", "");`);
          lines.push(`  ${elementId}.setAttribute("data-bind-type-${bindName}", "attr");`);
          lines.push(`  ${elementId}.setAttribute("data-bind-attr-${bindName}", "${attr.name}");`);
          lines.push(`  ${elementId}.setAttribute("${attr.name}", ${varName});`);
        } else if (attr.directiveType === 'ON') {
          // Handle event directive
          const eventName = attr.name.substring(3); // Remove 'on:' prefix
          const handlerCode = typeof attr.value === 'string' ? attr.value : this.generateExpression(attr.value);

          // Store event handler for later binding
          const handlerId = `handler_${this.generateUniqueId()}`;
          this.eventHandlers.set(`${elementId}:${eventName}`, handlerCode);

          // Add ID to element for event binding
          lines.push(`  ${elementId}.id = "${elementId}";`);
        }
      } else {
        // Regular attribute
        const attrValue = typeof attr.value === 'string'
          ? `"${attr.value}"`
          : this.generateExpression(attr.value);

        lines.push(`  ${elementId}.setAttribute("${attr.name}", ${attrValue});`);
      }
    }

    // Add children
    for (const child of element.children) {
      if (child.type === 'HTMLElement') {
        const childCode = this.generateElementCode(child as HTMLElementNode);
        lines.push(`  ${elementId}.appendChild(${childCode});`);
      } else if (child.type === 'Text') {
        const textNode = child as TextNode;
        lines.push(`  ${elementId}.appendChild(document.createTextNode("${this.escapeString(textNode.content)}"));`);
      } else if (child.type === 'Interpolation') {
        const interpolation = child as InterpolationNode;
        const varName = this.generateExpression(interpolation.expression);
        const textNodeId = `text_${this.generateUniqueId()}`;

        // Create text node for interpolation
        lines.push(`  const ${textNodeId} = document.createTextNode(${varName});`);
        lines.push(`  ${textNodeId}.setAttribute("data-bind-${varName}", "");`);
        lines.push(`  ${textNodeId}.setAttribute("data-bind-type-${varName}", "text");`);
        lines.push(`  ${elementId}.appendChild(${textNodeId});`);
      }
    }

    lines.push(`  return ${elementId};`);
    lines.push(`})()`);

    return lines.join('\n');
  }

  /**
   * Generate HTML for markup block
   */
  private generateMarkupBlockHTML(markupBlock: MarkupBlockNode): string {
    // For simplicity, we'll assume there's a single root element
    if (markupBlock.elements.length === 0) {
      return '<div></div>';
    }

    // Generate HTML for the first element
    return this.generateElementHTML(markupBlock.elements[0]);
  }

  /**
   * Generate HTML for element
   */
  private generateElementHTML(element: HTMLElementNode): string {
    let html = `<${element.tagName}`;

    // Add attributes
    for (const attr of element.attributes) {
      if (!attr.isDirective) {
        const attrValue = typeof attr.value === 'string'
          ? attr.value
          : ''; // For server rendering, we'll use empty string for expressions

        html += ` ${attr.name}="${this.escapeString(attrValue)}"`;
      } else {
        // Add data attributes for directives to help with hydration
        if (attr.directiveType === 'BIND') {
          const bindName = attr.name.substring(5); // Remove 'bind:' prefix
          html += ` data-bind="${bindName}"`;
        } else if (attr.directiveType === 'ON') {
          const eventName = attr.name.substring(3); // Remove 'on:' prefix
          html += ` data-event="${eventName}"`;
        }
      }
    }

    // Add hydration markers
    html += ` data-ordojs-hydrate="true"`;

    if (element.isSelfClosing || element.isVoidElement) {
      html += ' />';
      return html;
    }

    html += '>';

    // Add children
    for (const child of element.children) {
      if (child.type === 'HTMLElement') {
        html += this.generateElementHTML(child as HTMLElementNode);
      } else if (child.type === 'Text') {
        const textNode = child as TextNode;
        html += this.escapeString(textNode.content);
      } else if (child.type === 'Interpolation') {
        // For server rendering, we'll use a placeholder with data attributes for hydration
        const interpolation = child as InterpolationNode;
        const varName = interpolation.expression.expressionType === 'IDENTIFIER'
          ? interpolation.expression.identifier
          : '';

        if (varName) {
          html += `<span data-interpolation="${varName}"><!-- ${varName} --></span>`;
        } else {
          html += `<span data-interpolation="expression"><!-- expression --></span>`;
        }
      }
    }

    html += `</${element.tagName}>`;

    return html;
  }

  /**
   * Generate code for expression
   */
  private generateExpression(expr: ExpressionNode): string {
    switch (expr.expressionType) {
      case 'LITERAL':
        if (typeof expr.value === 'string') {
          return `"${this.escapeString(expr.value)}"`;
        }
        return String(expr.value);

      case 'IDENTIFIER':
        return expr.identifier || '';

      case 'BINARY':
        if (expr.left && expr.right && expr.operator) {
          const left = this.generateExpression(expr.left);
          const right = this.generateExpression(expr.right);
          return `(${left} ${expr.operator} ${right})`;
        }
        return '';

      case 'UNARY':
        if (expr.right && expr.operator) {
          const right = this.generateExpression(expr.right);
          return `(${expr.operator}${right})`;
        }
        return '';

      case 'CALL':
        if (expr.callee && expr.arguments) {
          const callee = this.generateExpression(expr.callee);
          const args = expr.arguments.map(arg => this.generateExpression(arg)).join(', ');
          return `${callee}(${args})`;
        }
        return '';

      case 'MEMBER':
        if (expr.object && expr.property) {
          const object = this.generateExpression(expr.object);
          const property = this.generateExpression(expr.property);
          return `${object}.${property}`;
        }
        return '';

      case 'ASSIGNMENT':
        if (expr.left && expr.right && expr.operator) {
          const left = this.generateExpression(expr.left);
          const right = this.generateExpression(expr.right);
          return `${left} ${expr.operator} ${right}`;
        }
        return '';

      default:
        return '';
    }
  }

  /**
   * Generate code for a statement
   */
  private generateStatement(statement: StatementNode): string {
    if (statement.statementType === 'EXPRESSION' && statement.expression) {
      return `${this.generateExpression(statement.expression)};`;
    } else if (statement.statementType === 'RETURN' && statement.expression) {
      return `return ${this.generateExpression(statement.expression)};`;
    } else {
      // For other statement types, we'd need more complex handling
      return '/* Statement not implemented */';
    }
  }

  /**
   * Generate function parameters string
   */
  private generateFunctionParameters(parameters: any[]): string {
    return parameters.map(param => {
      let result = param.name;
      if (param.defaultValue) {
        result += ` = ${this.generateExpression(param.defaultValue)}`;
      }
      return result;
    }).join(', ');
  }

  /**
   * Helper to generate indentation
   */
  private indent(text: string): string {
    return '  '.repeat(this.indentLevel) + text;
  }

  /**
   * Helper to escape string literals
   */
  private escapeString(str: string): string {
    return str
      .replace(/\\/g, '\\\\')
      .replace(/"/g, '\\"')
      .replace(/\n/g, '\\n')
      .replace(/\r/g, '\\r')
      .replace(/\t/g, '\\t');
  }

  /**
   * Generate a unique ID for elements
   */
  private generateUniqueId(): string {
    return Math.random().toString(36).substring(2, 10);
  }
}
