<template>
  <div class="business-topology-page">
    <a-page-header title="业务拓扑" subtitle="业务管理 / 业务拓扑">
      <template #extra>
        <a-space wrap>
          <a-button :loading="loading" :disabled="pageUnauthorized" @click="fetchData">
            <template #icon><icon-refresh /></template>
            刷新
          </a-button>
          <a-button :disabled="pageUnauthorized || !selectedBusiness" @click="handleResetLayout">重置布局</a-button>
        </a-space>
      </template>
    </a-page-header>

    <a-card :bordered="false" class="filter-card">
      <a-form :model="filters" layout="inline" class="filter-form">
        <a-form-item field="businessId" label="业务系统">
          <a-select
            v-model="selectedBusinessId"
            allow-search
            placeholder="请选择业务系统"
            :loading="loading"
            :disabled="pageUnauthorized || !businessSystems.length"
            @change="handleBusinessChange"
          >
            <a-option v-for="item in businessSystems" :key="item.id" :value="item.id">
              {{ item.name || item.code }}
            </a-option>
          </a-select>
        </a-form-item>
        <a-form-item field="health" label="健康状态">
          <a-select v-model="filters.health" placeholder="全部" :disabled="pageUnauthorized">
            <a-option value="all">全部</a-option>
            <a-option value="healthy">健康</a-option>
            <a-option value="degraded">降级</a-option>
            <a-option value="critical">严重</a-option>
            <a-option value="unknown">未知</a-option>
          </a-select>
        </a-form-item>
        <a-form-item field="mode" label="视图模式">
          <a-select v-model="filters.mode" placeholder="业务视图" :disabled="pageUnauthorized">
            <a-option value="business">业务视图</a-option>
            <a-option value="resource">资源视图</a-option>
            <a-option value="dependency">依赖视图</a-option>
          </a-select>
        </a-form-item>
        <a-form-item field="keyword" label="搜索">
          <a-input v-model="filters.keyword" allow-clear placeholder="业务名称 / 编码" :disabled="pageUnauthorized" />
        </a-form-item>
      </a-form>
    </a-card>

    <PageState :status="pageStatus" :error-text="errorMessage" :partial-text="partialMessage" @retry="fetchData">
    <div v-if="pageStatus !== 'empty' && pageStatus !== 'error' && pageStatus !== 'unauthorized'" class="topology-shell">
      <aside class="business-list-panel">
        <div class="panel-title">业务系统列表</div>
        <a-spin :loading="loading || healthLoading" class="list-spin">
          <div v-if="filteredBusinessSystems.length" class="business-list">
            <button
              v-for="item in filteredBusinessSystems"
              :key="item.id"
              type="button"
              class="business-list-item"
              :class="{ active: item.id === selectedBusinessId }"
              @click="selectBusiness(item.id)"
            >
              <span class="business-avatar" :style="{ backgroundColor: healthDotColor(item) }">
                {{ businessInitial(item) }}
              </span>
              <span class="business-info">
                <span class="business-name">{{ item.name || '-' }}</span>
                <span class="business-code">{{ item.code || '-' }}</span>
              </span>
              <span class="business-health" :class="{ unknown: typeof item.health_score !== 'number' }">
                {{ healthScoreText(item.health_score) }}
              </span>
            </button>
          </div>
          <a-empty v-else description="暂无匹配业务系统" />
        </a-spin>
      </aside>

      <main class="topology-canvas-panel">
        <a-spin :loading="selectionLoading" class="topology-spin">
          <a-empty v-if="!selectedBusiness" description="请选择业务系统" class="topology-empty" />
          <div v-else-if="!graph.nodes.length" class="topology-empty">
            <a-empty description="暂无业务拓扑数据" />
          </div>
          <vue-flow
            v-else
            class="business-flow"
            :nodes="graph.nodes"
            :edges="graph.edges"
            :fit-view-on-init="true"
            :min-zoom="0.2"
            :max-zoom="2"
            @node-click="handleNodeClick"
            @edge-click="handleEdgeClick"
            @pane-click="clearSelectedItem"
          >
            <background pattern-color="#c9cdd4" :gap="18" />
            <controls />
            <template #node-businessTopology="{ data }">
              <div class="flow-node" :class="`flow-node-${data.kind}`">
                <div class="flow-node-header">
                  <span class="flow-node-dot" :style="{ backgroundColor: statusColor(data.status) }"></span>
                  <span class="flow-node-title">{{ data.label || '-' }}</span>
                </div>
                <div class="flow-node-meta">
                  <template v-if="data.kind === 'business'">
                    <span>{{ typeof data.healthScore === 'number' ? `健康分 ${healthScoreText(data.healthScore)}` : '业务系统' }}</span>
                  </template>
                  <template v-else>
                    <span>{{ data.resourceType || '资源' }}</span>
                    <span v-if="data.resourceUID">{{ data.resourceUID }}</span>
                  </template>
                </div>
              </div>
            </template>
          </vue-flow>
        </a-spin>
      </main>

      <aside class="detail-panel">
        <div class="panel-title">详情</div>
        <a-empty v-if="!selectedItem" description="请选择节点或链路" />
        <a-descriptions v-else :column="1" bordered size="small">
          <a-descriptions-item label="类型">
            {{ selectedTypeLabel }}
          </a-descriptions-item>
          <a-descriptions-item label="名称">
            {{ selectedItem.data.label || '-' }}
          </a-descriptions-item>
          <a-descriptions-item label="状态">
            <a-tag :color="selectedStatusTagColor">{{ selectedItem.data.status || '未知' }}</a-tag>
          </a-descriptions-item>
          <a-descriptions-item v-if="selectedItem.type === 'node' && selectedItem.data.kind === 'business'" label="健康分">
            {{ healthScoreText(selectedItem.data.healthScore) }}
          </a-descriptions-item>
          <a-descriptions-item v-if="selectedItem.type === 'node' && selectedItem.data.kind === 'resource' && selectedItem.data.resourceUID" label="资源 UID">
            <a-typography-text copyable>{{ selectedItem.data.resourceUID }}</a-typography-text>
          </a-descriptions-item>
          <a-descriptions-item v-if="selectedItem.type === 'node' && selectedItem.data.kind === 'resource' && selectedItem.data.resourceType" label="资源类型">
            {{ selectedItem.data.resourceType }}
          </a-descriptions-item>
          <a-descriptions-item v-if="selectedItem.type === 'node' && selectedItem.data.kind === 'resource' && selectedItem.data.resourceUID" label="操作">
            <a-button
              type="secondary"
              status="danger"
              size="small"
              :disabled="!memberByResourceUID.has(selectedItem.data.resourceUID)"
              @click="confirmUnbindResource(selectedItem.data.resourceUID)"
            >
              <template #icon><icon-delete /></template>
              解除绑定
            </a-button>
          </a-descriptions-item>
          <a-descriptions-item v-if="selectedItem.type === 'edge' && selectedItem.data.kind === 'business_dependency'" label="依赖说明">
            {{ selectedItem.data.dependency?.description || '-' }}
          </a-descriptions-item>
        </a-descriptions>
      </aside>
    </div>

    <a-card v-if="selectedBusiness" :bordered="false" class="bottom-tabs">
      <a-tabs default-active-key="dependencies">
        <a-tab-pane key="dependencies" title="业务依赖">
          <a-table row-key="id" :data="topology?.dependencies || []" :pagination="false" size="small">
            <template #columns>
              <a-table-column title="业务 ID" data-index="business_system_id" :width="110" />
              <a-table-column title="依赖业务 ID" data-index="depends_on_business_system_id" :width="130" />
              <a-table-column title="类型" data-index="dependency_type" :width="120" />
              <a-table-column title="重要性" data-index="criticality" :width="100" />
              <a-table-column title="说明" data-index="description" />
            </template>
          </a-table>
        </a-tab-pane>
        <a-tab-pane key="resources" title="资源清单">
          <div class="tab-toolbar">
            <span class="tab-count">共 {{ topologyResources.length }} 个资源</span>
            <a-button type="primary" :disabled="pageUnauthorized || !selectedBusiness" @click="openBindResource">
              <template #icon><icon-link /></template>
              绑定资源
            </a-button>
          </div>
          <a-table row-key="resource_uid" :data="topologyResources" :pagination="false" size="small" :scroll="{ x: 'max-content' }">
            <template #columns>
              <a-table-column title="资源 UID" data-index="resource_uid" />
              <a-table-column title="名称" data-index="display_name" />
              <a-table-column title="类型" data-index="resource_category" />
              <a-table-column title="状态" data-index="status" />
              <a-table-column title="操作" :width="96" fixed="right">
                <template #cell="{ record }">
                  <a-tooltip content="解除绑定">
                    <a-button
                      type="text"
                      status="danger"
                      size="small"
                      :disabled="!memberByResourceUID.has(record.resource_uid)"
                      @click.stop="confirmUnbindResource(record.resource_uid)"
                    >
                      <template #icon><icon-delete /></template>
                    </a-button>
                  </a-tooltip>
                </template>
              </a-table-column>
            </template>
          </a-table>
        </a-tab-pane>
        <a-tab-pane key="impact" title="影响分析">
          <a-alert type="info" show-icon>第一版展示业务依赖和资源链路作为影响范围依据。</a-alert>
        </a-tab-pane>
        <a-tab-pane key="timeline" title="时间线">
          <a-timeline v-if="timeline.length">
            <a-timeline-item v-for="item in timeline" :key="`${item.source}-${item.source_id}-${item.occurred_at}`" :label="item.occurred_at">
              <strong>{{ item.title || item.source }}</strong>
              <div class="timeline-message">{{ item.message || '-' }}</div>
            </a-timeline-item>
          </a-timeline>
          <a-empty v-else description="暂无时间线" />
        </a-tab-pane>
      </a-tabs>
    </a-card>

    <a-modal
      v-model:visible="bindModalVisible"
      title="绑定资源"
      :confirm-loading="bindSubmitting"
      :on-before-ok="submitBindResource"
      unmount-on-close
      @cancel="resetBindForm"
    >
      <a-form :model="bindForm" layout="vertical">
        <div class="bind-filter">
          <a-input
            v-model="bindFilters.resource_category"
            allow-clear
            placeholder="资源分类"
            @press-enter="fetchResourceCandidates"
          />
          <a-input
            v-model="bindFilters.keyword"
            allow-clear
            placeholder="搜索 UID / 名称 / 标识"
            @press-enter="fetchResourceCandidates"
          />
          <a-button :loading="resourceCandidateLoading" @click="fetchResourceCandidates">查询</a-button>
        </div>
        <a-form-item field="resource_uid" label="资源" required>
          <a-select v-model="bindForm.resource_uid" allow-search placeholder="请选择资源" :loading="resourceCandidateLoading">
            <a-option v-for="resource in availableResourceCandidates" :key="resource.resource_uid" :value="resource.resource_uid">
              <div class="resource-option">
                <span class="resource-option-title">{{ resource.display_name || resource.resource_uid }}</span>
                <span class="resource-option-meta">{{ resource.resource_uid }} / {{ resource.resource_category || '-' }}</span>
              </div>
            </a-option>
          </a-select>
        </a-form-item>
        <a-form-item field="role" label="角色">
          <a-select v-model="bindForm.role">
            <a-option value="member">member</a-option>
            <a-option value="primary">primary</a-option>
            <a-option value="standby">standby</a-option>
            <a-option value="database">database</a-option>
            <a-option value="cache">cache</a-option>
            <a-option value="queue">queue</a-option>
            <a-option value="storage">storage</a-option>
            <a-option value="probe">probe</a-option>
          </a-select>
        </a-form-item>
        <a-form-item field="criticality" label="重要性">
          <a-select v-model="bindForm.criticality">
            <a-option value="high">high</a-option>
            <a-option value="medium">medium</a-option>
            <a-option value="low">low</a-option>
          </a-select>
        </a-form-item>
        <a-form-item field="weight" label="权重">
          <a-input-number v-model="bindForm.weight" :min="1" :max="10" />
        </a-form-item>
        <a-form-item field="description" label="说明">
          <a-textarea v-model="bindForm.description" :auto-size="{ minRows: 3, maxRows: 6 }" placeholder="说明该资源在当前业务中的作用" />
        </a-form-item>
      </a-form>
    </a-modal>
    </PageState>
  </div>
