import type { UserInfo } from '@vben/types';

import type { OpenOfficeDocumentConfig } from '#/components/GlobalOfficeDocumentModal/types';
import type { ReportViewConfig } from '#/components/GlobalReportViewModal/types';

import { createApp, h } from 'vue';
import type { RouteLocationRaw } from 'vue-router';

import { useGlobSetting, useMessage } from '@jnpf/hooks';
import { isNullOrUnDef, isNumber, isString } from '@jnpf/utils';

import { useAccessStore, useUserStore } from '@vben/stores';

import { Spin } from 'ant-design-vue';
import dayjs from 'dayjs';
import { cloneDeep } from 'lodash-es';
import mitt from 'mitt';

import { defHttp } from '#/api/request';
import { buildGlobalAuditRequestHeaders } from '#/components/FormGenerator/src/helper/auditDisplay';
import { $t } from '#/locales';
import { router } from '#/router';

import { APP_BACKEND_PREFIX, APP_PREFIX } from './constants';
import { ELECTRONIC_SIGNATURE_MODEL_ID, REVIEW_SIGNATURE_MODEL_ID } from './constants/electronicSignature';
import { activateCustomViewParams, getCustomViewParam } from './custom-view-context';

export const JNPF_ROUTE_TITLE_QUERY = 'jnpfTitle';

// 创建事件总线
const emitter = mitt<{
  CLOSE_REPORT_VIEW: undefined;
  OPEN_CUSTOM_VIEW_MODAL: OpenCustomViewEvent;
  OPEN_FLOW_DETAIL: OpenFlowDetailConfig;
  OPEN_FLOW_EDIT: OpenFlowEditConfig;
  OPEN_FLOW_FORM: OpenFlowFormConfig;
  OPEN_FLOW_LIST_MODAL: OpenFlowListConfig;
  OPEN_FORM_MODAL: OpenFormModalConfig;
  OPEN_LIST_MODAL: OpenListConfig;
  OPEN_OFFICE_DOCUMENT: OpenOfficeDocumentConfig;
  OPEN_PRINT_MODAL: {
    data: any;
    onDownloadPdf?: (data: any) => void;
    onError?: (error: any) => void;
    onPrint?: (data: any) => void;
    showPdfBtn?: boolean;
    template: string;
    title?: string;
    type: 'html' | 'html-file' | 'vue';
  };
  OPEN_REPORT_VIEW: ReportViewConfig;
}>();

interface OnlineUserInfo extends UserInfo {
  token?: string;
}

interface OpenFlowDetailConfig {
  f_id?: number | string;
  f_flow_state?: number | string;
  flow_id?: number | string;
  flowId?: number | string;
  flowState?: number | string;
  flowTaskId?: number | string;
  flow_state?: number | string;
  id?: number | string;
  isFlow?: number | string;
  opType?: number | string;
  operatorId?: number | string;
  taskId?: number | string;
  [key: string]: any;
}

interface OpenFlowEditConfig extends OpenFlowDetailConfig {
  defaultFullscreen?: boolean;
  hideCancelBtn?: boolean;
  hideSaveBtn?: boolean;
  showFullscreen?: boolean;
}

interface OpenFlowFormConfig {
  data?: Record<string, any>;
  defaultFullscreen?: boolean;
  flow_id?: number | string;
  flowId?: number | string;
  formData?: Record<string, any>;
  hideCancelBtn?: boolean;
  hideSaveBtn?: boolean;
  id?: number | string;
  isFlow?: number | string;
  onSuccess?: () => void;
  params?: Record<string, any>;
  query?: Record<string, any>;
  showFullscreen?: boolean;
  success?: () => void;
  template?: number | string;
  title?: string;
  [key: string]: any;
}

interface OpenFlowListConfig {
  flow_id?: number | string;
  flowId?: number | string;
  menuId?: number | string;
  params?: Record<string, any>;
  path?: string;
  query?: Record<string, any>;
  routeQuery?: Record<string, any>;
  title?: string;
}

interface OpenFormModalConfig {
  fieldMapping?: Record<string, string>;
  id?: string;
  mode?: 'detail' | 'form';
  modelId: string;
  onCancel?: () => void;
  onConfirm?: (data: any) => void;
  onSubmit?: (data: any) => Promise<void> | void;
  params?: Record<string, any>;
  submitMode?: 'custom' | 'default';
  title?: string;
  type?: 'drawer' | 'fullScreen' | 'modal';
  width?: string;
}

interface SignMetaData {
  biz_button?: string;
  biz_data?: any[];
  biz_form_id?: string;
  biz_module?: string;
  biz_title?: string;
  is_biz_form?: boolean;
  is_review_button?: boolean;
}

