<script setup lang="ts">
import {
  NButton,
  NDataTable,
  NDropdown,
  NPopconfirm,
  NSpin,
  useDialog,
  type DataTableColumns,
} from 'naive-ui'
import { computed, h, ref, watch } from 'vue'
import { RouterLink, useRoute, useRouter } from 'vue-router'

import { getBootVolume } from '@/api/bootVolumes'
import { getConfig } from '@/api/configs'
import {
  changePublicIp,
  deleteConsoleConnection,
  detachBootVolume,
  detachVolume,
  getImage,
  getInstance,
  instanceAction,
  listBootVolumeAttachments,
  listConsoleConnections,
  listVolumeAttachments,
  terminateInstance,
  updateInstance,
} from '@/api/instances'
import FootNote from '@/components/FootNote.vue'
import AttachVolumeModal from '@/components/instance/AttachVolumeModal.vue'
import ConsoleConnModal from '@/components/instance/ConsoleConnModal.vue'
import EditBootVolumeModal from '@/components/instance/EditBootVolumeModal.vue'
import InstanceTrafficPanel from '@/components/instance/InstanceTrafficPanel.vue'
import ReplaceBootVolumeModal from '@/components/instance/ReplaceBootVolumeModal.vue'
import ResizeModal from '@/components/instance/ResizeModal.vue'
import VnicPanel from '@/components/instance/VnicPanel.vue'
import { useConsoleStore } from '@/stores/console'
import LifecycleBadge from '@/components/LifecycleBadge.vue'
import OcidText from '@/components/OcidText.vue'
import RenameModal from '@/components/RenameModal.vue'
import { fmtTime, shortAd, truncOcid, vpusLabel } from '@/composables/useFormat'
import { useRegionAlias } from '@/composables/useRegionAlias'
import { useAsync } from '@/composables/useAsync'
import {
  ATTACHMENT_TRANSIENT_STATES,
  INSTANCE_TRANSIENT_STATES,
  useTransientPoll,
} from '@/composables/useTransientPoll'
import type { BootVolume, ConsoleConnection, PowerAction, VolumeAttachment } from '@/types/api'
import { useToast } from '@/composables/useToast'

const route = useRoute()
const router = useRouter()
const message = useToast()
const dialog = useDialog()
const { alias: regionAlias } = useRegionAlias()

const cfgId = computed(() => Number(route.params.cfgId))
const instanceId = computed(() => String(route.params.instanceId))
/** 实例所在区域：列表页跳转时经 query 传入，缺失时回退实例对象上的短名（后端已归一化） */
const region = computed(
  () => (route.query.region as string) || instance.data.value?.region || undefined,
)

const config = useAsync(() => getConfig(cfgId.value))
const instance = useAsync(() =>
  getInstance(cfgId.value, instanceId.value, (route.query.region as string) || undefined),
)
const bvAtts = useAsync(() =>
  listBootVolumeAttachments(cfgId.value, instanceId.value, region.value),
)
const volAtts = useAsync(() => listVolumeAttachments(cfgId.value, instanceId.value, region.value))
const consoles = useAsync(() =>
  listConsoleConnections(cfgId.value, instanceId.value, region.value),
)

/** 按 imageId 补镜像名称，查询失败时回退 OCID 展示 */
const image = useAsync(
  () => {
    const inst = instance.data.value
    if (!inst?.imageId) return Promise.resolve(null)
    return getImage(cfgId.value, inst.imageId, inst.region).catch(() => null)
  },
  false,
)
watch(
  () => instance.data.value?.imageId,
  (v) => {
    if (v) void image.run()
  },
)

/** 引导卷挂载补齐卷详情（名称 / 大小 / VPU），支持就地调整 */
const bvDetails = useAsync<BootVolume[]>(async () => {
  const atts = bvAtts.data.value ?? []
  const list = await Promise.all(
    atts.map((a) => getBootVolume(cfgId.value, a.bootVolumeId, region.value).catch(() => null)),
  )
  return list.filter((b): b is BootVolume => b !== null)
}, false)
watch(
  () => bvAtts.data.value,
  () => void bvDetails.run(),
)

const bvById = computed(() => new Map((bvDetails.data.value ?? []).map((b) => [b.id, b])))