</template>

<script setup lang="ts">
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { VueFlow, useVueFlow } from '@vue-flow/core'
import { Background } from '@vue-flow/background'
import { Controls } from '@vue-flow/controls'
import '@vue-flow/core/dist/style.css'
import '@vue-flow/core/dist/theme-default.css'
import '@vue-flow/controls/dist/style.css'
import { Message, Modal } from '@arco-design/web-vue'
import { IconDelete, IconLink, IconRefresh } from '@arco-design/web-vue/es/icon'
import {
  createBusinessMember,
  deleteBusinessMember,
  fetchBusinessHealth,
  fetchBusinessMembers,
  fetchBusinessSystems,
  fetchBusinessTimeline,
  fetchBusinessTopology,
  type BusinessHealth,
  type BusinessResourceMember,
  type BusinessResourceMemberPayload,
  type BusinessSystemItem,
  type BusinessTimelineItem,
  type BusinessTopology,
  type BusinessTopologyResource,
} from '@/api/ops/businessSystem'
import { fetchControlResources, type ControlResource } from '@/api/ops/dcControl'
import { buildBusinessTopologyGraph, healthFilterMatch, healthLevelFromScore, statusColor } from './transform'
import type { BusinessTopologyHealthFilter, BusinessTopologySelection, BusinessTopologyViewMode } from './types'

