import * as fs from "node:fs";
import * as path from "node:path";
import { parentPort } from "node:worker_threads";

import {
  Problem,
  ProblemReport,
  SyncVirtualFileSystem,
  WorkerInput,
  WorkerOutput,
} from "../../shared/tsc_types";
import { SyncVirtualFileSystemImpl } from "../../shared/VirtualFilesystem";

function loadLocalTypeScript(appPath: string): typeof import("typescript") {
  try {
    // Try to load TypeScript from the project's node_modules
    const requirePath = require.resolve("typescript", { paths: [appPath] });
    const ts = require(requirePath);
    return ts;
  } catch (error) {
    throw new Error(
      `Failed to load TypeScript from ${appPath} because of ${error}`,
    );
  }
}

function findTypeScriptConfig(appPath: string): string {
  const possibleConfigs = [
    // For vite applications, we want to check tsconfig.app.json, since it's the
    // most important one (client-side app).
    // The tsconfig.json in vite apps is a project reference and doesn't
    // actually check anything unless you do "--build" which requires a complex
    // programmatic approach
    "tsconfig.app.json",
    // For Next.js applications, it typically has a single tsconfig.json file
    "tsconfig.json",
  ];

  for (const config of possibleConfigs) {
    const configPath = path.join(appPath, config);
    if (fs.existsSync(configPath)) {
      return configPath;
    }
  }

  throw new Error(
    `No TypeScript configuration file found in ${appPath}. Expected one of: ${possibleConfigs.join(", ")}`,
  );
}

async function runTypeScriptCheck(
  ts: typeof import("typescript"),
  vfs: SyncVirtualFileSystem,
  {
    appPath,
    tsconfigPath,
    tsBuildInfoCacheDir,
  }: {
    appPath: string;
    tsconfigPath: string;
    tsBuildInfoCacheDir: string;
  },
): Promise<ProblemReport> {
  return runSingleProject(ts, vfs, {
    appPath,
    tsconfigPath,
    tsBuildInfoCacheDir,
  });
}

async function runSingleProject(
  ts: typeof import("typescript"),
  vfs: SyncVirtualFileSystem,
  {
    appPath,
    tsconfigPath,
    tsBuildInfoCacheDir,
  }: {
    appPath: string;
    tsconfigPath: string;
    tsBuildInfoCacheDir: string;
  },
): Promise<ProblemReport> {
  // Use the idiomatic way to parse TypeScript config
  const parsedCommandLine = ts.getParsedCommandLineOfConfigFile(
    tsconfigPath,
    undefined, // No additional options
    {
      // Custom system object that can handle our virtual files
      ...ts.sys,
      fileExists: (fileName: string) => vfs.fileExists(fileName),
      readFile: (fileName: string) => vfs.readFile(fileName),
      onUnRecoverableConfigFileDiagnostic: (
        diagnostic: import("typescript").Diagnostic,
      ) => {
        throw new Error(
          `TypeScript config error: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`,
        );
      },
    },
  );

  if (!parsedCommandLine) {
    throw new Error(`Failed to parse TypeScript config: ${tsconfigPath}`);
  }

  // Enable incremental compilation by setting tsBuildInfoFile if not already set
  const options = { ...parsedCommandLine.options };
  if (!options.tsBuildInfoFile && options.incremental !== false) {
    // Place the buildinfo file in a temp directory to avoid polluting the project
    const tmpDir = tsBuildInfoCacheDir;
    if (!fs.existsSync(tmpDir)) {
      fs.mkdirSync(tmpDir, { recursive: true });
    }

    // Create a unique filename based on both the app path and tsconfig path to prevent collisions
    const configName = path.basename(tsconfigPath, path.extname(tsconfigPath));
    const appHash = Buffer.from(appPath)
      .toString("base64")
      .replace(/[/+=]/g, "_");
    options.tsBuildInfoFile = path.join(
      tmpDir,
      `${appHash}-${configName}.tsbuildinfo`,
    );
    options.incremental = true;
  }

  let rootNames = parsedCommandLine.fileNames;

  // Add any virtual files that aren't already included
  const virtualTsFiles = vfs
    .getVirtualFiles()
    .map((file) => path.resolve(appPath, file.path))
    .filter(isTypeScriptFile);

  // Remove deleted files from rootNames
  const deletedFiles = vfs
    .getDeletedFiles()
    .map((file) => path.resolve(appPath, file));
  rootNames = rootNames.filter((fileName) => {
    const resolvedPath = path.resolve(fileName);
    return !deletedFiles.includes(resolvedPath);
  });

  for (const virtualFile of virtualTsFiles) {
    if (!rootNames.includes(virtualFile)) {
      rootNames.push(virtualFile);
    }
  }

  // Create custom compiler host
  const host = createVirtualCompilerHost(ts, appPath, vfs, options);

  // Create incremental program - TypeScript will automatically use the tsBuildInfo file
  const builderProgram = ts.createIncrementalProgram({
    rootNames,
    options,
    host,
    configFileParsingDiagnostics:
      ts.getConfigFileParsingDiagnostics(parsedCommandLine),
  });

  // Get diagnostics - the incremental program optimizes this by only checking changed files
  const diagnostics = [
    ...builderProgram.getSyntacticDiagnostics(),
    ...builderProgram.getSemanticDiagnostics(),
    ...builderProgram.getGlobalDiagnostics(),
  ];

  // Emit the build info file to persist the incremental state
  builderProgram.emit();

  // Convert diagnostics to our format
  const problems: Problem[] = [];

  for (const diagnostic of diagnostics) {
    if (!diagnostic.file) continue;

    const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(
      diagnostic.start!,
    );
    const message = ts.flattenDiagnosticMessageText(
      diagnostic.messageText,
      "\n",
    );

    if (diagnostic.category !== ts.DiagnosticCategory.Error) {
      continue;
    }

    problems.push({
      file: normalizePath(path.relative(appPath, diagnostic.file.fileName)),
      line: line + 1, // Convert to 1-based
      column: character + 1, // Convert to 1-based
      message,
      code: diagnostic.code,
    });
  }

  return {
    problems,
  };
}

