/**
 * TypeScript/JavaScript Parser
 *
 * Parses TypeScript and JavaScript files using ts-morph.
 */

import { promises as fs } from 'fs';
import path from 'path';
import crypto from 'crypto';
import {
  Project,
  SourceFile,
  ClassDeclaration,
  FunctionDeclaration,
  MethodDeclaration,
  ArrowFunction,
  FunctionExpression,
  ImportDeclaration,
  SyntaxKind,
  Node,
  VariableDeclaration,
} from 'ts-morph';
import { BaseLanguageParser } from './base-parser.js';
import type {
  FileEntity,
  ClassEntity,
  FunctionEntity,
  ModuleEntity,
  ImportStatement,
  FileEntities,
} from '../../graph/types.js';
import type { SupportedLanguage } from '../types.js';

/**
 * Common external packages (npm)
 */
const COMMON_EXTERNAL_PACKAGES = new Set([
  // Node.js built-ins
  'fs', 'path', 'os', 'crypto', 'http', 'https', 'url', 'util', 'events',
  'stream', 'buffer', 'child_process', 'cluster', 'dgram', 'dns', 'net',
  'readline', 'repl', 'tls', 'tty', 'vm', 'zlib', 'assert', 'async_hooks',
  'console', 'constants', 'domain', 'inspector', 'module', 'perf_hooks',
  'process', 'punycode', 'querystring', 'string_decoder', 'timers', 'trace_events',
  'v8', 'worker_threads',
  // Common npm packages
  'react', 'react-dom', 'next', 'express', 'fastify', 'hono', 'koa',
  'lodash', 'underscore', 'ramda', 'axios', 'node-fetch', 'got',
  'typescript', 'ts-node', 'ts-morph', 'esbuild', 'vite', 'webpack',
  'jest', 'vitest', 'mocha', 'chai', 'sinon',
  'zod', 'yup', 'joi', 'ajv',
  'prisma', 'drizzle-orm', 'typeorm', 'sequelize', 'mongoose',
  'pino', 'winston', 'bunyan',
  'dotenv', 'commander', 'yargs', 'inquirer',
]);

/**
 * TypeScript/JavaScript parser using ts-morph
 */
export class TypeScriptParser extends BaseLanguageParser {
  private project: Project;
  private sourceFile: SourceFile | null = null;

  constructor(filePath: string, repoRoot?: string) {
    super(filePath, repoRoot);
    this.project = new Project({
      compilerOptions: {
        allowJs: true,
        checkJs: false,
        noEmit: true,
        skipLibCheck: true,
      },
      skipAddingFilesFromTsConfig: true,
      skipFileDependencyResolution: true,
    });
  }

  getSupportedExtensions(): string[] {
    return ['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts', '.mjs', '.cjs'];
  }

  getLanguage(): SupportedLanguage {
    const ext = this.getExtension();
    if (ext === '.js' || ext === '.jsx' || ext === '.mjs' || ext === '.cjs') {
      return 'javascript';
    }
    return 'typescript';
  }

  async parse(): Promise<FileEntities> {
    try {
      // Read file content
      const content = await fs.readFile(this.filePath, 'utf-8');
      const stats = await fs.stat(this.filePath);

      // Add source file to project
      this.sourceFile = this.project.createSourceFile(this.filePath, content, {
        overwrite: true,
      });

      // Extract file entity
      const fileEntity = this.extractFileEntity(content, stats);

      // Extract entities
      const classes = this.extractClasses();
      const functions = this.extractFunctions();
      const { imports, modules } = this.extractImports();

      // Build call graph (populate function.calls)
      this.buildCallGraph(functions);

      return {
        file: fileEntity,
        classes,
        functions,
        modules,
        imports,
      };
    } catch (error) {
      // Return empty entities on parse error
      console.warn(`Failed to parse ${this.filePath}: ${error}`);
      return {
        file: undefined,
        classes: [],
        functions: [],
        modules: [],
        imports: [],
      };
    }
  }