const route = useRoute()
const router = useRouter()
const { fitView } = useVueFlow()

const loading = ref(false)
const healthLoading = ref(false)
const selectionLoading = ref(false)
const businessSystems = ref<BusinessSystemItem[]>([])
const selectedBusinessId = ref<number | undefined>()
const selectedItem = ref<BusinessTopologySelection | null>(null)
const selectedHealth = ref<BusinessHealth | null>(null)
const businessMembers = ref<BusinessResourceMember[]>([])
const resourceCandidates = ref<ControlResource[]>([])
const resourceCandidateLoading = ref(false)
const bindModalVisible = ref(false)
const bindSubmitting = ref(false)

const bindFilters = reactive({
  keyword: '',
  resource_category: undefined as string | undefined,
})

const bindForm = reactive<BusinessResourceMemberPayload>({
  resource_uid: '',
  role: 'member',
  criticality: 'medium',
  weight: 1,
  description: '',
})
const topology = ref<BusinessTopology | null>(null)
const timeline = ref<BusinessTimelineItem[]>([])
const errorMessage = ref('')
const partialMessage = ref('')
const pageUnauthorized = ref(false)
let selectionRequestSerial = 0

const filters = reactive({
	health: 'all' as BusinessTopologyHealthFilter,
	mode: 'resource' as BusinessTopologyViewMode,
	keyword: '',
})

