<!--
  - This file is part of the kernelstudio package.
  -
  - (c) 2014-2026 zlin <admin@kernelstudio.com>
  -
  - For the full copyright and license information, please view the LICENSE file
  - that was distributed with this source code.
-->
<template>
  <div
    ref="graphShellRef"
    class="rule-knowledge-graph"
    :class="{
      'rule-knowledge-graph--four-blocks': density === 'four-blocks',
      'rule-knowledge-graph--decision-tree': density === 'decision-tree',
      'rule-knowledge-graph--fullscreen': isFullscreen,
    }"
  >
    <div class="rule-knowledge-graph__toolbar">
      <a-radio-group v-model:value="density" size="small" button-style="solid">
        <a-radio-button value="decision-tree-simple">决策树（简化）</a-radio-button>
        <a-radio-button value="decision-tree">决策树</a-radio-button>
        <a-radio-button value="full">完整图谱</a-radio-button>
        <a-radio-button value="four-blocks">四块分区</a-radio-button>
      </a-radio-group>
      <span v-if="density === 'decision-tree-simple'" class="rule-knowledge-graph__hint">
        决策树（简化）：把装备、目标、阵位、航迹四个规则块合并成一棵总树，只保留关键参数、核心规则和阶段输出
      </span>
      <span v-else-if="density === 'decision-tree'" class="rule-knowledge-graph__hint">
        决策树：按装备、目标、阵位、航迹四个规则块展示输入参数、运算步骤、命中规则与结果产出
      </span>
      <span v-else-if="density === 'full'" class="rule-knowledge-graph__hint">
        完整图谱包含参数、任务类型与执行顺序
      </span>
      <span v-else class="rule-knowledge-graph__hint rule-knowledge-graph__hint--compact">
        四块分区：可拖动画布，滚轮缩放
      </span>
      <a-button type="default" size="small" class="rule-knowledge-graph__fullscreen-btn" @click="toggleFullscreen">
        <template #icon>
          <FullscreenExitOutlined v-if="isFullscreen" />
          <FullscreenOutlined v-else />
        </template>
        {{ isFullscreen ? '退出全屏' : '全屏' }}
      </a-button>
    </div>
    <RuleDecisionTreeSimplePanel v-if="density === 'decision-tree-simple'" :query="query" :refresh-key="refreshKey" />
    <RuleDecisionTreePanel v-else-if="density === 'decision-tree'" :query="query" :refresh-key="refreshKey" />
    <RuleFourBlocksPanel v-else-if="density === 'four-blocks'" :refresh-key="refreshKey" />
    <template v-else>
      <div v-if="errorMsg" class="rule-knowledge-graph__banner rule-knowledge-graph__banner--error">
        {{ errorMsg }}
      </div>
      <div v-else-if="emptyHint" class="rule-knowledge-graph__banner">
        {{ emptyHint }}
      </div>
      <div v-show="!errorMsg && !emptyHint" ref="hostRef" class="rule-knowledge-graph__host" />
    </template>
  </div>
</template>