interface SignConfig {
  allowMyself?: boolean;
  isFaceToFace?: boolean;
  metaData?: SignMetaData;
  onCancel?: OpenFormModalConfig['onCancel'];
  onSubmit?: OpenFormModalConfig['onSubmit'];
  title?: string;
}

interface OpenListConfig {
  menuId?: number | string;
  modelId?: number | string;
  params?: Record<string, any>;
  path?: string;
  query?: Record<string, any>;
  replace?: boolean;
  routeQuery?: Record<string, any>;
  title?: string;
}

export interface OpenCustomViewConfig {
  /** 相对 src/views 的页面路径，仅支持 x/** 目录。 */
  page: string;
  /** 弹窗关闭时回传的数据。 */
  onClose?: (result?: any) => void;
  /** 传给自定义页面的参数，可通过 onlineUtils.getViewParam 获取。 */
  params?: Record<string, any>;
  /** 弹窗标题。 */
  title?: string;
}

interface OpenCustomViewEvent extends OpenCustomViewConfig {
  instanceId: number;
  params: Record<string, any>;
}

interface RequestFormDataConfig {
  data?: Record<string, any>;
  f_id?: number | string;
  flow_id?: number | string;
  flowId?: number | string;
  id?: number | string;
  menuId?: number | string;
  modelId?: number | string;
  onlineUtilsOpen?: boolean;
  propsValue?: any;
  row?: Record<string, any>;
  rowKey?: string;
  useDataChange?: boolean;
  [key: string]: any;
}

interface OnlineRouteOptions {
  query?: Record<string, any>;
  replace?: boolean;
  title?: string;
}

type OnlineRouteConfig = OnlineRouteOptions & {
  hash?: string;
  name?: string;
  params?: Record<string, any>;
  path?: string;
};

export function getJnpfAppEnCode() {
  let appEnCode: string = '';
  if (window.location.pathname?.startsWith(`/${APP_PREFIX}`)) {
    const list = window.location.pathname.split('/');
    appEnCode = list[1] ? list[1].replace(APP_PREFIX, '') : '';
  }
  if (window.location.pathname?.startsWith(`/${APP_PREFIX}`.toUpperCase())) {
    const list = window.location.pathname.split('/');
    appEnCode = list[1] ? list[1].replace(APP_PREFIX.toUpperCase(), '') : '';
  }
  return appEnCode;
}
export function getRealJnpfAppEnCode() {
  let appEnCode: string = getJnpfAppEnCode();
  if (!appEnCode) return appEnCode;
  if (appEnCode.startsWith(`${APP_BACKEND_PREFIX}`)) {
    appEnCode = appEnCode.replace(APP_BACKEND_PREFIX, '');
  }
  if (appEnCode.startsWith(`${APP_BACKEND_PREFIX}`.toUpperCase())) {
    appEnCode = appEnCode.replace(APP_BACKEND_PREFIX.toUpperCase(), '');
  }
  return appEnCode;
}

export function getJnpfRouteTitle(query?: Record<string, any>) {
  const value = query?.[JNPF_ROUTE_TITLE_QUERY];
  const title = Array.isArray(value) ? value[0] : value;
  if (isNullOrUnDef(title) || title === '') return '';
  const titleString = String(title);
  try {
    return decodeURIComponent(titleString);
  } catch {
    return titleString;
  }
}

function parseJnpfListQuery(value) {
  const raw = Array.isArray(value) ? value[0] : value;
  if (!raw || typeof raw !== 'string') return {};
  const parseJson = (str) => {
    try {
      const data = JSON.parse(str);
      return data && typeof data === 'object' && !Array.isArray(data) ? data : {};
    } catch {
      return null;
    }
  };
  const data = parseJson(raw);
  if (data) return data;
  try {
    return parseJson(decodeURIComponent(raw)) || {};
  } catch {
    return {};
  }
}

export function getJnpfRouteParam(paramName: string) {
  if (!paramName) return undefined;
  const query = router.currentRoute.value?.query || {};
  const listQueryParams = parseJnpfListQuery(query.jnpfListQuery);
  if (Object.prototype.hasOwnProperty.call(listQueryParams, paramName)) return listQueryParams[paramName];
  if (Object.prototype.hasOwnProperty.call(query, paramName)) {
    const value = query[paramName];
    return Array.isArray(value) ? value[0] : value;
  }
  return undefined;
}

function normalizeRouteQuery(query: Record<string, any> = {}) {
  return Object.keys(query).reduce<Record<string, any>>((res, key) => {
    const value = query[key];
    if (!isNullOrUnDef(value)) res[key] = value;
    return res;
  }, {});
}

