import Vue from 'vue';
import { RuntimeError } from '@ibiz-template/core';
import { DefectModelError } from '@ibiz-template/model';
import {
  IPluginFactory,
  RemotePluginConfig,
  RemotePluginItem,
} from '@ibiz-template/runtime';
import { IPSAppPFPluginRef, IPSSysPFPlugin } from '@ibiz/dynamic-model-api';
import { basename, dirname, join } from 'path-browserify';

/**
 * 插件工具类
 *
 * @author chitanda
 * @date 2022-10-21 16:10:29
 * @export
 * @class PluginFactory
 */
export class PluginFactory implements IPluginFactory {
  /**
   * 是否为 http || https 开头
   *
   * @author chitanda
   * @date 2022-11-07 14:11:28
   * @protected
   */
  protected urlReg = /^http[s]?:\/\/[^\s]*/;

  /**
   * 是否已经加载过文件缓存
   *
   * @author chitanda
   * @date 2022-10-31 14:10:17
   * @protected
   * @type {Map<string, boolean>}
   */
  protected cache: Map<string, boolean> = new Map();

  /**
   * 本地开发测试包，只在本地开发生效
   *
   * @author chitanda
   * @date 2022-11-02 21:11:41
   * @protected
   */
  protected devPackages: Map<string, () => Promise<unknown>> = new Map();

  /**
   * 插件缓存
   *
   * @author chitanda
   * @date 2022-10-31 14:10:28
   * @protected
   * @type {Map<string, RemotePluginItem>}
   */
  protected pluginCache: Map<string, RemotePluginItem> = new Map();

  /**
   * 设置开发插件，用于本地调试
   *
   * @author chitanda
   * @date 2022-11-02 21:11:56
   * @param {string} name
   * @param {() => Promise<unknown>} fn
   */
  setDevPlugin(name: string, fn: () => Promise<unknown>): void {
    this.devPackages.set(name, fn);
  }

  /**
   * 加载插件
   *
   * @author chitanda
   * @date 2022-10-31 14:10:13
   * @param {IPSSysPFPlugin} plugin
   * @return {*}  {Promise<boolean>}
   */
  async loadPlugin(plugin: IPSSysPFPlugin): Promise<boolean> {
    if (plugin.runtimeObject === true) {
      const pluginRef = plugin as unknown as IPSAppPFPluginRef;
      if (pluginRef) {
        return this.loadPluginRef(pluginRef);
      }
    }
    return false;
  }

  /**
   * 加载应用饮用插件
   *
   * @author chitanda
   * @date 2022-10-31 16:10:57
   * @param {IPSAppPFPluginRef} pluginRef
   * @return {*}  {Promise<boolean>}
   */
  async loadPluginRef(pluginRef: IPSAppPFPluginRef): Promise<boolean> {
    if (this.pluginCache.has(pluginRef.rTObjectName)) {
      return true;
    }
    let config: IParams = {};
    try {
      config = JSON.parse(pluginRef.rTObjectRepo);
    } catch (err) {
      throw new DefectModelError(
        pluginRef,
        `插件[${pluginRef.name}]参数格式异常请检查`,
      );
    }
    const remotePlugin = new RemotePluginItem(
      pluginRef.rTObjectName,
      config as unknown as RemotePluginConfig,
    );
    if (remotePlugin) {
      try {
        await this.loadScript(remotePlugin);
        this.pluginCache.set(pluginRef.rTObjectName, remotePlugin);
        return true;
      } catch (error) {
        ibiz.log.error(error);
      }
    }
    return false;
  }

  /**
   * 加载插件
   *
   * @author chitanda
   * @date 2022-11-02 14:11:31
   * @protected
   * @param {RemotePluginItem} remotePlugin
   * @return {*}  {Promise<void>}
   */
  protected async loadScript(remotePlugin: RemotePluginItem): Promise<void> {
    const { baseUrl, name, version, system, module } = remotePlugin.config;
    const scriptUrl = this.devPackages.has(name)
      ? join(baseUrl, `${name}@${version}`, module)
      : join(baseUrl, `${name}@${version}`, system);
    if (scriptUrl) {
      if (this.cache.has(scriptUrl)) {
        return;
      }
      let data: IParams | null = null;
      const url = this.parseUrl(scriptUrl);
      // 本地开发模式下的包引用加载
      if (this.devPackages.has(name)) {
        const fn = this.devPackages.get(name)!;
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const pluginModule = (await fn()) as any;
        Vue.use(pluginModule.default, remotePlugin);
        this.cache.set(scriptUrl, true);
        const mateUrl: string = pluginModule.default.mateUrl || '';
        await this.loadStyles(
          remotePlugin,
          mateUrl.split('/').splice(3).join('/'),
        );
      } else {
        data = await System.import(url);
        if (data) {
          if (data.default) {
            Vue.use(data.default, remotePlugin);
          } else {
            throw new RuntimeError(
              `远程插件加载失败, 远程插件未找到[default]默认导出`,
            );
          }
          this.cache.set(scriptUrl, true);
          await this.loadStyles(remotePlugin, url);
        } else {
          throw new RuntimeError(`远程插件加载失败, 未找到文件`);
        }
      }
    }
  }

  /**
   * 加载插件样式文件
   *
   * @author chitanda
   * @date 2022-11-02 21:11:51
   * @protected
   * @param {RemotePluginItem} remotePlugin
   * @param {string} [rootJsPath] 入口 js 文件全路径，用于计算拼接 css 文件所在路径
   * @return {*}  {Promise<void>}
   */
  protected async loadStyles(
    remotePlugin: RemotePluginItem,
    rootJsPath: string,
  ): Promise<void> {
    const { styles } = remotePlugin.config;
    if (styles) {
      const arr = styles instanceof Array ? styles : [styles];
      if (arr && arr.length > 0) {
        const folder = dirname(rootJsPath);
        const all = arr.map(styleUrl => {
          const url = `${folder}/${basename(styleUrl)}`;
          if (this.cache.has(url)) {
            return false;
          }
          return new Promise((resolve, reject) => {
            const linkDom = document.createElement('link');
            linkDom.setAttribute('type', 'text/css');
            linkDom.setAttribute('rel', 'stylesheet');
            linkDom.setAttribute('href', url);
            linkDom.onload = resolve;
            linkDom.onerror = reject;
            document.head.appendChild(linkDom);
          });
        });
        await Promise.all(all);
      }
    }
  }

  /**
   * 编译请求文件地址
   *
   * @author chitanda
   * @date 2022-10-31 14:10:19
   * @protected
   * @param {string} script
   * @return {*}  {string}
   */
  protected parseUrl(script: string): string {
    if (this.urlReg.test(script)) {
      return script;
    }
    if (this.urlReg.test(ibiz.env.pluginBaseUrl)) {
      if (script.startsWith('/')) {
        return ibiz.env.pluginBaseUrl + script;
      }
      return `${ibiz.env.pluginBaseUrl}/${script}`;
    }
    return `./${join(ibiz.env.pluginBaseUrl, script)}`;
  }
}
