import globalExternals from "@fal-works/esbuild-plugin-global-externals";
import style from "@hyrious/esbuild-plugin-style";
import { dir } from "dir";
import { context, formatMessages } from "esbuild";
import { cleanPlugin } from "esbuild-clean-plugin";
import { watch } from "fs";
import { existsAsync } from "fs-jetpack";
import { appendFile } from "node:fs/promises";
import { server } from "../../../editor/code/server-main";
import { conns } from "../../../entity/conn";
import { user } from "../../../entity/user";
import { sendWS } from "../../../sync-handler";
import { SyncType } from "../../../type";
import { code } from "../../code";
import { $ } from "bun";
const pending = {} as any;

export const initFrontEnd = async (
  root: string,
  id_site: string,
  force?: boolean
) => {
  let existing = code.internal.frontend[id_site];

  if (existing) {
    if (existing.npm) {
      if (pending[id_site]) return;
      pending[id_site] = true;
      await existing.npm;
      delete pending[id_site];
      delete existing.npm;
    } else {
      if (force) {
        try {
          existing.watch.close();
          await existing.ctx.dispose();
          delete code.internal.frontend[id_site];
        } catch (e) {}
      } else {
        if (existing.ctx) {
          return;
        }
      }
    }
  } else {
    if (
      !(await existsAsync(code.path(id_site, "site", "src", "node_modules")))
    ) {
      return;
    }
  }

  try {
    await isInstalling(id_site);

    const broadcastLoading = async () => {
      const client_ids = user.active
        .findAll({ site_id: id_site })
        .map((e) => e.client_id);

      const now = Date.now();

      client_ids.forEach((client_id) => {
        const ws = conns.get(client_id)?.ws;
        if (ws)
          sendWS(ws, {
            type: SyncType.Event,
            event: "code_changes",
            data: { ts: now, mode: "frontend", status: "building" },
          });
      });
    };

    code.internal.frontend[id_site] = {
      ctx: await initBuildCtx({ id_site, root }),
      timeout: null,
      rebuilding: false,
      watch: watch(
        dir.data(root),
        {
          recursive: true,
        },
        async (event, filename) => {
          const fe = code.internal.frontend[id_site];
          const srv = code.internal.server[id_site];
          if (
            filename?.startsWith("node_modules") ||
            filename?.startsWith("typings")
          )
            return;
          if (
            filename?.endsWith(".tsx") ||
            filename?.endsWith(".ts") ||
            filename?.endsWith(".css") ||
            filename?.endsWith(".html")
          ) {
            if (typeof fe !== "undefined" && !fe.rebuilding) {
              fe.rebuilding = true;
              clearTimeout(fe.timeout);
              fe.timeout = setTimeout(async () => {
                const build_timeout = setTimeout(async () => {
                  console.log(`Build unfinished ${id_site} ${filename}`);
                  await fe.ctx.dispose();
                  fe.ctx = await initBuildCtx({ id_site, root });
                }, 3000);
                
                try {
                  broadcastLoading();
                  await fe.ctx.rebuild();
                  clearTimeout(build_timeout);
                } catch (e: any) {
                  console.error(`Frontend failed rebuild (site: ${id_site})`);
                  console.error(e.messsage);
                }
                fe.rebuilding = false;
              }, 500);
            }

            if (typeof srv !== "undefined" && !srv.rebuilding && srv.ctx) {
              srv.rebuilding = true;
              try {
                await srv.ctx.rebuild();
                await server.init(id_site);
              } catch (e) {}
              srv.rebuilding = false;
            }
          }
        }
      ),
    };
    const fe = code.internal.frontend[id_site];
    fe.rebuilding = true;
    try {
      await fe.ctx.rebuild();
    } catch (e) {}
    fe.rebuilding = false;
  } catch (e: any) {
    console.error("Error building front end", id_site);
    delete code.internal.frontend[id_site];
  }
};

const codeError = async (id_site: string, error: string, append?: boolean) => {
  const path = code.path(id_site, "site", "src", "index.log");

  if (error) console.log(error);
  if (append) {
    await appendFile(path, error);
    return;
  }
  await Bun.write(path, error);
};

const isInstalling = async (id_site: string) => {
  const path = code.path(id_site, "site", "src", "index.log");
  const file = Bun.file(path);
  try {
    const text = await file.text();
    if (typeof text === "string" && text.startsWith("Installing dependencies"))
      return true;
  } catch (e) {}

  return false;
};

const initBuildCtx = async ({
  id_site,
  root,
}: {
  id_site: string;
  root: string;
}) => {
  const out_dir_temp = dir.data(`code/${id_site}/site/build-temp`);
  const out_dir_switch = dir.data(`code/${id_site}/site/build-switch`);
  const out_dir = dir.data(`code/${id_site}/site/build`);
  return await context({
    absWorkingDir: dir.data(root),
    entryPoints: ["index.tsx"],
    outdir: out_dir_temp,
    format: "esm",
    bundle: true,
    minify: true,
    treeShaking: true,
    splitting: true,
    logLevel: "silent",
    sourcemap: true,
    metafile: true,
    plugins: [
      cleanPlugin(),
      style(),
      globalExternals({
        react: {
          varName: "window.React",
          type: "cjs",
        },
        "react-dom": {
          varName: "window.ReactDOM",
          type: "cjs",
        },
      }),
      {
        name: "prasi",
        async setup(setup) {
          try {
            setup.onEnd(async (res) => {
              const client_ids = user.active
                .findAll({ site_id: id_site })
                .map((e) => e.client_id);
              if (res.errors.length > 0) {
                await codeError(
                  id_site,
                  (await formatMessages(res.errors, { kind: "error" })).join(
                    "\n\n"
                  )
                );

                const now = Date.now();
                client_ids.forEach((client_id) => {
                  const ws = conns.get(client_id)?.ws;
                  if (ws)
                    sendWS(ws, {
                      type: SyncType.Event,
                      event: "code_changes",
                      data: { ts: now, mode: "frontend", status: "error" },
                    });
                });
              } else {
                await codeError(id_site, "");

                await $`rm -rf ${out_dir_switch}`.quiet();
                await $`mv ${out_dir} ${out_dir_switch}`.quiet();
                await $`mv ${out_dir_temp} ${out_dir}`.quiet();

                const now = Date.now();
                client_ids.forEach((client_id) => {
                  const ws = conns.get(client_id)?.ws;
                  if (ws)
                    sendWS(ws, {
                      type: SyncType.Event,
                      event: "code_changes",
                      data: { ts: now, mode: "frontend", status: "ok" },
                    });
                });
              }
            });
          } catch (e) {
            console.log("ERROR");
          }
        },
      },
    ],
  });
};