function appendQueryToUrl(url: string, query: Record<string, any> = {}) {
  if (!url || !Object.keys(query).length) return url;
  const hashIndex = url.indexOf('#');
  const pathWithQuery = hashIndex > -1 ? url.slice(0, hashIndex) : url;
  const hash = hashIndex > -1 ? url.slice(hashIndex) : '';
  const queryIndex = pathWithQuery.indexOf('?');
  const path = queryIndex > -1 ? pathWithQuery.slice(0, queryIndex) : pathWithQuery;
  const search = queryIndex > -1 ? pathWithQuery.slice(queryIndex + 1) : '';
  const searchParams = new URLSearchParams(search);

  Object.keys(query).forEach((key) => {
    const value = query[key];
    if (isNullOrUnDef(value)) return;
    searchParams.delete(key);
    if (Array.isArray(value)) {
      value.forEach((item) => {
        if (!isNullOrUnDef(item)) searchParams.append(key, String(item));
      });
      return;
    }
    searchParams.set(key, String(value));
  });

  const newSearch = searchParams.toString();
  return `${path}${newSearch ? `?${newSearch}` : ''}${hash}`;
}

function isPlainRecord(value: any): value is Record<string, any> {
  return value && typeof value === 'object' && !Array.isArray(value);
}

function getRequestFormDataId(config: RequestFormDataConfig) {
  const row = isPlainRecord(config.row) ? config.row : {};
  const rowKey = config.rowKey || 'id';
  return config.id ?? config.f_id ?? row[rowKey] ?? row.id ?? row.f_id;
}

function getRequestFormDataFlowId(config: RequestFormDataConfig) {
  return config.flowId ?? config.flow_id;
}

function parseRequestFormData(res: any, id: number | string) {
  const dataForm = isPlainRecord(res?.data) ? res.data : isPlainRecord(res) ? res : {};
  const rawData = dataForm.data;
  if (!rawData) return { id: dataForm.id || id };
  if (isPlainRecord(rawData)) return { ...rawData, id: dataForm.id || rawData.id || id };
  try {
    const formData = JSON.parse(rawData);
    return { ...formData, id: dataForm.id || formData.id || id };
  } catch {
    throw new Error('[onlineUtils.requestFormData] invalid form data');
  }
}

const flowTemplateIdCache = new Map<string, Promise<number | string | undefined>>();
const flowFormModelIdCache = new Map<string, Promise<number | string | undefined>>();

async function getTemplateIdByFlowVersionId(flowId: number | string) {
  const key = String(flowId);
  if (!flowTemplateIdCache.has(key)) {
    flowTemplateIdCache.set(
      key,
      defHttp
        .get({ url: `/api/workflow/template/Info/${key}` }, { errorMessageMode: 'none' })
        .then((res) => {
          const flowInfo = res?.data ?? res;
          return flowInfo?.id && (!flowInfo?.flowId || String(flowInfo.flowId) === key) ? flowInfo.id : undefined;
        })
        .catch(() => undefined),
    );
  }
  return flowTemplateIdCache.get(key);
}

function normalizeIdResult(res: any) {
  const value = res?.data ?? res;
  return isNullOrUnDef(value) || value === '' ? undefined : value;
}

function getFormModelIdByTemplateId(templateId: number | string) {
  return defHttp
    .get({ url: `/api/workflow/template/StartFormId/${templateId}` }, { errorMessageMode: 'none' })
    .then((res) => {
      const data = res?.data ?? res;
      return normalizeIdResult(data?.formId ?? data?.id);
    })
    .catch(() => undefined);
}

async function getFormModelIdByFlowId(flowId: number | string) {
  const key = String(flowId);
  if (!flowFormModelIdCache.has(key)) {
    flowFormModelIdCache.set(
      key,
      getFormModelIdByTemplateId(key).then(async (modelId) => {
        if (modelId) return modelId;
        const templateId = await getTemplateIdByFlowVersionId(key);
        return templateId ? getFormModelIdByTemplateId(templateId) : undefined;
      }),
    );
  }
  return flowFormModelIdCache.get(key);
}

export async function resolveOpenFlowEditConfig<T extends Record<string, any>>(config: T) {
  const flowId = config.flowId ?? config.flow_id;
  if (!flowId) return config;
  const templateId = await getTemplateIdByFlowVersionId(flowId);
  return templateId ? { ...config, flowId: templateId } : config;
}

export async function resolveOpenFlowListConfig<T extends Record<string, any>>(config: T) {
  const flowId = config.flowId ?? config.flow_id;
  if (!flowId) return config;
  const templateId = await getTemplateIdByFlowVersionId(flowId);
  return templateId ? { ...config, flowId: templateId } : config;
}

