import type * as monaco from 'monaco-editor';
import type { IExecutionEnvironment } from '../../00group-runtime/index.js';

// Monaco TypeScript API types (runtime API still exists, types deprecated in 0.55+)
interface IExtraLibDisposable {
  dispose(): void;
}

interface IMonacoTypeScriptAPI {
  typescriptDefaults: {
    setCompilerOptions(options: Record<string, unknown>): void;
    setDiagnosticsOptions(options: Record<string, unknown>): void;
    addExtraLib(content: string, filePath?: string): IExtraLibDisposable;
    setEagerModelSync(value: boolean): void;
  };
  ScriptTarget: { ES2020: number };
  ModuleKind: { ESNext: number };
  ModuleResolutionKind: { NodeJs: number; Bundler?: number };
}

/**
 * Manages TypeScript IntelliSense by loading type definitions
 * from the virtual filesystem into Monaco.
 */
export class TypeScriptIntelliSenseManager {
  private loadedLibs: Set<string> = new Set();
  private notFoundPackages: Set<string> = new Set(); // Packages checked but not found
  private monacoInstance: typeof monaco | null = null;
  private executionEnvironment: IExecutionEnvironment | null = null;

  // Cache of file contents for synchronous access and module resolution
  private fileCache: Map<string, string> = new Map();

  // Track extra libs added for cleanup
  private addedExtraLibs: Map<string, IExtraLibDisposable> = new Map();

  /**
   * Get TypeScript API with proper typing for Monaco 0.55+
   */
  private get tsApi(): IMonacoTypeScriptAPI | null {
    if (!this.monacoInstance) return null;
    return (this.monacoInstance.languages as any).typescript as IMonacoTypeScriptAPI;
  }

  /**
   * Initialize with Monaco and execution environment
   */
  public async init(
    monacoInst: typeof monaco,
    env: IExecutionEnvironment
  ): Promise<void> {
    this.monacoInstance = monacoInst;
    this.executionEnvironment = env;
    this.configureCompilerOptions();
    // Load all project TypeScript/JavaScript files into Monaco for cross-file resolution
    await this.loadAllProjectFiles();
  }

  /**
   * Recursively load all .ts/.js files from the virtual filesystem into Monaco
   */
  private async loadAllProjectFiles(): Promise<void> {
    if (!this.executionEnvironment) return;
    await this.loadFilesFromDirectory('/');
  }

  /**
   * Recursively load files from a directory
   */
  private async loadFilesFromDirectory(dirPath: string): Promise<void> {
    if (!this.executionEnvironment) return;

    try {
      const entries = await this.executionEnvironment.readDir(dirPath);

      for (const entry of entries) {
        const fullPath = dirPath === '/' ? `/${entry.name}` : `${dirPath}/${entry.name}`;

        // Skip node_modules - too large and handled separately via addExtraLib
        if (entry.name === 'node_modules') continue;

        if (entry.type === 'directory') {
          await this.loadFilesFromDirectory(fullPath);
        } else if (entry.type === 'file') {
          const ext = entry.name.split('.').pop()?.toLowerCase();
          if (ext === 'ts' || ext === 'tsx' || ext === 'js' || ext === 'jsx') {
            try {
              const content = await this.executionEnvironment.readFile(fullPath);
              this.addFileModel(fullPath, content);
            } catch {
              // Ignore files that can't be read
            }
          }
        }
      }
    } catch {
      // Directory might not exist or not be readable
    }
  }

  private configureCompilerOptions(): void {
    const ts = this.tsApi;
    if (!ts) return;

    ts.typescriptDefaults.setCompilerOptions({
      target: ts.ScriptTarget.ES2020,
      module: ts.ModuleKind.ESNext,
      // Use Bundler resolution if available (Monaco 0.45+), fallback to NodeJs
      moduleResolution: ts.ModuleResolutionKind.Bundler ?? ts.ModuleResolutionKind.NodeJs,
      allowSyntheticDefaultImports: true,
      esModuleInterop: true,
      strict: true,
      noEmit: true,
      allowJs: true,
      checkJs: false,
      allowNonTsExtensions: true,
      lib: ['es2020', 'dom', 'dom.iterable'],
      // Set baseUrl to root for resolving absolute imports
      baseUrl: '/',
      // Allow importing .ts extensions directly (useful for some setups)
      allowImportingTsExtensions: true,
      // Resolve JSON modules
      resolveJsonModule: true,
    });

    ts.typescriptDefaults.setDiagnosticsOptions({
      noSemanticValidation: false,
      noSyntaxValidation: false,
    });

    // Enable eager model sync so TypeScript immediately processes all models
    // This is critical for cross-file IntelliSense to work without requiring edits
    ts.typescriptDefaults.setEagerModelSync(true);
  }

