/* global require, module, __dirname */
const { existsSync } = require("fs");
const path = require("path");
const rspack = require("@rspack/core");
// eslint-disable-next-line @typescript-eslint/naming-convention
const { RsdoctorRspackPlugin } = require("@rsdoctor/rspack-plugin");
// eslint-disable-next-line @typescript-eslint/naming-convention
const { StatsWriterPlugin } = require("webpack-stats-plugin");
const filterStats = require("@bundle-stats/plugin-webpack-filter");
// eslint-disable-next-line @typescript-eslint/naming-convention
const TerserPlugin = require("terser-webpack-plugin");
// eslint-disable-next-line @typescript-eslint/naming-convention
const { WebpackManifestPlugin } = require("rspack-manifest-plugin");
const log = require("fancy-log");
// eslint-disable-next-line @typescript-eslint/naming-convention
const SafeWebpackBar = require("./safe-webpackbar.cjs");
const paths = require("./paths.cjs");
const bundle = require("./bundle.cjs");

class LogStartCompilePlugin {
  ignoredFirst = false;

  apply(compiler) {
    compiler.hooks.beforeCompile.tap("LogStartCompilePlugin", () => {
      if (!this.ignoredFirst) {
        this.ignoredFirst = true;
        return;
      }
      log("Changes detected. Starting compilation");
    });
  }
}