<script setup lang="ts">
import { FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons-vue';
import { Graph } from '@antv/g6';
import { message } from 'ant-design-vue';
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { findRuleConfigGraph } from './api';
import RuleDecisionTreeSimplePanel from './RuleDecisionTreeSimplePanel.vue';
import RuleDecisionTreePanel from './RuleDecisionTreePanel.vue';
import RuleFourBlocksPanel from './RuleFourBlocksPanel.vue';
import type { RuleConfigRequest, RuleGraphPayload } from './types';

type RuleGraphDensityMode = 'decision-tree-simple' | 'decision-tree' | 'full' | 'four-blocks';

const emit = defineEmits<{
  densityChange: [RuleGraphDensityMode],
}>();

const props = defineProps<{
  query: RuleConfigRequest,
  refreshKey: number,
}>();

const graphShellRef = ref<HTMLElement | null>(null);
const hostRef = ref<HTMLDivElement | null>(null);
const isFullscreen = ref(false);
const errorMsg = ref<string | null>(null);
const emptyHint = ref<string | null>(null);
const density = ref<RuleGraphDensityMode>('decision-tree-simple');
const lastPayload = ref<RuleGraphPayload | null>(null);

let graph: Graph | null = null;
let resizeObserver: ResizeObserver | null = null;

const NODE_COLORS: Record<string, string> = {
  level: '#5B8FF9',
  kind: '#61DDAA',
  module: '#65789B',
  rule: '#F6903D',
  param: '#9270CA',
  taskType: '#269A99',
};

const toGraphData = (payload: RuleGraphPayload) => ({
  nodes: payload.nodes.map((n) => ({
    id: n.id,
    data: {
      label: n.label,
      nodeType: n.nodeType,
      payload: n.payload ?? {},
    },
  })),
  edges: payload.edges.map((e) => ({
    id: e.id,
    source: e.source,
    target: e.target,
    data: {
      edgeType: e.edgeType ?? '',
      label: e.label ?? '',
    },
  })),
});

const syncFullscreenState = () => {
  const el = graphShellRef.value;
  isFullscreen.value = Boolean(el && document.fullscreenElement === el);
};

const refitMainGraphAfterLayout = () => {
  if (!graph || !hostRef.value) {
    return;
  }
  const w = Math.max(hostRef.value.clientWidth, 280);
  const h = Math.max(hostRef.value.clientHeight, 240);
  graph.setSize(w, h);
  void graph.fitView();
};

const onFullscreenChange = () => {
  syncFullscreenState();
  void nextTick(() => {
    refitMainGraphAfterLayout();
    window.dispatchEvent(new Event('resize'));
    window.setTimeout(() => {
      refitMainGraphAfterLayout();
    }, 120);
  });
};

const toggleFullscreen = async () => {
  const el = graphShellRef.value;
  if (!el) {
    return;
  }
  try {
    if (document.fullscreenElement === el) {
      await document.exitFullscreen();
    } else {
      await el.requestFullscreen();
    }
  } catch {
    message.warning('无法切换全屏，请检查浏览器权限或使用 Chrome / Edge');
  }
};

const disposeGraph = async () => {
  resizeObserver?.disconnect();
  resizeObserver = null;
  if (graph) {
    try {
      graph.destroy();
    } catch {
      /* ignore */
    }
    graph = null;
  }
};

const buildGraph = async (payload: RuleGraphPayload) => {
  await disposeGraph();
  const el = hostRef.value;
  if (!el) {
    return;
  }
  const data = toGraphData(payload);
  if (!payload.nodes.length) {
    return;
  }

  const width = Math.max(el.clientWidth, 280);
  const height = Math.max(el.clientHeight, 240);

  graph = new Graph({
    container: el,
    width,
    height,
    data,
    layout: {
      type: 'd3-force' as const,
    },
    node: {
      style: {
        size: (d: { data?: { nodeType?: string } }) => (d.data?.nodeType === 'param' ? 5 : 11),
        fill: (d: { data?: { nodeType?: string } }) => NODE_COLORS[d.data?.nodeType ?? ''] ?? '#8B8B8B',
        labelText: (d: { data?: { label?: string }; id: string }) => d.data?.label ?? String(d.id),
        labelFill: '#e8f4f8',
        labelFontSize: 9,
        labelMaxWidth: 100,
        labelWordWrap: true,
        lineWidth: 1.5,
        stroke: '#0d1f2c',
      },
    },
    edge: {
      style: {
        stroke: (d: { data?: { edgeType?: string } }) =>
          (d.data?.edgeType === 'rule_exec_before' ? '#faad14' : 'rgba(150, 175, 190, 0.35)'),
        lineWidth: (d: { data?: { edgeType?: string } }) =>
          (d.data?.edgeType === 'rule_exec_before' ? 2 : 1),
        endArrow: true,
      },
    },
    behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],
  });
  await graph.render();
  await graph.fitView();
  resizeObserver = new ResizeObserver(() => {
    if (!graph || !hostRef.value) {
      return;
    }
    const w = Math.max(hostRef.value.clientWidth, 280);
    const h = Math.max(hostRef.value.clientHeight, 240);
    graph.setSize(w, h);
    void graph.fitView();
  });
  resizeObserver.observe(el);
};

