import {
  ModalRoot,
  PanelSection,
  PanelSectionRow,
  QuickAccessTab,
  Router,
  findSP,
  quickAccessMenuClasses,
  showModal,
  sleep,
} from 'decky-frontend-lib';
import { FC, lazy } from 'react';
import { FaExclamationCircle, FaPlug } from 'react-icons/fa';

import { DeckyState, DeckyStateContextProvider, UserInfo, useDeckyState } from './components/DeckyState';
import { File, FileSelectionType } from './components/modals/filepicker';
import { deinitFilepickerPatches, initFilepickerPatches } from './components/modals/filepicker/patches';
import MultiplePluginsInstallModal from './components/modals/MultiplePluginsInstallModal';
import PluginInstallModal from './components/modals/PluginInstallModal';
import PluginUninstallModal from './components/modals/PluginUninstallModal';
import NotificationBadge from './components/NotificationBadge';
import PluginView from './components/PluginView';
import WithSuspense from './components/WithSuspense';
import { FrozenPluginService } from './frozen-plugins-service';
import { HiddenPluginsService } from './hidden-plugins-service';
import Logger from './logger';
import { NotificationService } from './notification-service';
import { InstallType, Plugin, PluginLoadType } from './plugin';
import RouterHook from './router-hook';
import { deinitSteamFixes, initSteamFixes } from './steamfixes';
import { checkForPluginUpdates } from './store';
import TabsHook from './tabs-hook';
import Toaster from './toaster';
import { getVersionInfo } from './updater';
import { getSetting, setSetting } from './utils/settings';
import TranslationHelper, { TranslationClass } from './utils/TranslationHelper';

const StorePage = lazy(() => import('./components/store/Store'));
const SettingsPage = lazy(() => import('./components/settings'));

const FilePicker = lazy(() => import('./components/modals/filepicker'));

declare global {
  interface Window {
    __DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyPluginBackendAPIInit?: {
      connect: (version: number, key: string) => any; // Returns the backend API used above, no real point adding types to this.
    };
  }
}

const callPluginMethod = DeckyBackend.callable<[pluginName: string, method: string, ...args: any], any>(
  'loader/call_plugin_method',
);

class PluginLoader extends Logger {
  private plugins: Plugin[] = [];
  private tabsHook: TabsHook = new TabsHook();
  private routerHook: RouterHook = new RouterHook();
  public toaster: Toaster = new Toaster();
  private deckyState: DeckyState = new DeckyState();

  public frozenPluginsService = new FrozenPluginService(this.deckyState);
  public hiddenPluginsService = new HiddenPluginsService(this.deckyState);
  public notificationService = new NotificationService(this.deckyState);

  private reloadLock: boolean = false;
  // stores a list of plugin names which requested to be reloaded
  private pluginReloadQueue: { name: string; version?: string }[] = [];

  constructor() {
    super(PluginLoader.name);

    DeckyBackend.addEventListener('loader/notify_updates', this.notifyUpdates.bind(this));
    DeckyBackend.addEventListener('loader/import_plugin', this.importPlugin.bind(this));
    DeckyBackend.addEventListener('loader/unload_plugin', this.unloadPlugin.bind(this));
    DeckyBackend.addEventListener('loader/add_plugin_install_prompt', this.addPluginInstallPrompt.bind(this));
    DeckyBackend.addEventListener(
      'loader/add_multiple_plugins_install_prompt',
      this.addMultiplePluginsInstallPrompt.bind(this),
    );

    this.tabsHook.init();

    const TabBadge = () => {
      const { updates, hasLoaderUpdate } = useDeckyState();
      return <NotificationBadge show={(updates && updates.size > 0) || hasLoaderUpdate} />;
    };

    this.tabsHook.add({
      id: QuickAccessTab.Decky,
      title: null,
      content: (
        <DeckyStateContextProvider deckyState={this.deckyState}>
          <PluginView />
        </DeckyStateContextProvider>
      ),
      icon: (
        <DeckyStateContextProvider deckyState={this.deckyState}>
          <FaPlug />
          <TabBadge />
        </DeckyStateContextProvider>
      ),
    });

    this.routerHook.addRoute('/decky/store', () => (
      <WithSuspense route={true}>
        <StorePage />
      </WithSuspense>
    ));
    this.routerHook.addRoute('/decky/settings', () => {
      return (
        <DeckyStateContextProvider deckyState={this.deckyState}>
          <WithSuspense route={true}>
            <SettingsPage />
          </WithSuspense>
        </DeckyStateContextProvider>
      );
    });

    initSteamFixes();

    initFilepickerPatches();

    this.initPluginBackendAPI();

    Promise.all([this.getUserInfo(), this.updateVersion()])
      .then(() => this.loadPlugins())
      .then(() => this.checkPluginUpdates())
      .then(() => this.log('Initialized'));
  }

