/* eslint-disable class-methods-use-this */
/* eslint-disable max-classes-per-file */
import fs from 'fs-extra';
import path from 'path';
import { Compiler } from 'webpack';
import ManifestPlugin from 'webpack-manifest-plugin';
import { camelCase } from 'change-case';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import VirtualModulePlugin from 'webpack-virtual-modules';
import { compile } from 'ejs';
import { knapsackEvents, EVENTS, KnapsackEventsData } from './events';
import * as log from '../cli/log';
import { KnapsackRendererBase } from './renderer-base';
import {
  KnapsackTemplateRendererBase,
  KnapsackConfig,
  KsTemplateRendererWrapHtmlParams,
} from '../schemas/knapsack-config';
import {
  KnapsackPatternTemplate,
  KnapsackTemplateDemo,
} from '../schemas/patterns';

// should root be `dataDir` or CWD?
const entryPath = path.join(process.cwd(), 'ks-entry.js');
const ksBootstrapEntryPath = path.join(process.cwd(), 'ks-boostrap.js');

function upperCamelCase(str: string): string {
  const cased = camelCase(str);
  return cased.charAt(0).toUpperCase() + cased.slice(1);
}

const renderEntryTemplate = compile(
  fs.readFileSync(
    path.join(__dirname, './templates/renderer-webpack-base-entry.ejs'),
    'utf-8',
  ),
  {
    filename: 'renderer-webpack-base-entry.ejs',
    async: false,
  },
);

interface KsEntryItem {
  id: string;
  path: string;
  name: string;
  alias?: string;
}

interface KsEntryTemplate extends KsEntryItem {
  demos?: KsEntryItem[];
}

type KsEntryData = {
  patterns: {
    id: string;
    templates: KsEntryTemplate[];
  }[];
  extras?: KsEntryItem[];
};

function getEntryString({
  entryData: { patterns, extras = [] },
  format,
}: {
  entryData: KsEntryData;
  format?: boolean;
}): string {
  let entryString = renderEntryTemplate({ patterns, extras });

  if (format) {
    entryString = KnapsackRendererBase.formatCode({
      code: entryString,
      language: 'ts',
    });
  }
  return entryString;
}

type KsWebpackEntriesManifest = {
  entrypoints: {
    [entryId: string]: string[];
  };
};

