import esbuild, { PluginBuild } from "esbuild";
import linaria from "@linaria/esbuild";
import fs from "node:fs";
import path from "node:path";
import postcss from "postcss";
import sass from "sass";
import postcssrc from "postcss-load-config";

type Assets = {
  base: string;
  files: (string | { orig: string; dest: string })[];
};

export function getFilesInDirectory(startPath: string, regex?: RegExp): Assets {
  if (!fs.existsSync(startPath)) {
    return {
      base: startPath,
      files: [],
    };
  }
  const files = fs.readdirSync(startPath);
  const result = files
    .flatMap((file) => {
      const filename = path.join(startPath, file);

      const stat = fs.lstatSync(filename);
      if (stat.isDirectory()) {
        return getFilesInDirectory(filename, regex).files;
      }
      if (!regex || regex.test(filename)) {
        return [filename];
      }
      return [];
    })
    .filter((x) => !!x);

  return {
    base: startPath,
    files: result,
  };
}

function git_hash(root: string) {
  const rev = fs
    .readFileSync(path.join(root, ".git", "HEAD"))
    .toString()
    .trim()
    .split(/.*[: ]/)
    .slice(-1)[0];
  if (rev.indexOf("/") === -1) {
    return rev;
  } else {
    return fs
      .readFileSync(path.join(root, ".git", rev))
      .toString()
      .trim();
  }
}

// FIXME: Put this into some helper library.
function copyFilesPlugin(assets: Assets | Assets[]) {
  return {
    name: "copy-files",
    setup(build: PluginBuild) {
      if (!assets || (assets instanceof Array && !assets.length)) {
        return;
      }
      const list = assets instanceof Array ? assets : [assets];

      const outDir = build.initialOptions.outdir;
      if (outDir === undefined) {
        throw Error("esbuild build options does not specify outdir");
      }
      build.onEnd(() => {
        list.forEach((as) => {
          for (const fileName of as.files) {
            const { orig, dest } =
              typeof fileName === "string"
                ? { orig: fileName, dest: fileName }
                : fileName;
            const destination = path.join(outDir, path.relative(as.base, dest));
            const dirname = path.dirname(destination);
            if (!fs.existsSync(dirname)) {
              fs.mkdirSync(dirname, { recursive: true });
            }
            fs.copyFileSync(orig, destination);
          }
        });
      });
    },
  };
}

const DEFAULT_SASS_FILTER = /\.(s[ac]ss|css)$/;

const sassPlugin: esbuild.Plugin = {
  name: "custom-build-sass",
  setup(build) {
    build.onLoad({ filter: DEFAULT_SASS_FILTER }, ({ path: file }) => {
      const resolveDir = path.dirname(file);
      const { css: contents } = sass.compile(file, { loadPaths: ["./"] });

      return {
        resolveDir,
        loader: "css",
        contents,
      };
    });
  },
};

/**
 * Problem:
 *   No loader is configured for ".node" files: ../../node_modules/.pnpm/fsevents@2.3.3/node_modules/fsevents/fsevents.node
 *
 * Reference:
 *   https://github.com/evanw/esbuild/issues/1051#issuecomment-806325487
 */
const nativeNodeModulesPlugin: esbuild.Plugin = {
  name: "native-node-modules",
  setup(build) {
    // If a ".node" file is imported within a module in the "file" namespace, resolve
    // it to an absolute path and put it into the "node-file" virtual namespace.
    build.onResolve({ filter: /\.node$/, namespace: "file" }, (args) => ({
      path: require.resolve(args.path, { paths: [args.resolveDir] }),
      namespace: "node-file",
    }));

    // Files in the "node-file" virtual namespace call "require()" on the
    // path from esbuild of the ".node" file in the output directory.
    build.onLoad({ filter: /.*/, namespace: "node-file" }, (args) => ({
      contents: `
        import path from ${JSON.stringify(args.path)}
        try { module.exports = require(path) }
        catch {}
      `,
    }));

    // If a ".node" file is imported within a module in the "node-file" namespace, put
    // it in the "file" namespace where esbuild's default loading behavior will handle
    // it. It is already an absolute path since we resolved it to one above.
    build.onResolve({ filter: /\.node$/, namespace: "node-file" }, (args) => ({
      path: args.path,
      namespace: "file",
    }));

    // Tell esbuild's default loading behavior to use the "file" loader for
    // these ".node" files.
    let opts = build.initialOptions;
    opts.loader = opts.loader || {};
    opts.loader[".node"] = "file";
  },
};

const postCssPlugin: esbuild.Plugin = {
  name: "custom-build-postcss",
  setup(build) {
    build.onLoad({ filter: DEFAULT_SASS_FILTER }, async ({ path: file }) => {
      const resolveDir = path.dirname(file);
      const sourceBuffer = fs.readFileSync(file);
      const source = sourceBuffer.toString("utf-8");
      const postCssConfig = await postcssrc();
      postCssConfig.options.from = file;

      const { css: contents } = await postcss(postCssConfig.plugins).process(
        source,
        postCssConfig.options,
      );

      return {
        resolveDir,
        loader: "css",
        contents,
      };
    });
  },
};

/**
 * This should be able to load the plugin but but some reason it does not work
 *
 *       text: "Cannot find module '../plugins/preeval'\n" +
 *
 */
