import * as fsSync from "node:fs";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import * as vite from "vite";
import { existsSync } from "node:fs";
import ora from "ora";
import {
  EXTERNAL_LIBRARY_GLOBAL_HOOKS_META_NAME,
  getComponentLibraryConfig,
  getContentHash,
  getGlobalHooksMeta,
} from "@embeddable.com/sdk-utils";

import { ResolvedEmbeddableConfig } from "./defineConfig";
import { RollupWatcher, RollupWatcherEvent } from "rollup";

const TEMP_JS_HOOK_FILE = "embeddableThemeHook.js";
const LIFECYCLE_OUTPUT_NAME = "embeddable-lifecycle";
const THEME_PROVIDER_OUTPUT_NAME = "embeddable-theme";

export default async (ctx: ResolvedEmbeddableConfig) => {
  const watch = ctx.dev?.watch;
  const progress = watch ? undefined : ora("Building global hooks...").start();

  try {
    const { fileName: themeProvider, watcher: themeWatcher } =
      await buildThemeHook(ctx);
    const { lifecycleHooks, watcher: lifecycleWatcher } =
      await buildLifecycleHooks(ctx);

    await saveGlobalHooksMeta(ctx, themeProvider, lifecycleHooks);

    progress?.succeed("Global hooks build completed");

    return { themeWatcher, lifecycleWatcher };
  } catch (error) {
    progress?.fail("Global hooks build failed");
    throw error;
  }
};

/**
 * Build theme hooks for a given component library.
 */
async function buildThemeHook(ctx: ResolvedEmbeddableConfig) {
  const componentLibraries = ctx.client.componentLibraries;
  const repoThemeHookExists = existsSync(ctx.client.customizationFile);
  const imports = [];
  const functionNames = [];
  for (let i = 0; i < componentLibraries.length; i++) {
    const libraryConfig = componentLibraries[i];
    const { libraryName } = getComponentLibraryConfig(libraryConfig);
    const libMeta = await getGlobalHooksMeta(ctx, libraryName);

    const themeProvider = libMeta.themeProvider;

    if (!themeProvider) continue;

    // Prepare imports: library theme + repo theme (if exists)
    const functionName = `libraryThemeProvider${i}`;
    const libraryThemeImport = `import ${functionName} from '${libraryName}/dist/${themeProvider}'`;
    functionNames.push(functionName);
    imports.push(libraryThemeImport);
  }

  if (!imports.length && !repoThemeHookExists) {
    return { fileName: undefined, watcher: undefined };
  }

  const repoThemeImport = repoThemeHookExists
    ? `import localThemeProvider from '${ctx.client.customizationFile}';`
    : "const localThemeProvider = () => {};";

  // Generate a temporary file that imports both library and repo theme
  await generateTemporaryHookFile(ctx, imports, functionNames, repoThemeImport);

  // Build the temporary file with Vite
  const buildResults = await buildWithVite(
    ctx,
    getTempHookFilePath(ctx),
    THEME_PROVIDER_OUTPUT_NAME,
    ctx.dev?.watch,
    !ctx.dev?.watch,
  );
  // Cleanup temporary file
  if (!ctx.dev?.watch) {
    await cleanupTemporaryHookFile(ctx);
  }

  return buildResults;
}

/**
 * Build theme hooks for a given component library.
 */
async function buildLifecycleHooks(ctx: ResolvedEmbeddableConfig) {
  const componentLibraries = ctx.client.componentLibraries;
  const builtLifecycleHooks: string[] = [];
  const repoLifecycleExist = existsSync(ctx.client.lifecycleHooksFile);

  let lifecycleWatcher: RollupWatcher | undefined = undefined;

  // If lifecycle exists, build it right away to get the hashed output
  if (repoLifecycleExist) {
    const { fileName: repoLifecycleFileName, watcher } = await buildWithVite(
      ctx,
      ctx.client.lifecycleHooksFile,
      LIFECYCLE_OUTPUT_NAME,
      ctx.dev?.watch,
      false,
    );
    if (ctx.dev?.watch) {
      lifecycleWatcher = watcher;
    }
    builtLifecycleHooks.push(repoLifecycleFileName);
  }
  for (const libraryConfig of componentLibraries) {
    const { libraryName } = getComponentLibraryConfig(libraryConfig);
    const libMeta = await getGlobalHooksMeta(ctx, libraryName);

    const lifecycleHooks = libMeta.lifecycleHooks;

    for (const lifecycleHook of lifecycleHooks) {
      const libLifecycleHook = path.resolve(
        ctx.client.rootDir,
        "node_modules",
        libraryName,
        "dist",
        lifecycleHook,
      );
      const { fileName: lifecycleHookFileName } = await buildWithVite(
        ctx,
        libLifecycleHook,
        LIFECYCLE_OUTPUT_NAME,
      );

      builtLifecycleHooks.push(lifecycleHookFileName);
    }
  }

  return { lifecycleHooks: builtLifecycleHooks, watcher: lifecycleWatcher };
}

