import { computed, ref } from 'vue';
import type { LocationQueryRaw, RouteLocationNormalized, RouteLocationRaw } from 'vue-router';
import { defineStore } from 'pinia';
import type { ElegantConstRoute } from '@elegant-router/types';
import {
  OBJECT_CONTEXT_QUERY_KEY,
  getObjectContextDomainConfigByPath,
  isObjectContextEntryPath
} from '@/constants/object-context';
import { fetchGetObjectContext } from '@/service/api/object-context';
import { $t } from '@/locales';
import { SetupStoreId } from '@/enum';
import { useRouteStore } from '../route';

function createEmptyState(): App.ObjectContext.State {
  return {
    domainKey: '',
    objectType: '',
    objectId: '',
    objectName: '',
    objectSummary: null,
    contextScopedMenus: [],
    buttonCodes: [],
    defaultRouteKey: '',
    defaultRoutePath: '',
    isReady: false
  };
}

function normalizePath(path: string) {
  if (!path) {
    return '/';
  }

  return path.endsWith('/') && path !== '/' ? path.slice(0, -1) : path;
}

function isRouteMatchedByPrefix(path: string, prefix: string) {
  const normalizedPath = normalizePath(path);
  const normalizedPrefix = normalizePath(prefix);

  return normalizedPath === normalizedPrefix || normalizedPath.startsWith(`${normalizedPrefix}/`);
}

function findDomainRootRoute(
  routes: ElegantConstRoute[],
  config: App.ObjectContext.DomainConfig
): ElegantConstRoute | null {
  for (const route of routes) {
    if (config.routePathPrefixes.some(prefix => isRouteMatchedByPrefix(route.path, prefix))) {
      return route;
    }

    if (route.children?.length) {
      const matchedChild = findDomainRootRoute(route.children, config);

      if (matchedChild) {
        return matchedChild;
      }
    }
  }

  return null;
}

function isEntryRoute(route: ElegantConstRoute, config: App.ObjectContext.DomainConfig) {
  return route.name === config.entryRouteKey || normalizePath(route.path) === normalizePath(config.entryRoutePath);
}

function getContextMenuLabel(route: ElegantConstRoute) {
  const routeName = String(route.name || route.path);

  return route.meta?.i18nKey ? $t(route.meta.i18nKey) : String(route.meta?.title || routeName);
}

type ContextRouteLookupItem = {
  key: string;
  label: string;
  routeKey: string | null;
  routePath: string | null;
};

function createContextRouteLookup(
  routes: ElegantConstRoute[],
  lookup = new Map<string, ContextRouteLookupItem>()
): Map<string, ContextRouteLookupItem> {
  routes.forEach(route => {
    const routeName = route.name ? String(route.name) : '';
    const routePath = route.path ? String(route.path) : '';
    const item: ContextRouteLookupItem = {
      key: routeName || routePath,
      label: getContextMenuLabel(route),
      routeKey: routeName || null,
      routePath: routePath || null
    };

    if (routeName) {
      lookup.set(routeName, item);
    }

    if (routePath) {
      lookup.set(routePath, item);
    }

    route.children?.forEach(child => {
      createContextRouteLookup([child], lookup);
    });
  });

  return lookup;
}

function enrichContextMenu(
  menu: App.ObjectContext.Menu,
  routeLookup: Map<string, ContextRouteLookupItem>
): App.ObjectContext.Menu {
  const matchedRoute =
    routeLookup.get(String(menu.routeKey || '')) ||
    routeLookup.get(String(menu.routePath || '')) ||
    routeLookup.get(menu.key);

  return {
    key: matchedRoute?.key || menu.key,
    label: menu.label || matchedRoute?.label || menu.key,
    routeKey: menu.routeKey || matchedRoute?.routeKey || null,
    routePath: menu.routePath || matchedRoute?.routePath || null,
    children: menu.children?.map(child => enrichContextMenu(child, routeLookup)) || []
  };
}

function getLeafRoutes(routes: ElegantConstRoute[]): ElegantConstRoute[] {
  return routes.flatMap(route => {
    if (route.children?.length) {
      return getLeafRoutes(route.children);
    }

    return [route];
  });
}