function linariaPlugin() {
  const linariaCssPlugin: esbuild.Plugin = (linaria as any)({
    babelOptions: {
      presets: ["@babel/preset-typescript", "@babel/preset-react", "@linaria"],
    },
    sourceMap: true,
  });
  return linariaCssPlugin;
}

const defaultEsBuildConfig: esbuild.BuildOptions = {
  bundle: true,
  loader: {
    ".svg": "file",
    ".inline.svg": "text",
    ".png": "dataurl",
    ".jpeg": "dataurl",
    ".ttf": "file",
    ".woff": "file",
    ".woff2": "file",
    ".eot": "file",
  },
  target: ["es2020"],
  format: "esm",
  platform: "browser",
  jsxFactory: "h",
  jsxFragment: "Fragment",
  alias: {
    react: "preact/compat",
    "react-dom": "preact/compat",
  },
};
import nodePath from "node:path";
import nodeUrl from "node:url";
export interface BuildParams {
  type: "development" | "test" | "production";
  /**
   * Assign this to import.meta so the script can be called from any directory
   */
  importMeta?: ImportMeta;
  source: {
    assets: Assets | Assets[];
    js: string[] | Record<string, string>;
  };
  public?: string;
  /**
   * Location directory of the output. Can be redirected with INSTALL_DIR environment
   */
  destination: string;
  css: "sass" | "postcss" | "linaria";
  linariaPlugin?: () => esbuild.Plugin;
}

function getPackageAndGitRoot(meta: undefined | ImportMeta) {
  if (meta) {
    const root = nodePath.dirname(nodeUrl.fileURLToPath(meta.url));
    process.chdir(root);
  }
  const baseDir = process.cwd();

  let GIT_ROOT = baseDir;
  while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") {
    GIT_ROOT = path.join(GIT_ROOT, "../");
  }
  if (GIT_ROOT === "/") {
    return { version: 'not defined', hash: 'not defined', baseDir }
  }
  const hash = git_hash(GIT_ROOT);

  const buf = fs.readFileSync(path.join(baseDir, "package.json"));
  const version = JSON.parse(buf.toString("utf-8")).version;
  return { version, hash, baseDir };
}

export function computeConfig(params: BuildParams): {
  esBuildOptions: esbuild.BuildOptions;
  version: string;
} {
  const {
    version,
    hash: GIT_HASH,
    baseDir,
  } = getPackageAndGitRoot(params.importMeta);

  if (!params.type) {
    throw Error(
      `missing build type, it should be "test", "development" or "production"`,
    );
  }

  if (!params.destination) {
    throw Error(`missing destination folder`);
  }

  const plugins: Array<esbuild.Plugin> = [
    copyFilesPlugin(params.source.assets),
  ];

  if (params.css) {
    switch (params.css) {
      case "sass": {
        plugins.push(sassPlugin);
        break;
      }
      case "postcss": {
        plugins.push(postCssPlugin);
        break;
      }

      case "linaria": {
        if (params.linariaPlugin) {
          plugins.push(params.linariaPlugin());
        }
        break;
      }

      default: {
        const cssType: never = params.css;
        throw Error(`not supported: ${cssType}`);
      }
    }
  }

  const esBuildOptions: esbuild.BuildOptions = {
    ...defaultEsBuildConfig,
    absWorkingDir: baseDir,
    entryPoints: params.source.js,
    publicPath: params.public,
    outdir: process.env.INSTALL_DIR ?? params.destination,
    treeShaking: true,
    minify: false, //params.type === "production",
    sourcemap: true, //params.type !== "production",
    define: {
      "process.env.NODE_ENV": JSON.stringify(params.type),
      __VERSION__: `"${version}"`,
      __GIT_HASH__: `"${GIT_HASH}"`,
    },
    plugins,
  };

  return { esBuildOptions, version };
}

/**
 * Build sources for prod environment
 */
export async function build(config: BuildParams) {
  const { esBuildOptions, version } = computeConfig(config);
  const res = await esbuild.build(esBuildOptions);
  fs.writeFileSync(`${esBuildOptions.outdir}/version.txt`, version);
  return res;
}

const LIVE_RELOAD_SCRIPT =
  "./node_modules/@gnu-taler/web-util/lib/live-reload.mjs";
const LIVE_RELOAD_SCRIPT_LOCALLY = "./lib/live-reload.mjs";

/**
 * Do startup for development environment
 *
 * To be used from web-utils project
 */
export function initializeDevOnWebUtils(
  config: BuildParams,
): () => Promise<esbuild.BuildResult> {
  function buildDevelopment() {
    const { esBuildOptions } = computeConfig(config);
    esBuildOptions.inject = [LIVE_RELOAD_SCRIPT_LOCALLY];
    return esbuild.build(esBuildOptions);
  }
  return buildDevelopment;
}

/**
 * Do startup for development environment
 *
 * To be used when web-utils is a library
 */
export function initializeDev(
  config: BuildParams,
  options: { dontDoLiveReload?: boolean } = {},
): () => Promise<esbuild.BuildResult> {
  function buildDevelopment() {
    const { esBuildOptions } = computeConfig(config);
    if (!options.dontDoLiveReload) {
      esBuildOptions.inject = [LIVE_RELOAD_SCRIPT];
    }
    return esbuild.build(esBuildOptions);
  }
  return buildDevelopment;
}
