<script lang="ts">
const createDeviceNodeKey = (stationCode?: Station['code'], device?: NdmDeviceResultVO) => {
  return `${stationCode ?? ''}-${device?.id ?? ''}`;
};
</script>

<script setup lang="ts">
import { initStationDevices, type NdmDeviceResultVO, type NdmNvrResultVO, type Station } from '@/apis';
import { useDeviceTree, usePermission, type UseDeviceTreeReturn } from '@/composables';
import { DEVICE_TYPE_NAMES, DEVICE_TYPE_LITERALS, tryGetDeviceType, type DeviceType, PERMISSION_TYPE_LITERALS } from '@/enums';
import { createNvrClusterRelationship, isNvrCluster, nvrInCluster } from '@/helpers';
import { useDeviceStore, usePermissionStore } from '@/stores';
import { watchDebounced, watchImmediate } from '@vueuse/core';
import destr from 'destr';
import { isFunction } from 'es-toolkit';
import {
  NButton,
  NDropdown,
  NFlex,
  NGrid,
  NGridItem,
  NInput,
  NRadio,
  NRadioGroup,
  NSelect,
  NTab,
  NTabs,
  NTag,
  NTree,
  useThemeVars,
  type DropdownOption,
  type SelectOption,
  type TagProps,
  type TreeInst,
  type TreeOption,
  type TreeOverrideNodeClickBehavior,
  type TreeProps,
} from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, h, nextTick, onBeforeUnmount, onMounted, ref, toRefs, useTemplateRef, watch, type CSSProperties } from 'vue';

const props = defineProps<{
  /**
   * 支持渲染指定车站的设备树
   */
  station?: Station;
  /**
   * 允许的事件类型
   *
   * - `select`：允许选择设备
   * - `manage`：允许右键菜单管理设备
   */
  events?: ('select' | 'manage')[];
  /**
   * 是否同步路由参数
   */
  syncRoute?: boolean;
  /**
   * 设备节点的前缀按钮文字
   */
  devicePrefixLabel?: string;
}>();

const emit = defineEmits<{
  afterSelectDevice: [device: NdmDeviceResultVO, stationCode: Station['code']];
  exposeSelectDeviceFn: [selectDeviceFn: UseDeviceTreeReturn['selectDevice']];
}>();

const { station, events, syncRoute, devicePrefixLabel } = toRefs(props);

const themeVars = useThemeVars();

const { hasPermission } = usePermission();

const {
  // 设备选择
  selectedStationCode,
  selectedDeviceType,
  selectedDevice,
  syncFromRoute,
  syncToRoute,
  selectDevice,
  // 设备管理
  exportDevice,
  exportDeviceTemplate,
  importDevice,
  deleteDevice,
} = useDeviceTree();

// 将 `selectDevice` 函数暴露给父组件
emit('exposeSelectDeviceFn', selectDevice);

const onSelectDevice = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
  // 仅当事件列表包含 `select` 时才触发选择事件
  if (!events.value) return;
  if (!events.value.includes('select')) return;

  selectDevice(device, stationCode);
  emit('afterSelectDevice', device, stationCode);
};

const permissionStore = usePermissionStore();
const stations = computed(() => permissionStore.stations.VIEW ?? []);

const deviceStore = useDeviceStore();
const { lineDevices } = storeToRefs(deviceStore);

const deviceTabPanes = Object.values(DEVICE_TYPE_LITERALS).map((deviceType) => ({
  name: deviceType,
  tab: DEVICE_TYPE_NAMES[deviceType],
}));
const activeTab = ref<DeviceType>(deviceTabPanes.at(0)!.name);
watchImmediate(selectedDeviceType, (newDeviceType) => {
  if (newDeviceType) {
    activeTab.value = newDeviceType;
  }
});

const selectedKeys = computed(() => (selectedDevice.value?.id ? [createDeviceNodeKey(selectedStationCode.value, selectedDevice.value)] : undefined));
watch([selectedKeys, selectedDevice, selectedStationCode], ([, device, code]) => {
  if (device && code) {
    onSelectDevice(device, code);
  }
});