const selectedBusiness = computed(() => businessSystems.value.find((item) => item.id === selectedBusinessId.value))

const topologyResources = computed<BusinessTopologyResource[]>(() => topology.value?.resources ?? [])

const memberByResourceUID = computed(() => {
  const map = new Map<string, BusinessResourceMember>()
  businessMembers.value.forEach((member) => {
    if (member.resource_uid) {
      map.set(member.resource_uid, member)
    }
  })
  return map
})

const boundResourceUIDs = computed(() => {
  const resourceUIDs = new Set<string>()
  businessMembers.value.forEach((member) => {
    if (member.resource_uid) {
      resourceUIDs.add(member.resource_uid)
    }
  })
  topology.value?.resources?.forEach((resource) => {
    if (resource.resource_uid) {
      resourceUIDs.add(resource.resource_uid)
    }
  })
  return resourceUIDs
})

const availableResourceCandidates = computed(() =>
  resourceCandidates.value.filter((resource) => resource.resource_uid && !boundResourceUIDs.value.has(resource.resource_uid))
)

const currentHealthScore = computed(() => selectedHealth.value?.health?.score ?? selectedBusiness.value?.health_score)

const graph = computed(() => {
  if (!selectedBusiness.value || !topology.value) {
    return { nodes: [], edges: [] }
  }

  return buildBusinessTopologyGraph({
    currentBusiness: selectedBusiness.value,
    topology: topology.value,
    health: selectedHealth.value,
    mode: filters.mode,
    keyword: filters.keyword,
  })
})

