const { deepmerge, createDebug, winPath, resolve } = require('@umijs/utils');
const { ESBuildPlugin, ESBuildMinifyPlugin } = require('esbuild-loader');
const fs = require('fs');
const path = require('path');
const webpack = require('webpack');
const Config = require('webpack-chain');
const lodash = require('lodash');
const { getTargetsAndBrowsersList, getBabelPresetOpts, getBabelOpts } = require('@umijs/bundler-utils');
const { join } = require('lodash');
const terserOptions = require('./terserOptions');
const { css, createCSSRule } = require('./css');
const pkg = require('../../package.json');
const ASSET_PATH = process.env.ASSET_PATH || '/';
const { ThemeColorReplacer, themePluginOption } = require('../../config/plugin.config');
let defineConfig = require('../../config/config');
const resolveDefine = require('./resolveDefine');
const { getPkgPath, shouldTransform } = require('./pkgMatch');
const { TYPE_ALL_EXCLUDE, isMatch, excludeToPkgs, es5ImcompatibleVersionsToPkg } = require('./nodeModulesTransform');

function getUserLibDir({ library }) {
  if (
    (pkg.dependencies && pkg.dependencies[library]) ||
    (pkg.devDependencies && pkg.devDependencies[library]) ||
    // egg project using `clientDependencies` in ali tnpm
    (pkg.clientDependencies && pkg.clientDependencies[library])
  ) {
    return winPath(
      path.dirname(
        // 通过 resolve 往上找,可支持 lerna 仓库
        // lerna 仓库如果用 yarn workspace 的依赖不一定在 node_modules,可能被提到根目录,并且没有 link
        resolve.sync(`${library}/package.json`, {
          basedir: process.cwd(),
        }),
      ),
    );
  }
  return null;
}
const defaultConfig = {
  autoprefixer: {
    flexbox: 'no-2009',
  },
  cssnano: { mergeRules: false, minifyFontValues: { removeQuotes: false } },
  nodeModulesTransform: {
    type: 'all',
    exclude: [],
  },
  hash: true,
  targets: {
    node: true,
    chrome: 49,
    firefox: 64,
    safari: 10,
    edge: 13,
    ios: 10,
    ie: 11,
  },
};
module.exports = options => {
  const chainConfig = new Config();
  chainConfig.mode(options.mode);
  defineConfig = Object.assign(defineConfig, defaultConfig);
  const { env } = process;
  const isDev = process.env.NODE_ENV === 'development';
  const isProd = process.env.NODE_ENV === 'production';
  const disableCompress = process.env.COMPRESS === 'none';
  const isWebpack5 = webpack.version.startsWith('5');
  const cwd = process.cwd();
  const devtool = options.devtool || false;

  chainConfig.devtool(isDev ? (devtool === false ? false : devtool || 'cheap-module-source-map') : devtool);
  const { mfsu } = defineConfig;
  const useHash = defineConfig.hash && isProd;

  const absOutputPath = process.env.npm_config_releasepath
    ? path.resolve(process.env.npm_config_releasepath, pkg.name.toLocaleLowerCase())
    : path.resolve(process.cwd(), pkg.name.toLocaleLowerCase());

  chainConfig.output
    .path(absOutputPath)
    .filename(useHash ? `[name].[contenthash:8].js` : `[name].js`)
    .chunkFilename(useHash ? `[name].[contenthash:8].async.js` : `[name].js`)
    .publicPath(`/${pkg.name.toLocaleLowerCase()}/`)
    // .futureEmitAssets(true)
    .crossOriginLoading('anonymous')
    .pathinfo(isDev || disableCompress);

  if (!isWebpack5) {
    chainConfig.output.futureEmitAssets(true);
  }
  if (isWebpack5) {
    chainConfig.cache({
      type: 'filesystem',
      buildDependencies: {
        config: [__filename, path.join(process.cwd(), 'package.json')],
      },
      cacheDirectory: path.resolve(process.cwd(), 'node_modules/.cache'),
      compression: 'gzip',
      version: `${process.env.GIT_REV}`,
    });
  }
  chainConfig.resolve.modules
    .add('node_modules')
    .add('src')
    .end()
    .extensions.merge([
      '.js',
      '.jsx',
      '.react.js',
      '.web.js',
      '.wasm',
      '.mjs',
      '.js',
      '.web.jsx',
      '.jsx',
      '.web.ts',
      '.ts',
      '.web.tsx',
      '.tsx',
      '.json',
    ])
    .end()
    .mainFields.merge(['browser', 'jsnext:main', 'main']);

  const libraries = [
    {
      name: 'react',
      path: path.dirname(require.resolve(`react/package.json`)),
    },
    {
      name: 'react-dom',
      path: path.dirname(require.resolve(`react-dom/package.json`)),
    },
  ];

  libraries.forEach(library => {
    chainConfig.resolve.alias.set(library.name, getUserLibDir({ library: library.name }) || library.path);
  });

  chainConfig.resolve.alias.set('@', path.join(__dirname, '../../', 'src'));

  if (defineConfig.alias) {
    Object.keys(defineConfig.alias).forEach(key => {
      chainConfig.resolve.alias.set(key, defineConfig.alias[key]);
    });
  }

  chainConfig.target('web');

  const { targets, browserslist } = getTargetsAndBrowsersList({
    config: defineConfig,
    type: 'csr',
  });

  const presetOpts = getBabelPresetOpts({
    config: defineConfig,
    env: process.env.NODE_ENV,
    targets,
  });

  // eslint-disable-next-line global-require
  const preset = require('./babel-preset');
  const getPreset = preset({
    ...presetOpts,
    env: {
      useBuiltIns: 'entry',
      corejs: 3,
      modules: false,
    },
    react: {
      development: isDev,
    },
    reactRemovePropTypes: isProd,
    reactRequire: true,
    lockCoreJS3: {},
    // import: (presetOpts.import || []).concat([{ libraryName: 'antd', libraryDirectory: 'es', style: true }]),
  });

  const babelOpts = {
    sourceType: 'unambiguous',
    babelrc: false,
    presets: [...getPreset.presets, ...(defineConfig.extraBabelPresets || [])],
    plugins: [...getPreset.plugins, ...(defineConfig.extraBabelPlugins || [])].filter(Boolean),
    env: getPreset.env,
    compact: false,
  };

  chainConfig.module
    .rule('js')
    .test(/\.(js|mjs|jsx|ts|tsx)$/)
    .include.add([cwd, ...(process.env.APP_ROOT ? [process.cwd()] : [])])
    .end()
    .exclude.add(/node_modules/)
    .add(/\.mfsu/)
    .end()
    .use('babel-loader')
    .loader(require.resolve('@umijs/deps/compiled/babel-loader'))
    .options(babelOpts);

  if (defineConfig.extraBabelIncludes) {
    defineConfig.extraBabelIncludes.forEach((include, index) => {
      const rule = `extraBabelInclude_${index}`;
      chainConfig.module
        .rule(rule)
        .test(/\.(js|mjs|jsx)$/)
        .include.add(a => {
          if (path.isAbsolute(include)) {
            return path.isAbsolute(include);
          }
          if (!a.includes('node_modules')) return false;
          const pkgPath = getPkgPath(a);
          return shouldTransform(pkgPath, include);
        })
        .end()
        .use('babel-loader')
        .loader(require.resolve('babel-loader'))
        .options(babelOpts);
    });
  }

  const staticDir = mfsu ? 'mf-static' : 'static';

  chainConfig.module
    .rule('images')
    .test(/\.(png|jpe?g|gif|webp|ico)(\?.*)?$/)
    .use('url-loader')
    .loader(require.resolve('url-loader'))
    .options({
      limit: defineConfig.inlineLimit || 10000,
      name: `${staticDir}/[name].[hash:8].[ext]`,
      // require 图片的时候不用加 .default
      esModule: false,
      fallback: {
        loader: require.resolve('file-loader'),
        options: {
          name: `${staticDir}/[name].[hash:8].[ext]`,
          esModule: false,
        },
      },
    });

  chainConfig.module
    .rule('avif')
    .test(/\.(avif)(\?.*)?$/)
    .use('file-loader')
    .loader(require.resolve('file-loader'))
    .options({
      name: `${staticDir}/[name].[hash:8].[ext]`,
      esModule: false,
    });

  chainConfig.module
    .rule('svg')
    .test(/\.(svg)(\?.*)?$/)
    .use('file-loader')
    .loader(require.resolve('@umijs/deps/compiled/file-loader'))
    .options({
      name: `${staticDir}/[name].[hash:8].[ext]`,
      esModule: false,
    })
    .end();

  chainConfig.module
    .rule('fonts')
    .test(/\.(eot|woff|woff2|ttf)(\?.*)?$/)
    .use('file-loader')
    .loader(require.resolve('file-loader'))
    .options({
      name: `${staticDir}/[name].[hash:8].[ext]`,
      esModule: false,
    });

  chainConfig.module
    .rule('mdeia')
    .test(/\.(mp4|webm)$/)
    .use('url-loader')
    .loader(require.resolve('url-loader'))
    .options({
      limit: 10000,
      outputPath: 'static',
    });

  chainConfig.module
    .rule('plaintext')
    .test(/\.(txt|text|md)$/)
    .use('raw-loader')
    .loader(require.resolve('raw-loader'));

  if (defineConfig.workerLoader) {
    chainConfig.module
      .rule('worker')
      .test(/.*worker.(ts|js)/)
      .use('worker-loader')
      .loader(require.resolve('@umijs/deps/compiled/worker-loader'))
      .options(defineConfig.workerLoader);
  }

  css({
    type: 'csr',
    config: defineConfig,
    webpackConfig: chainConfig,
    isDev,
    disableCompress,
    browserslist,
    miniCSSExtractPluginPath: '',
    miniCSSExtractPluginLoaderPath: '',
  });

  if (defineConfig.externals) {
    chainConfig.externals(defineConfig.externals);
  }
  if (!isWebpack5) {
    chainConfig.node.merge({
      setImmediate: false,
      module: 'empty',
      dns: 'mock',
      http2: 'empty',
      process: 'mock',
      dgram: 'empty',
      fs: 'empty',
      net: 'empty',
      tls: 'empty',
      child_process: 'empty',
      ...options.node,
    });
  }

  if (defineConfig.ignoreMomentLocale) {
    chainConfig.plugin('ignore-moment-locale').use(webpack.IgnorePlugin, [
      {
        resourceRegExp: /^\.\/locale$/,
        contextRegExp: /moment$/,
      },
    ]);
  }

  chainConfig.plugin('define').use(webpack.DefinePlugin, [
    resolveDefine({
      define: Object.assign(defineConfig.define || {}, {
        'process.env.ASSET_PATH': JSON.stringify(ASSET_PATH),
        'process.env.MOCK': JSON.stringify(true),
        paths: {
          cwd: process.cwd(),
          absNodeModulesPath: path.join(process.cwd(), 'node_modules'),
          absSrcPath: path.join(process.cwd(), 'src'),
          absPagesPath: path.join(process.cwd(), 'src/pages'),
        },
      }),
    }),
  ]);

  const baseHtmlOptions = {
    inject: true,
    template: 'src/index.ejs',
    favicon: defineConfig.favicon ? defineConfig.favicon : false,
    title: defineConfig.title && defineConfig.title !== false ? defineConfig.title : '',
    chunks: defaultConfig.chunks ? defaultConfig.chunks : 'all',
  };
  const htmlPluginOptions = isDev
    ? {
      ...babelOpts,
    }
    : {
      ...baseHtmlOptions,
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeRedundantAttributes: true,
        useShortDoctype: true,
        removeEmptyAttributes: true,
        removeStyleLinkTypeAttributes: true,
        keepClosingSlash: true,
        minifyJS: true,
        minifyCSS: true,
        minifyURLs: true,
      },
    };

  chainConfig.plugin('htmlPlugins').use(require.resolve('html-webpack-plugin'), [htmlPluginOptions]);

  chainConfig.plugin('updateHtmlParams').use(require.resolve('./plugins/HtmlGenerator'), [
    {
      config: defineConfig,
    },
  ]);

  chainConfig.plugin('replaceTheme').use(ThemeColorReplacer, [themePluginOption]);

  const copyPatterns = [
    fs.existsSync(path.join(process.cwd(), 'public')) && {
      from: path.join(cwd, 'public'),
      to: path.resolve(process.env.npm_config_releasepath || process.cwd(), pkg.name.toLocaleLowerCase()),
    },
    ...(defineConfig.copy
      ? defineConfig.copy.map(item => {
        if (typeof item === 'string') {
          return {
            from: path.join(cwd, item),
            to: path.resolve(process.env.npm_config_releasepath || process.cwd(), pkg.name.toLocaleLowerCase()),
          };
        }
        return {
          from: path.join(cwd, item.from),
          to: path.resolve(
            process.env.npm_config_releasepath || process.cwd(),
            `${pkg.name.toLocaleLowerCase()}/${item.to}`,
          ),
        };
      })
      : []),
  ].filter(Boolean);

  if (copyPatterns.length) {
    chainConfig.plugin('copy').use(require.resolve('copy-webpack-plugin'), [copyPatterns]);
  }

  if (isDev && defineConfig.fastRefresh) {
    const debug = createDebug('civ:preset-build-in:fastRefresh');
    chainConfig
      .plugin('fastRefresh')
      .after('hmr')
      .use(require('@pmmmwh/react-refresh-webpack-plugin'), [{ overlay: false }]);
    debug('FastRefresh webpack loaded');
  }

  if (chainConfig.extraBabelIncludes) {
    chainConfig.extraBabelIncludes.forEach((include, index) => {
      const rule = `extraBabelInclude_${index}`;
      // prettier-ignore
      chainConfig.module
        .rule(rule)
        .test(/\.(js|mjs|jsx)$/)
        .include
        .add((a) => {
          // 支持绝对路径匹配
          if (path.isAbsolute(include)) {
            return path.isAbsolute(include);
          }

          // 支持 node_modules 下的 npm 包
          if (!a.includes('node_modules')) return false;
          const pkgPath = getPkgPath(a);
          return shouldTransform(pkgPath, include);
        })
        .end()
        .use('babel-loader')
        .loader(require.resolve('babel-loader'))
        .options(babelOpts);
    });
  }

  chainConfig.module
    .rule('ts-in-node_modules')
    .test(/\.(jsx|ts|tsx)$/)
    .include.add(/node_modules/)
    .end()
    .use('babel-loader')
    .loader(require.resolve('babel-loader'))
    .options(babelOpts);

  const rule = chainConfig.module.rule('js-in-node_modules').test(/\.(js|mjs)$/);
  const nodeModulesTransform = defineConfig.nodeModulesTransform || {
    type: 'all',
    exclude: [],
  };

  if (nodeModulesTransform.type === 'all') {
    const exclude = lodash.uniq([...TYPE_ALL_EXCLUDE, ...(nodeModulesTransform.exclude || [])]);
    const pkgs = excludeToPkgs({ exclude });
    rule.include
      .add(/node_modules/)
      .end()
      .exclude.add(path => isMatch({ path, pkgs }))
      .end();
  } else {
    const pkgs = {
      ...es5ImcompatibleVersionsToPkg(),
      ...excludeToPkgs({ exclude: nodeModulesTransform.exclude || [] }),
    };
    rule.include
      .add(path =>
        isMatch({
          path,
          pkgs,
        }),
      )
      .end();
  }

  // rule
  //   .use('babel-loader')
  //   .loader(require.resolve('@umijs/deps/compiled/babel-loader'))
  //   .options(babelOpts);

  if ((isProd && defineConfig.analyze) || process.env.ANALYZE) {
    const mergeAnalyze = Object.assign(
      {
        analyzerMode: process.env.ANALYZE_MODE || 'server',
        analyzerPort: process.env.ANALYZE_PORT || 8888,
        openAnalyzer: process.env.ANALYZE_OPEN !== 'none',
        generateStatsFile: !!process.env.ANALYZE_DUMP,
        statsFilename: process.env.ANALYZE_DUMP || 'stats.json',
        logLevel: process.env.ANALYZE_LOG_LEVEL || 'info',
        defaultSizes: 'parsed', // stat  // gzip
      },
      defineConfig.analyze,
    );
    chainConfig
      .plugin('bundle-analyzer')
      .use(require('umi-webpack-bundle-analyzer').BundleAnalyzerPlugin, [mergeAnalyze || {}]);
  }

  if (process.env.PROGRESS !== 'none') {
    chainConfig.plugin('progress').use(require.resolve('webpackbar'), [
      mfsu
        ? {
            name: 'MFSU',
            color: '#facc00',
          }
        : defineConfig.ssr
        ? {
            name: type === 'ssr' ? 'Server' : 'Client',
          }
        : {},
    ]);
  }

  // if (process.env.FRIENDLY_ERROR !== 'none') {
  //   chainConfig
  //     .plugin('friendly-error')
  //     .use(require.resolve('friendly-errors-webpack-plugin'), [
  //       {
  //         clearConsole: false,
  //       },
  //     ]);
  // }

  if (process.env.WEBPACK_PROFILE) {
    chainConfig.profile(true);
    const statsInclude = ['verbose', 'normal', 'minimal'];
    chainConfig.stats(statsInclude.includes(process.env.WEBPACK_PROFILE) ? process.env.WEBPACK_PROFILE : 'verbose');
    const StatsPlugin = require('stats-webpack-plugin');
    chainConfig.plugin('stats-webpack-plugin').use(
      new StatsPlugin('stats.json', {
        chunkModules: true,
      }),
    );
  }

  chainConfig.when(
    isDev,
    chainConfig => {
      if (isDev) {
        chainConfig.plugin('hmr').use(webpack.HotModuleReplacementPlugin);
      }
    },
    chainConfig => {
      chainConfig.optimization.noEmitOnErrors(true);
      chainConfig.performance.hints(false);

      if (!isWebpack5) {
        chainConfig.plugin('hash-module-ids').use(webpack.HashedModuleIdsPlugin, []);
      }

      if (disableCompress) {
        chainConfig.optimization.minimize(false);
      } else if (!options.__disableTerserForTest) {
        chainConfig.optimization.minimizer('terser').use(require.resolve('terser-webpack-plugin'), [
          {
            terserOptions: deepmerge(terserOptions, defineConfig.terserOptions || {}),
            sourceMap: defineConfig.devtool !== false,
            cache: process.env.TERSER_CACHE !== 'none',
            parallel: true,
            extractComments: false,
          },
        ]);
      }
    },
  );

  function createCSSRuleFn(opts) {
    createCSSRule({
      webpackConfig: chainConfig,
      config: defineConfig,
      isDev,
      type: 'csr',
      browserslist,
      miniCSSExtractPluginLoaderPath: '',
      ...opts,
    });
  }

  if (defineConfig.esbuild) {
    const { target = 'es2015' } = defineConfig.esbuild || {};
    const optsMap = {
      csr: {
        minify: true,
        target,
      },
    };
    const opt = optsMap.csr;
    chainConfig.optimization.minimize = true;
    chainConfig.optimization.minimizer = [new ESBuildMinifyPlugin(opt)];
    chainConfig.plugin('es-build').use(new ESBuildPlugin());
  }

  if (defineConfig.chainWebpack) {
    defineConfig.chainWebpack(chainConfig, {
      type: 'csr',
      // mfsu,
      env: process.env,
      webpack,
      createCSSRule: createCSSRuleFn,
    });
  }

  // if (defineConfig.chunks) {
  //   chainConfig
  //     .plugin('htmlPlugins')
  //     .use(require('html-webpack-plugin'))
  //     .tap(options => {
  //       options.chunks = defineConfig.chunks;
  //       return options;
  //     });
  // }

  let ret = chainConfig.toConfig();

  // speed-measure-webpack-plugin
  if (process.env.SPEED_MEASURE) {
    const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
    const smpOption =
      process.env.SPEED_MEASURE === 'CONSOLE'
        ? { outputFormat: 'human', outputTarget: console.log }
        : {
          outputFormat: 'json',
            outputTarget: join(process.cwd(), 'speed-measure.json'),
        };
    const smp = new SpeedMeasurePlugin(smpOption);
    ret = smp.wrap(ret);
  }

  const { entry } = options;
  if (defineConfig.runtimePublicPath) {
    entry.push(require.resolve('./runtimePublicPathEntry'));
  }

  const nodeLibs = require('node-libs-browser');
  // const ProvidePlugin = webpack.ProvidePlugin;
  if (isWebpack5) {
    ret.plugins.push(
      new webpack.ProvidePlugin({
        process: nodeLibs.process,
      }),
    );
    ret.resolve.fallback = {
      ...ret.resolve.fallback,
      ...Object.keys(nodeLibs).reduce((memo, key) => {
        if (nodeLibs[key]) {
          memo[key] = nodeLibs[key];
        } else {
          memo[key] = false;
        }
        return memo;
      }, {}),
      http: false,
      https: false,
    };
  }

  ret = {
    ...ret,
    // mode: options.mode,
    entry,
    plugins: options.plugins.concat([...ret.plugins]),
    optimization: {
      ...options.optimization,
      ...ret.optimization,
    },
    performance: options.performance || {},
  };
  return ret;
};