const abortController = ref(new AbortController());
const contextmenu = ref<{ x: number; y: number; stationCode?: Station['code']; deviceType?: DeviceType | null; device?: NdmDeviceResultVO }>({ x: 0, y: 0, deviceType: null });
const showContextmenu = ref(false);
const contextmenuOptions = computed<DropdownOption[]>(() => [
  {
    label: '导出设备',
    key: 'export-device',
    show: !!contextmenu.value.deviceType,
    onSelect: () => {
      const { stationCode, deviceType } = contextmenu.value;
      // console.log(stationCode, deviceType);
      showContextmenu.value = false;
      if (!stationCode || !deviceType) return;
      abortController.value.abort();
      abortController.value = new AbortController();
      exportDevice({ deviceType, stationCode, signal: abortController.value.signal });
    },
  },
  {
    label: '导入设备',
    key: 'import-device',
    show: !!contextmenu.value.deviceType,
    onSelect: () => {
      const { stationCode, deviceType } = contextmenu.value;
      // console.log(stationCode, deviceType);
      showContextmenu.value = false;
      if (!stationCode || !deviceType) return;
      abortController.value.abort();
      abortController.value = new AbortController();
      importDevice({ deviceType, stationCode, signal: abortController.value.signal });
    },
  },
  {
    label: '下载导入模板',
    key: 'export-template',
    // 导出模板功能有缺陷，暂时不展示
    show: false,
    onSelect: () => {
      const { stationCode, deviceType } = contextmenu.value;
      // console.log(stationCode, deviceType);
      showContextmenu.value = false;
      if (!stationCode || !deviceType) return;
      abortController.value.abort();
      abortController.value = new AbortController();
      exportDeviceTemplate({ deviceType, stationCode, signal: abortController.value.signal });
    },
  },
  {
    label: '删除设备',
    key: 'delete-device',
    show: !!contextmenu.value.device,
    onSelect: () => {
      const { stationCode, device } = contextmenu.value;
      // console.log(stationCode, device);
      showContextmenu.value = false;
      if (!stationCode || !device) return;
      const id = device.id;
      const deviceType = tryGetDeviceType(device.deviceType);
      if (!id || !deviceType) return;
      window.$dialog.destroyAll();
      window.$dialog.warning({
        title: '删除设备',
        content: `确认删除设备 ${device.name || device.deviceId || device.id} 吗？`,
        positiveText: '确认',
        negativeText: '取消',
        onPositiveClick: () => {
          abortController.value.abort();
          abortController.value = new AbortController();
          deleteDevice({ id, deviceType, stationCode, signal: abortController.value.signal });
        },
      });
    },
  },
]);
const onSelectDropdownOption = (key: string, option: DropdownOption) => {
  const onSelect = option['onSelect'];
  if (isFunction(onSelect)) {
    onSelect();
  }
};
onBeforeUnmount(() => {
  abortController.value.abort();
});

// ========== 设备树节点交互 ==========
const override: TreeOverrideNodeClickBehavior = ({ option }) => {
  const hasChildren = (option.children?.length ?? 0) > 0;
  const isDeviceNode = !!option['device'];
  if (hasChildren || !isDeviceNode) {
    return 'toggleExpand';
  } else {
    return 'none';
  }
};
const nodeProps: TreeProps['nodeProps'] = ({ option }) => {
  return {
    onDblclick: (payload) => {
      if (option['device']) {
        payload.stopPropagation();

        const device = option['device'] as NdmDeviceResultVO;
        const stationCode = option['stationCode'] as Station['code'];

        onSelectDevice(device, stationCode);
      }
    },
    onContextmenu: (payload) => {
      payload.stopPropagation();
      payload.preventDefault();

      // 如果事件列表不包含 `manage`，则直接结束逻辑
      if (!events.value?.includes('manage')) return;

      const stationCode = option['stationCode'] as Station['code'];

      // 仅当用户在该车站拥有操作权限时才显示右键菜单
      if (!hasPermission(stationCode, PERMISSION_TYPE_LITERALS.OPERATION)) return;

      const deviceType = option['deviceType'] as DeviceType | undefined;
      const device = option['device'] as NdmDeviceResultVO | undefined;
      const { clientX, clientY } = payload;
      contextmenu.value = { x: clientX, y: clientY, stationCode, deviceType, device };
      showContextmenu.value = true;
    },
  };
};