/**
 * Write the final global hooks metadata to disk (themeHooksMeta, lifecycleHookMeta).
 */
async function saveGlobalHooksMeta(
  ctx: ResolvedEmbeddableConfig,
  themeProvider?: string,
  lifecycleHooks?: string[],
) {
  const metaFilePath = path.resolve(
    ctx.client.buildDir,
    EXTERNAL_LIBRARY_GLOBAL_HOOKS_META_NAME,
  );
  const data = JSON.stringify({ themeProvider, lifecycleHooks }, null, 2);
  fsSync.writeFileSync(metaFilePath, data);
}

/**
 * Generate a temporary file which imports the library theme and repository theme,
 * replacing template placeholders.
 */
async function generateTemporaryHookFile(
  ctx: ResolvedEmbeddableConfig,
  libraryThemeImports: string[],
  functionNames: string[],
  repoThemeImport: string,
) {
  const templatePath = path.resolve(
    ctx.core.templatesDir,
    "embeddableThemeHook.js.template",
  );
  const templateContent = await fs.readFile(templatePath, "utf8");

  const newContent = templateContent
    .replace("{{LIBRARY_THEME_IMPORTS}}", libraryThemeImports.join("\n"))
    .replace("{{ARRAY_OF_LIBRARY_THEME_PROVIDERS}}", functionNames.join("\n"))
    .replace("{{LOCAL_THEME_IMPORT}}", repoThemeImport);

  // Write to temporary hook file
  await fs.writeFile(getTempHookFilePath(ctx), newContent, "utf8");
}

/**
 * Build a file with Vite and return the hashed output file name (e.g., embeddable-theme-xxxx.js).
 */
async function buildWithVite(
  ctx: ResolvedEmbeddableConfig,
  entryFile: string,
  outputFile: string,
  watch = false,
  useHash = true,
) {
  const fileContent = await fs.readFile(entryFile, "utf8");
  const fileHash = getContentHash(fileContent);
  // Bundle using Vite
  const fileName = useHash ? `${outputFile}-${fileHash}` : outputFile;
  const fileWatcher = await vite.build({
    logLevel: watch ? "info" : "error",
    build: {
      emptyOutDir: false,
      lib: {
        entry: entryFile,
        formats: ["es"],
        fileName: fileName,
      },
      outDir: ctx.client.buildDir,
      watch: watch ? {} : undefined,
    },
  });

  if (watch) {
    await waitForInitialBuild(fileWatcher as RollupWatcher);
  }

  const watcher: RollupWatcher | undefined = watch
    ? (fileWatcher as RollupWatcher)
    : undefined;

  return { fileName: `${fileName}.js`, watcher };
}

/**
 * Remove the temporary hook file after building.
 */
async function cleanupTemporaryHookFile(ctx: ResolvedEmbeddableConfig) {
  await fs.rm(getTempHookFilePath(ctx), { force: true });
}

/**
 * Get the path to the temporary hook file in the build directory.
 */
function getTempHookFilePath(ctx: ResolvedEmbeddableConfig): string {
  return path.resolve(ctx.client.buildDir, TEMP_JS_HOOK_FILE);
}

function waitForInitialBuild(watcher: RollupWatcher): Promise<void> {
  return new Promise((resolve, reject) => {
    function onEvent(event: RollupWatcherEvent) {
      if (event.code === "END") {
        watcher.off("event", onEvent);
        resolve();
      } else if (event.code === "ERROR") {
        watcher.off("event", onEvent);
        reject(event.error);
      }
    }

    watcher.on("event", onEvent);
  });
}