export function normalizeOpenFlowFormConfig(configOrFlowId: number | OpenFlowFormConfig | string, paramsArg?: Record<string, any>) {
  const config: OpenFlowFormConfig = typeof configOrFlowId === 'object' ? { ...configOrFlowId } : { flowId: configOrFlowId, params: paramsArg };
  const flowId = config.flowId ?? config.flow_id ?? config.template ?? config.id;
  if (!flowId) return null;
  const params = config.params || config.query || config.data || {};
  const normalizedParams = isPlainRecord(params) ? params : {};
  return {
    ...config,
    flowId,
    formData: { ...config.formData, ...normalizedParams },
    id: '',
    isFlow: config.isFlow ?? 0,
    opType: '-1',
    params: normalizedParams,
  };
}

export function isDraftFlowConfig(config: Record<string, any> = {}) {
  const flowState = config.flowState ?? config.f_flow_state ?? config.flow_state;
  return flowState !== undefined && String(flowState) === '0';
}

export function normalizeOpenFlowEditConfig(configOrFlowId: number | OpenFlowEditConfig | string, taskIdArg?: number | string) {
  const config: OpenFlowEditConfig = typeof configOrFlowId === 'object' ? { ...configOrFlowId } : { flowId: configOrFlowId, taskId: taskIdArg };
  const flowId = config.flowId ?? config.flow_id;
  const isDraftFlow = isDraftFlowConfig(config);
  const taskIdValue = isDraftFlow
    ? (config.id ?? config.f_id ?? config.taskId ?? config.flowTaskId)
    : (config.taskId ?? config.flowTaskId ?? config.id ?? config.f_id);
  if (!flowId || !taskIdValue) return null;
  return {
    ...config,
    flowId,
    id: taskIdValue,
    isFlow: config.isFlow ?? 0,
    opType: config.opType ?? '-1',
    showHeaderCancelBtn: config.showHeaderCancelBtn ?? true,
    taskId: taskIdValue,
  };
}

let globalLoadingApp: null | ReturnType<typeof createApp> = null;
let globalLoadingEl: HTMLElement | null = null;

function createLoadingEl(tip?: string): HTMLElement {
  const el = document.createElement('div');
  el.dataset.jnpfLoading = '';
  el.style.cssText = 'position:fixed;inset:0;z-index:9999;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,0.65)';
  globalLoadingApp = createApp({ render: () => h(Spin, { spinning: true, tip }) });
  globalLoadingApp.mount(el);
  return el;
}