// ========== 设备树数据 ==========
const renderStationNodePrefix = (station: Station) => {
  const { online } = station;
  const tagType: TagProps['type'] = online ? 'success' : 'error';
  const tagText = online ? '在线' : '离线';
  return h(NTag, { type: tagType, size: 'tiny' }, () => tagText);
};
const renderIcmpStatistics = (onlineCount: number, offlineCount: number, count: number) => {
  return h('span', null, [
    '(',
    h('span', { style: { color: themeVars.value.successColor } }, `${onlineCount}`),
    '/',
    h('span', { style: { color: themeVars.value.errorColor } }, `${offlineCount}`),
    '/',
    `${count}`,
    ')',
  ]);
};
const renderDeviceNodePrefix = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
  const renderViewDeviceButton = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
    if (!devicePrefixLabel.value) return null;
    return h(
      NButton,
      {
        text: true,
        size: 'tiny',
        type: 'info',
        style: {
          marginRight: 8,
        } as CSSProperties,
        onClick: (e: MouseEvent) => {
          e.stopPropagation();

          onSelectDevice(device, stationCode);
        },
      },
      () => devicePrefixLabel.value,
    );
  };
  const renderDeviceStatusTag = (device: NdmDeviceResultVO) => {
    const { deviceStatus } = device;
    const color = deviceStatus === '10' ? themeVars.value.successColor : deviceStatus === '20' ? themeVars.value.errorColor : themeVars.value.warningColor;
    return h('div', { style: { color } }, { default: () => '◉' });
  };
  return h(NFlex, { size: 'small' }, { default: () => [renderViewDeviceButton(device, stationCode), renderDeviceStatusTag(device)] });
};
// 全线设备树
const lineDeviceTreeData = computed<Record<DeviceType, TreeOption[]>>(() => {
  const treeData: Record<DeviceType, TreeOption[]> = {
    [DEVICE_TYPE_LITERALS.ndmCamera]: [],
    [DEVICE_TYPE_LITERALS.ndmNvr]: [],
    [DEVICE_TYPE_LITERALS.ndmSwitch]: [],
    [DEVICE_TYPE_LITERALS.ndmDecoder]: [],
    [DEVICE_TYPE_LITERALS.ndmSecurityBox]: [],
    [DEVICE_TYPE_LITERALS.ndmMediaServer]: [],
    [DEVICE_TYPE_LITERALS.ndmVideoServer]: [],
    [DEVICE_TYPE_LITERALS.ndmKeyboard]: [],
    [DEVICE_TYPE_LITERALS.ndmAlarmHost]: [],
  };
  deviceTabPanes.forEach(({ name: paneName /* , tab: paneTab */ }) => {
    treeData[paneName] = stations.value.map<TreeOption>((station) => {
      const { name: stationName, code: stationCode } = station;
      const devices = lineDevices.value[stationCode]?.[paneName] ?? ([] as NdmDeviceResultVO[]);
      const onlineDevices = devices?.filter((device) => device.deviceStatus === '10');
      const offlineDevices = devices?.filter((device) => device.deviceStatus === '20');
      // 对于录像机，需要根据clusterList字段以分号分隔设备IP，进一步形成子树结构
      if (paneName === DEVICE_TYPE_LITERALS.ndmNvr) {
        const nvrDevices = devices as NdmNvrResultVO[];

        const { nvrClusters, nvrTreeMap, nvrStandalones } = createNvrClusterRelationship(nvrDevices);

        return {
          label: stationName,
          key: stationCode,
          prefix: () => renderStationNodePrefix(station),
          suffix: () => renderIcmpStatistics(onlineDevices?.length ?? 0, offlineDevices?.length ?? 0, devices?.length ?? 0),
          children: [
            ...nvrClusters.map((cluster) => {
              return {
                label: `${cluster.name}`,
                key: createDeviceNodeKey(stationCode, cluster),
                prefix: () => renderDeviceNodePrefix(cluster, stationCode),
                suffix: () => `${cluster.ipAddress}`,
                children: (nvrTreeMap.get(cluster.ipAddress ?? '') ?? []).map((clusterNode) => {
                  return {
                    label: `${clusterNode.name}`,
                    key: createDeviceNodeKey(stationCode, clusterNode),
                    prefix: () => renderDeviceNodePrefix(clusterNode, stationCode),
                    suffix: () => `${clusterNode.ipAddress}`,
                    // 当选择设备时，能获取到设备的所有信息，以及设备所属的车站
                    stationCode,
                    device: clusterNode,
                  };
                }),
                // 当选择设备时，能获取到设备的所有信息，以及设备所属的车站
                stationCode,
                device: cluster,
              };
            }),
            ...nvrStandalones.map((device) => {
              return {
                label: `${device.name}`,
                key: createDeviceNodeKey(stationCode, device),
                prefix: () => renderDeviceNodePrefix(device, stationCode),
                suffix: () => `${device.ipAddress}`,
                // 当选择设备时，能获取到设备的所有信息，以及设备所属的车站
                stationCode,
                device: device,
              };
            }),
          ],
          stationCode,
          deviceType: activeTab.value,
        };
      }
      // 非录像机设备
      return {
        label: stationName,
        key: stationCode,
        prefix: () => renderStationNodePrefix(station),
        suffix: () => renderIcmpStatistics(onlineDevices?.length ?? 0, offlineDevices?.length ?? 0, devices?.length ?? 0),
        children:
          lineDevices.value[stationCode]?.[paneName]?.map<TreeOption>((dev) => {
            const device = dev as NdmDeviceResultVO;
            return {
              label: `${device.name}`,
              key: createDeviceNodeKey(stationCode, device),
              prefix: () => renderDeviceNodePrefix(device, stationCode),
              suffix: () => `${device.ipAddress}`,
              // 当选择设备时，能获取到设备的所有信息，以及设备所属的车站
              stationCode,
              device,
            };
          }) ?? [],
        stationCode,
        deviceType: activeTab.value,
      };
    });
  });
  return treeData;
});
// 车站设备树
const stationDeviceTreeData = computed<TreeOption[]>(() => {
  const stationCode = station.value?.code;
  if (!stationCode) return [];
  return Object.values(DEVICE_TYPE_LITERALS).map<TreeOption>((deviceType) => {
    const stationDevices = lineDevices.value[stationCode] ?? initStationDevices();
    const onlineCount = stationDevices[deviceType].filter((device) => device.deviceStatus === '10').length;
    const offlineCount = stationDevices[deviceType].filter((device) => device.deviceStatus === '20').length;
    if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) {
      const nvrDevices = stationDevices[deviceType] as NdmNvrResultVO[];

      const { nvrClusters, nvrTreeMap, nvrStandalones } = createNvrClusterRelationship(nvrDevices);

      return {
        label: `${DEVICE_TYPE_NAMES[deviceType]}`,
        key: deviceType,
        suffix: () => renderIcmpStatistics(onlineCount, offlineCount, nvrDevices.length),
        children: [
          ...nvrClusters.map((cluster) => {
            return {
              label: `${cluster.name}`,
              key: createDeviceNodeKey(stationCode, cluster),
              prefix: () => renderDeviceNodePrefix(cluster, stationCode),
              suffix: () => `${cluster.ipAddress}`,
              children: (nvrTreeMap.get(cluster.ipAddress ?? '') ?? []).map((clusterNode) => {
                return {
                  label: `${clusterNode.name}`,
                  key: createDeviceNodeKey(stationCode, clusterNode),
                  prefix: () => renderDeviceNodePrefix(clusterNode, stationCode),
                  suffix: () => `${clusterNode.ipAddress}`,
                  stationCode,
                  device: clusterNode,
                };
              }),
              stationCode,
              device: cluster,
            };
          }),
          ...nvrStandalones.map((device) => {
            return {
              label: `${device.name}`,
              key: createDeviceNodeKey(stationCode, device),
              prefix: () => renderDeviceNodePrefix(device, stationCode),
              suffix: () => `${device.ipAddress}`,
              stationCode,
              device,
            };
          }),
        ],
        stationCode,
        deviceType,
      };
    }
    // 非录像机设备
    return {
      label: `${DEVICE_TYPE_NAMES[deviceType]}`,
      key: deviceType,
      suffix: () => renderIcmpStatistics(onlineCount, offlineCount, stationDevices[deviceType].length),
      children: stationDevices[deviceType].map<TreeOption>((device) => {
        return {
          label: `${device.name}`,
          key: createDeviceNodeKey(stationCode, device),
          prefix: () => renderDeviceNodePrefix(device, stationCode),
          suffix: () => `${device.ipAddress}`,
          stationCode,
          device,
        };
      }),
      stationCode,
      deviceType,
    };
  });
});

