/**
 * @fileoverview OrdoJS Code Splitter - Implements code splitting and lazy loading
 */

import {
    type ComponentAST,
    type ComponentNode,
    type HTMLElementNode,
    type OptimizationError
} from '../types/index.js';
import { type Route } from './fs-router.js';

/**
 * Code splitting configuration
 */
export interface CodeSplittingConfig {
  /**
   * Whether to enable code splitting
   */
  enabled: boolean;

  /**
   * Chunk size threshold in bytes
   */
  chunkSizeThreshold: number;

  /**
   * Whether to enable route-based splitting
   */
  routeBasedSplitting: boolean;

  /**
   * Whether to enable component-based splitting
   */
  componentBasedSplitting: boolean;

  /**
   * Maximum number of chunks
   */
  maxChunks: number;

  /**
   * Paths to always include in the main bundle
   */
  alwaysIncludeInMain: string[];
}

/**
 * Default code splitting configuration
 */
const DEFAULT_CONFIG: CodeSplittingConfig = {
  enabled: true,
  chunkSizeThreshold: 50000, // 50KB
  routeBasedSplitting: true,
  componentBasedSplitting: true,
  maxChunks: 10,
  alwaysIncludeInMain: []
};

/**
 * Chunk information
 */
export interface ChunkInfo {
  /**
   * Chunk ID
   */
  id: string;

  /**
   * Chunk name
   */
  name: string;

  /**
   * Components in this chunk
   */
  components: string[];

  /**
   * Routes in this chunk
   */
  routes: string[];

  /**
   * Dependencies on other chunks
   */
  dependencies: string[];

  /**
   * Estimated size in bytes
   */
  estimatedSize: number;

  /**
   * Whether this is the main chunk
   */
  isMain: boolean;

  /**
   * Entry point for this chunk
   */
  entryPoint?: string;
}

/**
 * Code splitting result
 */
export interface CodeSplittingResult {
  /**
   * Chunks generated
   */
  chunks: ChunkInfo[];

  /**
   * Import map for dynamic imports
   */
  importMap: Record<string, string>;

  /**
   * Lazy loading code
   */
  lazyLoadingCode: string;

  /**
   * Errors encountered during code splitting
   */
  errors: OptimizationError[];

  /**
   * Warnings encountered during code splitting
   */
  warnings: OptimizationError[];
}

/**
 * Code splitter for automatic code splitting and lazy loading
 */
export class CodeSplitter {
  private config: CodeSplittingConfig;
  private errors: OptimizationError[] = [];
  private warnings: OptimizationError[] = [];
  private componentRegistry: Map<string, ComponentAST> = new Map();
  private routeRegistry: Map<string, Route> = new Map();
  private dependencyGraph: Map<string, Set<string>> = new Map();
  private chunks: ChunkInfo[] = [];

  constructor(config: Partial<CodeSplittingConfig> = {}) {
    this.config = { ...DEFAULT_CONFIG, ...config };
  }

  /**
   * Register a component for code splitting analysis
   */
  registerComponent(component: ComponentAST): void {
    this.componentRegistry.set(component.component.name, component);
  }

  /**
   * Register multiple components
   */
  registerComponents(components: ComponentAST[]): void {
    for (const component of components) {
      this.registerComponent(component);
    }
  }

  /**
   * Register routes for route-based code splitting
   */
  registerRoutes(routes: Route[]): void {
    for (const route of routes) {
      this.routeRegistry.set(route.path, route);
    }
  }

