import { DebugLogger } from '@affine/debug';
import {
  builtinPluginPaths,
  enabledPluginAtom,
  invokeCleanup,
  pluginPackageJson,
} from '@toeverything/infra/__internal__/plugin';
import {
  getCurrentStore,
  loadedPluginNameAtom,
} from '@toeverything/infra/atom';
import { packageJsonOutputSchema } from '@toeverything/infra/type';
import type { z } from 'zod';

import { createSetup } from './plugins/setup';

const logger = new DebugLogger('register-plugins');

declare global {
  // eslint-disable-next-line no-var
  var __pluginPackageJson__: unknown[];
}

Object.defineProperty(globalThis, '__pluginPackageJson__', {
  get() {
    return getCurrentStore().get(pluginPackageJson);
  },
});

export async function bootstrapPluginSystem(
  rootStore: ReturnType<typeof getCurrentStore>
) {
  const { evaluatePluginEntry, setupPluginCode } = createSetup(rootStore);
  rootStore.sub(enabledPluginAtom, () => {
    const added = new Set<string>();
    const removed = new Set<string>();
    const enabledPlugin = new Set(rootStore.get(enabledPluginAtom));
    enabledPlugin.forEach(pluginName => {
      if (!enabledPluginSet.has(pluginName)) {
        added.add(pluginName);
      }
    });
    enabledPluginSet.forEach(pluginName => {
      if (!enabledPlugin.has(pluginName)) {
        removed.add(pluginName);
      }
    });
    // update plugins
    enabledPluginSet.clear();
    enabledPlugin.forEach(pluginName => {
      enabledPluginSet.add(pluginName);
    });
    added.forEach(pluginName => {
      evaluatePluginEntry(pluginName);
    });
    removed.forEach(pluginName => {
      invokeCleanup(pluginName);
    });
  });
  const enabledPluginSet = new Set(rootStore.get(enabledPluginAtom));
  const loadedAssets = new Set<string>();

  // we will load all plugins in parallel from builtinPlugins
  return Promise.all(
    [...builtinPluginPaths].map(url => {
      return fetch(`${url}/package.json`)
        .then(async res => {
          const packageJson = (await res.json()) as z.infer<
            typeof packageJsonOutputSchema
          >;
          packageJsonOutputSchema.parse(packageJson);
          const {
            name: pluginName,
            affinePlugin: {
              release,
              entry: { core },
              assets,
            },
          } = packageJson;
          rootStore.set(pluginPackageJson, json => [...json, packageJson]);
          logger.debug(`registering plugin ${pluginName}`);
          logger.debug(`package.json: ${packageJson}`);
          if (!release && !runtimeConfig.enablePlugin) {
            return;
          }
          const baseURL = url;
          const entryURL = `${baseURL}/${core}`;
          rootStore.set(loadedPluginNameAtom, prev => [...prev, pluginName]);
          await setupPluginCode(baseURL, pluginName, core);
          console.log(`prepareImports for ${pluginName} done`);
          await fetch(entryURL).then(async () => {
            if (assets.length > 0) {
              await Promise.all(
                assets.map(async (asset: string) => {
                  const loadedAssetName = `${pluginName}_${asset}`;
                  // todo(himself65): add assets into shadow dom
                  if (loadedAssets.has(loadedAssetName)) {
                    return;
                  }
                  if (asset.endsWith('.css')) {
                    loadedAssets.add(loadedAssetName);
                    const res = await fetch(`${baseURL}/${asset}`);
                    if (res.ok) {
                      // todo: how to put css file into sandbox?
                      return res.text().then(text => {
                        const style = document.createElement('style');
                        style.setAttribute('plugin-id', pluginName);
                        style.textContent = text;
                        document.head.append(style);
                      });
                    }
                    return null;
                  } else {
                    return;
                  }
                })
              );
            }
            if (!enabledPluginSet.has(pluginName)) {
              logger.debug(`plugin ${pluginName} is not enabled`);
            } else {
              logger.debug(`plugin ${pluginName} is enabled`);
              evaluatePluginEntry(pluginName);
            }
          });
        })
        .catch(e => {
          console.error(`error when fetch plugin from ${url}`, e);
        });
    })
  ).then(() => {
    console.info('All plugins loaded');
  });
}
