#!/usr/bin/env node
/* eslint-disable no-console */

const { exec, execSync } = require("child_process");
const fs = require("fs");
const pathLib = require("path");

const generate = require("@babel/generator").default;
const babelParser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const { isNotEmpty, existsBy } = require("@bigbinary/neeto-cist");
const enhancedResolve = require("enhanced-resolve");
const { isEmpty, includes, __, mergeDeepLeft } = require("ramda");

const {
  RAILS_MOUNTED_COMPONENTS_REGEX,
  ADMIN_AND_PUBLIC_APP_REGEX,
  TYPES,
  PACKS_FILES_REGEX,
} = require("./constants");

// @ts-ignore
const projectESBuildConfig = require("../../../../../../config/esbuild/config");
const baseESBuildAlias = require("../../esbuild/alias");
const baseESBuildExtensions = require("../../esbuild/index");

const mergedESBuildAlias = mergeDeepLeft(
  projectESBuildConfig.alias,
  baseESBuildAlias
);

let modifiedFiles = [];
let importedItems = [];
let unusedFiles = [];
let iterationCount = 1;

const javascriptDir = pathLib.resolve(
  __dirname,
  "../../../../../../app/javascript"
);

const resolver = enhancedResolve.create.sync({
  alias: mergedESBuildAlias,
  extensions: baseESBuildExtensions.config.resolveExtensions,
  conditionNames: ["require", "import", "node", "default", "types"],
});

const isRailsMountedOrPacksFiles = filePath =>
  RAILS_MOUNTED_COMPONENTS_REGEX.test(filePath) ||
  PACKS_FILES_REGEX.test(filePath) ||
  ADMIN_AND_PUBLIC_APP_REGEX.test(filePath);

const isInImportedList = (item, filePath) =>
  existsBy({ item: includes(__, [item, "*"]), filePath }, importedItems);

const findImports = filePath => {
  const code = fs.readFileSync(filePath, "utf8");
  const ast = babelParser.parse(code, {
    sourceType: "module",
    plugins: ["jsx"],
  });

  traverse(ast, {
    ImportDeclaration(path) {
      const { node } = path;

      const currentDir = pathLib.dirname(filePath);
      const importSource = node.source.value;

      try {
        const resolvedPath = resolver(currentDir, importSource);
        const isNodeModule = resolvedPath.includes("node_modules");
        if (isNodeModule) return;

        if (isEmpty(node.specifiers)) {
          importedItems.push({ item: "*", filePath: resolvedPath });
        }

        node.specifiers.forEach(specifier => {
          if (specifier.type === TYPES.IMPORT_NAMESPACE_SPECIFIER) {
            importedItems.push({ item: "*", filePath: resolvedPath });
          } else if (specifier.type === TYPES.IMPORT_DEFAULT_SPECIFIER) {
            importedItems.push({ item: "default", filePath: resolvedPath });
          } else {
            const item = specifier.imported.name;
            importedItems.push({ item, filePath: resolvedPath });
          }
        });
      } catch (err) {
        // Skip resolution errors (e.g., malformed package exports in node_modules)
        // This typically happens with external packages that have invalid exports(like BrowserPreview in          neeto-molecules)
        console.warn(
          `Failed to resolve import "${importSource}" in ${filePath}: ${err.message}`
        );

        return;
      }
    },

    // Handle React.lazy(() => import("...")) patterns and dynamic imports
    CallExpression(path) {
      const { node } = path;

      // Check for React.lazy pattern
      if (
        node.callee.type === "MemberExpression" &&
        node.callee.object.name === "React" &&
        node.callee.property.name === "lazy" &&
        node.arguments.length === 1 &&
        node.arguments[0].type === "ArrowFunctionExpression"
      ) {
        const arrowFunction = node.arguments[0];
        if (
          arrowFunction.body.type === "CallExpression" &&
          arrowFunction.body.callee.type === "Import"
        ) {
          const importArg = arrowFunction.body.arguments[0];
          if (importArg && importArg.type === "StringLiteral") {
            const currentDir = pathLib.dirname(filePath);
            const importSource = importArg.value;

            try {
              const resolvedPath = resolver(currentDir, importSource);
              if (!resolvedPath || typeof resolvedPath !== "string") return;

              const isNodeModule = resolvedPath.includes("node_modules");
              if (isNodeModule) return;

              importedItems.push({ item: "default", filePath: resolvedPath });
            } catch (err) {
              console.warn(
                `⚠️  Failed to resolve React.lazy import "${importSource}" in ${filePath}: ${err.message}`
              );
            }
          }
        }
      }

      // Handle direct dynamic imports: import("...")
      if (
        node.callee.type === "Import" &&
        node.arguments.length === 1 &&
        node.arguments[0].type === "StringLiteral"
      ) {
        const currentDir = pathLib.dirname(filePath);
        const importSource = node.arguments[0].value;

        try {
          const resolvedPath = resolver(currentDir, importSource);
          if (!resolvedPath || typeof resolvedPath !== "string") return;

          const isNodeModule = resolvedPath.includes("node_modules");
          if (isNodeModule) return;

          importedItems.push({ item: "default", filePath: resolvedPath });
        } catch (err) {
          console.warn(
            `Failed to resolve dynamic import "${importSource}" in ${filePath}: ${err.message}`
          );
        }
      }
    },
  });
};