  private extractFileEntity(content: string, stats: import('fs').Stats): FileEntity {
    const hash = crypto.createHash('sha256').update(content).digest('hex');
    const lines = content.split('\n').length;

    return {
      path: this.filePath,
      name: this.getFileName(),
      extension: this.getExtension(),
      sizeBytes: stats.size,
      linesOfCode: lines,
      hash,
      lastModified: stats.mtime,
    };
  }

  private extractClasses(): ClassEntity[] {
    if (!this.sourceFile) return [];

    const classes: ClassEntity[] = [];

    for (const classDecl of this.sourceFile.getClasses()) {
      const entity = this.extractClassEntity(classDecl);
      if (entity) {
        classes.push(entity);
      }
    }

    return classes;
  }

  private extractClassEntity(classDecl: ClassDeclaration): ClassEntity | null {
    const name = classDecl.getName();
    if (!name) return null;

    const startLine = classDecl.getStartLineNumber();
    const endLine = classDecl.getEndLineNumber();

    // Get decorators
    const decorators = classDecl.getDecorators().map((d) => d.getName());

    // Get base classes
    const baseClasses: string[] = [];
    const extendsExpr = classDecl.getExtends();
    if (extendsExpr) {
      baseClasses.push(extendsExpr.getText());
    }

    // Get implemented interfaces (treat as base classes for graph purposes)
    for (const impl of classDecl.getImplements()) {
      baseClasses.push(impl.getText());
    }

    // Check if abstract
    const isAbstract = classDecl.isAbstract();

    // Get JSDoc comment
    const docstring = this.getJsDocComment(classDecl);

    return {
      qualifiedName: this.qualifiedName(name),
      name,
      filePath: this.filePath,
      lineStart: startLine,
      lineEnd: endLine,
      docstring,
      isAbstract,
      decorators,
      baseClasses,
    };
  }

  private extractFunctions(): FunctionEntity[] {
    if (!this.sourceFile) return [];

    const functions: FunctionEntity[] = [];

    // Extract top-level functions
    for (const funcDecl of this.sourceFile.getFunctions()) {
      const entity = this.extractFunctionEntity(funcDecl);
      if (entity) {
        functions.push(entity);
      }
    }

    // Extract class methods
    for (const classDecl of this.sourceFile.getClasses()) {
      const className = classDecl.getName();
      if (!className) continue;

      for (const method of classDecl.getMethods()) {
        const entity = this.extractMethodEntity(method, className);
        if (entity) {
          functions.push(entity);
        }
      }

      // Extract constructor as a method
      const constructor = classDecl.getConstructors()[0];
      if (constructor) {
        const entity = this.extractConstructorEntity(constructor, className);
        if (entity) {
          functions.push(entity);
        }
      }
    }

    // Extract arrow functions and function expressions assigned to variables
    for (const varStmt of this.sourceFile.getVariableStatements()) {
      for (const decl of varStmt.getDeclarations()) {
        const entity = this.extractVariableFunctionEntity(decl);
        if (entity) {
          functions.push(entity);
        }
      }
    }

    return functions;
  }

  private extractFunctionEntity(funcDecl: FunctionDeclaration): FunctionEntity | null {
    const name = funcDecl.getName();
    if (!name) return null;

    return {
      qualifiedName: this.qualifiedName(name),
      name,
      filePath: this.filePath,
      lineStart: funcDecl.getStartLineNumber(),
      lineEnd: funcDecl.getEndLineNumber(),
      docstring: this.getJsDocComment(funcDecl),
      parameters: this.extractParameters(funcDecl),
      returnAnnotation: funcDecl.getReturnType().getText(),
      isAsync: funcDecl.isAsync(),
      isMethod: false,
      isStatic: false,
      decorators: funcDecl.getDecorators().map((d) => d.getName()),
      calls: [], // Populated by buildCallGraph
    };
  }