  /**
   * Parse imports from TypeScript/JavaScript content
   */
  public parseImports(content: string): string[] {
    const imports: string[] = [];

    // Match ES6 imports: import { x } from 'package' or import 'package'
    const importRegex = /import\s+(?:[\w*{}\s,]+from\s+)?['"]([^'"]+)['"]/g;
    let match: RegExpExecArray | null;

    while ((match = importRegex.exec(content)) !== null) {
      const importPath = match[1];
      // Only process non-relative imports (npm packages)
      if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
        const packageName = importPath.startsWith('@')
          ? importPath.split('/').slice(0, 2).join('/') // @scope/package
          : importPath.split('/')[0]; // package
        imports.push(packageName);
      }
    }

    // Match require calls: require('package')
    const requireRegex = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
    while ((match = requireRegex.exec(content)) !== null) {
      const importPath = match[1];
      if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
        const packageName = importPath.startsWith('@')
          ? importPath.split('/').slice(0, 2).join('/')
          : importPath.split('/')[0];
        imports.push(packageName);
      }
    }

    return [...new Set(imports)];
  }

  /**
   * Load type definitions for a package from virtual FS
   */
  public async loadTypesForPackage(packageName: string): Promise<void> {
    if (!this.monacoInstance || !this.executionEnvironment) return;
    if (this.loadedLibs.has(packageName)) return;
    if (this.notFoundPackages.has(packageName)) return; // Skip packages we already checked

    try {
      let typesLoaded = await this.tryLoadPackageTypes(packageName);
      if (!typesLoaded) {
        typesLoaded = await this.tryLoadAtTypesPackage(packageName);
      }
      if (typesLoaded) {
        this.loadedLibs.add(packageName);
      } else {
        // Cache that this package wasn't found to avoid repeated filesystem checks
        this.notFoundPackages.add(packageName);
      }
    } catch (error) {
      console.warn(`Failed to load types for ${packageName}:`, error);
    }
  }

  private async tryLoadPackageTypes(packageName: string): Promise<boolean> {
    const ts = this.tsApi;
    if (!this.executionEnvironment || !ts) return false;

    const basePath = `/node_modules/${packageName}`;

    try {
      // Check package.json for types field
      const packageJsonPath = `${basePath}/package.json`;
      const packageJsonExists = await this.executionEnvironment.exists(packageJsonPath);

      if (packageJsonExists) {
        const packageJsonContent = await this.executionEnvironment.readFile(packageJsonPath);
        const packageJson = JSON.parse(packageJsonContent);

        // Add package.json to Monaco so TypeScript can resolve the types field
        ts.typescriptDefaults.addExtraLib(packageJsonContent, `file://${packageJsonPath}`);

        const typesPath = packageJson.types || packageJson.typings;
        if (typesPath) {
          // Load all .d.ts files from the package, not just the entry point
          // Modern packages often have multiple declaration files with imports
          await this.loadAllDtsFilesFromPackage(basePath);
          return true;
        }
      }

      // Try common locations - if any exist, load all .d.ts files
      const commonPaths = [
        `${basePath}/index.d.ts`,
        `${basePath}/dist/index.d.ts`,
        `${basePath}/lib/index.d.ts`,
      ];

      for (const dtsPath of commonPaths) {
        if (await this.executionEnvironment.exists(dtsPath)) {
          await this.loadAllDtsFilesFromPackage(basePath);
          return true;
        }
      }

      return false;
    } catch (error) {
      console.error(`Failed to load package types for ${packageName}:`, error);
      return false;
    }
  }

  /**
   * Recursively load all .d.ts files from a package directory
   */
  private async loadAllDtsFilesFromPackage(basePath: string): Promise<void> {
    const ts = this.tsApi;
    if (!this.executionEnvironment || !ts) return;

    await this.loadDtsFilesFromDirectory(basePath);
  }

  /**
   * Recursively load .d.ts files from a directory
   */
  private async loadDtsFilesFromDirectory(dirPath: string): Promise<void> {
    const ts = this.tsApi;
    if (!this.executionEnvironment || !ts) return;

    try {
      const entries = await this.executionEnvironment.readDir(dirPath);

      for (const entry of entries) {
        const fullPath = dirPath === '/' ? `/${entry.name}` : `${dirPath}/${entry.name}`;

        // Skip nested node_modules (shouldn't happen in a package but be safe)
        if (entry.name === 'node_modules') continue;

        if (entry.type === 'directory') {
          await this.loadDtsFilesFromDirectory(fullPath);
        } else if (entry.type === 'file' && entry.name.endsWith('.d.ts')) {
          try {
            const content = await this.executionEnvironment.readFile(fullPath);
            ts.typescriptDefaults.addExtraLib(content, `file://${fullPath}`);
          } catch {
            // Ignore files that can't be read
          }
        }
      }
    } catch {
      // Directory might not be readable
    }
  }

  private async tryLoadAtTypesPackage(packageName: string): Promise<boolean> {
    if (!this.executionEnvironment) return false;

    // Handle scoped packages: @scope/package -> @types/scope__package
    const typesPackageName = packageName.startsWith('@')
      ? `@types/${packageName.slice(1).replace('/', '__')}`
      : `@types/${packageName}`;

    const basePath = `/node_modules/${typesPackageName}`;

    try {
      const indexPath = `${basePath}/index.d.ts`;
      if (await this.executionEnvironment.exists(indexPath)) {
        // Load all .d.ts files from the @types package
        await this.loadAllDtsFilesFromPackage(basePath);
        return true;
      }
      return false;
    } catch {
      return false;
    }
  }

  /**
   * Process content change and load types for any new imports
   */
  public async processContentChange(content: string): Promise<void> {
    const imports = this.parseImports(content);

    for (const packageName of imports) {
      if (!this.loadedLibs.has(packageName)) {
        await this.loadTypesForPackage(packageName);
      }
    }
  }

  /**
   * Scan node_modules for packages and load types for any not yet loaded.
   * Called when node_modules changes (e.g., after pnpm install).
   */
  public async scanAndLoadNewPackageTypes(): Promise<void> {
    if (!this.executionEnvironment) return;

    // Clear not-found cache so newly installed packages can be detected
    this.notFoundPackages.clear();

    try {
      // Check if node_modules exists
      if (!await this.executionEnvironment.exists('/node_modules')) return;

      // Read top-level node_modules
      const entries = await this.executionEnvironment.readDir('/node_modules');

      for (const entry of entries) {
        if (entry.type !== 'directory') continue;

        if (entry.name.startsWith('@')) {
          // Scoped package - read subdirectories
          try {
            const scopedPath = `/node_modules/${entry.name}`;
            const scopedEntries = await this.executionEnvironment.readDir(scopedPath);
            for (const scopedEntry of scopedEntries) {
              if (scopedEntry.type === 'directory') {
                const packageName = `${entry.name}/${scopedEntry.name}`;
                await this.loadTypesForPackage(packageName);
              }
            }
          } catch {
            // Skip if we can't read scoped directory
          }
        } else if (!entry.name.startsWith('.')) {
          // Regular package
          await this.loadTypesForPackage(entry.name);
        }
      }
    } catch (error) {
      console.warn('Failed to scan node_modules:', error);
    }
  }

  /**
   * Add a file model to Monaco for cross-file IntelliSense
   * Also registers the file with TypeScript via addExtraLib for module resolution
   */
  public addFileModel(path: string, content: string): void {
    if (!this.monacoInstance) return;

    // Cache the content for sync access
    this.fileCache.set(path, content);

    // Create/update the editor model
    const uri = this.monacoInstance.Uri.parse(`file://${path}`);
    const existingModel = this.monacoInstance.editor.getModel(uri);

    if (existingModel) {
      // Only update if content actually changed (prevents cursor reset)
      if (existingModel.getValue() !== content) {
        existingModel.setValue(content);
      }
    } else {
      const language = this.getLanguageFromPath(path);
      this.monacoInstance.editor.createModel(content, language, uri);
    }

    // Also add as extra lib for TypeScript module resolution
    // This is critical - TypeScript's resolver uses extra libs, not editor models
    this.addFileAsExtraLib(path, content);
  }

  /**
   * Add a file as an extra lib for TypeScript module resolution.
   * This enables TypeScript to resolve imports to project files.
   */
  private addFileAsExtraLib(path: string, content: string): void {
    const ts = this.tsApi;
    if (!ts) return;

    // Dispose existing lib if present (for updates)
    const existing = this.addedExtraLibs.get(path);
    if (existing) {
      existing.dispose();
    }

    // Add the file with its actual path
    const filePath = `file://${path}`;
    const disposable = ts.typescriptDefaults.addExtraLib(content, filePath);
    this.addedExtraLibs.set(path, disposable);

    // For .ts files, also add with .js extension to handle ESM imports
    // (e.g., import from './utils.js' should resolve to ./utils.ts)
    if (path.endsWith('.ts') && !path.endsWith('.d.ts')) {
      const jsPath = path.replace(/\.ts$/, '.js');
      const jsFilePath = `file://${jsPath}`;
      const jsDisposable = ts.typescriptDefaults.addExtraLib(content, jsFilePath);
      this.addedExtraLibs.set(jsPath, jsDisposable);
      this.fileCache.set(jsPath, content);
    } else if (path.endsWith('.tsx')) {
      const jsxPath = path.replace(/\.tsx$/, '.jsx');
      const jsxFilePath = `file://${jsxPath}`;
      const jsxDisposable = ts.typescriptDefaults.addExtraLib(content, jsxFilePath);
      this.addedExtraLibs.set(jsxPath, jsxDisposable);
      this.fileCache.set(jsxPath, content);
    }
  }

  /**
   * Get cached file content for synchronous access
   */
  public getFileContent(path: string): string | undefined {
    return this.fileCache.get(path);
  }

  private getLanguageFromPath(path: string): string {
    const ext = path.split('.').pop()?.toLowerCase();
    switch (ext) {
      case 'ts':
      case 'tsx':
        return 'typescript';
      case 'js':
      case 'jsx':
        return 'javascript';
      case 'json':
        return 'json';
      default:
        return 'plaintext';
    }
  }
}