function createVirtualCompilerHost(
  ts: typeof import("typescript"),
  appPath: string,
  vfs: SyncVirtualFileSystem,
  compilerOptions: import("typescript").CompilerOptions,
): import("typescript").CompilerHost {
  const host = ts.createIncrementalCompilerHost(compilerOptions);

  // Override file reading to use virtual files
  host.readFile = (fileName: string) => {
    return vfs.readFile(fileName);
  };

  // Override file existence check
  host.fileExists = (fileName: string) => {
    return vfs.fileExists(fileName);
  };

  // Override getCurrentDirectory to ensure proper resolution
  host.getCurrentDirectory = () => appPath;

  // Override writeFile to handle virtual file system
  // This is important for writing the tsBuildInfo file
  const originalWriteFile = host.writeFile;
  host.writeFile = (
    fileName: string,
    data: string,
    writeByteOrderMark?: boolean,
    onError?: (message: string) => void,
  ) => {
    // Only write build info files to disk, not emit files
    if (fileName.endsWith(".tsbuildinfo")) {
      originalWriteFile?.call(
        host,
        fileName,
        data,
        !!writeByteOrderMark,
        onError,
      );
    }
    // Ignore other emit files since we're only doing type checking
  };

  return host;
}

function isTypeScriptFile(fileName: string): boolean {
  const ext = path.extname(fileName).toLowerCase();
  return [".ts", ".tsx", ".js", ".jsx"].includes(ext);
}

async function processTypeScriptCheck(
  input: WorkerInput,
): Promise<WorkerOutput> {
  try {
    const { appPath, virtualChanges, tsBuildInfoCacheDir } = input;

    // Load the local TypeScript version from the app's node_modules
    const ts = loadLocalTypeScript(appPath);

    const vfs = new SyncVirtualFileSystemImpl(appPath, {
      fileExists: (fileName: string) => ts.sys.fileExists(fileName),
      readFile: (fileName: string) => ts.sys.readFile(fileName),
    });
    vfs.applyResponseChanges(virtualChanges);

    // Find TypeScript config - throw error if not found
    const tsconfigPath = findTypeScriptConfig(appPath);

    // Create TypeScript program with virtual file system
    const result = await runTypeScriptCheck(ts, vfs, {
      appPath,
      tsconfigPath,
      tsBuildInfoCacheDir,
    });

    return {
      success: true,
      data: result,
    };
  } catch (error) {
    return {
      success: false,
      error: error instanceof Error ? error.message : String(error),
    };
  }
}

// Handle messages from main thread
parentPort?.on("message", async (input: WorkerInput) => {
  const output = await processTypeScriptCheck(input);
  parentPort?.postMessage(output);
});

/**
 * Normalize the path to use forward slashes instead of backslashes.
 * This is important to prevent weird Git issues, particularly on Windows.
 * @param path Source path.
 * @returns Normalized path.
 */
function normalizePath(path: string): string {
  return path.replace(/\\/g, "/");
}