// 电源 / 挂载操作提交后 OCI 状态异步迁移：过渡态期间每 5 秒静默刷新直到稳态
useTransientPoll(
  () => instance.data.value,
  (v) => !!v && INSTANCE_TRANSIENT_STATES.includes(v.lifecycleState),
  () => void instance.run({ silent: true }),
)
useTransientPoll(
  () => bvAtts.data.value,
  (list) => !!list?.some((a) => ATTACHMENT_TRANSIENT_STATES.includes(a.lifecycleState)),
  () => void bvAtts.run({ silent: true }),
)
useTransientPoll(
  () => volAtts.data.value,
  (list) => !!list?.some((a) => ATTACHMENT_TRANSIENT_STATES.includes(a.lifecycleState)),
  () => void volAtts.run({ silent: true }),
)
useTransientPoll(
  () => consoles.data.value,
  (list) => !!list?.some((c) => ['CREATING', 'DELETING'].includes(c.lifecycleState)),
  () => void consoles.run({ silent: true }),
)

const isStopped = computed(() => instance.data.value?.lifecycleState === 'STOPPED')
/** 终止 / 终止中：一切变更类操作不可用 */
const isEnded = computed(() =>
  ['TERMINATED', 'TERMINATING'].includes(instance.data.value?.lifecycleState ?? ''),
)

// 区块数据首次加载中禁用对应头部按钮(loading && !data:静默轮询刷新不闪禁用态)
const bvLoading = computed(
  () => (bvAtts.loading.value || bvDetails.loading.value) && !bvDetails.data.value,
)
const volLoading = computed(() => volAtts.loading.value && !volAtts.data.value)
const consolesLoading = computed(() => consoles.loading.value && !consoles.data.value)
const attachedVolumeIds = computed(() => (volAtts.data.value ?? []).map((a) => a.volumeId))

/** 创建时若选了随机密码，密码写在 TAG RootPassword 中，这里回显 */
const rootPassword = computed(() => instance.data.value?.freeformTags?.RootPassword ?? '')
// 密码默认掩码(S-01),点击查看后本页会话内保持可见
const pwdRevealed = ref(false)

function revealRootPassword() {
  pwdRevealed.value = true
}

async function copyRootPassword() {
  await navigator.clipboard.writeText(rootPassword.value)
  message.success('已复制 ROOT 密码')
}

async function copyImageOcid() {
  await navigator.clipboard.writeText(instance.data.value?.imageId ?? '')
  message.success('已复制镜像 OCID')
}

const showRename = ref(false)
const renaming = ref(false)
const showResize = ref(false)
const showConsole = ref(false)
const showReplaceBv = ref(false)
const showAttachVol = ref(false)
const showEditBv = ref(false)
const editingBv = ref<BootVolume | null>(null)

function openEditBv(bv: BootVolume) {
  editingBv.value = bv
  showEditBv.value = true
}

const consoleStore = useConsoleStore()

/** VNC 与串行共用实例唯一的控制台连接资源（创建时互相清理），同时只开一个 */
function openVnc() {
  consoleStore.close()
  const href = router.resolve({
    name: 'vnc-console',
    params: { cfgId: cfgId.value, instanceId: instanceId.value },
    query: { region: region.value, name: instance.data.value?.displayName },
  }).href
  window.open(href, '_blank')
}

function openSerial() {
  void consoleStore.openSerial({
    cfgId: cfgId.value,
    instanceId: instanceId.value,
    region: region.value,
    instanceName: instance.data.value?.displayName,
  })
}

async function doRename(name: string) {
  renaming.value = true
  try {
    await updateInstance(cfgId.value, instanceId.value, { displayName: name }, region.value)
    message.success('已重命名')
    showRename.value = false
    void instance.run()
  } catch (e) {
    message.error(e instanceof Error ? e.message : '重命名失败')
  } finally {
    renaming.value = false
  }
}

async function act(fn: () => Promise<unknown>, ok: string) {
  try {
    await fn()
    message.success(ok)
    void instance.run()
  } catch (e) {
    message.error(e instanceof Error ? e.message : '操作失败')
  }
}

// 换 IP 请求期间的按钮 loading，操作发出立即有反馈
const changeIpBusy = ref(false)

function power(action: PowerAction) {
  void act(
    () => instanceAction(cfgId.value, instanceId.value, action, region.value),
    `${action} 已提交`,
  )
}