const createRspackConfig = ({
  name,
  entry,
  outputPath,
  publicPath,
  defineOverlay,
  isProdBuild,
  latestBuild,
  isStatsBuild,
  isTestBuild,
  isLandingPageBuild,
  dontHash,
}) => {
  if (!dontHash) {
    dontHash = new Set();
  }
  const ignorePackages = bundle.ignorePackages({ latestBuild });
  return {
    name,
    mode: isProdBuild ? "production" : "development",
    target: `browserslist:${latestBuild ? "modern" : "legacy"}`,
    // For tests/CI, source maps are skipped to gain build speed
    // For production, generate source maps for accurate stack traces without source code
    // For development, generate "cheap" versions that can map to original line numbers
    devtool: isTestBuild
      ? false
      : isProdBuild
        ? "nosources-source-map"
        : "eval-cheap-module-source-map",
    entry,
    node: false,
    module: {
      rules: [
        {
          test: /\.m?js$|\.ts$/,
          exclude: /node_modules[\\/]core-js/,
          use: (info) => [
            {
              loader: "babel-loader",
              options: {
                ...bundle.babelOptions({
                  latestBuild,
                  isProdBuild,
                  isTestBuild,
                  sw: info.issuerLayer === "sw",
                }),
                cacheDirectory: !isProdBuild,
                cacheCompression: false,
              },
            },
            {
              loader: "builtin:swc-loader",
              options: bundle.swcOptions(),
            },
          ],
          resolve: {
            fullySpecified: false,
          },
          parser: {
            worker: ["*context.audioWorklet.addModule()", "..."],
          },
        },
        {
          test: /\.css$/,
          type: "asset/source",
        },
      ],
    },
    optimization: {
      minimizer: [
        new TerserPlugin({
          parallel: true,
          extractComments: true,
          terserOptions: bundle.terserOptions({ latestBuild, isTestBuild }),
        }),
      ],
      moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
      chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
      splitChunks: {
        // Disable splitting for web workers and worklets because imports of
        // external chunks are broken for:
        chunks: !isProdBuild
          ? // improve incremental build speed, but blows up bundle size
            new RegExp(
              `^(?!(${Object.keys(entry).join("|")}|.*work(?:er|let))$)`
            )
          : // - ESM output: https://github.com/webpack/webpack/issues/17014
            // - Worklets use `importScripts`: https://github.com/webpack/webpack/issues/11543
            (chunk) =>
              !chunk.canBeInitial() &&
              !new RegExp(
                `^.+-work${latestBuild ? "(?:let|er)" : "let"}$`
              ).test(chunk.name),
      },
    },
    plugins: [
      !isStatsBuild && new SafeWebpackBar({ fancy: !isProdBuild }),
      new WebpackManifestPlugin({
        // Only include the JS of entrypoints
        filter: (file) => file.isInitial && !file.name.endsWith(".map"),
      }),
      new rspack.DefinePlugin(
        bundle.definedVars({ isProdBuild, latestBuild, defineOverlay })
      ),
      new rspack.IgnorePlugin({
        checkResource(resource, context) {
          // Only use ignore to intercept imports that we don't control
          // inside node_module dependencies.
          if (
            !context.includes("/node_modules/") ||
            // calling define.amd will call require("!!webpack amd options")
            resource.startsWith("!!webpack") ||
            // loaded by webpack dev server but doesn't exist.
            resource === "webpack/hot" ||
            resource.startsWith("@swc/helpers")
          ) {
            return false;
          }
          let fullPath;
          try {
            fullPath = resource.startsWith(".")
              ? path.resolve(context, resource)
              : require.resolve(resource);
          } catch (err) {
            console.error(
              "Error in Home Assistant ignore plugin",
              resource,
              context
            );
            throw err;
          }

          return ignorePackages.some((toIgnorePath) =>
            fullPath.startsWith(toIgnorePath)
          );
        },
      }),
      bundle.emptyPackages({ isLandingPageBuild }).length
        ? new rspack.NormalModuleReplacementPlugin(
            new RegExp(bundle.emptyPackages({ isLandingPageBuild }).join("|")),
            path.resolve(paths.root_dir, "src/util/empty.js")
          )
        : false,
      // core-js ships a Node-only helper that evaluates
      // `Function('return require("...")')()` when its runtime environment
      // detection mis-classifies the page as Node. That produces a
      // ReferenceError on browsers (observed on Safari 14). Since browser
      // bundles never need to access Node built-in modules, replace it with
      // a CommonJS no-op stub matching the helper's API (returns undefined).
      new rspack.NormalModuleReplacementPlugin(
        /core-js[\\/]internals[\\/]get-built-in-node-module(?:\.js)?$/,
        path.resolve(__dirname, "get-built-in-node-module-shim.cjs")
      ),
      !isProdBuild && new LogStartCompilePlugin(),
      isProdBuild &&
        new StatsWriterPlugin({
          filename: path.relative(
            outputPath,
            path.join(paths.build_dir, "stats", `${name}.json`)
          ),
          stats: { assets: true, chunks: true, modules: true },
          transform: (stats) => JSON.stringify(filterStats(stats)),
        }),
      isProdBuild &&
        isStatsBuild &&
        new RsdoctorRspackPlugin({
          reportDir: path.join(paths.build_dir, "rsdoctor"),
          features: ["plugins", "bundle"],
          supports: {
            generateTileGraph: true,
          },
        }),
    ].filter(Boolean),
    resolve: {
      extensions: [".ts", ".js", ".json"],
      alias: {
        "lit/static-html$": "lit/static-html.js",
        "lit/decorators$": "lit/decorators.js",
        "lit/directive$": "lit/directive.js",
        "lit/directives/until$": "lit/directives/until.js",
        "lit/directives/ref$": "lit/directives/ref.js",
        "lit/directives/class-map$": "lit/directives/class-map.js",
        "lit/directives/style-map$": "lit/directives/style-map.js",
        "lit/directives/if-defined$": "lit/directives/if-defined.js",
        "lit/directives/guard$": "lit/directives/guard.js",
        "lit/directives/cache$": "lit/directives/cache.js",
        "lit/directives/join$": "lit/directives/join.js",
        "lit/directives/repeat$": "lit/directives/repeat.js",
        "lit/directives/live$": "lit/directives/live.js",
        "lit/directives/keyed$": latestBuild
          ? "lit/directives/keyed.js"
          : path.resolve(__dirname, "../src/common/lit/keyed-es5.ts"),
        "lit/polyfill-support$": "lit/polyfill-support.js",
        "@lit-labs/virtualizer/layouts/grid":
          "@lit-labs/virtualizer/layouts/grid.js",
        "@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver":
          "@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js",
        "@lit-labs/observers/resize-controller":
          "@lit-labs/observers/resize-controller.js",
        "@formatjs/intl-durationformat/should-polyfill$":
          "@formatjs/intl-durationformat/should-polyfill.js",
        "@formatjs/intl-durationformat/polyfill-force$":
          "@formatjs/intl-durationformat/polyfill-force.js",
        "@formatjs/intl-datetimeformat/should-polyfill":
          "@formatjs/intl-datetimeformat/should-polyfill.js",
        "@formatjs/intl-datetimeformat/polyfill-force":
          "@formatjs/intl-datetimeformat/polyfill-force.js",
        "@formatjs/intl-displaynames/should-polyfill":
          "@formatjs/intl-displaynames/should-polyfill.js",
        "@formatjs/intl-displaynames/polyfill-force":
          "@formatjs/intl-displaynames/polyfill-force.js",
        "@formatjs/intl-getcanonicallocales/should-polyfill":
          "@formatjs/intl-getcanonicallocales/should-polyfill.js",
        "@formatjs/intl-getcanonicallocales/polyfill-force":
          "@formatjs/intl-getcanonicallocales/polyfill-force.js",
        "@formatjs/intl-listformat/should-polyfill":
          "@formatjs/intl-listformat/should-polyfill.js",
        "@formatjs/intl-listformat/polyfill-force":
          "@formatjs/intl-listformat/polyfill-force.js",
        "@formatjs/intl-locale/should-polyfill":
          "@formatjs/intl-locale/should-polyfill.js",
        "@formatjs/intl-locale/polyfill-force":
          "@formatjs/intl-locale/polyfill-force.js",
        "@formatjs/intl-numberformat/should-polyfill":
          "@formatjs/intl-numberformat/should-polyfill.js",
        "@formatjs/intl-numberformat/polyfill-force":
          "@formatjs/intl-numberformat/polyfill-force.js",
        "@formatjs/intl-pluralrules/should-polyfill":
          "@formatjs/intl-pluralrules/should-polyfill.js",
        "@formatjs/intl-pluralrules/polyfill-force":
          "@formatjs/intl-pluralrules/polyfill-force.js",
        "@formatjs/intl-relativetimeformat/should-polyfill":
          "@formatjs/intl-relativetimeformat/should-polyfill.js",
        "@formatjs/intl-relativetimeformat/polyfill-force":
          "@formatjs/intl-relativetimeformat/polyfill-force.js",
      },
    },
    output: {
      module: latestBuild,
      filename: ({ chunk }) =>
        !isProdBuild || isStatsBuild || dontHash.has(chunk.name)
          ? "[name].js"
          : "[name].[contenthash].js",
      chunkFilename:
        isProdBuild && !isStatsBuild ? "[name].[contenthash].js" : "[name].js",
      assetModuleFilename:
        isProdBuild && !isStatsBuild ? "[id].[contenthash][ext]" : "[id][ext]",
      crossOriginLoading: "use-credentials",
      hashFunction: "xxhash64",
      path: outputPath,
      publicPath,
      // To silence warning in worker plugin
      globalObject: "self",
      // Since production source maps don't include sources, we need to point to them elsewhere
      // For dependencies, just provide the path (no source in browser)
      // Otherwise, point to the raw code on GitHub for browser to load
      ...Object.fromEntries(
        ["", "Fallback"].map((v) => [
          `devtool${v}ModuleFilenameTemplate`,
          !isTestBuild && isProdBuild
            ? (info) => {
                if (
                  !path.isAbsolute(info.absoluteResourcePath) ||
                  !existsSync(info.resourcePath) ||
                  info.resourcePath.startsWith("./node_modules")
                ) {
                  // Source URLs are unknown for dependencies, so we use a relative URL with a
                  // non - existent top directory.  This results in a clean source tree in browser
                  // dev tools, and they stay happy getting 404s with valid requests.
                  return `/unknown${path.resolve("/", info.resourcePath)}`;
                }
                return new URL(info.resourcePath, bundle.sourceMapURL()).href;
              }
            : undefined,
        ])
      ),
    },
    experiments: {
      outputModule: true,
    },
  };
};

const createAppConfig = ({
  isProdBuild,
  latestBuild,
  isStatsBuild,
  isTestBuild,
}) =>
  createRspackConfig(
    bundle.config.app({ isProdBuild, latestBuild, isStatsBuild, isTestBuild })
  );

const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) =>
  createRspackConfig(
    bundle.config.demo({ isProdBuild, latestBuild, isStatsBuild })
  );

const createCastConfig = ({ isProdBuild, latestBuild }) =>
  createRspackConfig(bundle.config.cast({ isProdBuild, latestBuild }));

const createGalleryConfig = ({ isProdBuild, latestBuild }) =>
  createRspackConfig(bundle.config.gallery({ isProdBuild, latestBuild }));

const createLandingPageConfig = ({ isProdBuild, latestBuild }) =>
  createRspackConfig(bundle.config.landingPage({ isProdBuild, latestBuild }));

module.exports = {
  createAppConfig,
  createDemoConfig,
  createCastConfig,
  createGalleryConfig,
  createRspackConfig,
  createLandingPageConfig,
};