// ========== 设备树搜索 ==========
const searchInput = ref('');
const searchTypeOptions: SelectOption[] = [
  { label: '设备名称', value: 'name' },
  { label: 'IP地址', value: 'ipAddress' },
];
type SearchType = 'name' | 'ipAddress';
const typeInput = ref<SearchType>('name');
const statusInput = ref('');
// 设备树将搜索框、选择器以及单选框的值都交给NTree的pattern属性
// 但是如果一个车站下没有匹配的设备，那么这个车站节点也不会显示
const searchPattern = computed(() => {
  const search = searchInput.value;
  const status = statusInput.value;
  if (!search && !status) return ''; // 如果pattern非空会导致NTree组件认为筛选完成，UI上发生全量匹配
  return JSON.stringify({ search: searchInput.value, type: typeInput.value, status: statusInput.value });
});
const searchFilter = (pattern: string, node: TreeOption): boolean => {
  const { search, type, status } = destr<{ search: string; type: SearchType; status: string }>(pattern);
  const device = node['device'] as NdmDeviceResultVO | undefined;
  const { deviceStatus } = device ?? {};
  const searchMatched = !!device?.[type]?.includes(search);
  const statusMatched = status === '' || status === deviceStatus;
  return searchMatched && statusMatched;
};

// ========== 设备树交互 ==========
const animated = ref(true);
const expandedKeys = ref<string[]>([]);
const deviceTreeInst = useTemplateRef<TreeInst>('deviceTreeInst');
const onFoldDeviceTree = () => {
  expandedKeys.value = [];
};
const onLocateDeviceTree = async () => {
  if (!selectedStationCode.value) return;
  const stationCode = selectedStationCode.value;
  if (!selectedDevice.value) return;
  const deviceType = tryGetDeviceType(selectedDevice.value.deviceType);
  if (!deviceType) return;
  if (!deviceTreeInst.value) return;

  animated.value = false;

  // 定位设备类型
  activeTab.value = deviceType;

  // 展开选择的车站
  expandedKeys.value.push(stationCode);

  // 当选择录像机时，如果不是集群，进一步展开该录像机所在的集群节点
  if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) {
    const stationDevices = lineDevices.value[stationCode];
    if (stationDevices) {
      const selectedNvr = selectedDevice.value as NdmNvrResultVO;
      if (!isNvrCluster(selectedNvr)) {
        const nvrDevices = stationDevices[DEVICE_TYPE_LITERALS.ndmNvr];
        const clusters = nvrDevices.filter((device) => {
          if (!isNvrCluster(device)) return false;
          const cluster = device;
          return nvrInCluster(selectedNvr, cluster);
        });
        expandedKeys.value.push(...clusters.map((cluster) => createDeviceNodeKey(stationCode, cluster)));
      }
    }
  }

  // 等待设备树展开完成，滚动到选择的设备
  await nextTick();
  deviceTreeInst.value.scrollTo({ key: createDeviceNodeKey(stationCode, selectedDevice.value), behavior: 'smooth' });

  animated.value = true;
};