  private getPluginsFromBackend = DeckyBackend.callable<
    [],
    { name: string; version: string; load_type: PluginLoadType }[]
  >('loader/get_plugins');

  private async loadPlugins() {
    // wait for SP window to exist before loading plugins
    while (!findSP()) {
      await sleep(100);
    }
    const plugins = await this.getPluginsFromBackend();
    const pluginLoadPromises = [];
    const loadStart = performance.now();
    for (const plugin of plugins) {
      if (!this.hasPlugin(plugin.name))
        pluginLoadPromises.push(this.importPlugin(plugin.name, plugin.version, plugin.load_type, false));
    }
    await Promise.all(pluginLoadPromises);
    const loadEnd = performance.now();
    this.log(`Loaded ${plugins.length} plugins in ${loadEnd - loadStart}ms`);

    this.checkPluginUpdates();
  }

  public async getUserInfo() {
    const userInfo = await DeckyBackend.call<[], UserInfo>('utilities/get_user_info');
    setSetting('user_info.user_name', userInfo.username);
    setSetting('user_info.user_home', userInfo.path);
  }

  public async updateVersion() {
    const versionInfo = await getVersionInfo();
    this.deckyState.setVersionInfo(versionInfo);

    return versionInfo;
  }

  public async notifyUpdates() {
    const versionInfo = await this.updateVersion();
    if (versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current) {
      this.deckyState.setHasLoaderUpdate(true);
      if (this.notificationService.shouldNotify('deckyUpdates')) {
        this.toaster.toast({
          title: <TranslationHelper trans_class={TranslationClass.PLUGIN_LOADER} trans_text="decky_title" />,
          body: (
            <TranslationHelper
              trans_class={TranslationClass.PLUGIN_LOADER}
              trans_text="decky_update_available"
              i18n_args={{ tag_name: versionInfo?.remote?.tag_name }}
            />
          ),
          onClick: () => Router.Navigate('/decky/settings'),
        });
      }
    }
    await sleep(7000);
    await this.notifyPluginUpdates();
  }

  public async checkPluginUpdates() {
    const frozenPlugins = this.deckyState.publicState().frozenPlugins;

    const updates = await checkForPluginUpdates(this.plugins.filter((p) => !frozenPlugins.includes(p.name)));
    this.deckyState.setUpdates(updates);
    return updates;
  }

  public async notifyPluginUpdates() {
    const updates = await this.checkPluginUpdates();
    if (updates?.size > 0 && this.notificationService.shouldNotify('pluginUpdates')) {
      this.toaster.toast({
        title: <TranslationHelper trans_class={TranslationClass.PLUGIN_LOADER} trans_text="decky_title" />,
        body: (
          <TranslationHelper
            trans_class={TranslationClass.PLUGIN_LOADER}
            trans_text="plugin_update"
            i18n_args={{ count: updates.size }}
          />
        ),
        onClick: () => Router.Navigate('/decky/settings/plugins'),
      });
    }
  }

  public addPluginInstallPrompt(
    artifact: string,
    version: string,
    request_id: string,
    hash: string,
    install_type: number,
  ) {
    showModal(
      <PluginInstallModal
        artifact={artifact}
        version={version}
        hash={hash}
        installType={install_type}
        onOK={() => DeckyBackend.call<[string]>('utilities/confirm_plugin_install', request_id)}
        onCancel={() => DeckyBackend.call<[string]>('utilities/cancel_plugin_install', request_id)}
      />,
    );
  }

  public addMultiplePluginsInstallPrompt(
    request_id: string,
    requests: { name: string; version: string; hash: string; install_type: InstallType }[],
  ) {
    showModal(
      <MultiplePluginsInstallModal
        requests={requests}
        onOK={() => DeckyBackend.call<[string]>('utilities/confirm_plugin_install', request_id)}
        onCancel={() => DeckyBackend.call<[string]>('utilities/cancel_plugin_install', request_id)}
      />,
    );
  }

  public uninstallPlugin(name: string, title: string, buttonText: string, description: string) {
    showModal(<PluginUninstallModal name={name} title={title} buttonText={buttonText} description={description} />);
  }

  public hasPlugin(name: string) {
    return Boolean(this.plugins.find((plugin) => plugin.name == name));
  }

  public dismountAll() {
    for (const plugin of this.plugins) {
      this.log(`Dismounting ${plugin.name}`);
      plugin.onDismount?.();
    }
  }