const selectedTypeLabel = computed(() => {
  const item = selectedItem.value

  if (!item) {
    return '-'
  }

  if (item.type === 'node') {
    return item.data.kind === 'business' ? '业务节点' : '资源节点'
  }

  return item.data.kind === 'business_dependency' ? '业务依赖' : '资源链路'
})

const selectedStatusTagColor = computed(() => {
  const status = selectedItem.value?.data.status?.trim().toLowerCase()

  if (!status) {
    return 'gray'
  }

  if (['critical', 'error', 'failed', 'failure', 'offline', 'inactive', 'disabled', 'high'].includes(status)) {
    return 'red'
  }

  if (['degraded', 'warning', 'warn', 'pending', 'maintenance', 'medium'].includes(status)) {
    return 'orange'
  }

  if (['healthy', 'normal', 'ok', 'online', 'active', 'running', 'enabled', 'low'].includes(status)) {
    return 'green'
  }

  return 'arcoblue'
})

const filteredBusinessSystems = computed(() => {
  const keyword = filters.keyword.trim().toLowerCase()

  return businessSystems.value.filter((item) => {
    if (!healthFilterMatch(item, filters.health)) {
      return false
    }

    if (!keyword) {
      return true
    }

    const values = [item.name, item.code, item.system_type, item.description]
    return values.some((value) => value?.toLowerCase().includes(keyword))
  })
})

const pageStatus = computed(() => {
  if (pageUnauthorized.value) return 'unauthorized'
  if (loading.value && !businessSystems.value.length) return 'loading'
  if (errorMessage.value && !businessSystems.value.length) return 'error'
  if (!businessSystems.value.length) return 'empty'
  if (partialMessage.value) return 'partial'
  return 'success'
})

const queryBusinessId = computed(() => normalizeId(Array.isArray(route.query.id) ? route.query.id[0] : route.query.id))

const isForbidden = (error: any) => error?.response?.status === 403 || error?.code === 403 || error?.code === 401

const setLoadError = (error: any, fallback: string) => {
  if (isForbidden(error)) {
    pageUnauthorized.value = true
    errorMessage.value = '当前账号没有访问业务拓扑的权限。'
    return
  }

  errorMessage.value = error?.message || fallback
}

const normalizeId = (value: unknown) => {
  const id = Number(value)
  return Number.isFinite(id) && id > 0 ? id : undefined
}

const healthScoreText = (score?: number) => (typeof score === 'number' && !Number.isNaN(score) ? `${Math.round(score)}` : '未知')

const healthDotColor = (item: BusinessSystemItem) => {
  if (typeof item.health_score !== 'number' || Number.isNaN(item.health_score)) {
    return '#8c8c8c'
  }

  const level = item.health_level?.trim() || healthLevelFromScore(item.health_score)
  return statusColor(level)
}

const businessInitial = (item: BusinessSystemItem) => (item.name || item.code || '-').slice(0, 1).toUpperCase()

const handleNodeClick = ({ node }: any) => {
  selectedItem.value = { type: 'node', id: node.id, data: node.data }
}

const handleEdgeClick = ({ edge }: any) => {
  selectedItem.value = { type: 'edge', id: edge.id, data: edge.data }
}

const clearSelectedItem = () => {
  selectedItem.value = null
}

const resetBindForm = () => {
  Object.assign(bindForm, {
    resource_uid: '',
    role: 'member',
    criticality: 'medium',
    weight: 1,
    description: '',
  })
}

const fetchResourceCandidates = async () => {
  resourceCandidateLoading.value = true
  try {
    const response = await fetchControlResources({
      page: 1,
      size: 100,
      keyword: bindFilters.keyword,
      resource_category: bindFilters.resource_category,
    })
    resourceCandidates.value = response.details?.data || []
  } catch (error) {
    console.error('获取资源库候选资源失败:', error)
    Message.error('获取资源库候选资源失败')
  } finally {
    resourceCandidateLoading.value = false
  }
}

const openBindResource = async () => {
  resetBindForm()
  bindModalVisible.value = true
  await fetchResourceCandidates()
}