  private extractMethodEntity(method: MethodDeclaration, className: string): FunctionEntity | null {
    const name = method.getName();

    return {
      qualifiedName: this.qualifiedName(name, className),
      name,
      filePath: this.filePath,
      lineStart: method.getStartLineNumber(),
      lineEnd: method.getEndLineNumber(),
      docstring: this.getJsDocComment(method),
      parameters: this.extractParameters(method),
      returnAnnotation: method.getReturnType().getText(),
      isAsync: method.isAsync(),
      isMethod: true,
      isStatic: method.isStatic(),
      decorators: method.getDecorators().map((d) => d.getName()),
      calls: [],
      parentClass: this.qualifiedName(className),
    };
  }

  private extractConstructorEntity(
    constructor: Node,
    className: string
  ): FunctionEntity | null {
    return {
      qualifiedName: this.qualifiedName('constructor', className),
      name: 'constructor',
      filePath: this.filePath,
      lineStart: constructor.getStartLineNumber(),
      lineEnd: constructor.getEndLineNumber(),
      parameters: [],
      isAsync: false,
      isMethod: true,
      isStatic: false,
      decorators: [],
      calls: [],
      parentClass: this.qualifiedName(className),
    };
  }

  private extractVariableFunctionEntity(decl: VariableDeclaration): FunctionEntity | null {
    const initializer = decl.getInitializer();
    if (!initializer) return null;

    // Check if it's an arrow function or function expression
    if (!Node.isArrowFunction(initializer) && !Node.isFunctionExpression(initializer)) {
      return null;
    }

    const name = decl.getName();
    const func = initializer as ArrowFunction | FunctionExpression;

    return {
      qualifiedName: this.qualifiedName(name),
      name,
      filePath: this.filePath,
      lineStart: decl.getStartLineNumber(),
      lineEnd: decl.getEndLineNumber(),
      docstring: this.getJsDocComment(decl),
      parameters: this.extractArrowParameters(func),
      returnAnnotation: func.getReturnType().getText(),
      isAsync: func.isAsync(),
      isMethod: false,
      isStatic: false,
      decorators: [],
      calls: [],
    };
  }

  private extractParameters(
    func: FunctionDeclaration | MethodDeclaration
  ): string[] {
    return func.getParameters().map((param) => {
      const name = param.getName();
      const type = param.getType().getText();
      const isOptional = param.isOptional();
      const isRest = param.isRestParameter();

      let paramStr = isRest ? `...${name}` : name;
      if (type && type !== 'any') {
        paramStr += `: ${type}`;
      }
      if (isOptional) {
        paramStr += '?';
      }
      return paramStr;
    });
  }

  private extractArrowParameters(func: ArrowFunction | FunctionExpression): string[] {
    return func.getParameters().map((param) => {
      const name = param.getName();
      const type = param.getType().getText();
      return type && type !== 'any' ? `${name}: ${type}` : name;
    });
  }

  private extractImports(): { imports: ImportStatement[]; modules: ModuleEntity[] } {
    if (!this.sourceFile) return { imports: [], modules: [] };

    const imports: ImportStatement[] = [];
    const moduleMap = new Map<string, ModuleEntity>();

    for (const importDecl of this.sourceFile.getImportDeclarations()) {
      const { importStmt, module } = this.extractImportDeclaration(importDecl);
      if (importStmt) {
        imports.push(importStmt);
      }
      if (module && !moduleMap.has(module.name)) {
        moduleMap.set(module.name, module);
      }
    }

    return {
      imports,
      modules: Array.from(moduleMap.values()),
    };
  }