  public init() {
    getSetting('developer.enabled', false).then((val) => {
      if (val) import('./developer').then((developer) => developer.startup());
    });

    // Grab and set plugin order
    getSetting<string[]>('pluginOrder', []).then((pluginOrder) => {
      this.debug('pluginOrder: ', pluginOrder);
      this.deckyState.setPluginOrder(pluginOrder);
    });

    this.frozenPluginsService.init();
    this.hiddenPluginsService.init();
    this.notificationService.init();
  }

  public deinit() {
    this.routerHook.removeRoute('/decky/store');
    this.routerHook.removeRoute('/decky/settings');
    deinitSteamFixes();
    deinitFilepickerPatches();
  }

  public unloadPlugin(name: string) {
    const plugin = this.plugins.find((plugin) => plugin.name === name);
    plugin?.onDismount?.();
    this.plugins = this.plugins.filter((p) => p !== plugin);
    this.deckyState.setPlugins(this.plugins);
  }

  public async importPlugin(
    name: string,
    version?: string | undefined,
    loadType: PluginLoadType = PluginLoadType.ESMODULE_V1,
    useQueue: boolean = true,
  ) {
    if (useQueue && this.reloadLock) {
      this.log('Reload currently in progress, adding to queue', name);
      this.pluginReloadQueue.push({ name, version: version });
      return;
    }

    try {
      this.reloadLock = true;
      this.log(`Trying to load ${name}`);

      this.unloadPlugin(name);
      const startTime = performance.now();
      await this.importReactPlugin(name, version, loadType);
      const endTime = performance.now();

      this.deckyState.setPlugins(this.plugins);
      this.log(`Loaded ${name} in ${endTime - startTime}ms`);
    } catch (e) {
      throw e;
    } finally {
      if (useQueue) {
        this.reloadLock = false;
        const nextPlugin = this.pluginReloadQueue.shift();
        if (nextPlugin) {
          this.importPlugin(nextPlugin.name, nextPlugin.version);
        }
      }
    }
  }