export const useObjectContextStore = defineStore(SetupStoreId.ObjectContext, () => {
  const routeStore = useRouteStore();
  const domainKey = ref<App.ObjectContext.DomainKey>('');
  const objectType = ref<App.ObjectContext.ObjectType>('');
  const objectId = ref('');
  const objectName = ref('');
  const objectSummary = ref<App.ObjectContext.Summary | null>(null);
  const contextScopedMenus = ref<App.ObjectContext.Menu[]>([]);
  const buttonCodes = ref<string[]>([]);
  const defaultRouteKey = ref('');
  const defaultRoutePath = ref('');
  const isReady = ref(false);

  const hasContext = computed(() => isReady.value && Boolean(domainKey.value) && Boolean(objectId.value));

  function patchState(state: App.ObjectContext.State) {
    domainKey.value = state.domainKey;
    objectType.value = state.objectType;
    objectId.value = state.objectId;
    objectName.value = state.objectName;
    objectSummary.value = state.objectSummary;
    contextScopedMenus.value = state.contextScopedMenus;
    buttonCodes.value = state.buttonCodes;
    defaultRouteKey.value = state.defaultRouteKey;
    defaultRoutePath.value = state.defaultRoutePath;
    isReady.value = state.isReady;
  }

  function clearContext() {
    patchState(createEmptyState());
  }

  function resolveDefaultRoute(
    config: App.ObjectContext.DomainConfig,
    domainRoutes: ElegantConstRoute[],
    context: Api.ObjectContext.ContextInfo
  ) {
    const leafRoutes = getLeafRoutes(domainRoutes);
    const defaultRouteByKey = (routeKey?: string | null) => leafRoutes.find(route => route.name === routeKey);
    const defaultRouteByPath = (routePath?: string | null) =>
      leafRoutes.find(route => normalizePath(route.path) === normalizePath(routePath || ''));

    const matchedContextByKey = defaultRouteByKey(context.defaultRouteKey);

    if (matchedContextByKey?.name && matchedContextByKey.path) {
      return {
        defaultRouteKey: String(matchedContextByKey.name),
        defaultRoutePath: matchedContextByKey.path
      };
    }

    const matchedContextByPath = defaultRouteByPath(context.defaultRoutePath);

    if (matchedContextByPath?.name && matchedContextByPath.path) {
      return {
        defaultRouteKey: String(matchedContextByPath.name),
        defaultRoutePath: matchedContextByPath.path
      };
    }

    const matchedFallbackByKey = defaultRouteByKey(config.fallbackDefaultRouteKey);

    if (matchedFallbackByKey?.name && matchedFallbackByKey.path) {
      return {
        defaultRouteKey: String(matchedFallbackByKey.name),
        defaultRoutePath: matchedFallbackByKey.path
      };
    }

    const matchedFallbackByPath = defaultRouteByPath(config.fallbackDefaultRoutePath);

    if (matchedFallbackByPath?.name && matchedFallbackByPath.path) {
      return {
        defaultRouteKey: String(matchedFallbackByPath.name),
        defaultRoutePath: matchedFallbackByPath.path
      };
    }

    const [firstLeafRoute] = leafRoutes;

    if (firstLeafRoute?.name && firstLeafRoute.path) {
      return {
        defaultRouteKey: String(firstLeafRoute.name),
        defaultRoutePath: firstLeafRoute.path
      };
    }

    return {
      defaultRouteKey: context.defaultRouteKey || config.fallbackDefaultRouteKey,
      defaultRoutePath: context.defaultRoutePath || config.fallbackDefaultRoutePath
    };
  }

  function applyContext(config: App.ObjectContext.DomainConfig, context: Api.ObjectContext.ContextInfo) {
    const domainRootRoute = findDomainRootRoute(routeStore.authRoutes, config);
    const domainRoutes: ElegantConstRoute[] =
      domainRootRoute?.children?.filter((route: ElegantConstRoute) => !isEntryRoute(route, config)) || [];
    const routeLookup = createContextRouteLookup(domainRoutes);
    // 对象上下文菜单以接口返回为准，前端只补全跳转所需的本地路由信息。
    const contextMenus = context.contextScopedMenus.map(menu => enrichContextMenu(menu, routeLookup));
    const resolvedDefaultRoute = resolveDefaultRoute(config, domainRoutes, context);

    patchState({
      ...context,
      contextScopedMenus: contextMenus,
      defaultRouteKey: resolvedDefaultRoute.defaultRouteKey,
      defaultRoutePath: resolvedDefaultRoute.defaultRoutePath,
      isReady: true
    });
  }

  function getObjectIdFromRoute(route: Pick<RouteLocationNormalized, 'query'>) {
    const routeObjectId = route.query?.[OBJECT_CONTEXT_QUERY_KEY];

    if (Array.isArray(routeObjectId)) {
      return String(routeObjectId[0] || '');
    }

    if (routeObjectId === null || routeObjectId === undefined) {
      return '';
    }

    return String(routeObjectId);
  }

  function getContextQuery(targetObjectId = objectId.value): LocationQueryRaw {
    if (!targetObjectId) {
      return {};
    }

    return {
      [OBJECT_CONTEXT_QUERY_KEY]: targetObjectId
    };
  }

  function createEntryLocation(config: App.ObjectContext.DomainConfig): RouteLocationRaw {
    return {
      path: config.entryRoutePath
    };
  }

  function createDefaultLocation(config: App.ObjectContext.DomainConfig, targetObjectId: string): RouteLocationRaw {
    const query = getContextQuery(targetObjectId);

    if (defaultRouteKey.value) {
      return {
        name: defaultRouteKey.value,
        query
      };
    }

    if (defaultRoutePath.value) {
      return {
        path: defaultRoutePath.value,
        query
      };
    }

    return {
      path: config.fallbackDefaultRoutePath,
      query
    };
  }

  async function enterContext(config: App.ObjectContext.DomainConfig, targetObjectId: string) {
    const result = await fetchGetObjectContext(config, targetObjectId);

    if (!result.error && result.data) {
      applyContext(config, result.data);
    }

    return result;
  }

  async function switchContext(config: App.ObjectContext.DomainConfig, targetObjectId: string) {
    return enterContext(config, targetObjectId);
  }

  async function ensureContextByRoute(to: RouteLocationNormalized): Promise<RouteLocationRaw | null> {
    const domainConfig = getObjectContextDomainConfigByPath(to.path);

    if (!domainConfig) {
      if (hasContext.value) {
        clearContext();
      }

      return null;
    }

    const routeObjectId = getObjectIdFromRoute(to);

    if (!routeObjectId) {
      clearContext();

      if (isObjectContextEntryPath(to.path, domainConfig)) {
        return null;
      }

      return createEntryLocation(domainConfig);
    }

    const isSameContext =
      hasContext.value && domainKey.value === domainConfig.domainKey && objectId.value === routeObjectId;

    if (!isSameContext) {
      const { error } = await enterContext(domainConfig, routeObjectId);

      if (error) {
        clearContext();
        return createEntryLocation(domainConfig);
      }
    }

    if (isObjectContextEntryPath(to.path, domainConfig)) {
      return createDefaultLocation(domainConfig, routeObjectId);
    }

    return null;
  }

  function getMenuRouteLocation(
    menu: App.ObjectContext.Menu,
    targetObjectId = objectId.value
  ): RouteLocationRaw | null {
    const query = getContextQuery(targetObjectId);

    if (menu.routeKey) {
      return {
        name: menu.routeKey,
        query
      };
    }

    if (menu.routePath) {
      return {
        path: menu.routePath,
        query
      };
    }

    return null;
  }

  return {
    domainKey,
    objectType,
    objectId,
    objectName,
    objectSummary,
    contextScopedMenus,
    buttonCodes,
    defaultRouteKey,
    defaultRoutePath,
    isReady,
    hasContext,
    clearContext,
    enterContext,
    switchContext,
    ensureContextByRoute,
    getContextQuery,
    getMenuRouteLocation
  };
});