const submitBindResource = async () => {
  if (bindSubmitting.value) {
    return false
  }

  const businessId = selectedBusinessId.value
  if (!businessId || !bindForm.resource_uid) {
    Message.warning('请选择要绑定的资源')
    return false
  }

  if (!availableResourceCandidates.value.some((resource) => resource.resource_uid === bindForm.resource_uid)) {
    Message.warning('请选择有效且未绑定的资源')
    return false
  }

  bindSubmitting.value = true
  try {
    await createBusinessMember(businessId, {
      resource_uid: bindForm.resource_uid,
      role: bindForm.role || 'member',
      criticality: bindForm.criticality || 'medium',
      weight: bindForm.weight || 1,
      description: bindForm.description,
    })
    Message.success('资源已绑定')
    await fetchSelectedBusinessData(businessId)
    return true
  } catch (error) {
    console.error('绑定资源失败:', error)
    Message.error('绑定资源失败')
    return false
  } finally {
    bindSubmitting.value = false
  }
}

const confirmUnbindResource = (resourceUID?: string) => {
  const businessId = selectedBusinessId.value
  if (!businessId || !resourceUID) {
    Message.warning('未找到资源绑定关系')
    return
  }

  const member = memberByResourceUID.value.get(resourceUID)
  if (!member) {
    Message.warning('未找到资源绑定关系')
    return
  }

  Modal.warning({
    title: '解除资源绑定',
    content: `确认将资源「${resourceUID}」从当前业务移除？该操作只解除绑定，不删除资源库中的全局资源。`,
    hideCancel: false,
    okText: '解除绑定',
    cancelText: '取消',
    onOk: async () => {
      try {
        await deleteBusinessMember(businessId, member.id)
        Message.success('资源绑定已解除')
        selectedItem.value = null
        await fetchSelectedBusinessData(businessId)
      } catch (error) {
        console.error('解除资源绑定失败:', error)
        Message.error('解除资源绑定失败')
      }
    },
  })
}

const hydrateHealth = async (items: BusinessSystemItem[]) => {
  if (!items.length) {
    return []
  }

  healthLoading.value = true
  try {
    const settled = await Promise.allSettled(items.map((item) => fetchBusinessHealth(item.id)))
    const healthFailed = settled.some((item) => item.status === 'rejected')

    if (healthFailed) {
      partialMessage.value = '部分业务健康数据加载失败，列表已展示基础业务信息。'
    }

    return items.map((item, index) => {
      const result = settled[index]

      if (result.status !== 'fulfilled' || !result.value.details) {
        return item
      }

      const health = result.value.details
      return {
        ...item,
        health_score: health.health.score,
        health_level: health.health.level,
        resource_count: health.signal_counts.resources,
        active_incident_count: health.signal_counts.active_incidents,
      }
    })
  } finally {
    healthLoading.value = false
  }
}

const fetchData = async () => {
  loading.value = true
  errorMessage.value = ''
  partialMessage.value = ''
  pageUnauthorized.value = false

  try {
    const response = await fetchBusinessSystems({ page: 1, size: 200 })
    const list = response.details?.data || []
    businessSystems.value = await hydrateHealth(list)

    const targetId = resolveInitialBusinessId()
    selectedBusinessId.value = targetId

    if (targetId) {
      await replaceBusinessQuery(targetId)
      await fetchSelectedBusinessData(targetId)
    } else {
      selectedHealth.value = null
      businessMembers.value = []
      topology.value = null
      timeline.value = []
    }
  } catch (error) {
    console.error('获取业务拓扑数据失败:', error)
    setLoadError(error, '获取业务拓扑数据失败')
    Message.error('获取业务拓扑数据失败')
  } finally {
    loading.value = false
  }
}

const resolveInitialBusinessId = () => {
  const requestedId = queryBusinessId.value

  if (requestedId && businessSystems.value.some((item) => item.id === requestedId)) {
    return requestedId
  }

  return businessSystems.value[0]?.id
}