export class KnapsackRendererWebpackBase extends KnapsackRendererBase
  implements KnapsackTemplateRendererBase {
  webpack: typeof import('webpack');

  webpackConfig: import('webpack').Configuration;

  entryData: KsEntryData;

  publicPath: string;

  language: string;

  restartWebpackWatch: () => void;

  webpackCompiler: import('webpack').Compiler;

  entriesManifest: KsWebpackEntriesManifest;

  webpackWatcher: import('webpack').Compiler.Watching;

  patterns: import('@knapsack/app/src/server/patterns').Patterns;

  virtualModules: VirtualModulePlugin;

  extraScripts: string[];

  private webpackEntryPathsManifest: string;

  constructor({
    id,
    extension,
    language,
    webpackConfig,
    webpack,
    extraScripts = [],
  }: {
    id: string;
    extension: string;

    language: string;
    webpackConfig: import('webpack').Configuration;
    webpack: typeof import('webpack');
    extraScripts?: string[];
  }) {
    super({
      id,
      extension,
      language,
    });
    this.webpack = webpack;
    this.webpackConfig = webpackConfig;
    this.extraScripts = extraScripts;
  }

  createWebpackCompiler(entryData: KsEntryData) {
    const { plugins = [] } = this.webpackConfig;

    const { patterns, extras } = entryData;

    const entryString = getEntryString({
      entryData: { patterns, extras },
      format: true,
    });

    // for debug, upcomment:
    // fs.writeFileSync(path.join(process.cwd(), 'ks-entry--fyi.js'), entryString);

    const virtualWebpackEntries = {
      [entryPath]: entryString,
      [ksBootstrapEntryPath]: `
import knapsack from '${entryPath}';

//console.log('Multi Entry Knapsack!', { knapsack });
window.knapsack = knapsack;

// create and dispatch the event
const ksReadyEvent = new CustomEvent('KsRendererClientManifestReady', {
  detail: knapsack,
});

document.dispatchEvent(ksReadyEvent);
      `,
    };

    this.virtualModules = new VirtualModulePlugin(virtualWebpackEntries);

    const newWebpackConfig: import('webpack').Configuration = {
      optimization: {
        minimize: process.env.NODE_ENV === 'production',
        runtimeChunk: 'single',
        splitChunks: {
          name: true,
          chunks: 'all',
          maxInitialRequests: 8,
          maxAsyncRequests: 20,
          maxSize: 300000,
        },
      },
      ...this.webpackConfig,
      entry: {
        main: [...this.extraScripts, ...Object.keys(virtualWebpackEntries)],
      },
      mode:
        process.env.NODE_ENV === 'production' ? 'production' : 'development',
      externals: {
        react: 'React',
        'react-dom': 'ReactDOM',
      },
      output: {
        filename: '[name].bundle.[hash].js',
        path: this.outputDir,
        publicPath: this.publicPath,
        chunkFilename: '[name].chunk.[hash].js',
      },
      plugins: [
        ...plugins,
        this.virtualModules,
        new ManifestPlugin({
          writeToFileEmit: true,
          generate: (seed, files, entrypoints) => {
            // Tapping into this so we can get the actual entrypoints: if `entry.main` is the key, then a `string[]` of all the JS/CSS needed for it is desired. The original `manifest.json` made didn't work as it contained a single `string` & only described the output, not all the CSS/JS needed to make that entrypoint work. Originally, we had an entrypoint per React component to render, but now we have a single entrypoint that has a bunch of async functions to fetch any React Component needed.
            const data: KsWebpackEntriesManifest = { entrypoints: {} };
            Object.keys(entrypoints).forEach(id => {
              const assets = entrypoints[id];
              data.entrypoints[id] = assets.map(asset =>
                encodeURI(path.join(this.publicPath, asset)),
              );
            });

            fs.writeFileSync(
              this.webpackEntryPathsManifest,
              JSON.stringify(data),
            );

            // the original default "generate the manfiest" function
            return files.reduce(
              (manifest, { name, path: filePath }) => ({
                ...manifest,
                [name]: filePath,
              }),
              seed,
            );
          },
        }),
      ],
    };
    this.webpackCompiler = this.webpack(newWebpackConfig);
    log.verbose(
      'New Webpack Config and Compiler created',
      null,
      this.logPrefix,
    );
  }

  createWebpackEntryDataFromPatterns(
    patterns: import('@knapsack/app/src/server/patterns').Patterns,
  ): KsEntryData {
    const entryData: KsEntryData = { patterns: [], extras: [] };

    patterns.getPatterns().forEach(pattern => {
      const patternTemplates: KsEntryTemplate[] = [];
      pattern.templates
        .filter(t => t.templateLanguageId === this.id)
        .forEach(template => {
          const templateDemos: KsEntryItem[] = [];
          const absPath = patterns.getTemplateAbsolutePath({
            patternId: pattern.id,
            templateId: template.id,
          });

          const demos = Object.values(template?.demosById ?? {});
          if (demos) {
            demos
              .filter(KnapsackRendererWebpackBase.isTemplateDemo)
              .forEach(demo => {
                if (demo?.templateInfo?.path) {
                  const demoAbsPath = patterns.getTemplateDemoAbsolutePath({
                    patternId: pattern.id,
                    templateId: template.id,
                    demoId: demo.id,
                  });

                  const entryItem: KsEntryItem = {
                    id: demo.id,
                    path: demoAbsPath,
                    alias: demo.templateInfo.alias,
                    name: this.getReactName({
                      pattern,
                      template,
                      demo,
                    }),
                  };
                  templateDemos.push(entryItem);
                }
              });
          }

          const entryItem: KsEntryItem = {
            id: template.id,
            path: absPath,
            alias: template.alias,
            name: this.getReactName({ pattern, template }),
          };
          patternTemplates.push({ ...entryItem, demos: templateDemos });
        });

      entryData.patterns.push({
        id: pattern.id,
        templates: patternTemplates,
      });
    });

    return {
      patterns: entryData.patterns,
      extras: entryData.extras,
    };
  }

  getReactName({
    pattern,
    template,
    demo,
  }: {
    pattern: KnapsackPattern;
    template: KnapsackPatternTemplate;
    demo?: KnapsackTemplateDemo;
  }): string {
    const pId = pattern.id;
    const tId = template.id;
    if (demo) {
      if (!KnapsackRendererWebpackBase.isTemplateDemo(demo)) {
        log.inspect(demo, 'demo');
        throw new Error(`Can't run getReactName on non-template demos`);
      }
      const { alias } = demo.templateInfo;
      const isNamedImport = alias && alias !== 'default';
      return upperCamelCase(
        `${pId} ${tId} ${isNamedImport ? alias : ''} Demo ${demo.id}`,
      );
    }
    const { alias, templateLanguageId } = template;
    const isNamedImport = alias && alias !== 'default';
    const isOnlyLanguage =
      pattern.templates.filter(t => t.templateLanguageId === templateLanguageId)
        ?.length === 1;

    if (isNamedImport) {
      const isOnlyWithThisNamedImport =
        pattern.templates.filter(t => t.alias === alias)?.length === 1;
      return isOnlyWithThisNamedImport
        ? alias
        : upperCamelCase(`${alias} ${tId}`);
    }
    return upperCamelCase(isOnlyLanguage ? pId : `${pId} ${tId}`);
  }

  async init(opt: {
    config: KnapsackConfig;
    patterns: import('@knapsack/app/src/server/patterns').Patterns;
    cacheDir: string;
  }): Promise<void> {
    await super.init(opt);
    this.publicPath = `/${path.relative(this.cacheDir, this.outputDir)}/`;
    this.patterns = opt.patterns;
    this.webpackEntryPathsManifest = path.join(
      this.outputDir,
      'manifest--entries.json',
    );
  }

  setManifest() {
    return fs
      .readFile(this.webpackEntryPathsManifest, 'utf8')
      .then(manifestString => JSON.parse(manifestString))
      .then(manifest => {
        this.entriesManifest = manifest;
      })
      .catch(error => {
        log.error('setManifest()', error);
        throw new Error(
          `Error getting WebPack manifest--entries.json file. ${error.message}`,
        );
      });
  }

  setManifestSync() {
    try {
      const manifestString = fs.readFileSync(
        this.webpackEntryPathsManifest,
        'utf8',
      );
      const manifest = JSON.parse(manifestString);
      this.entriesManifest = manifest;
    } catch (error) {
      log.error('setManifest()', error);
      throw new Error(
        `Error getting WebPack manifest--entries.json file. ${error.message}`,
      );
    }
  }

  getWebPackEntryPath(id: string): string[] {
    if (!this.entriesManifest) this.setManifestSync();
    if (!this.entriesManifest) {
      throw new Error(
        `Webpack has not been built yet, cannot access id "${id}"`,
      );
    }
    const result = this.entriesManifest?.entrypoints[id];
    if (!result) {
      const msg = `Could not find webpack entry "${id}".`;
      console.error(
        `Possible ids: "${Object.keys(
          this.entriesManifest?.entrypoints ?? {},
        )}"`,
      );
      throw new Error(msg);
    }
    return result;
  }

  build(): Promise<void> {
    return new Promise((resolve, reject) => {
      this.entryData = this.createWebpackEntryDataFromPatterns(this.patterns);
      this.createWebpackCompiler(this.entryData);

      this.webpackCompiler.run(async (err, stats) => {
        if (err || stats.hasErrors()) {
          log.error(stats.toString(), err, this.logPrefix);
          reject();
          return;
        }
        await this.setManifest();
        resolve();
      });
    });
  }

  webpackWatch(): import('webpack').Compiler.Watching {
    log.verbose('Starting Webpack watch...', null, this.logPrefix);

    const watchOptions: import('webpack').Compiler.WatchOptions = {};

    return this.webpackCompiler.watch(
      watchOptions,
      async (err: Error, stats: import('webpack').Stats) => {
        if (err || stats.hasErrors()) {
          log.error(stats.toString(), err, this.logPrefix);
          return;
        }
        await this.setManifest();
        log.info('Webpack recompiled', null, this.logPrefix);
        super.onChange({
          path: '',
        }); // @todo get path of file changed from `stats` and pass it in here
      },
    );
  }

  async watch({ templatePaths }: { templatePaths: string[] }) {
    await super.watch({ templatePaths });
    this.entryData = this.createWebpackEntryDataFromPatterns(this.patterns);
    this.createWebpackCompiler(this.entryData);

    knapsackEvents.on(
      EVENTS.PATTERNS_DATA_READY,
      (allPatterns: KnapsackEventsData['PATTERNS_DATA_READY']) => {
        const entryData = this.createWebpackEntryDataFromPatterns(
          this.patterns,
        );
        if (JSON.stringify(this.entryData) !== JSON.stringify(entryData)) {
          // @todo enure the new data from `entryData` does trigger the proper re-render w/o restarting WebPack. This event is usually fired when a new pattern template or template demo is added
          this.entryData = entryData;
          const entryString = getEntryString({
            entryData: this.entryData,
            format: true,
          });
          this.virtualModules.writeModule(entryPath, entryString);

          // Old "restart WebPack watcher" code below:
          // this.createWebpackCompiler(entryData);
          // if (this.restartWebpackWatch) {
          //   this.restartWebpackWatch();
          // }
        }
      },
    );
    this.restartWebpackWatch = () => {
      log.verbose('Restarting Webpack Watch', null, this.logPrefix);
      this.webpackWatcher.close(() => {
        log.verbose('Restarted Webpack Watch', null, this.logPrefix);
        this.webpackWatcher = this.webpackWatch();
      });
    };
    this.webpackWatcher = this.webpackWatch();
  }

  // eslint-disable-next-line class-methods-use-this
  onChange() {
    // overwriting so we can call event after webpack compiles
  }
}