const moreOptions = [
  { label: 'SOFTSTOP（系统内关机）', key: 'SOFTSTOP' },
  { label: 'SOFTRESET（系统内重启）', key: 'SOFTRESET' },
]

function terminate() {
  dialog.warning({
    title: '终止实例',
    content: '终止后实例不可恢复。是否同时保留引导卷？',
    positiveText: '终止并删除引导卷',
    negativeText: '终止但保留引导卷',
    onPositiveClick: () =>
      act(
        () => terminateInstance(cfgId.value, instanceId.value, false, region.value),
        '终止请求已提交',
      ),
    onNegativeClick: () =>
      act(
        () => terminateInstance(cfgId.value, instanceId.value, true, region.value),
        '终止请求已提交（保留引导卷）',
      ),
  })
}

async function doChangeIp() {
  changeIpBusy.value = true
  await act(() => changePublicIp(cfgId.value, instanceId.value, region.value), '公网 IP 已更换')
  changeIpBusy.value = false
}

const volColumns: DataTableColumns<VolumeAttachment> = [
  {
    title: '块卷',
    key: 'volumeName',
    minWidth: 170,
    render: (r) =>
      h('div', { class: 'min-w-0' }, [
        h(
          'div',
          { class: 'font-medium' },
          r.volumeName || r.displayName || truncOcid(r.volumeId, 18),
        ),
        r.volumeId.includes('.bootvolume.')
          ? h('div', { class: 'text-xs text-ink-3 mt-px' }, '引导卷 · 作数据卷附加')
          : null,
      ]),
  },
  {
    title: '设备路径',
    key: 'device',
    minWidth: 200,
    render: (r) => h('span', { class: 'mono' }, r.device || '—'),
  },
  {
    title: '读写',
    key: 'isReadOnly',
    width: 80,
    render: (r) => h('span', { class: 'text-[13px]' }, r.isReadOnly ? '只读' : '读写'),
  },
  {
    title: '状态',
    key: 'lifecycleState',
    width: 120,
    render: (r) => h(LifecycleBadge, { state: r.lifecycleState }),
  },
  {
    title: '操作',
    key: 'actions',
    width: 90,
    render: (r) =>
      h(
        NPopconfirm,
        {
          onPositiveClick: () =>
            act(() => detachVolume(cfgId.value, r.id, region.value), '分离请求已提交'),
        },
        {
          trigger: () => h(NButton, { size: 'tiny', quaternary: true }, { default: () => '分离' }),
          default: () => '分离该块卷？OS 内请先卸载文件系统',
        },
      ),
  },
]

/** DELETED 是 OCI 保留的终态记录，不可操作也无展示价值，直接过滤 */
const liveConsoles = computed(() =>
  (consoles.data.value ?? []).filter((c) => c.lifecycleState !== 'DELETED'),
)

const deletingConn = ref('')

async function doDeleteConsoleConn(id: string) {
  deletingConn.value = id
  try {
    await deleteConsoleConnection(cfgId.value, id, region.value)
    message.success('删除请求已提交')
    void consoles.run()
  } catch (e) {
    message.error(e instanceof Error ? e.message : '删除失败')
  } finally {
    deletingConn.value = ''
  }
}

/** 连接串行：类型标签 + 截断命令 + 行内复制，避免整串撑爆表格 */
function connStringRow(label: string, value: string): ReturnType<typeof h> {
  return h('div', { class: 'flex min-w-0 max-w-[640px] items-center gap-1.5' }, [
    h('span', { class: 'shrink-0 rounded bg-wash px-1.5 text-[11px] leading-5 text-ink-2' }, label),
    h('code', { class: 'mono min-w-0 flex-1 truncate text-xs', title: value }, value),
    h(
      NButton,
      {
        size: 'tiny',
        quaternary: true,
        onClick: async () => {
          await navigator.clipboard.writeText(value)
          message.success(`已复制${label}连接串`)
        },
      },
      { default: () => '复制' },
    ),
  ])
}