// 当选择的设备发生变化时，定位设备树，并同步选中状态到路由参数
// 暂时不考虑多次执行的问题，因为当选择的设备在设备树视口内时，不会发生滚动
watch(selectedDevice, async (newDevice, oldDevice) => {
  if (!!station.value) return;
  if (newDevice?.id === oldDevice?.id) return;
  // console.log('selectedDevice changed');
  onLocateDeviceTree();
  syncToRoute();
});

// 当全线设备发生变化时，从路由参数同步选中状态
// 但lineDevices是shallowRef，因此需要深度侦听才能获取内部变化，
// 而单纯的深度侦听又可能会引发性能问题，因此尝试使用防抖侦听
watchDebounced(
  lineDevices,
  (newLineDevices) => {
    if (syncRoute.value) {
      // console.log('lineDevices changed');
      syncFromRoute(newLineDevices);
    }
  },
  {
    debounce: 500,
    deep: true,
  },
);

onMounted(() => {
  if (syncRoute.value) {
    syncFromRoute(lineDevices.value);
  }
});
</script>

<template>
  <div style="height: 100%; display: flex; flex-direction: column">
    <!-- 搜索和筛选 -->
    <div style="padding: 12px; flex: 0 0 auto">
      <NGrid :cols="10" :x-gap="8">
        <NGridItem :span="7">
          <NInput v-model:value="searchInput" placeholder="搜索设备名称或IP地址" clearable />
        </NGridItem>
        <NGridItem :span="3">
          <NSelect v-model:value="typeInput" :options="searchTypeOptions" placeholder="搜索类型" />
        </NGridItem>
      </NGrid>
      <NFlex align="center">
        <NRadioGroup v-model:value="statusInput">
          <NRadio value="">全部</NRadio>
          <NRadio value="10">在线</NRadio>
          <NRadio value="20">离线</NRadio>
        </NRadioGroup>
        <template v-if="!station">
          <NButton text size="tiny" type="info" @click="onFoldDeviceTree" style="margin-left: auto">收起</NButton>
          <NButton text size="tiny" type="info" @click="onLocateDeviceTree">定位</NButton>
        </template>
      </NFlex>
    </div>
    <!-- 设备树 -->
    <div
      style="overflow: hidden; flex: 1 1 auto; display: flex"
      :style="{
        // 当右键菜单显示时，禁用设备树的点击事件，避免在打开菜单时仍能点击设备树节点
        'pointer-events': showContextmenu ? 'none' : 'auto',
      }"
    >
      <template v-if="!station">
        <div style="height: 100%; flex: 0 0 auto">
          <NTabs v-model:value="activeTab" type="line" placement="left" style="height: 100%">
            <NTab v-for="pane in deviceTabPanes" :key="pane.name" :name="pane.name" :tab="pane.tab"></NTab>
          </NTabs>
        </div>
        <div style="min-width: 0; flex: 1 1 auto">
          <NTree
            style="height: 100%"
            v-model:expanded-keys="expandedKeys"
            block-line
            block-node
            show-line
            virtual-scroll
            :ref="'deviceTreeInst'"
            :animated="animated"
            :selected-keys="selectedKeys"
            :data="lineDeviceTreeData[activeTab]"
            :show-irrelevant-nodes="false"
            :pattern="searchPattern"
            :filter="searchFilter"
            :override-default-node-click-behavior="override"
            :node-props="nodeProps"
            :default-expand-all="false"
          />
        </div>
      </template>
      <template v-else>
        <NTree
          style="height: 100%"
          block-line
          block-node
          show-line
          virtual-scroll
          :data="stationDeviceTreeData"
          :animated="animated"
          :selected-keys="selectedKeys"
          :show-irrelevant-nodes="false"
          :pattern="searchPattern"
          :filter="searchFilter"
          :override-default-node-click-behavior="override"
          :node-props="nodeProps"
          :default-expand-all="false"
        />
      </template>
    </div>
  </div>

  <NDropdown
    placement="bottom-start"
    trigger="manual"
    :show="showContextmenu"
    :x="contextmenu.x"
    :y="contextmenu.y"
    :options="contextmenuOptions"
    @select="onSelectDropdownOption"
    @clickoutside="() => (showContextmenu = false)"
  />
</template>

<style scoped lang="scss"></style>
