// Import Libs and Handlers
import * as React from 'react';
import { autorun, observable, reaction } from 'mobx';
import { get, isEqual, noop } from 'lodash-es';
import { app, createPageApi, IPageAPI, $w as baseAPI } from '../app/global-api'; // 取对应子包app
import { AppRender } from '../handlers/RenderWrapper';
import { _WEDA_CLOUD_SDK as WEDA_CLOUD_SDK, AppContext } from '@cloudbase/weda-client';
import {
  CALC_RPX_ROOT_VAL_NAME,
  ENABLE_PAGE_ROOT_ID,
  generateEmit,
  reportTime,
  generateWidgetAPIContext,
} from '../utils';
import {
  bindFuncObj,
  checkAuth,
  createComputed,
  createWidgets,
  retryDataBinds,
  toCssText,
  defaultGenerateLoginUrl,
} from '../utils/index';
import { generatePageLifecycleHandlers, usePageLifeCycle } from '../utils/lifecycle';
import { useScrollTop, OwnerComponentContext } from '../utils/hooks';
import { IComponent, IPageComponent, processCommonStyle2CSSProperties, processListeners } from '@cloudbase/cals';
import { runtime } from '../utils/adapter';
import { loadMicroApp } from 'qiankun';
import { InjectContext, IWedaAppProps } from '../router';
import { useLocation } from 'react-router-dom';
import { generateDatasetQuery, generateEventFlows } from '../utils/adapter/generate';
import { watchForDebugExpr, resetDebugExprMap } from '../utils/debugExpr';

type TPage = IPageComponent | IComponent;

export interface ILowcodeModuleMap {
  state?: any;
  computed?: { [key: string]: Function };
  lifecycle: { [key: string]: Function };
  handler: { [key: string]: Function };
  renderer?: { [key: string]: Function };
  style?: { code: string }[];
}

const { createDataset, createStateDataSourceVar, generateParamsParser, EXTRA_API } = WEDA_CLOUD_SDK;

// Import Actions

interface IAppProps {
  page: TPage;
  generateLowcodeModule: () => ILowcodeModuleMap;
  generateComponentsMap: () => { [key: string]: React.FunctionComponent };
  generateActionsMap: () => { [key: string]: Function };
  generateWidgetsContext?: (page) => any;
  generateDataBinds?: (ctx, page) => any;
  generateComponentSchema?: (ctx, page) => any;
  generateContext?: (ctx, page) => { widgetContext; dataBinds; componentSchema };
  onMounted?: (props: { page: TPage; default: Function }) => void;
  onRender?: () => void;
  initDataContext?: any;
  options?: {
    reuseWidget?: boolean;
    looseError?: boolean;
    processCssUnit?: 'rem' | 'px';
  };
  renderLoading?: IWedaAppProps['renderLoading'];
}

function useMemoOrDeep(value, memo) {
  const ref = React.useRef(value);
  return React.useMemo(() => {
    if (memo || !isEqual(ref.current, value)) {
      ref.current = value;
    }
    return ref.current;
  }, [value]);
}

function useObserverControledDataContext(inited, injectContext, initDataContext) {
  const dataContextRef = React.useRef(observable(injectContext?.dataContext || initDataContext));
  return React.useMemo(() => {
    if (inited && injectContext?.dataContext) {
      if (dataContextRef.current) {
        // eslint-disable-next-line no-restricted-syntax
        for (const key in dataContextRef.current) {
          if (!injectContext.dataContext[key]) {
            dataContextRef.current[key] = null;
          }
        }
      }

      // eslint-disable-next-line no-restricted-syntax
      for (const key in injectContext.dataContext) {
        dataContextRef.current[key] = injectContext.dataContext[key];
      }

      // setFourceUpdate(Date.now());
    }
    return dataContextRef.current;
  }, [inited, injectContext?.dataContext]);
}