const renderFromCache = async () => {
  if (density.value !== 'full' || !lastPayload.value || !lastPayload.value.nodes?.length) {
    return;
  }
  await nextTick();
  await buildGraph(lastPayload.value);
};

const load = async () => {
  errorMsg.value = null;
  emptyHint.value = null;
  await disposeGraph();
  try {
    const r = await findRuleConfigGraph({
      pageNum: props.query.pageNum,
      pageSize: props.query.pageSize,
      ruleCode: props.query.ruleCode ?? undefined,
      ruleName: props.query.ruleName ?? undefined,
      levelCode: props.query.levelCode ?? undefined,
      kindCode: props.query.kindCode ?? undefined,
      moduleCode: props.query.moduleCode ?? undefined,
      enabled: props.query.enabled ?? undefined,
    });
    if (r.code !== 200 || !r.data) {
      errorMsg.value = r.msg ?? '加载知识图谱失败';
      lastPayload.value = null;
      return;
    }
    if (!r.data.nodes?.length) {
      emptyHint.value = '当前页无规则数据，图谱为空';
      lastPayload.value = null;
      return;
    }
    lastPayload.value = r.data;
    await renderFromCache();
  } catch (e: unknown) {
    const msg = e instanceof Error ? e.message : String(e);
    errorMsg.value = `图谱请求失败：${msg}`;
    lastPayload.value = null;
    message.error(errorMsg.value);
  }
};

watch(
  () => [props.query.pageNum, props.query.pageSize, props.refreshKey],
  () => {
    if (density.value !== 'full') {
      return;
    }
    void load();
  },
  { immediate: true },
);

watch(density, async (mode, prev) => {
  emit('densityChange', mode);
  if (mode !== 'full') {
    await disposeGraph();
    return;
  }
  if (prev !== 'full') {
    await load();
    return;
  }
  await renderFromCache();
});

onMounted(() => {
  syncFullscreenState();
  document.addEventListener('fullscreenchange', onFullscreenChange);
});

onBeforeUnmount(() => {
  document.removeEventListener('fullscreenchange', onFullscreenChange);
  void disposeGraph();
});
</script>

<style scoped lang="less">
.rule-knowledge-graph {
  flex: 1;
  min-height: 0;
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.rule-knowledge-graph__toolbar {
  flex: 0 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  padding: 0 2px 4px;
}

.rule-knowledge-graph__fullscreen-btn {
  margin-left: auto;
  flex-shrink: 0;
  color: #b8ccd6;
  border-color: rgba(120, 170, 200, 0.45);
  background: rgba(10, 28, 40, 0.65);
}

.rule-knowledge-graph__hint {
  font-size: 11px;
  color: #7a8d96;
  line-height: 1.35;
}

.rule-knowledge-graph__hint--compact {
  letter-spacing: 0.2px;
}

.rule-knowledge-graph__banner {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 12px;
  font-size: 13px;
  color: #a2b1ba;
  text-align: center;
}

.rule-knowledge-graph__banner--error {
  color: #ff9c9c;
}

.rule-knowledge-graph__host {
  flex: 1;
  min-height: 0;
  width: 100%;
  border: 1px solid rgba(80, 120, 150, 0.18);
  border-radius: 4px;
  background: rgba(4, 18, 28, 0.35);
}

.rule-knowledge-graph--fullscreen {
  background: #061522;
  padding: 12px;
}
</style>