export const onlineUtils = {
  /**
   * 计算指定日期的前后周期日期。
   * @param date 指定日期，支持毫秒时间戳和常用日期字符串
   * @param period 周期数，默认为 1
   * @param unit 周期单位：day | week | month | quarter | year
   * @param isAdvance 是否提前计算，默认为 false
   * @returns YYYY-MM-DD 格式的日期；参数无效时返回空字符串
   */
  calculateDate(date: Date | number | string, period: number = 1, unit: 'day' | 'month' | 'quarter' | 'week' | 'year', isAdvance = false) {
    const normalizedDate = isString(date) && /^\d{11,}$/.test(date.trim()) ? Number(date) : date;
    const targetDate = dayjs(normalizedDate);
    const amount = Number(period);
    if (!targetDate.isValid() || !Number.isFinite(amount)) return '';

    const units = {
      day: 'day',
      month: 'month',
      quarter: 'month',
      week: 'week',
      year: 'year',
    } as const;
    if (!(unit in units)) return '';

    const signedAmount = (isAdvance ? -1 : 1) * amount * (unit === 'quarter' ? 3 : 1);
    return targetDate.add(signedAmount, units[unit]).format('YYYY-MM-DD');
  },
  // 获取用户信息
  getUserInfo() {
    const accessStore = useAccessStore();
    const userStore = useUserStore();
    const userInfo: OnlineUserInfo = userStore.getUserInfo as OnlineUserInfo;
    userInfo.token = accessStore.accessToken as string;
    return userInfo;
  },
  // 获取设备信息
  getDeviceInfo() {
    const deviceInfo = { vueVersion: '3', origin: 'pc' };
    return deviceInfo;
  },
  // 请求
  request(url: string, method: string, data = {}, headers = {}) {
    const auditHeaders = buildGlobalAuditRequestHeaders(url, method, data, headers);
    return defHttp[method ? method.toLowerCase() : 'get']({ url, data, headers: auditHeaders });
  },
  /**
   * 获取低代码表单完整 formData
   * @param configOrModelId 表单模型ID，或查询配置对象
   * @param idArg 数据ID
   */
  requestFormData(configOrModelId?: number | RequestFormDataConfig | string, idArg?: number | string) {
    const config: RequestFormDataConfig =
      configOrModelId && typeof configOrModelId === 'object' ? { ...configOrModelId } : { modelId: configOrModelId, id: idArg };
    const modelId = config.modelId;
    const flowId = getRequestFormDataFlowId(config);
    const id = getRequestFormDataId(config);
    if (((isNullOrUnDef(modelId) || modelId === '') && (isNullOrUnDef(flowId) || flowId === '')) || isNullOrUnDef(id) || id === '') {
      return Promise.reject(new Error('[onlineUtils.requestFormData] modelId or flowId, and id are required'));
    }

    const resolveModelId = isNullOrUnDef(flowId) || flowId === '' ? Promise.resolve(modelId) : getFormModelIdByFlowId(flowId).then((id) => id || modelId);
    return resolveModelId.then((resolvedModelId) => {
      if (isNullOrUnDef(resolvedModelId) || resolvedModelId === '') {
        return Promise.reject(new Error('[onlineUtils.requestFormData] modelId not found by flowId'));
      }

      const data = isPlainRecord(config.data) ? config.data : {};
      const shouldUseDataChange = config.useDataChange ?? false;
      if (shouldUseDataChange) {
        const query: Record<string, any> = { id, menuId: config.menuId, onlineUtilsOpen: config.onlineUtilsOpen ?? true, ...data };
        if (!isNullOrUnDef(config.propsValue) && config.propsValue !== '') query.propsValue = config.propsValue;
        return defHttp.post({ url: `/api/visualdev/OnlineDev/${resolvedModelId}/DataChange`, data: query }).then((res) => parseRequestFormData(res, id));
      }

      return defHttp
        .get({
          url: `/api/visualdev/OnlineDev/${resolvedModelId}/${id}`,
          data: { menuId: config.menuId, onlineUtilsOpen: config.onlineUtilsOpen ?? true, ...data },
        })
        .then((res) => parseRequestFormData(res, id));
    });
  },
  /**
   * 获取当前 URL 或 openList 传入参数
   * @param paramName 参数名称
   */
  getParam(paramName: string) {
    return getJnpfRouteParam(paramName);
  },
  /** 获取当前自定义页面弹窗的参数。 */
  getViewParam(paramName: string) {
    return getCustomViewParam(paramName);
  },
  /**
   * 路由跳转
   * @param url 目标地址，或 vue-router 路由对象
   * @param options 扩展配置。传 title 后，页面标题和页签标题会优先显示该标题
   */
  route(url: OnlineRouteConfig | string, options: OnlineRouteOptions = {}) {
    if (!url) return;
    if (isString(url)) {
      const query = normalizeRouteQuery(options.query);
      if (options.title) query[JNPF_ROUTE_TITLE_QUERY] = options.title;
      const targetUrl = appendQueryToUrl(url, query);
      return options.replace ? router.replace(targetUrl) : router.push(targetUrl);
    }

    const { replace, title, ...routeConfig } = url;
    const query = normalizeRouteQuery({ ...(routeConfig.query || {}), ...(options.query || {}) });
    const routeTitle = options.title || title;
    if (routeTitle) query[JNPF_ROUTE_TITLE_QUERY] = routeTitle;
    const target = { ...routeConfig, query } as RouteLocationRaw;
    return options.replace || replace ? router.replace(target) : router.push(target);
  },
  /**
   * 打开低代码列表页
   * @param config 列表配置
   * @param config.menuId 菜单ID，优先通过菜单ID定位列表路由
   * @param config.modelId 模型ID，未传 menuId 时通过模型ID定位列表路由
   * @param config.path 目标路由路径，传入时优先使用
   * @param config.params 传给目标列表的参数，需在目标列表过滤规则中选择“URL/openList参数”后才参与查询
   * @param config.query 传给目标列表的参数，params 的别名
   * @param config.routeQuery 额外 URL query 参数
   * @param config.title 弹窗标题，不传时使用目标菜单或列表名称
   * @param config.replace 是否替换当前路由
   */
  openList(config: OpenListConfig) {
    if (!config?.path && !config?.menuId && !config?.modelId) {
      console.error('[onlineUtils.openList] path, menuId or modelId is required');
      return;
    }
    emitter.emit('OPEN_LIST_MODAL', config);
  },
  /**
   * 在当前页签内容区域打开 src/views/x 下的自定义页面。
   * @param config.page 相对 src/views 的页面路径，例如 x/eln/demo
   * @param config.params 页面参数，可通过 getViewParam 获取
   */
  openCustomView(config: OpenCustomViewConfig) {
    if (!config?.page) {
      console.error('[onlineUtils.openCustomView] page is required');
      return;
    }
    const params = isPlainRecord(config.params) ? { ...config.params } : {};
    const instanceId = activateCustomViewParams(params);
    emitter.emit('OPEN_CUSTOM_VIEW_MODAL', { ...config, instanceId, params });
  },
  /**
   * 打开流程详情
   * @param configOrFlowId 流程 flowId，或流程详情配置对象
   * @param taskIdArg 流程任务 taskId
   */
  openFlowDetail(configOrFlowId: number | OpenFlowDetailConfig | string, taskIdArg?: number | string) {
    const config: OpenFlowDetailConfig = typeof configOrFlowId === 'object' ? { ...configOrFlowId } : { flowId: configOrFlowId, taskId: taskIdArg };
    const flowId = config.flowId ?? config.flow_id;
    const taskIdValue = isDraftFlowConfig(config)
      ? (config.id ?? config.f_id ?? config.taskId ?? config.flowTaskId)
      : (config.taskId ?? config.flowTaskId ?? config.id ?? config.f_id);
    if (!flowId) {
      console.error('[onlineUtils.openFlowDetail] flowId is required');
      return;
    }
    if (!taskIdValue) {
      console.error('[onlineUtils.openFlowDetail] id is required');
      return;
    }
    const detailConfig = {
      ...config,
      flowId,
      id: taskIdValue,
      isFlow: config.isFlow ?? 0,
      opType: config.opType ?? 0,
      taskId: taskIdValue,
    };
    emitter.emit('OPEN_FLOW_DETAIL', detailConfig);
  },
  /**
   * 打开流程编辑/处理页面
   * @param configOrFlowId 流程 flowId，或流程编辑配置对象
   * @param taskIdArg 流程任务/业务数据 id
   */
  openFlowEdit(configOrFlowId: number | OpenFlowEditConfig | string, taskIdArg?: number | string) {
    const flowEditConfig = normalizeOpenFlowEditConfig(configOrFlowId, taskIdArg);
    if (!flowEditConfig) {
      console.error('[onlineUtils.openFlowEdit] flowId and id are required');
      return;
    }
    emitter.emit('OPEN_FLOW_EDIT', flowEditConfig);
  },
  /**
   * 打开发起流程表单
   * @param configOrFlowId 流程 flowId，或发起流程配置对象
   * @param paramsArg 初始表单参数
   */
  openFlowForm(configOrFlowId: number | OpenFlowFormConfig | string, paramsArg?: Record<string, any>) {
    const flowFormConfig = normalizeOpenFlowFormConfig(configOrFlowId, paramsArg);
    if (!flowFormConfig) {
      console.error('[onlineUtils.openFlowForm] flowId is required');
      return;
    }
    emitter.emit('OPEN_FLOW_FORM', flowFormConfig);
  },
  /**
   * 弹窗打开流程关联的数据列表页
   * @param configOrFlowId 流程模板ID，或流程列表配置对象
   * @param paramsArg 传给目标列表的参数，需在目标列表过滤规则中选择“URL/openList参数”后才参与查询
   */
  openFlowList(configOrFlowId: number | OpenFlowListConfig | string, paramsArg?: Record<string, any>) {
    const config: OpenFlowListConfig = typeof configOrFlowId === 'object' ? { ...configOrFlowId } : { flowId: configOrFlowId, params: paramsArg };
    const flowId = config?.flowId ?? config?.flow_id;
    if (!flowId && !config?.menuId && !config?.path) {
      console.error('[onlineUtils.openFlowList] flowId, menuId or path is required');
      return;
    }
    emitter.emit('OPEN_FLOW_LIST_MODAL', config);
  },
  // 消息提示
  toast(message: number | string, type: string = 'info', duration: number = 3000) {
    const { createMessage } = useMessage();
    if (!isString(message) && !isNumber(message)) return;
    const newDuration = duration / 1000;
    const config = { content: message, type, duration: newDuration };
    createMessage[type] && createMessage[type](config);
  },
  // 确认
  confirm(message: string, handleOk: () => void, handleCancel: () => void = () => {}) {
    const { createConfirm } = useMessage();
    if (!isString(message)) return;

    createConfirm({
      iconType: 'warning',
      title: $t('common.tipTitle'),
      content: message,
      onOk: () => {
        try {
          handleOk();
        } catch {}
      },
      onCancel: () => {
        try {
          handleCancel();
        } catch {}
      },
    });
  },
  /**
   * 打开低代码表单弹窗
   * @param config 弹窗配置
   * @param config.modelId 表单模型ID（必填）
   * @param config.id 数据ID（编辑时传入）
   * @param config.title 弹窗标题
   * @param config.width 弹窗宽度
   * @param config.type 弹窗类型：modal(居中弹窗) | drawer(右侧弹窗) | fullScreen(全屏)
   * @param config.params 额外参数
   * @param config.submitMode 提交模式：default(默认提交到后端) | custom(自定义提交)
   * @param config.onSubmit 自定义提交回调（submitMode='custom'时生效）
   * @param config.onConfirm 确认回调（submitMode='default'时生效）
   * @param config.onCancel 取消回调
   * @param config.mode 展示模式：form(表单编辑) | detail(详情展示)，默认为 form
   */
  openFormModal(config: OpenFormModalConfig) {
    if (!config.modelId) {
      console.error('[onlineUtils.openFormModal] modelId is required');
      return;
    }
    emitter.emit('OPEN_FORM_MODAL', config);
  },
  /**
   * 打开签名表单弹窗
   * @param config 签名配置
   * @param config.isFaceToFace 是否面签，默认 false
   * @param config.allowMyself 面签是否允许自己，默认 false
   * @param config.metaData 签名业务元数据，序列化后作为 meta_data 传给签名表单
   */
  sign(config: SignConfig = {}) {
    const { allowMyself = false, isFaceToFace = false, metaData, onCancel, onSubmit, title = '' } = config;
    const hasMetaDataValue =
      metaData &&
      Object.values(metaData).some((value) =>
        Array.isArray(value) ? value.length > 0 : typeof value === 'string' ? value.trim() !== '' : value !== undefined && value !== null,
      );
    const metaDataString = hasMetaDataValue ? (JSON.stringify({ is_review_button: false, ...metaData }) ?? '') : '';
    const formConfig: OpenFormModalConfig = {
      modelId: isFaceToFace ? REVIEW_SIGNATURE_MODEL_ID : ELECTRONIC_SIGNATURE_MODEL_ID,
      title,
      type: 'modal',
      width: '800px',
      submitMode: 'custom',
      onSubmit,
      onCancel,
      params: { meta_data: metaDataString },
      fieldMapping: { meta_data: 'meta_data' },
    };
    if (isFaceToFace && allowMyself === true) {
      formConfig.params.allow_self = 'yes';
      formConfig.fieldMapping.biz_action = 'allow_self';
    }
    this.openFormModal(formConfig);
  },
  // 获取事件总线（供组件监听使用）
  getEmitter() {
    return emitter;
  },
  /**
   * 打开自定义打印弹窗
   * @param config 打印配置
   * @param config.template 模板内容（HTML字符串、Vue组件名称或HTML文件路径）
   * @param config.type 模板类型：'html' | 'vue' | 'html-file'
   *   - 'html': 直接使用传入的HTML字符串作为模板
   *   - 'vue': 使用Vue组件名称作为模板
   *   - 'html-file': 通过文件路径加载独立HTML文件
   * @param config.data 打印数据（任意对象，模板中通过 window.PRINT_DATA 访问）
   * @param config.title 弹窗标题，默认"打印预览"
   * @param config.showPdfBtn 是否显示导出PDF按钮，默认true
   * @param config.onPrint 打印回调
   * @param config.onDownloadPdf 导出PDF回调
   * @param config.onError 错误回调
   * @example
   * // 1. HTML字符串方式
   * onlineUtils.print({
   *   template: '<div>...</div>',
   *   type: 'html',
   *   data: { name: '张三', age: 25 }
   * });
   *
   * // 2. Vue组件方式
   * onlineUtils.print({
   *   template: 'ReportTemplate',
   *   type: 'vue',
   *   data: { reportInfo: {...}, testItems: [...] }
   * });
   *
   * // 3. HTML文件方式（推荐，方便版本管理）
   * onlineUtils.print({
   *   template: '/print-templates/quality-report.html',
   *   type: 'html-file',
   *   data: { reportInfo: {...}, testItems: [...] }
   * });
   */
  print(config: {
    data: any;
    onDownloadPdf?: (data: any) => void;
    onError?: (error: any) => void;
    onPrint?: (data: any) => void;
    showPdfBtn?: boolean;
    template: string;
    title?: string;
    type: 'html' | 'html-file' | 'vue';
  }) {
    if (!config.template) {
      console.error('[onlineUtils.print] template is required');
      return;
    }
    emitter.emit('OPEN_PRINT_MODAL', config);
  },
  /** 打开报告查看弹窗 */
  openReportView(config: ReportViewConfig) {
    if (!config.tabs?.length) {
      console.error('[onlineUtils.openReportView] tabs is required');
      return;
    }
    emitter.emit('OPEN_REPORT_VIEW', config);
  },
  /** 关闭报告查看弹窗 */
  closeReportView() {
    emitter.emit('CLOSE_REPORT_VIEW');
  },
  /**
   * 用 OnlyOffice 在线打开附件（docx/xlsx/pptx 等），可编辑并保存回原附件。
   * @param config.file 附件字段里的 fileItem 对象，必填
   * @param config.mode edit | view，默认 edit；最终以后端返回的模式为准（他人持锁时会降级只读）
   * @param config.onSave 编辑期间有过改动、且弹窗关闭时触发；**不代表后端已写回**，详见类型定义注释
   * @example
   * onlineUtils.openOfficeDocument({ file });
   * onlineUtils.openOfficeDocument({
   *   file,
   *   mode: 'view',
   *   bizModule: 'lims_jianyan',
   *   bizDataId: row.f_id,
   *   onSave: () => reloadAttachments(),
   * });
   */
  openOfficeDocument(config: OpenOfficeDocumentConfig) {
    if (!config?.file?.fileId) {
      console.error('[onlineUtils.openOfficeDocument] file.fileId is required');
      return;
    }
    emitter.emit('OPEN_OFFICE_DOCUMENT', config);
  },
  showLoading(tip?: string) {
    if (globalLoadingEl) return;
    globalLoadingEl = createLoadingEl(tip);
    document.body.append(globalLoadingEl);
  },
  hideLoading() {
    if (!globalLoadingEl) return;
    globalLoadingApp?.unmount();
    globalLoadingApp = null;
    globalLoadingEl.remove();
    globalLoadingEl = null;
  },
};
export function getParamList(templateJson, data?, rowKey = 'id') {
  if (!templateJson?.length) return [];
  for (const e of templateJson) {
    if (e.sourceType == 1 && data) {
      e.defaultValue = data[e.relationField] || data[e.relationField] == 0 || data[e.relationField] == false ? data[e.relationField] : '';
    }
    if (e.sourceType == 4 && e.relationField == '@formId') e.defaultValue = data[rowKey] || '';
  }
  return templateJson;
}
export function getLaunchFlowParamList(transferList, data?, rowKey = 'id') {
  transferList = cloneDeep(transferList);
  if (!transferList?.length) return [];
  for (const e of transferList) {
    if (e.sourceType == 1) {
      if (e.sourceValue == '@formId') {
        e.defaultValue = data[rowKey] || '';
      } else {
        if (e.sourceValue.includes('-')) {
          const tableVModel = e.sourceValue.split('-')[0];
          const childVModel = e.sourceValue.split('-')[1];
          e.defaultValue = (data[tableVModel] || []).map((o) => o[`${childVModel}_jnpfId`]);
        } else {
          const key = `${e.sourceValue}_jnpfId`;
          e.defaultValue = isNullOrUnDef(data[key]) ? (isNullOrUnDef(data[e.sourceValue]) ? '' : data[e.sourceValue]) : data[key];
        }
      }
    } else {
      e.defaultValue = e.sourceValue;
    }
  }
  return transferList;
}