// Init
// eslint-disable-next-line react/display-name
export default function App({
  page: __page,
  generateComponentsMap,
  generateActionsMap,
  generateLowcodeModule,
  generateWidgetsContext,
  generateDataBinds,
  generateComponentSchema,
  generateContext,
  onRender,
  onMounted,
  initDataContext = {},
  renderLoading,
  options,
}: IAppProps) {
  const _page = useMemoOrDeep(__page, !options.reuseWidget);
  const { page, eventFlows = [] } = React.useMemo(() => {
    const { listeners, eventFlows } = processListeners(_page.listeners || []);
    return { page: { ..._page, listeners }, eventFlows };
  }, [_page]);

  const injectContext = React.useContext(InjectContext) as {
    $page?: any;
    dataContext?: any;
    onDataContextChange?: (id, data) => void;
  };
  const [pageContextInited, setPageContextInited] = React.useState(false);
  const [_, setFourceUpdate] = React.useState(Date.now());
  // 数据容器的上下文数据
  const widgetsContextRef = React.useRef<any>({});
  const { reuseWidget = false } = options || {};
  const globalCtxRef = React.useRef<any>({});

  // 兼容 this.state / $page.state 两种模式
  const pageCodeContextRef = React.useRef(injectContext?.$page || createPageApi());
  const $page = pageCodeContextRef.current;
  const lowcode = generateLowcodeModule();
  const { pathname } = useLocation();

  React.useEffect(() => {
    if (!injectContext?.$page) {
      type TDataset = { state: any; params: any; query: any };
      const dataset: Partial<TDataset> = createDataset(page.id);
      const state = observable(lowcode.state || {});
      state.dataset = dataset;
      Object.assign($page, {
        id: page.id,
        label: page?.attributes?.title,
        path: pathname,
        state,
        computed: createComputed(lowcode.computed || {}, pageCodeContextRef.current),
        handler: bindFuncObj(lowcode.handler || {}, pageCodeContextRef.current),
        randerer: lowcode.renderer,
        dataset,
      });
    }
    Object.assign($page, {
      __internal__: {
        ...$page.__internal__,
        $w: new Proxy(baseAPI, {
          get(obj, prop: string) {
            /**
             * 使用当前的实例进行覆盖
             */
            if (prop === '$page' || prop === 'page') {
              return $page;
            }

            // 代理页面级 query
            if ($page.dataset?.query?.[prop]) {
              return $page.dataset.query[prop];
            }

            // 代理页面 flow
            if ($page.__internal__.eventFlows?.[prop]) {
              return $page.__internal__.eventFlows[prop];
            }

            // 尝试代理页面级别组件实例
            let pageWidget = $page.widgets?.[prop];
            if (pageWidget) {
              return pageWidget._userWidget;
            }

            return obj[prop];
          },
        }),
      },
    });
    runtime.$page = pageCodeContextRef.current;
    if (!injectContext?.$page) {
      $page.dataset.query = generateDatasetQuery(
        page.dataset?.query || {},
        {
          $w: $page.__internal__.$w,
          $page,
          $app: app,
          actionsMap: generateActionsMap?.() || {},
        },
        {
          isComposite: false,
          looseError: options.looseError,
          useScopeWidgetData: true,
        },
      );
    }
    resetDebugExprMap();
    setPageContextInited(true);
  }, []);

  React.useEffect(() => {
    if (pageContextInited) {
      $page.__internal__.eventFlows = generateEventFlows(
        eventFlows,
        { $w: $page.__internal__.$w, $app: app, $page },
        {
          isComposite: false,
          looseError: options.looseError,
          useScopeWidgetData: true,
        },
      );
    }
  }, [pageContextInited, eventFlows]);

  const dataContext = useObserverControledDataContext(pageContextInited, injectContext, initDataContext);

  const { componentSchema, widgetsContext } = React.useMemo(() => {
    if (pageContextInited) {
      globalCtxRef.current = {
        $page,
        $app: app,
        actionsMap: generateActionsMap?.() || {},
      };
      const globalCtx = globalCtxRef.current;
      let widgetsContext;
      let databinds;
      let componentSchema;

      if (generateContext) {
        let context = generateContext(globalCtx, page);
        widgetsContext = context.widgetContext;
        databinds = context.dataBinds;
        componentSchema = context.componentSchema;
      } else {
        widgetsContext = generateWidgetsContext(page);
        databinds = generateDataBinds(globalCtx, page);
        componentSchema = generateComponentSchema(globalCtx, page);
      }

      // widgets 内的 dataBinds 可能需要关联 widgets，需要重新执行 dataBinds
      $page._dataBinds = databinds;

      return { componentSchema, widgetsContext };
    }
    return {};
  }, [pageContextInited, page]);

  widgetsContextRef.current = React.useMemo(() => {
    if (pageContextInited) {
      const globalCtx = globalCtxRef.current;
      const { widgets, nodeMap } = createWidgets(
        { apiContext: $page, ...(reuseWidget ? widgetsContextRef.current : {}), globalCtx },
        widgetsContext,
        $page._dataBinds || {},
        dataContext,
      );
      Object.entries(widgets).forEach(([key, value]) => {
        $page.widgets[key] = value;
      });
      retryDataBinds();
      if (reuseWidget) {
        /**
         * 复用时处理挂载新的
         * 还要处理老的
         */
        Object.keys($page.widgets).forEach((key) => {
          if (!(key in widgets)) {
            $page.widgets[key] = undefined;
          }
        });
        const { widgets: lastWdigets } = widgetsContextRef.current || {};
        if (lastWdigets?._disposers) {
          lastWdigets._disposers.forEach((dispose) => dispose());
          lastWdigets._disposers = [];
        }
      }
      return { widgetProps: widgetsContext, nodeMap, widgets };
    } else {
      return widgetsContextRef.current;
    }
  }, [widgetsContext, dataContext]);

  React.useEffect(() => {
    return () => {
      if (pageCodeContextRef.current?.widgets?._disposers) {
        pageCodeContextRef.current?.widgets?._disposers.forEach((dispose) => dispose());
        pageCodeContextRef.current.widgets._disposers = [];
      }

      if (pageCodeContextRef.current?.dataset?.query) {
        for (const key in pageCodeContextRef.current?.dataset?.query) {
          const query = pageCodeContextRef.current?.dataset?.query[key];
          query?.destroy?.();
        }
      }
    };
  }, []);

  React.useEffect(() => {
    let forDebugExprDispose = null;
    if (pageContextInited) {
      /**
       * 页面节点的表达式执行
       * 只在编辑器中开启调试表达式的功能
       */
      forDebugExprDispose = baseAPI.wedaContext.isEditorMode
        ? watchForDebugExpr({
            getDataContextFn: () => ({}),
            get$wFn: () => generateWidgetAPIContext($page?.__internal__?.$w, widgetsContext, {}),
            get$scopeFn: () => ({}),
            widgetId: 'page',
            subForItems: {},
            globalCtx: globalCtxRef.current,
          })
        : noop;
    }

    return function () {
      forDebugExprDispose?.();
    };
  }, [pageContextInited]);

  return (
    pageContextInited && (
      <AppContent
        page={page}
        lowcode={lowcode}
        componentSchema={componentSchema}
        pageCodeContext={pageCodeContextRef.current}
        virtualFields={generateComponentsMap()}
        onMounted={onMounted}
        onRender={onRender}
        // dataContext={dataContext}
        /**
         * 更新数据容器的上下文的方法
         * 会传递到事件emit的地方，将从组件获取到的数据data赋值给上下文
         * 当组件卸载时，传过来的data为undefined即可
         * {
         *   id1: [{...}],
         *   id2: {...},
         *   id3: undefined,
         *   id4: null,
         *   ...
         * }
         * @param id
         * @param data
         */
        // updateContext={function (id, data) {
        //   if (id && !injectContext?.dataContext) {
        //     dataContext[id] = { data };
        //   }
        //   injectContext?.onDataContextChange?.(id, data);
        // }}
        options={options}
        renderLoading={renderLoading}
      />
    )
  );
}