const fetchSelectedBusinessData = async (id: number) => {
  const requestSerial = ++selectionRequestSerial
  selectionLoading.value = true
  partialMessage.value = ''
  selectedHealth.value = null
  businessMembers.value = []
  selectedItem.value = null
  topology.value = null
  timeline.value = []

  try {
    const [healthResult, topologyResult, timelineResult, membersResult] = await Promise.allSettled([
      fetchBusinessHealth(id),
      fetchBusinessTopology(id),
      fetchBusinessTimeline(id, { limit: 100 }),
      fetchBusinessMembers(id),
    ])

    if (requestSerial !== selectionRequestSerial) {
      return
    }

    const results = [healthResult, topologyResult, timelineResult, membersResult]
    const forbidden = results.some((item) => item.status === 'rejected' && isForbidden(item.reason))

    if (forbidden) {
      pageUnauthorized.value = true
      errorMessage.value = '当前账号没有访问业务拓扑的权限。'
      return
    }

    if (healthResult.status === 'fulfilled') {
      selectedHealth.value = healthResult.value.details || null
    }

    if (topologyResult.status === 'fulfilled') {
      topology.value = topologyResult.value.details || null
    }

    if (timelineResult.status === 'fulfilled') {
      timeline.value = timelineResult.value.details?.list || []
    }

    if (membersResult.status === 'fulfilled') {
      businessMembers.value = membersResult.value.details?.list ?? []
    }

    if (results.some((item) => item.status === 'rejected')) {
      partialMessage.value = '部分业务拓扑数据加载失败，页面已展示可用信息。'
    }
  } finally {
    if (requestSerial === selectionRequestSerial) {
      selectionLoading.value = false
    }
  }
}

const replaceBusinessQuery = (id: number) => router.replace({ query: { ...route.query, id: String(id) } })

const selectBusiness = async (id: number, force = false) => {
  if (!force && id === selectedBusinessId.value) {
    return
  }

  selectedBusinessId.value = id
  await replaceBusinessQuery(id)
  await fetchSelectedBusinessData(id)
}

const handleBusinessChange = (value: unknown) => {
  const id = normalizeId(value)

  if (!id) {
    return
  }

  selectBusiness(id, true)
}

const handleResetLayout = () => {
  fitView({ padding: 0.2, duration: 300 })
}

watch(queryBusinessId, (id) => {
  if (!id || id === selectedBusinessId.value || !businessSystems.value.some((item) => item.id === id)) {
    return
  }

  selectBusiness(id)
})

watch([selectedBusinessId, topology, selectedHealth, () => filters.mode], async () => {
  if (!selectedBusinessId.value || !topology.value || !graph.value.nodes.length) {
    return
  }

  await nextTick()
  fitView({ padding: 0.2, duration: 300 })
})

onMounted(fetchData)
</script>

<script lang="ts">
export default {
  name: 'BusinessTopologyView',
}
</script>

<style scoped lang="less">
.business-topology-page {
  padding: 16px;
}

.filter-card {
  margin-bottom: 16px;
}

.filter-form {
  row-gap: 12px;
}

.filter-form :deep(.arco-select),
.filter-form :deep(.arco-input-wrapper) {
  width: 220px;
}

.topology-shell {
  display: grid;
  grid-template-columns: minmax(260px, 300px) minmax(0, 1fr) minmax(260px, 320px);
  gap: 16px;
  min-height: 560px;
}

.business-list-panel,
.topology-canvas-panel,
.detail-panel {
  min-width: 0;
  border: 1px solid var(--color-border-2);
  border-radius: 6px;
  background: var(--color-bg-2);
}

.business-list-panel,
.detail-panel {
  padding: 16px;
}

.panel-title {
  margin-bottom: 12px;
  color: var(--color-text-1);
  font-size: 15px;
  font-weight: 600;
}

.list-spin,
.topology-spin {
  display: block;
  width: 100%;
}

.business-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.business-list-item {
  display: grid;
  grid-template-columns: 36px minmax(0, 1fr) auto;
  gap: 10px;
  align-items: center;
  width: 100%;
  min-height: 64px;
  padding: 10px;
  text-align: left;
  cursor: pointer;
  border: 1px solid var(--color-border-2);
  border-radius: 6px;
  background: var(--color-fill-1);
  transition: border-color 0.2s ease, background-color 0.2s ease;
}