// 开始：解决老的vue2动态导入文件语法vite不支持的问题
const allModules: any = import.meta.glob('../views/**/*.vue');
export function importViewsFile(path): Promise<any> {
  if (path.startsWith('/')) {
    path = path.slice(1);
  }
  let page = '';
  let realPage = '';
  if (path.endsWith('.vue')) {
    page = `../views/${path}`;
    realPage = `../views/${path}`;
  } else {
    page = `../views/${path}.vue`;
    realPage = `../views/${path}/index.vue`;
  }
  return new Promise((resolve, reject) => {
    let flag = true;
    for (const path in allModules) {
      if (path == page || path == realPage) {
        flag = false;
        allModules[path]().then((mod) => {
          resolve(mod);
        });
      }
    }
    if (flag) {
      reject(new Error(`该文件不存在:${page}`));
    }
  });
}
// 结束：解决老的vue2动态导入文件语法 vite不支持的问题

export function getAuthMediaUrl(url, isRedirect = true) {
  if (!url) return '';
  // eslint-disable-next-line regexp/no-unused-capturing-group
  const base64WithPrefixRegex = /^data:([a-zA-Z0-9]+\/[a-zA-Z0-9-.+]+);base64,([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$/;
  if (base64WithPrefixRegex.test(url)) return url;
  const userStore = useUserStore();
  const userInfo: OnlineUserInfo = userStore.getUserInfo as OnlineUserInfo;
  const securityKey = userInfo?.securityKey || '';
  const globSetting = useGlobSetting();
  if (!securityKey) return globSetting.apiURL + url;
  const realUrl = `${globSetting.apiURL + url + (url.includes('?') ? '&' : '?')}s=${securityKey}${isRedirect ? '' : '&t=t'}`;
  return realUrl;
}