function AppContent({
  page,
  lowcode,
  componentSchema,
  pageCodeContext,
  virtualFields,
  onMounted = (props) => {
    // eslint-disable-next-line react/prop-types
    props?.default?.();
  },
  onRender = () => {},
  // dataContext,
  // updateContext,
  renderLoading = () => null,
  options,
}: {
  page: TPage;
  lowcode: ILowcodeModuleMap;
  componentSchema: any;
  pageCodeContext: IPageAPI;
  virtualFields: {
    [key: string]: React.FunctionComponent;
  };
  onMounted?: (props: { page: TPage; default: Function }) => void;
  onRender?: () => void;
  renderLoading?: IWedaAppProps['renderLoading'];
  // dataContext?;
  // updateContext?;
  options?: IAppProps['options'];
}) {
  const { processCssUnit = 'rem' } = options;
  // 检查权限
  const [weDaHasLogin, setWeDaHasLogin] = React.useState(false);
  const containerRef = React.useRef(null);
  const externalClassRef = React.useRef('');
  const microApp = React.useRef(null);
  const pureSrc = page.attributes?.src;
  const isPure = !!pureSrc;
  const { checkPageAuth = checkAuth, generateLoginUrl = defaultGenerateLoginUrl } = React.useContext(
    InjectContext,
  ) as any;
  const pageReadyResolveRef = React.useRef((data) => {});
  const pageReadyCalledRef = React.useRef(false);
  const onReady = React.useCallback(() => {
    pageReadyCalledRef.current = true;
    pageReadyResolveRef.current(null);
  }, []);

  const events = React.useMemo(
    () => getPageEvents(pageCodeContext, componentSchema.listenerInstances),
    [pageCodeContext, componentSchema.listenerInstances],
  );

  const lifecycle = React.useMemo(() => {
    return generatePageLifecycleHandlers(
      {
        ...lowcode?.lifecycle,
        beforePageCustomLaunch: async (query) => {
          onMounted({
            page,
            default: () => {
              const document = runtime?.document || window.document;
              document.title = `${page.attributes?.title || page.id}`;
            },
          });
          EXTRA_API.setParams(page.id, query || {});
          initWatchMethods(pageCodeContext);
          let checkAuthResult = await checkPageAuth(app, app.id, pageCodeContext, generateLoginUrl);
          setWeDaHasLogin(checkAuthResult);
          createStateDataSourceVar(
            page.id,
            generateParamsParser({ app, $page: pageCodeContext, $w: pageCodeContext?.__internal__.$w } as any),
          );

          // eslint-disable-next-line no-restricted-syntax
          for (const queryId in page?.dataset?.['query'] || {}) {
            if (page.dataset['query'][queryId].trigger === 'auto') {
              try {
                pageCodeContext?.dataset?.query?.[queryId]?.trigger?.();
              } catch (e) {
                console.error(`query ${queryId} 初始化失败：`, e);
              }
            }
          }
          onRender?.();
          reportTime('FIRST_PAGE_CONTENT_RENDERED', undefined, true);
        },
      },
      events,
    );
  }, []);

  // Web 环境页面级别生命周期
  usePageLifeCycle(
    {
      ...lifecycle,
      // eslint-disable-next-line @typescript-eslint/no-misused-promises
      onPageReady: async (...args) => {
        await new Promise((resolve) => {
          pageReadyResolveRef.current = resolve;
          if (pageReadyCalledRef.current) {
            resolve(null);
          }
        });
        const res = lifecycle?.onPageReady.call(this, ...args);
        events?.ready();
        return res;
      },
    },
    { app, page: pageCodeContext, options: { looseError: options?.looseError } },
    [],
  );

  // 切换页面滚动到顶部
  useScrollTop();

  React.useEffect(() => {
    if (isPure) {
      if (weDaHasLogin) {
        if (!microApp.current) {
          microApp.current = loadMicroApp(
            {
              name: page.id,
              entry: pureSrc,
              container: containerRef.current,
            },
            {
              fetch: runtime.fetch,
            },
          );
        } else {
          microApp.current.mount?.();
        }
      } else if (microApp.current) {
        microApp.current.unmount();
      }
    }
  }, [weDaHasLogin]);

  React.useEffect(() => {
    pageCodeContext.__internal__.active = true;
    app.__internal__.activePage = pageCodeContext;
    return () => {
      if (pageCodeContext.__internal__) {
        pageCodeContext.__internal__.active = false;
      }
      if (microApp.current) {
        microApp.current.unmount();
      }
    };
  }, []);

  React.useEffect(() => {
    function resize() {
      const className = `wd-${app?.__internal__?.isMobile?.() ? 'h5' : 'pc'}-root`;
      if (externalClassRef.current === className) {
        return;
      }
      if (containerRef.current.classList.contains(externalClassRef.current)) {
        containerRef.current.classList.replace(externalClassRef.current, className);
      } else {
        containerRef.current.classList.add(className);
      }
      externalClassRef.current = className;
    }
    resize();
    runtime?.addEventListener('resize', resize);
    return () => {
      runtime?.removeEventListener('resize', resize);
    };
  }, []);

  return (
    <div
      id={ENABLE_PAGE_ROOT_ID ? 'wd-page-root' : undefined}
      ref={containerRef}
      className={`weapps-page weapps-page-${page.id} ${externalClassRef.current}`}
    >
      <style
        id="wd-page-style-tag"
        type="text/css"
        dangerouslySetInnerHTML={{
          __html: `${toCssText(
            page.attributes?.style ||
              processCommonStyle2CSSProperties(page?.extra?.commonStyle || {}, {
                defaultUnit: processCssUnit,
                rpxCalcRoot: `var(${CALC_RPX_ROOT_VAL_NAME})`,
              }),
            'body',
          )}${(lowcode.style || []).map((item) => item.code).join('\n')}`,
        }}
      />

      {weDaHasLogin
        ? !isPure &&
          (lowcode.lifecycle?.onPageRender ? (
            lowcode.lifecycle?.onPageRender()
          ) : (
            <IAppRender
              virtualFields={virtualFields}
              componentSchema={componentSchema}
              codeContext={pageCodeContext}
              onReady={onReady}
              // dataContext={dataContext}
              // updateContext={updateContext}
            />
          ))
        : renderLoading({ message: '登录鉴权中...' })}
    </div>
  );
}