  private async importReactPlugin(
    name: string,
    version?: string,
    loadType: PluginLoadType = PluginLoadType.ESMODULE_V1,
  ) {
    try {
      switch (loadType) {
        case PluginLoadType.ESMODULE_V1:
          const plugin_exports = await import(`http://127.0.0.1:1337/plugins/${name}/dist/index.js`);
          let plugin = plugin_exports.default();

          this.plugins.push({
            ...plugin,
            name: name,
            version: version,
          });
          break;

        case PluginLoadType.LEGACY_EVAL_IIFE:
          let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`, {
            credentials: 'include',
            headers: {
              Authentication: deckyAuthToken,
            },
          });
          if (res.ok) {
            let plugin_export: (serverAPI: any) => Plugin = await eval(await res.text());
            let plugin = plugin_export(this.createLegacyPluginAPI(name));
            this.plugins.push({
              ...plugin,
              name: name,
              version: version,
            });
          } else throw new Error(`${name} frontend_bundle not OK`);
          break;

        default:
          throw new Error(`${name} has no defined loadType.`);
      }
    } catch (e) {
      this.error('Error loading plugin ' + name, e);
      const TheError: FC<{}> = () => (
        <PanelSection>
          <PanelSectionRow>
            <div className={quickAccessMenuClasses.FriendsTitle} style={{ display: 'flex', justifyContent: 'center' }}>
              <TranslationHelper trans_class={TranslationClass.PLUGIN_LOADER} trans_text="error" />
            </div>
          </PanelSectionRow>
          <PanelSectionRow>
            <pre style={{ overflowX: 'scroll' }}>
              <code>{e instanceof Error ? e.stack : JSON.stringify(e)}</code>
            </pre>
          </PanelSectionRow>
          <PanelSectionRow>
            <div className={quickAccessMenuClasses.Text}>
              <TranslationHelper
                trans_class={TranslationClass.PLUGIN_LOADER}
                trans_text="plugin_error_uninstall"
                i18n_args={{ name: name }}
              />
            </div>
          </PanelSectionRow>
        </PanelSection>
      );
      this.plugins.push({
        name: name,
        version: version,
        content: <TheError />,
        icon: <FaExclamationCircle />,
      });
      this.toaster.toast({
        title: (
          <TranslationHelper
            trans_class={TranslationClass.PLUGIN_LOADER}
            trans_text="plugin_load_error.toast"
            i18n_args={{ name: name }}
          />
        ),
        body: '' + e,
        icon: <FaExclamationCircle />,
      });
    }
  }

  async callServerMethod(methodName: string, args = {}) {
    this.warn(
      `Calling ${methodName} via callServerMethod, which is deprecated and will be removed in a future release. Please switch to the backend API.`,
    );
    return await DeckyBackend.call<[methodName: string, kwargs: any], any>(
      'utilities/_call_legacy_utility',
      methodName,
      args,
    );
  }

  openFilePickerLegacy(
    startPath: string,
    selectFiles?: boolean,
    regex?: RegExp,
  ): Promise<{ path: string; realpath: string }> {
    this.warn('openFilePicker is deprecated and will be removed. Please migrate to openFilePickerV2');
    if (selectFiles) {
      return this.openFilePicker(FileSelectionType.FILE, startPath, true, true, regex);
    } else {
      return this.openFilePicker(FileSelectionType.FOLDER, startPath, false, true, regex);
    }
  }

  openFilePicker(
    select: FileSelectionType,
    startPath: string,
    includeFiles?: boolean,
    includeFolders?: boolean,
    filter?: RegExp | ((file: File) => boolean),
    extensions?: string[],
    showHiddenFiles?: boolean,
    allowAllFiles?: boolean,
    max?: number,
  ): Promise<{ path: string; realpath: string }> {
    return new Promise((resolve, reject) => {
      const Content = ({ closeModal }: { closeModal?: () => void }) => (
        // Purposely outside of the FilePicker component as lazy-loaded ModalRoots don't focus correctly
        <ModalRoot
          onCancel={() => {
            reject('User canceled');
            closeModal?.();
          }}
        >
          <WithSuspense>
            <FilePicker
              startPath={startPath}
              includeFiles={includeFiles}
              includeFolders={includeFolders}
              filter={filter}
              validFileExtensions={extensions}
              allowAllFiles={allowAllFiles}
              defaultHidden={showHiddenFiles}
              onSubmit={resolve}
              closeModal={closeModal}
              fileSelType={select}
              max={max}
            />
          </WithSuspense>
        </ModalRoot>
      );
      showModal(<Content />);
    });
  }

  /* TODO replace with the following flow (or similar) so we can reuse the JS Fetch API
        frontend --request URL only--> backend (ws method)
        backend --new temporary backend URL--> frontend (ws response)
        frontend <--> backend <--> target URL (over http!)
      */
  async fetchNoCors(url: string, request: any = {}) {
    let method: string;
    const req = { headers: {}, ...request, data: request.body };
    req?.body && delete req.body;
    if (!request.method) {
      method = 'POST';
    } else {
      method = request.method;
      delete req.method;
    }
    // this is terrible but a. we're going to redo this entire method anyway and b. it was already terrible
    try {
      const ret = await DeckyBackend.call<
        [method: string, url: string, extra_opts?: any],
        { status: number; headers: { [key: string]: string }; body: string }
      >('utilities/http_request', method, url, req);
      return { success: true, result: ret };
    } catch (e) {
      return { success: false, result: e?.toString() };
    }
  }

  initPluginBackendAPI() {
    // Things will break *very* badly if plugin code touches this outside of @decky/backend, so lets make that clear.
    window.__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyPluginBackendAPIInit = {
      connect: (version: number, pluginName: string) => {
        if (version <= 0) {
          throw new Error(`Plugin ${pluginName} requested invalid backend api version ${version}.`);
        }

        const backendAPI = {
          call: (methodName: string, ...args: any) => {
            return callPluginMethod(pluginName, methodName, ...args);
          },
          callable: (methodName: string) => {
            return (...args: any) => callPluginMethod(pluginName, methodName, ...args);
          },
        };

        this.debug(`${pluginName} connected to backend API.`);
        return backendAPI;
      },
    };
  }

  createLegacyPluginAPI(pluginName: string) {
    const pluginAPI = {
      routerHook: this.routerHook,
      toaster: this.toaster,
      // Legacy
      callServerMethod: this.callServerMethod,
      openFilePicker: this.openFilePickerLegacy,
      openFilePickerV2: this.openFilePicker,
      // Legacy
      async callPluginMethod(methodName: string, args = {}) {
        return DeckyBackend.call<[pluginName: string, methodName: string, kwargs: any], any>(
          'loader/call_legacy_plugin_method',
          pluginName,
          methodName,
          args,
        );
      },
      fetchNoCors: this.fetchNoCors,
      executeInTab: DeckyBackend.callable<
        [tab: String, runAsync: Boolean, code: string],
        { success: boolean; result: any }
      >('utilities/execute_in_tab'),
      injectCssIntoTab: DeckyBackend.callable<[tab: string, style: string], string>('utilities/inject_css_into_tab'),
      removeCssFromTab: DeckyBackend.callable<[tab: string, cssId: string]>('utilities/remove_css_from_tab'),
    };

    return pluginAPI;
  }
}

export default PluginLoader;