const removeUnusedExports = filePath => {
  if (isRailsMountedOrPacksFiles(filePath)) return;

  if (!existsBy({ filePath }, importedItems)) {
    unusedFiles.push(filePath);

    return;
  }

  let isPathModified = false;
  const code = fs.readFileSync(filePath, "utf8");
  const ast = babelParser.parse(code, {
    sourceType: "module",
    plugins: ["jsx"],
  });

  traverse(ast, {
    ExportNamedDeclaration(path) {
      const { node } = path;
      if (node.declaration) {
        if (node.declaration.type === TYPES.VARIABLE_DECLARATION) {
          node.declaration.declarations.forEach(declaration => {
            const exportedItem = declaration.id.name;
            if (isInImportedList(exportedItem, filePath)) return;

            path.replaceWith(path.node.declaration);
            isPathModified = true;
          });
        } else if (
          node.declaration.type === TYPES.FUNCTION_DECLARATION ||
          node.declaration.type === TYPES.CLASS_DECLARATION
        ) {
          const exportedItem = node.declaration.id.name;
          if (isInImportedList(exportedItem, filePath)) return;

          path.replaceWith(path.node.declaration);
          isPathModified = true;
        }
      } else {
        path.get("specifiers").forEach(specifierPath => {
          const exportedItem = specifierPath.node.exported.name;
          if (isInImportedList(exportedItem, filePath)) return;

          node.specifiers.length === 1 ? path.remove() : specifierPath.remove();
          isPathModified = true;
        });
      }
    },
    ExportDefaultDeclaration(path) {
      const exportedItem = "default";
      if (isInImportedList(exportedItem, filePath)) return;

      path.remove();
      isPathModified = true;
    },
  });

  if (!isPathModified) return;

  const newCode = generate(ast, {
    retainLines: true,
    retainFunctionParens: true,
  }).code;

  fs.writeFileSync(filePath, newCode);
  modifiedFiles.push(filePath);
};

const traverseDirectoryAndExecuteFunc = (dir, func) => {
  const files = fs.readdirSync(dir);

  files.forEach(file => {
    const filePath = pathLib.join(dir, file);
    const stats = fs.statSync(filePath);

    if (stats.isFile() && [".js", ".jsx"].includes(pathLib.extname(filePath))) {
      func(filePath);
    } else if (stats.isDirectory()) {
      traverseDirectoryAndExecuteFunc(filePath, func);
    }
  });
};

const runLintersAndRemoveUnusedFiles = () => {
  const commands = [];

  if (isNotEmpty(unusedFiles)) commands.push(`rm -f ${unusedFiles.join(" ")}`);

  if (isNotEmpty(modifiedFiles)) {
    commands.push(`npx prettier --write ${modifiedFiles.join(" ")}`);
    commands.push(`npx eslint --fix ${modifiedFiles.join(" ")}`);
  }

  // script will be executed recursively until we have no modified files or unused files
  exec(commands.join(" && "), () => run());
};

const restoreEslintNeetoVersion = () => {
  execSync("git restore package.json yarn.lock", { stdio: "ignore" });
  execSync("yarn", { stdio: "ignore" });
};

const reportNonFixableEslintErrors = () => {
  const modifiedFiles = execSync("git diff --name-only --diff-filter=M")
    .toString()
    .split("\n")
    .join(" ");

  if (!modifiedFiles) {
    console.log("✅  Script executed successfully!\n");

    return;
  }

  exec(`npx eslint ${modifiedFiles} --fix --quiet`, (_, stdout) => {
    if (stdout) {
      console.log(stdout);
      console.log(
        "⭕️ Auto-fixing is not implemented for the above eslint errors. Please fix them manually.\n"
      );
    } else {
      console.log("- No errors found.\n\n✅  Script executed successfully!\n");
    }
  });
};

const postExecution = () => {
  console.log("\n4. Restoring @bigbinary/eslint-plugin-neeto version.");
  restoreEslintNeetoVersion();

  console.log("\n5. Checking if there are any non-fixable eslint errors.");
  reportNonFixableEslintErrors();
};

const installCustomEslintNeetoVersion = () =>
  execSync(
    "yarn add @bigbinary/eslint-plugin-neeto@1.0.43-no-unused-vars-autofix",
    { stdio: "ignore" }
  );

const run = () => {
  console.log(`\nIteration: ${iterationCount++}`);
  console.log(".............");

  modifiedFiles = [];
  importedItems = [];
  unusedFiles = [];

  console.log(
    "* Collecting all the imports (including React.lazy and dynamic imports)."
  );
  traverseDirectoryAndExecuteFunc(javascriptDir, findImports);

  console.log("* Removing exports which are not imported.");
  traverseDirectoryAndExecuteFunc(javascriptDir, removeUnusedExports);

  const filesCountMsg = `\nModified files: ${modifiedFiles.length}\nUnused files: ${unusedFiles.length}`;

  const isExecutionCompleted = isEmpty(modifiedFiles) && isEmpty(unusedFiles);
  if (isExecutionCompleted) {
    console.log(filesCountMsg);
    postExecution();
  } else {
    console.log(
      "* Running linters on modified files and removing unused files."
    );
    runLintersAndRemoveUnusedFiles();
    console.log(filesCountMsg);
  }
};

// Script execution starts here
console.log("\nPlease wait, this may take a few minutes...");

console.log(
  "\n1. Installing @bigbinary/eslint-plugin-neeto custom release required for this script."
);
installCustomEslintNeetoVersion();

console.log(
  "\n2. Executing script recursively until we have no modified or unused files."
);
run();