const consoleColumns: DataTableColumns<ConsoleConnection> = [
  {
    title: '状态',
    key: 'lifecycleState',
    width: 110,
    render: (r) => h(LifecycleBadge, { state: r.lifecycleState }),
  },
  {
    title: '连接串',
    key: 'connectionString',
    minWidth: 320,
    render: (r) =>
      h('div', { class: 'flex min-w-0 flex-col gap-0.5' }, [
        connStringRow('串口', r.connectionString),
        connStringRow('VNC', r.vncConnectionString),
      ]),
  },
  {
    title: '操作',
    key: 'actions',
    width: 90,
    render: (r) =>
      h(
        NPopconfirm,
        { onPositiveClick: () => doDeleteConsoleConn(r.id) },
        {
          trigger: () =>
            h(
              NButton,
              {
                size: 'tiny',
                quaternary: true,
                type: 'error',
                loading: deletingConn.value === r.id,
                disabled: deletingConn.value !== '' && deletingConn.value !== r.id,
              },
              { default: () => '删除' },
            ),
          default: () => '删除该控制台连接？',
        },
      ),
  },
]
</script>

<template>
  <div class="flex flex-col gap-4 p-6 pb-8 max-md:p-3">
    <div
      v-if="instance.loading.value && !instance.data.value"
      class="panel flex items-center justify-center py-20"
    >
      <NSpin size="small" />
    </div>
    <div
      v-else-if="instance.error.value"
      class="panel px-5 py-10 text-center text-[13px] text-ink-3"
    >
      {{ instance.error.value }}
    </div>

    <div v-if="instance.data.value" class="panel px-5 py-4">
      <RouterLink
        class="mb-2 inline-flex items-center gap-1.5 text-[13px] text-ink-3 hover:text-ink"
        :to="{ name: 'instances' }"
      >
        ← 返回实例列表
      </RouterLink>

      <div class="flex flex-wrap items-center gap-3">
        <h1 class="page-title">{{ instance.data.value.displayName }}</h1>
        <LifecycleBadge :state="instance.data.value.lifecycleState" />
        <div class="flex-1" />
        <NButton v-if="isStopped" size="small" :disabled="isEnded" @click="power('START')">
          启动
        </NButton>
        <NButton v-else size="small" :disabled="isEnded" @click="power('STOP')">停止</NButton>
        <NButton size="small" :disabled="isEnded" @click="power('RESET')">重启</NButton>
        <NButton size="small" type="error" ghost :disabled="isEnded" @click="terminate">
          终止
        </NButton>
        <NDropdown
          :options="moreOptions"
          :disabled="isEnded"
          @select="(key: string) => power(key as PowerAction)"
        >
          <NButton size="small" quaternary :disabled="isEnded">⋯</NButton>
        </NDropdown>
      </div>

      <div class="mt-3 grid grid-cols-5 gap-4 max-lg:grid-cols-3 max-md:grid-cols-1">
        <div class="min-w-0">
          <div class="text-xs text-ink-3">租户</div>
          <div class="mt-0.5 truncate text-[13px]" :title="config.data.value?.tenancyName">
            {{ config.data.value?.tenancyName || config.data.value?.alias || '…' }}
          </div>
        </div>
        <div class="min-w-0">
          <div class="text-xs text-ink-3">区域 / AD</div>
          <div class="mt-0.5 truncate text-[13px]">
            {{ regionAlias(instance.data.value.region) }} ·
            {{ shortAd(instance.data.value.availabilityDomain) }}
          </div>
        </div>
        <div class="min-w-0">
          <div class="text-xs text-ink-3">Shape</div>
          <div class="mono mt-0.5 truncate text-[13px]">{{ instance.data.value.shape }}</div>
        </div>
        <div class="min-w-0">
          <div class="text-xs text-ink-3">规格</div>
          <div class="mt-0.5 text-[13px]">
            {{ instance.data.value.ocpus }} OCPU · {{ instance.data.value.memoryInGBs }} GB
          </div>
        </div>
        <div class="min-w-0">
          <div class="text-xs text-ink-3">创建时间</div>
          <div class="mt-0.5 text-[13px]">{{ fmtTime(instance.data.value.timeCreated) }}</div>
        </div>
      </div>
    </div>

    <div class="grid grid-cols-2 items-start gap-4 max-lg:grid-cols-1">
      <!-- 基本信息 -->
      <div v-if="instance.data.value" class="panel">
        <div class="flex items-center justify-between border-b border-line-soft px-4.5 py-3.5">
          <div class="text-sm font-semibold">基本信息</div>
          <NButton size="tiny" quaternary :disabled="isEnded" @click="showRename = true">
            重命名
          </NButton>
        </div>
        <div class="flex flex-col px-4.5 py-1.5">
          <div class="flex gap-3 border-b border-line-soft py-2.5 max-md:flex-col max-md:gap-0.5">
            <div class="w-22 flex-none text-xs text-ink-3 max-md:w-auto">OCID</div>
            <div class="min-w-0 flex-1"><OcidText :ocid="instance.data.value.id" /></div>
          </div>
          <div class="flex gap-3 border-b border-line-soft py-2.5 max-md:flex-col max-md:gap-0.5">
            <div class="w-22 flex-none text-xs text-ink-3 max-md:w-auto">镜像</div>
            <div class="flex min-w-0 flex-1 items-center gap-1.5">
              <span class="truncate text-[13px]" :title="image.data.value?.displayName">
                {{ image.data.value?.displayName ?? (image.loading.value ? '…' : '未知镜像') }}
              </span>
              <NButton size="tiny" quaternary type="primary" @click="copyImageOcid">复制</NButton>
            </div>
          </div>
          <div
            v-if="rootPassword"
            class="flex gap-3 border-b border-line-soft py-2.5 max-md:flex-col max-md:gap-0.5"
          >
            <div class="w-22 flex-none text-xs text-ink-3 max-md:w-auto">ROOT 密码</div>
            <div class="flex min-w-0 flex-1 items-center gap-1.5">
              <template v-if="pwdRevealed">
                <code class="mono truncate text-[13px]" :title="rootPassword">{{ rootPassword }}</code>
                <NButton size="tiny" quaternary type="primary" @click="copyRootPassword">
                  复制
                </NButton>
              </template>
              <template v-else>
                <code class="mono text-[13px] tracking-widest select-none">••••••••</code>
                <NButton
                  size="tiny"
                  quaternary
                  type="primary"
                  title="验证身份后查看"
                  @click="revealRootPassword"
                >
                  <svg class="mr-1 h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                    <path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"></path>
                    <circle cx="12" cy="12" r="3"></circle>
                  </svg>
                  查看
                </NButton>
              </template>
            </div>
          </div>
          <div class="flex gap-3 py-2.5 max-md:flex-col max-md:gap-0.5">
            <div class="w-22 flex-none text-xs text-ink-3 max-md:w-auto">规格调整</div>
            <div class="min-w-0 flex-1 text-[13px]">
              {{ instance.data.value.ocpus }} OCPU · {{ instance.data.value.memoryInGBs }} GB
              <NButton
                size="tiny"
                quaternary
                type="primary"
                :disabled="isEnded"
                @click="showResize = true"
              >
                调整
              </NButton>
              <div class="mt-0.5 text-xs text-ink-3">
                可更换 shape 或调整 Flex 规格 · 运行中调整会自动重启
              </div>
            </div>
          </div>
        </div>
      </div>

      <!-- 网络 -->
      <div v-if="instance.data.value" class="panel">
        <div class="border-b border-line-soft px-4.5 py-3.5">
          <div class="text-sm font-semibold">网络</div>
          <div class="mt-0.5 text-xs text-ink-3">
            主网卡地址快捷视图；全部网卡与各卡 IPv6 管理见下方「网卡（VNIC）」
          </div>
        </div>
        <div class="flex flex-col px-4.5 py-1.5">
          <div class="flex gap-3 border-b border-line-soft py-2.5 max-md:flex-col max-md:gap-0.5">
            <div class="w-22 flex-none text-xs text-ink-3 max-md:w-auto">公网 IP</div>
            <div class="min-w-0 flex-1">
              <span class="mono">{{ instance.data.value.publicIp || '—' }}</span>
              <NPopconfirm @positive-click="doChangeIp">
                <template #trigger>
                  <NButton
                    size="tiny"
                    quaternary
                    type="primary"
                    :disabled="isEnded"
                    :loading="changeIpBusy"
                  >
                    更换
                  </NButton>
                </template>
                更换即删除旧临时公网 IP 再分配新的（保留 IP 会被拒绝），确认？
              </NPopconfirm>
              <div class="mt-0.5 text-xs text-ink-3">临时 IP · 更换即删旧分新</div>
            </div>
          </div>
          <div class="flex gap-3 border-b border-line-soft py-2.5 max-md:flex-col max-md:gap-0.5">
            <div class="w-22 flex-none text-xs text-ink-3 max-md:w-auto">私有 IP</div>
            <div class="mono min-w-0 flex-1">{{ instance.data.value.privateIp || '—' }}</div>
          </div>
          <div class="flex gap-3 border-b border-line-soft py-2.5 max-md:flex-col max-md:gap-0.5">
            <div class="w-22 flex-none text-xs text-ink-3 max-md:w-auto">IPv6</div>
            <div class="flex min-w-0 flex-1 flex-wrap items-center gap-1.5">
              <span
                v-for="addr in instance.data.value.ipv6Addresses"
                :key="addr"
                class="mono inline-flex max-w-full items-center rounded border border-line bg-white px-1.5 py-0.5 text-xs"
              >
                <span class="truncate">{{ addr }}</span>
              </span>
              <span v-if="!instance.data.value.ipv6Addresses?.length" class="text-[13px]">—</span>
              <div class="w-full text-xs text-ink-3">
                主网卡 IPv6 · 添加 / 删除在下方网卡（VNIC）区块按卡操作
              </div>
            </div>
          </div>
          <div class="flex gap-3 py-2.5 max-md:flex-col max-md:gap-0.5">
            <div class="w-22 flex-none text-xs text-ink-3 max-md:w-auto">子网</div>
            <div class="min-w-0 flex-1"><OcidText :ocid="instance.data.value.subnetId" /></div>
          </div>
        </div>
      </div>
    </div>

    <!-- 网卡(VNIC):每卡一主行 + IPv6 副行;变化后刷实例(主网卡快捷视图跟随) -->
    <VnicPanel
      v-if="instance.data.value"
      :cfg-id="cfgId"
      :instance-id="instanceId"
      :region="region"
      :disabled="isEnded"
      @changed="instance.run({ silent: true })"
    />

    <!-- 引导卷 -->
    <div class="panel">
      <div class="flex items-center justify-between border-b border-line-soft px-4.5 py-3.5">
        <div class="text-sm font-semibold">引导卷</div>
        <NButton
          size="small"
          :disabled="!isStopped || isEnded || bvLoading"
          :title="isStopped ? '' : '替换引导卷须实例处于 STOPPED'"
          @click="showReplaceBv = true"
        >
          替换引导卷
        </NButton>
      </div>
      <div
        v-if="(bvAtts.loading.value || bvDetails.loading.value) && !bvDetails.data.value"
        class="flex items-center justify-center py-8"
      >
        <NSpin size="small" />
      </div>
      <div v-else class="px-4.5 py-1.5">
        <div
          v-for="att in bvAtts.data.value ?? []"
          :key="att.id"
          class="flex flex-wrap items-center gap-3 border-b border-line-soft py-2.5 last:border-b-0"
        >
          <div class="min-w-0">
            <div class="font-medium text-ink">
              {{ bvById.get(att.bootVolumeId)?.displayName ?? truncOcid(att.bootVolumeId, 26) }}
            </div>
            <div class="text-xs text-ink-3">
              <template v-if="bvById.get(att.bootVolumeId)">
                {{ bvById.get(att.bootVolumeId)!.sizeInGBs }} GB · VPU
                {{ vpusLabel(bvById.get(att.bootVolumeId)!.vpusPerGB) }}
              </template>
              <span v-else class="mono">{{ truncOcid(att.bootVolumeId, 26) }}</span>
            </div>
          </div>
          <LifecycleBadge :state="att.lifecycleState" />
          <div class="flex-1" />
          <NButton
            v-if="bvById.get(att.bootVolumeId)"
            size="tiny"
            quaternary
            type="primary"
            :disabled="isEnded"
            @click="openEditBv(bvById.get(att.bootVolumeId)!)"
          >
            调整大小 / VPU
          </NButton>
          <NPopconfirm
            @positive-click="act(() => detachBootVolume(cfgId, att.id, region), '分离请求已提交')"
          >
            <template #trigger>
              <NButton size="tiny" quaternary :disabled="!isStopped || isEnded">分离</NButton>
            </template>
            分离引导卷？实例将无法启动直到挂载新引导卷
          </NPopconfirm>
        </div>
        <div
          v-if="!(bvAtts.data.value ?? []).length"
          class="py-6 text-center text-[13px] text-ink-3"
        >
          无挂载引导卷
        </div>
      </div>
      <FootNote>
        大小仅支持扩容（在线生效，OS 内需扩展文件系统）· 挂载 / 分离 / 替换要求实例处于 STOPPED
      </FootNote>
    </div>

    <!-- 块存储卷 -->
    <div class="panel">
      <div class="flex items-center justify-between border-b border-line-soft px-4.5 py-3.5">
        <div class="text-sm font-semibold">块存储卷</div>
        <NButton
          size="small"
          type="primary"
          :disabled="isEnded || volLoading"
          @click="showAttachVol = true"
        >
          附加块卷
        </NButton>
      </div>
      <NDataTable
        :columns="volColumns"
        :data="volAtts.data.value ?? []"
        :loading="volAtts.loading.value"
        :scroll-x="700"
        :row-key="(r: VolumeAttachment) => r.id"
      />
      <FootNote>
        半虚拟化附加，运行中实例可热插拔；附加后仍需在实例 OS 内分区挂载文件系统
      </FootNote>
    </div>

    <!-- 控制台连接 -->
    <div class="panel">
      <div class="flex items-center justify-between border-b border-line-soft px-4.5 py-3.5">
        <div class="text-sm font-semibold">控制台连接（VNC / 串口）</div>
        <div class="flex items-center gap-2">
          <NButton
            size="small"
            type="primary"
            :disabled="isEnded || consolesLoading"
            @click="openSerial"
          >
            串行终端
          </NButton>
          <NButton size="small" :disabled="isEnded || consolesLoading" @click="openVnc">
            网页 VNC
          </NButton>
          <NButton size="small" :disabled="isEnded || consolesLoading" @click="showConsole = true">
            创建连接
          </NButton>
        </div>
      </div>
      <NDataTable
        :columns="consoleColumns"
        :data="liveConsoles"
        :loading="consoles.loading.value"
        :scroll-x="640"
        :row-key="(r: ConsoleConnection) => r.id"
      />
      <FootNote>
        串行终端 / 网页 VNC 在浏览器内直连（创建会话时自动清理旧连接，二者同时只能开一个）·
        「创建连接」用于本地 ssh / VNC 客户端：公钥仅支持 RSA，串口直接执行串口连接串，VNC
        执行后客户端连 localhost:5900 · 串口登录需实例本地账户密码
      </FootNote>
    </div>

    <!-- 流量 -->
    <InstanceTrafficPanel
      v-if="instance.data.value"
      :cfg-id="cfgId"
      :instance-id="instanceId"
      :region="region"
    />

    <RenameModal
      v-model:show="showRename"
      title="重命名实例"
      :current="instance.data.value?.displayName ?? ''"
      :submitting="renaming"
      @submit="doRename"
    />
    <ResizeModal
      v-if="instance.data.value"
      v-model:show="showResize"
      :cfg-id="cfgId"
      :instance-id="instanceId"
      :region="region"
      :shape="instance.data.value.shape"
      :ocpus="instance.data.value.ocpus"
      :memory-in-g-bs="instance.data.value.memoryInGBs"
      @resized="instance.run()"
    />
    <ConsoleConnModal
      v-model:show="showConsole"
      :cfg-id="cfgId"
      :instance-id="instanceId"
      :region="region"
      @created="consoles.run()"
    />
    <ReplaceBootVolumeModal
      v-if="instance.data.value"
      v-model:show="showReplaceBv"
      :cfg-id="cfgId"
      :instance-id="instanceId"
      :region="region"
      :availability-domain="instance.data.value.availabilityDomain"
      @replaced="bvAtts.run()"
    />
    <AttachVolumeModal
      v-if="instance.data.value"
      v-model:show="showAttachVol"
      :cfg-id="cfgId"
      :instance-id="instanceId"
      :region="region"
      :availability-domain="instance.data.value.availabilityDomain"
      :attached-volume-ids="attachedVolumeIds"
      @attached="volAtts.run()"
    />
    <EditBootVolumeModal
      v-model:show="showEditBv"
      :cfg-id="cfgId"
      :region="region"
      :boot-volume="editingBv"
      @updated="bvDetails.run()"
    />
  </div>
</template>
