import * as fs from "node:fs/promises";
import * as path from "node:path";
import { createNodeLogger, createNodeSys } from "@stencil/core/sys/node";
import { createCompiler, loadConfig } from "@stencil/core/compiler";
import { PluginName, ResolvedEmbeddableConfig } from "./defineConfig";
import {
  findFiles,
  getComponentLibraryConfig,
} from "@embeddable.com/sdk-utils";

import * as sorcery from "sorcery";

const STYLE_IMPORTS_TOKEN = "{{STYLES_IMPORT}}";
const RENDER_IMPORT_TOKEN = "{{RENDER_IMPORT}}";

// stencil doesn't support dynamic component tag name, so we need to replace it manually
const COMPONENT_TAG_TOKEN = "replace-this-with-component-name";

export default async (
  ctx: ResolvedEmbeddableConfig,
  pluginName: PluginName,
) => {
  await injectCSS(ctx, pluginName);

  await injectBundleRender(ctx, pluginName);

  await runStencil(ctx);

  await generateSourceMap(ctx, pluginName);
};

async function injectCSS(
  ctx: ResolvedEmbeddableConfig,
  pluginName: PluginName,
) {
  const CUSTOMER_BUILD = path.resolve(
    ctx.client.buildDir,
    ctx[pluginName].outputOptions.buildName,
  );
  const allFiles = await fs.readdir(CUSTOMER_BUILD);

  const imports = allFiles
    .filter((fileName) => fileName.endsWith(".css"))
    .map(
      (fileName) =>
        `@import '../${ctx[pluginName].outputOptions.buildName}/${fileName}';`,
    );

  const componentLibraries = ctx.client.componentLibraries;
  for (const componentLibrary of componentLibraries) {
    const { libraryName } = getComponentLibraryConfig(componentLibrary);
    const allLibFiles = await fs.readdir(
      path.resolve(ctx.client.rootDir, "node_modules", libraryName, "dist"),
    );
    allLibFiles
      .filter((fileName) => fileName.endsWith(".css"))
      .forEach((fileName) =>
        imports.push(`@import '~${libraryName}/dist/${fileName}';`),
      );
  }

  const cssFilesImportsStr = imports.join("\n");

  const content = await fs.readFile(
    path.resolve(ctx.core.templatesDir, "style.css.template"),
    "utf8",
  );

  await fs.writeFile(
    path.resolve(ctx.client.componentDir, "style.css"),
    content.replace(STYLE_IMPORTS_TOKEN, cssFilesImportsStr),
  );
}

async function injectBundleRender(
  ctx: ResolvedEmbeddableConfig,
  pluginName: PluginName,
) {
  const importStr = `import render from '../${ctx[pluginName].outputOptions.buildName}/${ctx[pluginName].outputOptions.fileName}';`;

  let content = await fs.readFile(
    path.resolve(ctx.core.templatesDir, "component.tsx.template"),
    "utf8",
  );

  if (!!ctx.dev?.watch) {
    content = content.replace(COMPONENT_TAG_TOKEN, "embeddable-component");
  }

  await fs.writeFile(
    path.resolve(ctx.client.componentDir, "component.tsx"),
    content.replace(RENDER_IMPORT_TOKEN, importStr),
  );
}

async function addComponentTagName(filePath: string, bundleHash: string) {
  // find entry file with a name *.entry.js
  const entryFiles = await findFiles(path.dirname(filePath), /.*\.entry\.js/);

  if (!entryFiles.length) {
    return;
  }

  const entryFileName = entryFiles[0];
  const [entryFileContent, fileContent] = await Promise.all([
    fs.readFile(entryFileName[1], "utf8"),
    fs.readFile(filePath, "utf8"),
  ]);

  const newFileContent = fileContent.replace(
    COMPONENT_TAG_TOKEN,
    `embeddable-component-${bundleHash}`,
  );

  const newEntryFileContent = entryFileContent.replace(
    COMPONENT_TAG_TOKEN.replaceAll("-", "_"),
    `embeddable_component_${bundleHash}`,
  );

  await Promise.all([
    fs.writeFile(filePath, newFileContent),
    fs.writeFile(entryFileName[1], newEntryFileContent),
  ]);
}

async function runStencil(ctx: ResolvedEmbeddableConfig): Promise<void> {
  const logger = ctx.dev?.logger || createNodeLogger();
  const sys = ctx.dev?.sys || createNodeSys({ process });
  const devMode = !!ctx.dev?.watch;

  const isWindows = process.platform === "win32";

  const validated = await loadConfig({
    initTsConfig: true,
    logger,
    sys,
    config: {
      devMode,
      maxConcurrentWorkers: isWindows ? 0 : 8, // workers break on windows
      rootDir: ctx.client.buildDir,
      configPath: path.resolve(ctx.client.buildDir, "stencil.config.ts"),
      tsconfig: path.resolve(ctx.client.buildDir, "tsconfig.json"),
      namespace: "embeddable-wrapper",
      srcDir: path.resolve(ctx.client.buildDir, "component"),
      sourceMap: true, // always generate source maps in both dev and prod
      minifyJs: !devMode,
      minifyCss: !devMode,
      outputTargets: [
        {
          type: "dist",
        },
      ],
      watchDirs: [
        path.resolve(
          ctx.client.buildDir,
          ctx["sdk-react"].outputOptions.buildName,
        ),
      ],
    },
  });

  const compiler = await createCompiler(validated.config);
  const buildResults = await compiler.build();

  if (!devMode) {
    if (buildResults.hasError) {
      console.error("Stencil build error:", buildResults.diagnostics);
      throw new Error("Stencil build error");
    } else {
      await handleStencilBuildOutput(ctx);
    }
    await compiler.destroy();
  }

  process.chdir(ctx.client.rootDir);
}

async function handleStencilBuildOutput(ctx: ResolvedEmbeddableConfig) {
  const entryFilePath = path.resolve(
    ctx.client.stencilBuild,
    "embeddable-wrapper.esm.js",
  );

  let fileName = "embeddable-wrapper.esm.js";

  if (!ctx.dev?.watch && ctx.client.bundleHash) {
    fileName = `embeddable-wrapper.esm-${ctx.client.bundleHash}.js`;
    await addComponentTagName(entryFilePath, ctx.client.bundleHash);
  }

  await fs.rename(
    entryFilePath,
    path.resolve(ctx.client.stencilBuild, fileName),
  );
}

async function generateSourceMap(
  ctx: ResolvedEmbeddableConfig,
  pluginName: PluginName,
) {
  const componentBuildDir = path.resolve(
    ctx.client.buildDir,
    ctx[pluginName].outputOptions.buildName,
  );
  const stencilBuild = path.resolve(ctx.client.stencilBuild);

  const tmpComponentDir = path.resolve(
    stencilBuild,
    ctx[pluginName].outputOptions.buildName,
  );
  await fs.cp(componentBuildDir, tmpComponentDir, { recursive: true });

  const stencilFiles = await fs.readdir(stencilBuild);
  const jsFiles = stencilFiles.filter((file) =>
    file.toLowerCase().endsWith(".js"),
  );

  await Promise.all(
    jsFiles.map(async (jsFile) => {
      try {
        const chain = await sorcery.load(path.resolve(stencilBuild, jsFile));
        // overwrite the existing file
        await chain.write();
      } catch (e) {
        // do nothing if a map file can not be generated
      }
    }),
  );

  await fs.rm(tmpComponentDir, { recursive: true });
}