  /**
   * Analyze dependencies and create chunks
   */
  analyze(): CodeSplittingResult {
    this.reset();

    try {
      // Step 1: Build dependency graph
      this.buildDependencyGraph();

      // Step 2: Create initial chunks based on routes if enabled
      if (this.config.routeBasedSplitting) {
        this.createRouteBasedChunks();
      }

      // Step 3: Create component-based chunks if enabled
      if (this.config.componentBasedSplitting) {
        this.createComponentBasedChunks();
      }

      // Step 4: Optimize chunks (merge small chunks, split large ones)
      this.optimizeChunks();

      // Step 5: Generate import map and lazy loading code
      const importMap = this.generateImportMap();
      const lazyLoadingCode = this.generateLazyLoadingCode();

      return {
        chunks: this.chunks,
        importMap,
        lazyLoadingCode,
        errors: this.errors,
        warnings: this.warnings
      };
    } catch (error) {
      if (error instanceof Error) {
        this.errors.push({
          message: `Code splitting error: ${error.message}`,
          position: { line: 0, column: 0, offset: 0 },
          range: {
            start: { line: 0, column: 0, offset: 0 },
            end: { line: 0, column: 0, offset: 0 }
          },
          getErrorCode: () => 'OPT002',
          getSeverity: () => 'ERROR',
          getSuggestions: () => ['Check component dependencies', 'Verify route configuration'],
          toUserFriendlyMessage: () => `Code splitting error: ${error instanceof Error ? error.message : String(error)}`
        } as unknown as OptimizationError);
      }

      return {
        chunks: [],
        importMap: {},
        lazyLoadingCode: '',
        errors: this.errors,
        warnings: this.warnings
      };
    }
  }

  /**
   * Get errors encountered during code splitting
   */
  getErrors(): OptimizationError[] {
    return this.errors;
  }

  /**
   * Get warnings encountered during code splitting
   */
  getWarnings(): OptimizationError[] {
    return this.warnings;
  }

  /**
   * Reset code splitter state
   */
  private reset(): void {
    this.errors = [];
    this.warnings = [];
    this.dependencyGraph = new Map();
    this.chunks = [];
  }

  /**
   * Build dependency graph between components
   */
  private buildDependencyGraph(): void {
    // Initialize dependency graph
    for (const [componentName] of this.componentRegistry) {
      this.dependencyGraph.set(componentName, new Set());
    }

    // Analyze component dependencies
    for (const [componentName, ast] of this.componentRegistry) {
      const dependencies = this.extractComponentDependencies(ast);

      // Filter out dependencies that don't exist in our registry
      const validDependencies = dependencies.filter(dep =>
        this.componentRegistry.has(dep)
      );

      this.dependencyGraph.set(componentName, new Set(validDependencies));
    }
  }

  /**
   * Extract component dependencies from AST
   */
  private extractComponentDependencies(ast: ComponentAST): string[] {
    // This is a simplified implementation
    // In a real implementation, we would analyze the markup for component usage
    return ast.dependencies || [];
  }

  /**
   * Create route-based chunks
   */
  private createRouteBasedChunks(): void {
    // Group routes by directory structure
    const routeGroups = new Map<string, Route[]>();

    for (const route of this.routeRegistry.values()) {
      const routeDir = this.getRouteDirectory(route.path);

      if (!routeGroups.has(routeDir)) {
        routeGroups.set(routeDir, []);
      }

      routeGroups.get(routeDir)!.push(route);
    }

    // Create a chunk for each route group
    for (const [routeDir, routes] of routeGroups) {
      const chunkName = this.sanitizeChunkName(routeDir);
      const components = routes.map(route => route.componentName);

      // Skip empty chunks
      if (components.length === 0) continue;

      // Create chunk
      this.chunks.push({
        id: `chunk_${this.chunks.length}`,
        name: chunkName,
        components,
        routes: routes.map(route => route.path),
        dependencies: this.getChunkDependencies(components),
        estimatedSize: this.estimateChunkSize(components),
        isMain: routeDir === '/' || routeDir === ''
      });
    }
  }

  /**
   * Create component-based chunks
   */
  private createComponentBasedChunks(): void {
    // Find components not already in chunks
    const chunkedComponents = new Set<string>();
    for (const chunk of this.chunks) {
      for (const component of chunk.components) {
        chunkedComponents.add(component);
      }
    }

    const remainingComponents = Array.from(this.componentRegistry.keys())
      .filter(component => !chunkedComponents.has(component));

    // Group components by their dependencies
    const componentGroups = this.groupComponentsByDependencies(remainingComponents);

    // Create a chunk for each component group
    for (const [groupName, components] of componentGroups) {
      // Skip empty groups
      if (components.length === 0) continue;

      // Skip groups that are too small (will be merged into main chunk later)
      if (components.length === 1 && this.estimateChunkSize(components) < this.config.chunkSizeThreshold) {
        continue;
      }

      // Create chunk
      this.chunks.push({
        id: `chunk_${this.chunks.length}`,
        name: groupName,
        components,
        routes: [],
        dependencies: this.getChunkDependencies(components),
        estimatedSize: this.estimateChunkSize(components),
        isMain: false
      });
    }
  }