function IAppRender({ onReady, ...props }: any) {
  const [mount, setMounted] = React.useState(false);
  React.useEffect(() => {
    setMounted(true);
    return () => {
      setMounted(false);
    };
  }, []);

  /**
   * @important
   * 需要子节点先触发，page 的最后触发，
   * 因此不在 setMount 时直接调用
   */
  React.useEffect(() => {
    if (mount) {
      onReady();
    }
  }, [mount]);

  const { appContext, ownerComponentContext } = React.useMemo(() => {
    return { appContext: { ready: mount }, ownerComponentContext: { attached: mount } };
  }, [mount]);

  return (
    <AppContext.Provider value={appContext}>
      <OwnerComponentContext.Provider value={ownerComponentContext}>
        <AppRender {...props} />
      </OwnerComponentContext.Provider>
    </AppContext.Provider>
  );
}

function initWatchMethods(pageContext) {
  // const { watch = {}, watchState = {}, watchWidget = {}, watchEffects = {}} = pageAPI
  const { watch = {}, watchState = {}, watchWidget = {}, watchEffects = {} } = {};

  // # watch effect
  const watchEffectDisposers = Object.keys(watchEffects).map((fnName) => {
    return autorun(watchEffects[fnName].bind(pageContext));
  });
  const disposers = watchEffectDisposers;

  // # watch state
  Object.keys(watchState).map((key) => runWatcher(parseWatcher(watchState[key]), pageContext.state, key, 'watchState'));

  // # watch widgets
  Object.keys(watchWidget).map((key) =>
    runWatcher(parseWatcher(watchWidget[key]), pageContext.widgets, key, 'watchWidgets'),
  );

  function runWatcher({ handler, immediate } = {} as any, target, key, label) {
    if (!handler) {
      console.error(
        `Invalid ${label}(${key}) of ${pageContext.node.widgetType}, watch must a function or {handler: function}`,
      );
      return;
    }
    const disposer = reaction(() => get(target, key), handler.bind(pageContext), { fireImmediately: immediate });
    disposers.push(disposer);
  }
}

function parseWatcher(watcher) {
  if (!watcher) return;
  if (watcher instanceof Function) {
    return { handler: watcher, immediate: false };
  }
  const { handler, immediate = false } = watcher;
  if (!(handler instanceof Function)) return;
  return { handler, immediate };
}

function getPageEvents(
  pageCodeContext,
  listenerInstances,
): {
  [eventName: string]: () => void;
} {
  const emit = generateEmit(pageCodeContext.__internal__?.$w);

  return new Proxy(
    {},
    {
      get: (target, eventName) => {
        return (eventData) => emit(eventName, listenerInstances, eventData, {}, undefined, {}, {}, pageCodeContext.id);
      },
    },
  );
}