.business-list-item:hover,
.business-list-item.active {
  border-color: rgb(var(--primary-6));
  background: var(--color-primary-light-1);
}

.business-avatar {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  overflow: hidden;
  color: #fff;
  font-weight: 600;
  border-radius: 6px;
}

.business-info {
  min-width: 0;
}

.business-name,
.business-code {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.business-name {
  color: var(--color-text-1);
  font-weight: 600;
}

.business-code {
  margin-top: 4px;
  color: var(--color-text-3);
  font-size: 12px;
}

.business-health {
  color: var(--color-text-1);
  font-size: 18px;
  font-weight: 700;
}

.business-health.unknown {
  color: var(--color-text-3);
  font-size: 13px;
  font-weight: 500;
}

.topology-canvas-panel {
  display: flex;
  align-items: stretch;
  justify-content: center;
  padding: 0;
  overflow: hidden;
}

.topology-spin,
.topology-spin :deep(.arco-spin-children) {
  width: 100%;
  height: 100%;
}

.business-flow {
  width: 100%;
  height: min(680px, calc(100vh - 290px));
  min-height: 460px;
  background:
    linear-gradient(var(--color-border-1) 1px, transparent 1px),
    linear-gradient(90deg, var(--color-border-1) 1px, transparent 1px);
  background-color: var(--color-bg-1);
  background-size: 24px 24px;
}

.topology-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 560px;
  padding: 32px;
  background:
    linear-gradient(var(--color-border-1) 1px, transparent 1px),
    linear-gradient(90deg, var(--color-border-1) 1px, transparent 1px);
  background-color: var(--color-bg-1);
  background-size: 24px 24px;
}

.flow-node {
  width: 190px;
  padding: 10px 12px;
  border: 1px solid var(--color-border-2);
  border-radius: 6px;
  background: var(--color-bg-2);
  box-shadow: 0 6px 18px rgba(29, 33, 41, 0.08);
}

.flow-node-resource {
  width: 210px;
}

.flow-node-header {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}

.flow-node-dot {
  width: 8px;
  height: 8px;
  flex: 0 0 8px;
  border-radius: 50%;
}

.flow-node-title {
  min-width: 0;
  overflow: hidden;
  color: var(--color-text-1);
  font-size: 14px;
  font-weight: 600;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.flow-node-meta {
  display: flex;
  flex-direction: column;
  gap: 2px;
  overflow: hidden;
  margin-top: 8px;
  color: var(--color-text-3);
  font-size: 12px;
}

.flow-node-meta span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.bottom-tabs {
  margin-top: 16px;
}

.tab-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 12px;
}

.tab-count {
  color: var(--color-text-2);
  font-size: 13px;
}

.bind-filter {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.5fr) auto;
  gap: 8px;
  width: 100%;
  margin-bottom: 12px;
}

.resource-option {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.resource-option-title,
.resource-option-meta {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.resource-option-title {
  color: var(--color-text-1);
}

.resource-option-meta {
  margin-top: 2px;
  color: var(--color-text-3);
  font-size: 12px;
}

.timeline-message {
  margin-top: 4px;
  color: var(--color-text-2);
}

.detail-panel {
  display: flex;
  flex-direction: column;
}

.detail-panel :deep(.arco-empty) {
  margin: auto 0;
}

@media (max-width: 1200px) {
  .topology-shell {
    grid-template-columns: minmax(240px, 300px) minmax(0, 1fr);
  }

  .detail-panel {
    grid-column: 1 / -1;
    min-height: 220px;
  }
}

@media (max-width: 768px) {
  .business-topology-page {
    padding: 12px;
  }

  .filter-form {
    display: block;
  }

  .filter-form :deep(.arco-form-item) {
    width: 100%;
    margin-right: 0;
  }

  .filter-form :deep(.arco-select),
  .filter-form :deep(.arco-input-wrapper) {
    width: 100%;
  }

  .topology-shell {
    grid-template-columns: 1fr;
  }

  .bind-filter {
    grid-template-columns: 1fr;
  }

  .business-flow,
  .topology-empty {
    min-height: 360px;
    height: 420px;
  }

  :deep(.arco-page-header-wrapper) {
    padding-right: 0;
    padding-left: 0;
  }
}
</style>