  /**
   * Group components by their dependencies
   */
  private groupComponentsByDependencies(components: string[]): Map<string, string[]> {
    const groups = new Map<string, string[]>();

    // Simple grouping strategy: group by first-level dependencies
    for (const component of components) {
      const dependencies = this.dependencyGraph.get(component) || new Set();

      if (dependencies.size === 0) {
        // Components with no dependencies go to "standalone" group
        const groupName = 'standalone';
        if (!groups.has(groupName)) {
          groups.set(groupName, []);
        }
        groups.get(groupName)!.push(component);
      } else {
        // Use first dependency as group name
        const firstDep = Array.from(dependencies)[0];
        const groupName = `group_${firstDep}`;

        if (!groups.has(groupName)) {
          groups.set(groupName, []);
        }
        groups.get(groupName)!.push(component);
      }
    }

    return groups;
  }

  /**
   * Optimize chunks by merging small chunks and splitting large ones
   */
  private optimizeChunks(): void {
    // Find the main chunk
    const mainChunkIndex = this.chunks.findIndex(chunk => chunk.isMain);
    let mainChunk: ChunkInfo;

    if (mainChunkIndex === -1) {
      // Create main chunk if it doesn't exist
      mainChunk = {
        id: 'chunk_main',
        name: 'main',
        components: [],
        routes: [],
        dependencies: [],
        estimatedSize: 0,
        isMain: true
      };
      this.chunks.push(mainChunk);
    } else {
      mainChunk = this.chunks[mainChunkIndex];
    }

    // Merge small chunks into main
    const smallChunks = this.chunks.filter(chunk =>
      !chunk.isMain && chunk.estimatedSize < this.config.chunkSizeThreshold
    );

    for (const smallChunk of smallChunks) {
      // Add components to main chunk
      mainChunk.components.push(...smallChunk.components);
      mainChunk.routes.push(...smallChunk.routes);

      // Update main chunk size
      mainChunk.estimatedSize += smallChunk.estimatedSize;

      // Remove small chunk
      const index = this.chunks.findIndex(chunk => chunk.id === smallChunk.id);
      if (index !== -1) {
        this.chunks.splice(index, 1);
      }
    }

    // Split large chunks if needed
    const largeChunks = this.chunks.filter(chunk =>
      chunk.estimatedSize > this.config.chunkSizeThreshold * 2
    );

    for (const largeChunk of largeChunks) {
      // Skip main chunk
      if (largeChunk.isMain) continue;

      // Split chunk if it has enough components
      if (largeChunk.components.length > 2) {
        const midpoint = Math.floor(largeChunk.components.length / 2);
        const firstHalf = largeChunk.components.slice(0, midpoint);
        const secondHalf = largeChunk.components.slice(midpoint);

        // Create two new chunks
        const chunk1: ChunkInfo = {
          id: `${largeChunk.id}_1`,
          name: `${largeChunk.name}_1`,
          components: firstHalf,
          routes: largeChunk.routes.filter(route => {
            const componentName = this.getRouteComponentName(route);
            return firstHalf.includes(componentName);
          }),
          dependencies: this.getChunkDependencies(firstHalf),
          estimatedSize: this.estimateChunkSize(firstHalf),
          isMain: false
        };

        const chunk2: ChunkInfo = {
          id: `${largeChunk.id}_2`,
          name: `${largeChunk.name}_2`,
          components: secondHalf,
          routes: largeChunk.routes.filter(route => {
            const componentName = this.getRouteComponentName(route);
            return secondHalf.includes(componentName);
          }),
          dependencies: this.getChunkDependencies(secondHalf),
          estimatedSize: this.estimateChunkSize(secondHalf),
          isMain: false
        };

        // Replace large chunk with two smaller chunks
        const index = this.chunks.findIndex(chunk => chunk.id === largeChunk.id);
        if (index !== -1) {
          this.chunks.splice(index, 1, chunk1, chunk2);
        }
      }
    }

    // Ensure we don't exceed max chunks
    if (this.chunks.length > this.config.maxChunks) {
      // Sort non-main chunks by size (ascending)
      const nonMainChunks = this.chunks
        .filter(chunk => !chunk.isMain)
        .sort((a, b) => a.estimatedSize - b.estimatedSize);

      // Merge smallest chunks until we're under the limit
      while (this.chunks.length > this.config.maxChunks) {
        if (nonMainChunks.length < 2) break;

        const smallest1 = nonMainChunks.shift()!;
        const smallest2 = nonMainChunks.shift()!;

        // Create merged chunk
        const mergedChunk: ChunkInfo = {
          id: `merged_${smallest1.id}_${smallest2.id}`,
          name: `merged_${smallest1.name}_${smallest2.name}`,
          components: [...smallest1.components, ...smallest2.components],
          routes: [...smallest1.routes, ...smallest2.routes],
          dependencies: [...new Set([
            ...this.getChunkDependencies(smallest1.components),
            ...this.getChunkDependencies(smallest2.components)
          ])],
          estimatedSize: smallest1.estimatedSize + smallest2.estimatedSize,
          isMain: false
        };

        // Remove original chunks
        this.chunks = this.chunks.filter(chunk =>
          chunk.id !== smallest1.id && chunk.id !== smallest2.id
        );

        // Add merged chunk
        this.chunks.push(mergedChunk);

        // Add merged chunk back to sorted list
        nonMainChunks.push(mergedChunk);
        nonMainChunks.sort((a, b) => a.estimatedSize - b.estimatedSize);
      }
    }

    // Assign entry points to chunks
    for (const chunk of this.chunks) {
      if (chunk.routes.length > 0) {
        // Use first route component as entry point
        chunk.entryPoint = this.getRouteComponentName(chunk.routes[0]);
      } else if (chunk.components.length > 0) {
        // Use first component as entry point
        chunk.entryPoint = chunk.components[0];
      }
    }
  }