  private extractImportDeclaration(importDecl: ImportDeclaration): {
    importStmt: ImportStatement | null;
    module: ModuleEntity | null;
  } {
    const moduleSpecifier = importDecl.getModuleSpecifierValue();
    const lineNumber = importDecl.getStartLineNumber();

    // Determine if external
    const isExternal = this.isExternalModule(moduleSpecifier);

    // Get imported symbols
    const symbols: string[] = [];
    let alias: string | undefined;

    // Default import
    const defaultImport = importDecl.getDefaultImport();
    if (defaultImport) {
      symbols.push('default');
      alias = defaultImport.getText();
    }

    // Named imports
    for (const named of importDecl.getNamedImports()) {
      symbols.push(named.getName());
    }

    // Namespace import
    const namespaceImport = importDecl.getNamespaceImport();
    if (namespaceImport) {
      symbols.push('*');
      alias = namespaceImport.getText();
    }

    const importStmt: ImportStatement = {
      filePath: this.filePath,
      moduleName: moduleSpecifier,
      importType: symbols.includes('*') ? 'import' : 'from_import',
      lineNumber,
      alias,
      symbols: symbols.length > 0 ? symbols : undefined,
    };

    const module: ModuleEntity = {
      name: moduleSpecifier,
      importPath: moduleSpecifier,
      isExternal,
      package: isExternal ? this.getPackageName(moduleSpecifier) : undefined,
    };

    return { importStmt, module };
  }

  private isExternalModule(moduleSpecifier: string): boolean {
    // Relative imports are internal
    if (moduleSpecifier.startsWith('.') || moduleSpecifier.startsWith('/')) {
      return false;
    }

    // Check common external packages
    const packageName = this.getPackageName(moduleSpecifier);
    if (COMMON_EXTERNAL_PACKAGES.has(packageName)) {
      return true;
    }

    // Node.js built-in modules with node: prefix
    if (moduleSpecifier.startsWith('node:')) {
      return true;
    }

    // Assume non-relative imports are external (npm packages)
    return true;
  }

  private getPackageName(moduleSpecifier: string): string {
    // Handle node: prefix
    if (moduleSpecifier.startsWith('node:')) {
      return moduleSpecifier.slice(5).split('/')[0];
    }

    // Handle scoped packages (@org/package)
    if (moduleSpecifier.startsWith('@')) {
      const parts = moduleSpecifier.split('/');
      return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : moduleSpecifier;
    }

    // Regular package
    return moduleSpecifier.split('/')[0];
  }

  private buildCallGraph(functions: FunctionEntity[]): void {
    if (!this.sourceFile) return;

    // Build a map of function names for resolution
    const functionNames = new Set(functions.map((f) => f.name));

    for (const func of functions) {
      const calls = new Set<string>();

      // Find the AST node for this function
      const node = this.findFunctionNode(func);
      if (!node) continue;

      // Walk the AST to find call expressions
      node.forEachDescendant((child) => {
        if (Node.isCallExpression(child)) {
          const callName = this.getCallName(child);
          if (callName && functionNames.has(callName)) {
            calls.add(this.qualifiedName(callName));
          }
        }
      });

      func.calls = Array.from(calls);
    }
  }

  private findFunctionNode(func: FunctionEntity): Node | null {
    if (!this.sourceFile) return null;

    // Find by line number
    const pos = this.sourceFile.compilerNode.getPositionOfLineAndCharacter(
      func.lineStart - 1,
      0
    );

    return this.sourceFile.getDescendantAtPos(pos) ?? null;
  }

  private getCallName(callExpr: Node): string | null {
    const expression = callExpr.getChildAtIndex(0);
    if (!expression) return null;

    if (Node.isIdentifier(expression)) {
      return expression.getText();
    }

    if (Node.isPropertyAccessExpression(expression)) {
      // Get just the method name for now (could enhance to track full chain)
      return expression.getName();
    }

    return null;
  }

  private getJsDocComment(node: Node): string | undefined {
    // Try to get JSDoc comments
    const jsDocs = (node as any).getJsDocs?.();
    if (jsDocs && jsDocs.length > 0) {
      return jsDocs.map((doc: any) => doc.getDescription()).join('\n');
    }

    // Try leading comment
    const leadingComments = node.getLeadingCommentRanges();
    if (leadingComments.length > 0) {
      const lastComment = leadingComments[leadingComments.length - 1];
      const text = lastComment.getText();
      // Check if it's a doc comment (/** ... */)
      if (text.startsWith('/**')) {
        return text
          .replace(/^\/\*\*\s*/, '')
          .replace(/\s*\*\/$/, '')
          .replace(/^\s*\*\s?/gm, '')
          .trim();
      }
    }

    return undefined;
  }
}