  /**
   * Generate import map for dynamic imports
   */
  private generateImportMap(): Record<string, string> {
    const importMap: Record<string, string> = {};

    for (const chunk of this.chunks) {
      if (chunk.isMain) continue; // Skip main chunk

      for (const component of chunk.components) {
        importMap[component] = `chunks/${chunk.name}.js`;
      }
    }

    return importMap;
  }

  /**
   * Generate lazy loading code
   */
  private generateLazyLoadingCode(): string {
    return `
/**
 * Generated lazy loading utilities
 */

// Import map for dynamic imports
export const IMPORT_MAP = ${JSON.stringify(this.generateImportMap(), null, 2)};

// Chunk dependency map
export const CHUNK_DEPENDENCIES = ${JSON.stringify(
  this.chunks.reduce((map, chunk) => {
    map[chunk.name] = chunk.dependencies;
    return map;
  }, {} as Record<string, string[]>),
  null, 2
)};

// Loaded chunks cache
const loadedChunks = new Set();

/**
 * Lazy load a component
 */
export async function lazyLoad(componentName) {
  const chunkPath = IMPORT_MAP[componentName];

  if (!chunkPath) {
    // Component is in the main bundle
    return Promise.resolve();
  }

  if (loadedChunks.has(chunkPath)) {
    // Chunk already loaded
    return Promise.resolve();
  }

  try {
    // Load the chunk
    await import(/* webpackChunkName: "[request]" */ chunkPath);
    loadedChunks.add(chunkPath);
    return Promise.resolve();
  } catch (error) {
    console.error(\`Failed to load component \${componentName}: \${error instanceof Error ? error.message : String(error)}\`);
    return Promise.reject(error);
  }
}

/**
 * Preload a component
 */
export function preload(componentName) {
  const chunkPath = IMPORT_MAP[componentName];

  if (!chunkPath || loadedChunks.has(chunkPath)) {
    return Promise.resolve();
  }

  // Use link preload for modern browsers
  const link = document.createElement('link');
  link.rel = 'preload';
  link.as = 'script';
  link.href = chunkPath;
  document.head.appendChild(link);

  return Promise.resolve();
}

/**
 * Preload all components for a route
 */
export function preloadRoute(routePath) {
  const routeComponents = ${JSON.stringify(
    Array.from(this.routeRegistry.entries()).reduce((map, [path, route]) => {
      map[path] = route.componentName;
      return map;
    }, {} as Record<string, string>),
    null, 2
  )};

  const componentName = routeComponents[routePath];
  if (componentName) {
    return preload(componentName);
  }

  return Promise.resolve();
}
`;
  }

  /**
   * Get dependencies for a chunk
   */
  private getChunkDependencies(components: string[]): string[] {
    const dependencies = new Set<string>();

    for (const component of components) {
      const componentDeps = this.dependencyGraph.get(component) || new Set();

      for (const dep of componentDeps) {
        // Only add dependencies that are not in this chunk
        if (!components.includes(dep)) {
          dependencies.add(dep);
        }
      }
    }

    return Array.from(dependencies);
  }

  /**
   * Estimate chunk size based on component ASTs
   */
  private estimateChunkSize(components: string[]): number {
    let size = 0;

    for (const component of components) {
      const ast = this.componentRegistry.get(component);
      if (ast) {
        // This is a very rough estimate based on AST node count
        size += this.countASTNodes(ast.component);
      }
    }

    // Convert node count to approximate byte size
    return size * 20; // Rough estimate: each node is about 20 bytes of JS
  }

  /**
   * Count AST nodes in a component
   */
  private countASTNodes(component: ComponentNode): number {
    let count = 1; // Count the component itself

    // Count client block nodes
    if (component.clientBlock) {
      count += 1; // The block itself
      count += component.clientBlock.reactiveVariables.length;
      count += component.clientBlock.computedValues.length;
      count += component.clientBlock.eventHandlers.length;
      count += component.clientBlock.functions.length;
      count += component.clientBlock.lifecycle.length;
    }

    // Count server block nodes
    if (component.serverBlock) {
      count += 1; // The block itself
      count += component.serverBlock.functions.length;
      count += component.serverBlock.middleware.length;
      count += component.serverBlock.dataFetchers.length;
      count += component.serverBlock.imports.length;
    }

    // Count markup block nodes
    if (component.markupBlock) {
      count += 1; // The block itself
      count += component.markupBlock.elements.length;
      count += component.markupBlock.textNodes.length;
      count += component.markupBlock.interpolations.length;

      // Count HTML elements recursively
      for (const element of component.markupBlock.elements) {
        count += this.countHTMLElementNodes(element);
      }
    }

    return count;
  }

  /**
   * Count nodes in an HTML element recursively
   */
  private countHTMLElementNodes(element: HTMLElementNode): number {
    let count = 1; // Count the element itself

    // Count attributes
    count += element.attributes.length;

    // Count children recursively
    for (const child of element.children) {
      if (child.type === 'HTMLElement') {
        count += this.countHTMLElementNodes(child as HTMLElementNode);
      } else {
        count += 1; // Text or interpolation node
      }
    }

    return count;
  }

  /**
   * Get route directory from path
   */
  private getRouteDirectory(routePath: string): string {
    // Extract directory structure from route path
    const parts = routePath.split('/').filter(Boolean);

    if (parts.length === 0) {
      return '/';
    }

    // Remove dynamic parameters
    const staticParts = parts.filter(part => !part.startsWith(':') && !part.startsWith('*'));

    if (staticParts.length === 0) {
      return '/';
    }

    return staticParts[0];
  }

  /**
   * Get component name for a route
   */
  private getRouteComponentName(routePath: string): string {
    const route = this.routeRegistry.get(routePath);
    return route ? route.componentName : '';
  }

  /**
   * Sanitize chunk name for file system
   */
  private sanitizeChunkName(name: string): string {
    return name
      .replace(/^\/+/, '') // Remove leading slashes
      .replace(/[^a-zA-Z0-9_-]/g, '_') // Replace invalid chars
      .toLowerCase() || 'index';
  }
}
