<template>
  <v-container fluid class="pa-4 pa-md-6">
    <StatCardsRow
      :items="statItemsDisplay"
      :loading="statsBooting"
      :refreshing="statsRefreshing"
      @click="onStatCardClick"
      @refresh="refreshStatCards"
    />
    <WatchSlotRow />
    <div v-if="statusFilterLabel || unsetPathFocus" class="d-flex align-center flex-wrap ga-2 mb-3">
      <v-chip
        v-if="statusFilterLabel"
        color="primary"
        variant="tonal"
        size="small"
        closable
        label
        @click:close="clearStatusFilter"
      >
        {{ statusFilterLabel }}
      </v-chip>
      <v-chip
        v-if="unsetPathFocus"
        color="warning"
        variant="tonal"
        size="small"
        closable
        label
        @click:close="clearUnsetPathFocus"
      >
        已定位：未设置路径服务器
      </v-chip>
      <span class="text-caption text-medium-emphasis">点击顶部统计卡可切换筛选或定位</span>
    </div>

    <!-- Server Table -->
    <v-card elevation="0" rounded="lg" class="my-5" border>
      <v-card-title class="d-flex align-center">
        服务器列表
        <v-spacer />
          <v-combobox
          v-model="searchDraft"
          :items="serverSearchHistory"
          prepend-inner-icon="mdi-magnify"
          label="搜索名称/IP（含未设路径）"
          density="compact"
          hide-details
          rounded
          clearable
          :auto-select-first="false"
          style="max-width: 240px"
          variant="outlined"
          placeholder="输入后回车，或从历史选择"
          @update:model-value="onSearchComboboxUpdate"
          @keydown.enter.prevent="commitSearch"
        />
        <v-btn color="primary" variant="flat" class="ml-3" prepend-icon="mdi-plus" size="small" @click="openAddServer">
          添加
        </v-btn>
        <v-btn variant="tonal" class="ml-2" prepend-icon="mdi-flash" size="small" @click="openQuickAdd">
          快速添加(IP)
        </v-btn>
        <v-btn variant="tonal" class="ml-2" prepend-icon="mdi-ip-network" size="small" @click="openBatchAdd">
          批量添加
        </v-btn>
        <v-btn variant="tonal" class="ml-2" prepend-icon="mdi-key-outline" size="small" @click="showCredentialsDialog = true">
          凭据
        </v-btn>
        <v-btn
          variant="tonal"
          class="ml-2"
          prepend-icon="mdi-folder-search"
          size="small"
          :disabled="selectedIds.size === 0"
          title="请先勾选至少一台服务器"
          @click="openDetectPathConfirm('main')"
        >
          检测路径
        </v-btn>
        <v-btn-toggle v-model="viewMode" mandatory density="compact" class="ml-2" color="primary" variant="outlined" divided>
          <v-btn value="table" size="small" prepend-icon="mdi-table">列表</v-btn>
          <v-btn value="group" size="small" prepend-icon="mdi-folder-multiple">分类</v-btn>
        </v-btn-toggle>
      </v-card-title>

      <v-card-text class="pt-0 pb-2">
        <v-chip
          v-if="search.trim()"
          size="small"
          variant="tonal"
          color="info"
          label
          class="mb-2"
        >
          全库搜索中（含未设路径与连接失败；已忽略分类/在线筛选）
        </v-chip>
        <div class="d-flex align-center flex-wrap ga-2">
          <span class="text-caption text-medium-emphasis mr-1">分类筛选</span>
          <v-chip
            :color="categoryFilter === '' ? 'primary' : undefined"
            :variant="categoryFilter === '' ? 'flat' : 'outlined'"
            size="small"
            label
            @click="selectCategory('')"
          >
            全部 {{ totalCount }}
          </v-chip>
          <v-chip
            v-for="cat in categoryList"
            :key="cat.name"
            :color="categoryFilter === cat.name ? 'primary' : undefined"
            :variant="categoryFilter === cat.name ? 'flat' : 'outlined'"
            size="small"
            label
            @click="selectCategory(cat.name)"
          >
            {{ cat.label }} {{ cat.count }}
          </v-chip>
          <v-progress-circular v-if="categoriesLoading" indeterminate size="16" width="2" />
          <v-spacer />
          <v-btn
            size="small"
            variant="tonal"
            :disabled="total === 0"
            :loading="selectAllFilteredLoading"
            @click="selectAllFiltered"
          >
            全选筛选结果 ({{ total }})
          </v-btn>
        </div>
      </v-card-text>

      <!-- Batch Actions -->
      <ServerBatchActionBar
        :count="selectedIds.size"
        @batch-category="openBatchCategory('main')"
        @health-check="batchHealthCheck('main')"
        @detect-path="openDetectPathConfirm('main')"
        @install-agent="batchInstallAgent('main')"
        @upgrade-agent="batchUpgradeAgent('main')"
        @uninstall-agent="batchUninstallAgent('main')"
        @batch-delete="confirmBatchDelete('main')"
        @clear="selectedItems = []"
      />

      <v-skeleton-loader
        v-if="viewMode === 'table' && loading && !servers.length"
        type="table-heading, table-row-divider@8"
        class="mb-4"
      />
      <v-data-table-server
        v-if="viewMode === 'table'"
        v-show="!(loading && !servers.length)"
        :items="servers"
        :headers="headers"
        :items-length="total"
        :loading="loading"
        :page="page"
        :items-per-page="itemsPerPage"
        :items-per-page-options="dataTablePageOptions"
        hover
        density="comfortable"
        item-value="id"
        show-select
        v-model="selectedItems"
        v-model:sort-by="sortBy"
        v-model:expanded="expandedMainIdsModel"
        show-expand
        class="server-main-table"
        @update:page="onMainPageChange"
        @update:items-per-page="onMainItemsPerPageChange"
      >
        <template #item.data-table-expand />
        <template #item.status="{ item }">
          <v-chip :color="statusChipColor(item.status)" size="x-small" variant="tonal" label border="sm">
            {{ statusLabel(item.status) }}
          </v-chip>
        </template>

        <template #item.name="{ item }">
          <div
            class="d-flex align-center cursor-pointer text-primary"
            @click.stop="toggleMainExpand(item)"
          >
            <v-icon size="small" class="mr-1">
              {{ expandedMainIds.includes(item.id) ? 'mdi-chevron-down' : 'mdi-chevron-right' }}
            </v-icon>
            <span class="font-weight-medium">{{ item.name }}</span>
          </div>
        </template>

        <template #item.domain="{ item }">
          <span class="text-medium-emphasis">{{ item.domain }}:{{ item.port }}</span>
        </template>

        <template #item.category="{ item }">
          <v-chip v-if="item.category" color="primary" variant="tonal" size="small" label>
            {{ item.category }}
          </v-chip>
          <span v-else class="text-medium-emphasis">—</span>
        </template>

        <template #item.target_path="{ item }">
          <div v-if="editingPathId === item.id" class="d-flex align-center ga-1" @click.stop>
            <v-text-field
              v-model="editingPathValue"
              density="compact"
              variant="outlined"
              hide-details
              placeholder="/www/wwwroot"
              style="max-width: 140px"
              @keyup.enter="saveTargetPath(item.id)"
              @keyup.esc="cancelEditPath"
            />
            <v-btn icon="mdi-check" size="x-small" variant="text" color="primary" :loading="savingPathId === item.id" @click="saveTargetPath(item.id)" />
            <v-btn icon="mdi-close" size="x-small" variant="text" @click="cancelEditPath" />
          </div>
          <div v-else class="d-flex align-center ga-1">
            <span
              class="text-caption text-medium-emphasis text-truncate d-inline-block"
              style="max-width: 140px"
              :title="item.target_path || '未配置'"
            >
              {{ item.target_path?.trim() || '—' }}
            </span>
            <v-btn
              icon="mdi-pencil"
              size="x-small"
              variant="text"
              density="compact"
              title="编辑目标路径"
              @click.stop="startEditPath(item)"
            />
          </div>
        </template>

        <template #item.agent_version="{ item }">
          <div>
            <span class="text-medium-emphasis">{{ item.agent_version || '—' }}</span>
            <v-chip
              v-if="item.agent_action === 'install'"
              size="x-small"
              color="info"
              variant="tonal"
              label
              class="mt-1"
            >
              需安装
            </v-chip>
            <v-chip
              v-else-if="item.agent_action === 'upgrade'"
              size="x-small"
              color="warning"
              variant="tonal"
              label
              class="mt-1"
            >
              需升级
            </v-chip>
          </div>
        </template>

        <template #item.last_heartbeat="{ item }">
          <span class="text-caption text-medium-emphasis">{{ formatRelativeTime(item.last_heartbeat) }}</span>
        </template>

        <template #item.actions="{ item }">
          <div class="d-flex ga-1 align-center">
            <v-btn
              :icon="pinnedMap[item.id] != null ? 'mdi-check' : 'mdi-plus'"
              size="x-small"
              variant="text"
              :color="pinnedMap[item.id] != null ? 'success' : 'primary'"
              density="compact"
              :title="pinnedMap[item.id] != null ? `已在监测 · 槽 ${pinnedMap[item.id]! + 1}` : '加入实时监测'"
              :loading="pinLoadingId === item.id"
              @click.stop="onPinServer(item)"
            />
            <v-btn variant="text" size="x-small" color="primary" density="compact" @click.stop="openTerminal(item)">终端</v-btn>
            <v-btn
              v-if="item.domain"
              variant="text"
              size="x-small"
              density="compact"
              @click.stop="openBrowser(item)"
            >
              站点
            </v-btn>
            <v-btn variant="text" size="x-small" density="compact" @click.stop="openFiles(item)">文件</v-btn>
            <v-btn variant="text" size="x-small" density="compact" :loading="agentDiagnoseLoadingId === item.id" @click.stop="openAgentDiagnose(item)">诊断</v-btn>
            <v-btn variant="text" size="x-small" density="compact" @click.stop="editServer(item)">编辑</v-btn>
            <v-btn variant="text" size="x-small" color="error" density="compact" @click.stop="confirmDelete(item)">删除</v-btn>
          </div>
        </template>

        <template #expanded-row="{ columns, item }">
          <tr>
            <td :colspan="columns.length" class="pa-0">
              <ServerInlineDetail
                :server="item"
                :active="expandedMainIds.includes(item.id)"
                :agent-action-loading="agentActionLoadingId === item.id"
                @edit-path="startEditPath(item)"
                @install-agent="installAgentOne(item)"
                @upgrade-agent="upgradeAgentOne(item)"
                @diagnose-agent="openAgentDiagnose(item)"
              />
            </td>
          </tr>
        </template>

        <template #no-data>
          <div class="text-center text-medium-emphasis py-6">暂无服务器数据</div>
        </template>
      </v-data-table-server>

      <div v-else class="px-4 pb-4">
        <div v-if="groupLoading" class="text-center py-8">
          <v-progress-circular indeterminate size="32" />
          <div class="text-caption text-medium-emphasis mt-2">按分类加载中…</div>
        </div>
        <div v-else-if="groupedServers.length === 0" class="text-center text-medium-emphasis py-6">
          暂无服务器数据
        </div>
        <v-expansion-panels v-else v-model="expandedCategories" multiple variant="accordion" class="servers-category-panels">
          <v-expansion-panel v-for="group in groupedServers" :key="group.category">
            <v-expansion-panel-title>
              <div class="d-flex align-center ga-2 flex-grow-1">
                <v-chip color="primary" variant="tonal" size="small" label>{{ group.label }}</v-chip>
                <span class="text-caption text-medium-emphasis">{{ group.servers.length }} 台</span>
                <v-spacer />
                <v-btn
                  size="x-small"
                  variant="text"
                  @click.stop="toggleGroupAll(group.servers)"
                >
                  {{ isGroupAllSelected(group.servers) ? '取消本组' : '全选本组' }}
                </v-btn>
              </div>
            </v-expansion-panel-title>
            <v-expansion-panel-text>
              <v-table density="compact" hover>
                <thead>
                  <tr>
                    <th style="width: 40px" />
                    <th>状态</th>
                    <th>名称</th>
                    <th>地址</th>
                    <th>目标路径</th>
                    <th class="text-end">操作</th>
                  </tr>
                </thead>
                <tbody>
                  <tr
                    v-for="item in group.servers"
                    :key="item.id"
                  >
                    <td @click.stop>
                      <v-checkbox-btn
                        :model-value="selectedIds.has(item.id)"
                        density="compact"
                        hide-details
                        @update:model-value="toggleGroupSelection(item.id, $event)"
                      />
                    </td>
                    <td>
                      <v-chip :color="statusChipColor(item.status)" size="x-small" variant="tonal" label border="sm">
                        {{ statusLabel(item.status) }}
                      </v-chip>
                    </td>
                    <td class="font-weight-medium">{{ item.name }}</td>
                    <td class="text-medium-emphasis">{{ item.domain }}:{{ item.port }}</td>
                    <td class="text-caption text-medium-emphasis text-truncate" style="max-width: 180px">
                      {{ item.target_path?.trim() || '—' }}
                    </td>
                    <td class="text-end" @click.stop>
                      <v-btn variant="text" size="x-small" density="compact" @click="editServer(item)">编辑</v-btn>
                    </td>
                  </tr>
                </tbody>
              </v-table>
            </v-expansion-panel-text>
          </v-expansion-panel>
        </v-expansion-panels>
      </div>
    </v-card>

    <ServerUnsetPathPanel
      :highlight="unsetPathFocus"
      :servers="unsetPathServers"
      :loading="unsetPathLoading"
      :total="unsetPathTotal"
      :page="unsetPathPage"
      :items-per-page="unsetPathItemsPerPage"
      :items-per-page-options="dataTablePageOptions"
      :expanded-ids="expandedUnsetIds"
      v-model:selected-items="unsetSelectedItems"
      v-model:sort-by="sortBy"
      :select-all-loading="selectAllUnsetLoading"
      :editing-path-id="editingPathId"
      :editing-path-value="editingPathValue"
      :saving-path-id="savingPathId"
      @refresh="loadUnsetPathServers()"
      @update:page="onUnsetPageChange"
      @update:items-per-page="onUnsetItemsPerPageChange"
      @update:expanded-ids="expandedUnsetIds = $event"
      @update:editing-path-value="editingPathValue = $event"
      @select-all-filtered="selectAllUnsetFiltered"
      @toggle-expand="toggleUnsetExpand"
      @terminal="openTerminal"
      @files="openFiles"
      @detect-path-single="detectPathSingle"
      @agent-diagnose="openAgentDiagnose"
      @edit="editServer"
      @delete="confirmDelete"
      @edit-path="startEditPath"
      @save-path="saveTargetPath"
      @cancel-edit-path="cancelEditPath"
      @batch-category="openBatchCategory('unset')"
      @health-check="batchHealthCheck('unset')"
      @detect-path="openDetectPathConfirm('unset')"
      @install-agent="batchInstallAgent('unset')"
      @upgrade-agent="batchUpgradeAgent('unset')"
      @uninstall-agent="batchUninstallAgent('unset')"
      @batch-delete="confirmBatchDelete('unset')"
      @clear-selection="unsetSelectedItems = []"
    />

    <!-- Pending / failed SSH connections -->
    <v-card id="pending-servers-panel" elevation="0" rounded="lg" class="my-5" border>
      <v-card-title class="d-flex align-center">
        连接失败列表
        <v-chip v-if="pendingServers.length" size="small" class="ml-2" color="error" variant="tonal" label>
          {{ pendingServers.length }}
        </v-chip>
        <v-spacer />
        <v-btn variant="text" size="small" prepend-icon="mdi-refresh" :loading="pendingLoading" @click="loadPendingServers">
          刷新
        </v-btn>
      </v-card-title>
      <v-data-table
        :items="pendingServers"
        :headers="pendingHeaders"
        :loading="pendingLoading"
        hover
        density="comfortable"
        :items-per-page="10"
      >
        <template #item.address="{ item }">
          <span class="text-medium-emphasis">{{ item.domain }}:{{ item.port }}</span>
        </template>
        <template #item.last_attempt_at="{ item }">
          <span class="text-caption text-medium-emphasis">{{ formatRelativeTime(item.last_attempt_at) }}</span>
        </template>
        <template #item.last_error="{ item }">
          <span class="text-caption text-error text-truncate d-inline-block" style="max-width: 320px" :title="item.last_error">
            {{ item.last_error || '—' }}
          </span>
        </template>
        <template #item.actions="{ item }">
          <v-btn variant="text" size="x-small" color="primary" density="compact" :loading="retryingId === item.id" @click="retryPending(item)">
            重试
          </v-btn>
          <v-btn variant="text" size="x-small" color="error" density="compact" @click="confirmDeletePending(item)">删除</v-btn>
        </template>
        <template #no-data>
          <div class="text-center text-medium-emphasis py-6">暂无连接失败记录</div>
        </template>
      </v-data-table>
    </v-card>

    <ServerFormDialog
      v-model="serverFormVisible"
      :form="serverForm"
      :is-editing="isEditingServer"
      :saving="serverFormSaving"
      :lookups-loading="serverFormLookupsLoading"
      :password-presets="passwordPresets"
      :ssh-key-presets="sshKeyPresets"
      :category-options="categoryOptions"
      :platforms="serverFormPlatforms"
      :nodes="serverFormNodes"
      :edit-hints="serverFormEditHints"
      :files-elevation-items="filesElevationItems"
      @save="saveServerForm"
    />

    <!-- Delete Confirm -->
    <v-dialog v-model="showDelete" max-width="400">
      <v-card border>
        <v-card-title>确认删除</v-card-title>
        <v-card-text>确定要删除服务器 <strong>{{ deletingServer?.name }}</strong> 吗？</v-card-text>
        <v-card-actions>
          <v-spacer />
          <v-btn variant="text" @click="showDelete = false">取消</v-btn>
          <v-btn color="error" variant="flat" @click="doDelete" :loading="deleting">删除</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>

    <!-- Quick add by IP -->
    <v-dialog v-model="showQuickAdd" max-width="480" persistent>
      <v-card border>
        <v-card-title>快速添加服务器（仅 IP）</v-card-title>
        <v-card-text>
          <p class="text-body-2 text-medium-emphasis mb-3">
            将按顺序轮询凭据管理中的全部密码预设与 SSH 密钥预设尝试登录，首个成功即加入服务器列表。
          </p>
          <v-text-field
            v-model="quickAddForm.domain"
            label="IP 或域名"
            variant="outlined"
            density="compact"
            class="mb-2"
            placeholder="192.168.1.10"
            :rules="[v => !!v?.trim() || '必填']"
          />
          <v-text-field
            v-model.number="quickAddForm.port"
            label="SSH 端口"
            variant="outlined"
            density="compact"
            type="number"
            class="mb-2"
          />
          <v-text-field
            v-model="quickAddForm.name"
            label="显示名称（可选）"
            variant="outlined"
            density="compact"
            hint="留空则使用 IP"
            persistent-hint
          />
        </v-card-text>
        <v-card-actions>
          <v-spacer />
          <v-btn variant="text" :disabled="quickAddLoading" @click="showQuickAdd = false">取消</v-btn>
          <v-btn color="primary" variant="flat" :loading="quickAddLoading" @click="submitQuickAdd">开始轮询</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>

    <!-- Poll failure detail -->
    <v-dialog v-model="showPollFailure" max-width="560">
      <v-card border>
        <v-card-title class="text-error">SSH 连接失败</v-card-title>
        <v-card-text>
          <p class="text-body-2 mb-3">已尝试全部凭据预设，均未登录成功。该服务器已加入「连接失败列表」，可稍后重试。</p>
          <v-list v-if="pollFailureErrors.length" density="compact" class="bg-grey-lighten-4 rounded">
            <v-list-item v-for="(err, i) in pollFailureErrors" :key="i">
              <v-list-item-title class="text-body-2">
                [{{ err.preset_type === 'password' ? '密码' : err.preset_type === 'system' ? '系统' : '密钥' }}] {{ err.preset_name }} ({{ err.username }})
              </v-list-item-title>
              <v-list-item-subtitle class="text-error">{{ err.error }}</v-list-item-subtitle>
            </v-list-item>
          </v-list>
          <p v-else class="text-medium-emphasis">{{ pollFailureMessage }}</p>
        </v-card-text>
        <v-card-actions>
          <v-spacer />
          <v-btn color="primary" variant="flat" @click="showPollFailure = false">知道了</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>

    <!-- Delete pending confirm -->
    <v-dialog v-model="showDeletePending" max-width="400">
      <v-card border>
        <v-card-title>确认删除</v-card-title>
        <v-card-text>从失败列表移除 <strong>{{ deletingPending?.domain }}</strong>？</v-card-text>
        <v-card-actions>
          <v-spacer />
          <v-btn variant="text" @click="showDeletePending = false">取消</v-btn>
          <v-btn color="error" variant="flat" :loading="deletingPendingLoading" @click="doDeletePending">删除</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>

    <!-- Detect target_path confirm -->
    <v-dialog v-model="showDetectPathConfirm" max-width="480">
      <v-card border>
        <v-card-title>自动检测目标路径</v-card-title>
        <v-card-text>
          <p class="text-body-2 mb-2">
            将对选中的 <strong>{{ scopedSelectedCount }}</strong> 台服务器执行 SSH 检测。
          </p>
          <p class="text-body-2 text-medium-emphasis mb-0">
            在远程 <code>/www/wwwroot</code> 下搜索 <code>workerman.bat</code>，找到后将所在目录写入各服务器的
            <strong>target_path</strong>（找到第一个即停止）。
          </p>
        </v-card-text>
        <v-card-actions>
          <v-spacer />
          <v-btn variant="text" @click="showDetectPathConfirm = false">取消</v-btn>
          <v-btn color="primary" variant="flat" @click="runBatchDetectPath">开始检测</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>

    <!-- Batch category -->
    <v-dialog v-model="showBatchCategory" max-width="420">
      <v-card border>
        <v-card-title>批量修改分类</v-card-title>
        <v-card-text>
          <div class="text-body-2 text-medium-emphasis mb-3">
            已选择 {{ scopedSelectedCount }} 台服务器，将统一设置为以下分类（留空表示清除分类）。
          </div>
          <v-combobox
            v-model="batchCategoryValue"
            :items="batchCategoryOptions"
            label="分类"
            variant="outlined"
            density="compact"
            clearable
            hide-details
            placeholder="输入或选择分类"
          />
        </v-card-text>
        <v-card-actions>
          <v-spacer />
          <v-btn variant="text" @click="showBatchCategory = false">取消</v-btn>
          <v-btn color="primary" variant="flat" :loading="batchCategoryLoading" @click="submitBatchCategory">
            确认
          </v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>

    <!-- Batch add by IP dialog -->
    <v-dialog v-model="showBatchAdd" max-width="560">
      <v-card border>
        <v-card-title>批量添加服务器</v-card-title>
        <v-card-text>
          <div class="text-body-2 text-medium-emphasis mb-3">
            每行一个 IP 或域名；也可使用「名称 + Tab + IP」附带显示名称。空白行自动忽略，重复地址自动去重（不区分大小写）。SSH 凭据从「凭据」预设轮询匹配；连接失败将进入下方「连接失败列表」。
          </div>
          <v-textarea
            v-model="batchAddText"
            label="IP / 域名列表（可选：名称 Tab IP）"
            variant="outlined"
            density="compact"
            rows="10"
            auto-grow
            placeholder="武汉示例公司	192.168.1.10&#10;192.168.1.11&#10;10.0.0.5"
          />
          <div v-if="batchAddPreview.unique > 0" class="text-caption text-medium-emphasis mt-1">
            将添加 {{ batchAddPreview.unique }} 台
            <span v-if="batchAddPreview.deduped > 0">（已去重 {{ batchAddPreview.deduped }} 个重复）</span>
          </div>
          <v-row dense class="mt-1">
            <v-col cols="6">
              <v-text-field v-model.number="batchAddPort" label="SSH 端口" type="number" variant="outlined" density="compact" hide-details />
            </v-col>
            <v-col cols="6">
              <v-text-field v-model.number="batchAddAgentPort" label="Agent 端口" type="number" variant="outlined" density="compact" hide-details />
            </v-col>
          </v-row>
          <v-alert
            v-if="batchAddResult"
            :type="batchAddResult.pending > 0 ? 'warning' : 'success'"
            density="compact"
            variant="tonal"
            class="mt-3"
          >
            共 {{ batchAddResult.total }} 台：成功 {{ batchAddResult.created }}，待连接 {{ batchAddResult.pending }}，跳过 {{ batchAddResult.skipped }}
            <span v-if="(batchAddResult.duplicates_removed ?? 0) > 0">
              （输入 {{ batchAddResult.input_lines }} 行，去重 {{ batchAddResult.duplicates_removed }} 个）
            </span>
          </v-alert>
        </v-card-text>
        <v-card-actions>
          <v-spacer />
          <v-btn variant="text" @click="showBatchAdd = false">关闭</v-btn>
          <v-btn color="primary" variant="flat" @click="submitBatchAdd" :loading="batchAddLoading" :disabled="!batchAddText.trim()">
            开始添加
          </v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>

    <CredentialsDialog v-model="showCredentialsDialog" />

    <AgentDiagnoseDialog
      v-model="showAgentDiagnose"
      :loading="agentDiagnoseLoading"
      :result="agentDiagnoseResult"
      :target-name="agentDiagnoseTarget?.name"
    />

    <v-dialog v-model="showReplacePin" max-width="420">
      <v-card rounded="lg">
        <v-card-title>监测槽已满</v-card-title>
        <v-card-text class="text-body-2">
          请选择要替换的槽位（当前 Pin 的服务将被移除）：
        </v-card-text>
        <v-card-text>
          <v-row dense>
            <v-col v-for="slot in watchSlots" :key="slot.slot_index" cols="6">
              <v-btn
                block
                variant="tonal"
                :color="slot.empty ? 'primary' : 'warning'"
                @click="confirmReplaceSlot(slot.slot_index)"
              >
                槽 {{ slot.slot_index + 1 }}
                <span v-if="!slot.empty" class="text-caption ml-1">({{ slot.server_name }})</span>
                <span v-else class="text-caption ml-1">(空)</span>
              </v-btn>
            </v-col>
          </v-row>
        </v-card-text>
        <v-card-actions>
          <v-spacer />
          <v-btn variant="text" @click="showReplacePin = false">取消</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>
  </v-container>
</template>

<script setup lang="ts">
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick, type Ref } from 'vue'
import { usePageAutoRefresh } from '@/composables/usePageAutoRefresh'
import { usePageActivateRefresh } from '@/composables/usePageActivateRefresh'
import { CACHE_KEYS, cachedFetch, invalidateCachedQuery } from '@/composables/useCachedQuery'
import { toServerApiSort } from '@/utils/serverTableSort'
import { useRouter, useRoute } from 'vue-router'
import { http } from '@/api'
import { useSnackbar } from '@/composables/useSnackbar'
import { useServerPagination } from '@/composables/useServerPagination'
import { useServerSearchHistory } from '@/composables/useServerSearchHistory'
import { categoryDisplayLabel, useServerCategories } from '@/composables/useServerCategories'
import { statusChipColor, statusLabel, formatRelativeTime } from '@/utils/status'
import { fetchPagePerPage } from '@/utils/paginatedFetch'
import { isUnsetTargetPath } from '@/utils/serverTargetPath'
import type { AddByIpResponse, AddByIpsBatchResult, BatchJobStarted, PendingServerItem, PollErrorItem, ServerApiItem } from '@/types/api'
import { normalizeServerIds } from '@/utils/serverSelection'
import { formatApiError } from '@/utils/apiError'
import { guessSiteUrlFromDomain } from '@/utils/browserUrl'
import { useGlobalBrowser } from '@/composables/useGlobalBrowser'
import { registerServerBatchJob, onScriptExecutionComplete } from '@/composables/useScriptExecutionQueue'
import { showScriptSubmitToast } from '@/composables/useScriptSubmitToast'
import StatCardsRow from '@/components/StatCardsRow.vue'
import WatchSlotRow from '@/components/watch/WatchSlotRow.vue'
import { useWatchPins } from '@/composables/useWatchPins'
import ServerFormDialog from '@/components/servers/ServerFormDialog.vue'
import ServerBatchActionBar from '@/components/servers/ServerBatchActionBar.vue'
import ServerInlineDetail from '@/components/servers/ServerInlineDetail.vue'
import ServerUnsetPathPanel from '@/components/servers/ServerUnsetPathPanel.vue'
import CredentialsDialog from '@/components/credentials/CredentialsDialog.vue'
import AgentDiagnoseDialog from '@/components/servers/AgentDiagnoseDialog.vue'
import type { AgentDiagnoseResult } from '@/types/agentDiagnose'
import { useServerFormDialog } from '@/composables/servers/useServerFormDialog'
import { DATA_TABLE_ITEMS_PER_PAGE_OPTIONS } from '@/constants/dataTable'
import {
  SERVER_STAT_CARD_KEYS,
  applyServerStats,
  fetchServerStats,
  refreshServerStats,
  useServerStatsCards,
} from '@/composables/useServerStatsCards'

async function submitServerBatchJob(
  op: string,
  serverIds: number[],
  params?: Record<string, unknown>,
): Promise<BatchJobStarted | null> {
  const paths: Record<string, string> = {
    category: '/servers/batch/category',
    'health-check': '/servers/check',
    'detect-path': '/servers/batch/detect-path',
    'install-agent': '/servers/batch/install-agent',
    'upgrade-agent': '/servers/batch/upgrade-agent',
    'uninstall-agent': '/servers/batch/uninstall-agent',
  }
  const path = paths[op]
  if (!path) return null
  const body = op === 'category'
    ? { server_ids: serverIds, ...params }
    : { server_ids: serverIds }
  const res = await http.post<BatchJobStarted & { results?: unknown[] }>(path, body)
  if (!res?.job_id) {
    throw new Error('服务端未返回任务 ID，请确认已部署支持后台批量任务的新版本')
  }
  registerServerBatchJob(
    res.job_id,
    res.label,
    res.total,
    res.op || op,
    res.progress,
    res.status,
  )
  showScriptSubmitToast(`${res.label} 已提交`)
  return res
}

function registerOnboardingJob(
  jobId: number | undefined,
  label: string | undefined,
  total: number,
) {
  if (!jobId) return
  registerServerBatchJob(
    jobId,
    label || '新服务器初始化',
    total,
    'onboard',
    `0/${total}`,
    'running',
  )
  showScriptSubmitToast('新服务器初始化已在后台执行（sudo + 路径探测）')
}

onMounted(() => {
  offBatchComplete = onScriptExecutionComplete((alert) => {
    if (alert.taskKind !== 'server_batch') return
    invalidateCachedQuery(CACHE_KEYS.serverCategories)
    void Promise.all([loadServers(true), loadUnsetPathServers(true), loadCategories(), loadStats(true)])
  })
})

let offBatchComplete: (() => void) | null = null
onBeforeUnmount(() => {
  offBatchComplete?.()
})

defineOptions({ name: 'ServersPage' })

const dataTablePageOptions = [...DATA_TABLE_ITEMS_PER_PAGE_OPTIONS]

const snackbar = useSnackbar()
const { pinnedMap, pinServer: doPinServer, slots: watchSlots } = useWatchPins()
const pinLoadingId = ref<number | null>(null)
const showReplacePin = ref(false)
const replacePinServerId = ref<number | null>(null)

async function onPinServer(item: ServerApiItem) {
  const existingSlot = pinnedMap.value[item.id]
  if (existingSlot != null) {
    snackbar(`已在监测 · 槽 ${existingSlot + 1}`, 'info')
    return
  }
  pinLoadingId.value = item.id
  try {
    const res = await doPinServer(item.id)
    if (res.ok) {
      snackbar('已加入实时监测', 'success')
      return
    }
    if (res.full) {
      replacePinServerId.value = item.id
      showReplacePin.value = true
      return
    }
    snackbar(res.error || '加入监测失败', 'error')
  } finally {
    pinLoadingId.value = null
  }
}

async function confirmReplaceSlot(slotIndex: number) {
  if (replacePinServerId.value == null) return
  pinLoadingId.value = replacePinServerId.value
  try {
    const res = await doPinServer(replacePinServerId.value, slotIndex)
    if (res.ok) {
      snackbar(`已替换槽 ${slotIndex + 1}`, 'success')
      showReplacePin.value = false
      replacePinServerId.value = null
    } else {
      snackbar(res.error || '替换失败', 'error')
    }
  } finally {
    pinLoadingId.value = null
  }
}
const router = useRouter()
const globalBrowser = useGlobalBrowser()
const route = useRoute()
const showCredentialsDialog = ref(false)

function openCredentialsFromQuery() {
  if (route.query.credentials === '1') {
    showCredentialsDialog.value = true
    const q = { ...route.query }
    delete q.credentials
    router.replace({ path: route.path, query: q })
  }
}

watch(() => route.query.credentials, () => openCredentialsFromQuery(), { immediate: true })

// ── Paginated server list (composable) ──
const {
  servers, loading, page, itemsPerPage, total, search, categoryFilter, isOnlineFilter, sortBy,
  loadServers: _load, onPageChange, onItemsPerPageChange, onCategoryChange,
  onOnlineFilterChange, clearOnlineFilter,
  loadAllServers, listQueryParams,
} = useServerPagination({ targetPathUnset: false })

const { items: serverSearchHistory, record: recordServerSearch, loadHistoryOnly: loadServerSearchHistory } =
  useServerSearchHistory()

/** Combobox draft; applied filter is `search` from useServerPagination. */
const searchDraft = ref('')

function commitSearch() {
  const q = String(searchDraft.value ?? '').trim()
  if (q === String(search.value ?? '').trim()) return
  search.value = q
  searchDraft.value = q
  page.value = 1
  unsetPathPage.value = 1
  void recordServerSearch(q)
  void loadServers()
  void loadPendingServers(true)
}

function onSearchComboboxUpdate(val: string | null) {
  searchDraft.value = val ?? ''
  if (val == null || val === '') {
    if (!String(search.value ?? '').trim()) return
    search.value = ''
    page.value = 1
    unsetPathPage.value = 1
    void recordServerSearch('')
    void loadServers()
    void loadPendingServers(true)
    return
  }
  const picked = String(val).trim()
  if (serverSearchHistory.value.includes(picked)) {
    search.value = picked
    searchDraft.value = picked
    page.value = 1
    unsetPathPage.value = 1
    void recordServerSearch(picked)
    void loadServers()
    void loadPendingServers(true)
  }
}

const unsetPathServers = ref<ServerApiItem[]>([])
const unsetPathTotal = ref(0)
const unsetPathLoading = ref(false)
const unsetPathPage = ref(1)
const unsetPathItemsPerPage = ref(10)
const expandedMainIds = ref<number[]>([])
const expandedUnsetIds = ref<number[]>([])

/** v-data-table 运行时按 item-value（数字 id）匹配，类型声明为 string[] */
function expandedTableModel(source: Ref<number[]>) {
  return computed({
    get: () => source.value as unknown as readonly string[],
    set: (ids: readonly string[]) => {
      source.value = ids.map((id) => Number(id)).filter((n) => !Number.isNaN(n))
    },
  })
}

const expandedMainIdsModel = expandedTableModel(expandedMainIds)
const unsetSelectedItems = ref<Array<ServerApiItem | number>>([])
const unsetSelectedIds = computed(() => new Set(normalizeServerIds(unsetSelectedItems.value)))
const selectAllUnsetLoading = ref(false)

type BatchScope = 'main' | 'unset'
const batchScope = ref<BatchScope>('main')

function batchSelection(scope: BatchScope) {
  return scope === 'main'
    ? { items: selectedItems, ids: selectedIds }
    : { items: unsetSelectedItems, ids: unsetSelectedIds }
}

const scopedSelectedCount = computed(() => batchSelection(batchScope.value).ids.value.size)

/** 未设路径区块独立列表：不受顶部分类 chip 过滤，避免改分类后机器从面板消失 */
function unsetListQueryParams(): Record<string, unknown> {
  return {
    search: search.value || undefined,
    ...toServerApiSort(sortBy.value),
    target_path_unset: true,
  }
}

async function loadAllUnsetPathServers(): Promise<ServerApiItem[]> {
  const res = await fetchPagePerPage<ServerApiItem>(
    '/servers/',
    unsetListQueryParams(),
    1,
    -1,
  )
  return res.items
}

async function selectAllUnsetFiltered() {
  if (unsetPathTotal.value <= 0) return
  selectAllUnsetLoading.value = true
  try {
    const all = await loadAllUnsetPathServers()
    unsetSelectedItems.value = all.map(s => s.id)
    snackbar(`已选择未设路径筛选结果 ${all.length} 台`)
  } catch {
    snackbar('加载未设路径筛选结果失败', 'error')
  } finally {
    selectAllUnsetLoading.value = false
  }
}

async function loadUnsetPathServers(silent = false) {
  if (!silent) unsetPathLoading.value = true
  try {
    const res = await fetchPagePerPage<ServerApiItem>(
      '/servers/',
      unsetListQueryParams(),
      unsetPathPage.value,
      unsetPathItemsPerPage.value,
    )
    unsetPathServers.value = res.items
    unsetPathTotal.value = res.total
  } catch {
    unsetPathServers.value = []
    unsetPathTotal.value = 0
    if (!silent) snackbar('加载未设置路径服务器失败', 'error')
  } finally {
    if (!silent) unsetPathLoading.value = false
  }
}

function toggleMainExpand(item: ServerApiItem) {
  expandedMainIds.value = expandedMainIds.value.includes(item.id) ? [] : [item.id]
}

function toggleUnsetExpand(item: ServerApiItem) {
  expandedUnsetIds.value = expandedUnsetIds.value.includes(item.id) ? [] : [item.id]
}

function onMainPageChange(p: number) {
  expandedMainIds.value = []
  onPageChange(p)
}

function onMainItemsPerPageChange(n: number) {
  expandedMainIds.value = []
  onItemsPerPageChange(n)
}

function onUnsetPageChange(p: number) {
  expandedUnsetIds.value = []
  unsetSelectedItems.value = []
  unsetPathPage.value = p
  void loadUnsetPathServers()
}

function onUnsetItemsPerPageChange(n: number) {
  expandedUnsetIds.value = []
  unsetSelectedItems.value = []
  unsetPathItemsPerPage.value = n
  unsetPathPage.value = 1
  void loadUnsetPathServers()
}

async function detectPathSingle(item: ServerApiItem) {
  try {
    await submitServerBatchJob('detect-path', [item.id])
  } catch (e: unknown) {
    snackbar(formatApiError(e, '提交检测任务失败'), 'error')
  }
}

function clearExpandedForPathMigration(serverId: number, newPath: string) {
  const unset = isUnsetTargetPath(newPath)
  if (expandedMainIds.value.includes(serverId) && unset) expandedMainIds.value = []
  if (expandedUnsetIds.value.includes(serverId) && !unset) expandedUnsetIds.value = []
}

const {
  categories: categoryList,
  loading: categoriesLoading,
  totalCount,
  loadCategories,
} = useServerCategories()

const viewMode = ref<'table' | 'group'>('table')
const groupServers = ref<ServerApiItem[]>([])
const groupLoading = ref(false)
const expandedCategories = ref<string[]>([])

async function loadServers(silent = false) {
  try {
    await _load({ silent })
    await loadUnsetPathServers(silent)
    if (viewMode.value === 'group') {
      await loadGroupServers(silent)
    }
  } catch {
    if (!silent) snackbar('加载服务器列表失败', 'error')
  }
}

async function loadGroupServers(silent = false) {
  if (!silent) groupLoading.value = true
  try {
    groupServers.value = await loadAllServers()
    expandedCategories.value = groupedServers.value.map(g => g.category)
  } catch {
    if (!silent) {
      groupServers.value = []
      snackbar('加载分类视图失败', 'error')
    }
  } finally {
    if (!silent) groupLoading.value = false
  }
}

async function refreshServersSilent() {
  try {
    await _load({ silent: true })
    await loadUnsetPathServers(true)
    if (viewMode.value === 'group') await loadGroupServers(true)
  } catch {
    /* 后台刷新失败不打断用户操作 */
  }
}

usePageAutoRefresh(refreshServersSilent, 30_000)

const groupedServers = computed(() => {
  const groups: Record<string, ServerApiItem[]> = {}
  for (const s of groupServers.value) {
    const cat = s.category?.trim() || 'uncategorized'
    if (!groups[cat]) groups[cat] = []
    groups[cat].push(s)
  }
  return Object.entries(groups)
    .map(([category, list]) => ({
      category,
      label: categoryDisplayLabel(category),
      servers: list,
    }))
    .sort((a, b) => {
      if (a.category === 'uncategorized') return 1
      if (b.category === 'uncategorized') return -1
      return a.label.localeCompare(b.label, 'zh')
    })
})

function selectCategory(name: string) {
  onCategoryChange(name)
  if (viewMode.value === 'group') {
    void loadGroupServers()
  }
}

function toggleGroupSelection(id: number, checked: boolean | null) {
  const next = new Set(selectedItems.value.map(item => (typeof item === 'number' ? item : item.id)))
  if (checked) next.add(id)
  else next.delete(id)
  selectedItems.value = [...next]
}

const selectAllFilteredLoading = ref(false)

async function selectAllFiltered() {
  if (total.value <= 0) return
  selectAllFilteredLoading.value = true
  try {
    const all = await loadAllServers()
    selectedItems.value = all.map(s => s.id)
    snackbar(`已选择当前筛选结果 ${all.length} 台`)
  } catch {
    snackbar('加载筛选结果失败', 'error')
  } finally {
    selectAllFilteredLoading.value = false
  }
}

function isGroupAllSelected(groupServers: ServerApiItem[]): boolean {
  return groupServers.length > 0 && groupServers.every(s => selectedIds.value.has(s.id))
}

function toggleGroupAll(groupServers: ServerApiItem[]) {
  const allSelected = isGroupAllSelected(groupServers)
  const next = new Set(selectedIds.value)
  for (const s of groupServers) {
    if (allSelected) next.delete(s.id)
    else next.add(s.id)
  }
  selectedItems.value = [...next]
}

watch(categoryFilter, () => {
  selectedItems.value = []
  expandedMainIds.value = []
  void loadServers()
})
watch(sortBy, () => {
  selectedItems.value = []
  unsetSelectedItems.value = []
  expandedMainIds.value = []
  expandedUnsetIds.value = []
  unsetPathPage.value = 1
  if (viewMode.value === 'group') void loadGroupServers()
  void loadUnsetPathServers()
}, { deep: true })
watch(search, () => {
  selectedItems.value = []
  unsetSelectedItems.value = []
  expandedMainIds.value = []
  expandedUnsetIds.value = []
  unsetPathPage.value = 1
})

watch(
  () => [search.value, total.value, unsetPathTotal.value, pendingServers.value.length, loading.value] as const,
  ([q, mainTotal, unsetTotal, pendingCount, isLoading]) => {
    if (!String(q ?? '').trim() || isLoading) return
    if (mainTotal > 0 || unsetTotal > 0) return
    if (pendingCount <= 0) return
    void nextTick(() => {
      document.getElementById('pending-servers-panel')?.scrollIntoView({ behavior: 'smooth', block: 'start' })
    })
  },
)

watch(viewMode, mode => {
  if (mode === 'group') {
    void loadGroupServers()
  }
})

const showDelete = ref(false)
const deleting = ref(false)
const deletingServer = ref<ServerApiItem | null>(null)

// ── Quick add by IP + pending failure list ──
const showQuickAdd = ref(false)
const quickAddLoading = ref(false)
const quickAddForm = ref({ domain: '', port: 22, name: '' })
const pendingServers = ref<PendingServerItem[]>([])
const pendingLoading = ref(false)
const retryingId = ref<number | null>(null)
const showPollFailure = ref(false)
const pollFailureErrors = ref<PollErrorItem[]>([])
const pollFailureMessage = ref('')
const showDeletePending = ref(false)
const deletingPending = ref<PendingServerItem | null>(null)
const deletingPendingLoading = ref(false)

const pendingHeaders = [
  { title: '名称', key: 'name' },
  { title: '地址', key: 'address' },
  { title: '尝试次数', key: 'attempts', width: 100 },
  { title: '最后错误', key: 'last_error' },
  { title: '最后尝试', key: 'last_attempt_at', width: 160 },
  { title: '操作', key: 'actions', width: 140, align: 'end' as const },
]

function openQuickAdd() {
  quickAddForm.value = { domain: '', port: 22, name: '' }
  showQuickAdd.value = true
}

function showFailureDialog(errors: PollErrorItem[], message?: string) {
  pollFailureErrors.value = errors
  pollFailureMessage.value = message || ''
  showPollFailure.value = true
}

async function loadPendingServers(silent = false) {
  if (!silent) pendingLoading.value = true
  try {
    const q = String(search.value ?? '').trim()
    const res = await http.get<{ items: PendingServerItem[]; total: number }>(
      '/servers/pending',
      q ? { search: q } : undefined,
    )
    pendingServers.value = res.items || []
  } catch (e: unknown) {
    pendingServers.value = []
    snackbar(formatApiError(e, '加载失败列表失败'), 'error')
  } finally {
    if (!silent) pendingLoading.value = false
  }
}

async function submitQuickAdd() {
  const domain = quickAddForm.value.domain.trim()
  if (!domain) {
    snackbar('请填写 IP 或域名', 'error')
    return
  }
  quickAddLoading.value = true
  try {
    const body: Record<string, unknown> = {
      domain,
      port: quickAddForm.value.port || 22,
    }
    const name = quickAddForm.value.name.trim()
    if (name) body.name = name

    const res = await http.post<AddByIpResponse>('/servers/add-by-ip', body)
    if (res.success && res.server) {
      showQuickAdd.value = false
      registerOnboardingJob(res.onboarding_job_id, res.onboarding_label, 1)
      snackbar(`添加成功：${res.matched_preset} (${res.matched_username})`)
      loadServers()
      loadStats()
    } else {
      showQuickAdd.value = false
      showFailureDialog(res.errors || [], res.message)
      loadPendingServers()
    }
  } catch (e: unknown) {
    snackbar(formatApiError(e, '添加失败'), 'error')
  } finally {
    quickAddLoading.value = false
  }
}

async function retryPending(item: PendingServerItem) {
  retryingId.value = item.id
  try {
    const res = await http.post<AddByIpResponse>(`/servers/pending/${item.id}/retry`, {})
    if (res.success && res.server) {
      registerOnboardingJob(res.onboarding_job_id, res.onboarding_label, 1)
      snackbar(`重试成功：${res.matched_preset} (${res.matched_username})`)
      loadServers()
      loadStats()
      loadPendingServers()
    } else {
      showFailureDialog(res.errors || [], res.message)
      loadPendingServers()
    }
  } catch (e: unknown) {
    snackbar(formatApiError(e, '重试失败'), 'error')
  } finally {
    retryingId.value = null
  }
}

function confirmDeletePending(item: PendingServerItem) {
  deletingPending.value = item
  showDeletePending.value = true
}

async function doDeletePending() {
  if (!deletingPending.value) return
  deletingPendingLoading.value = true
  try {
    await http.delete(`/servers/pending/${deletingPending.value.id}`)
    showDeletePending.value = false
    snackbar('已从失败列表移除')
    loadPendingServers()
  } catch (e: unknown) {
    snackbar(formatApiError(e, '删除失败'), 'error')
  } finally {
    deletingPendingLoading.value = false
  }
}

const {
  visible: serverFormVisible,
  form: serverForm,
  isEditing: isEditingServer,
  saving: serverFormSaving,
  lookupsLoading: serverFormLookupsLoading,
  passwordPresets,
  sshKeyPresets,
  categoryOptions,
  platforms: serverFormPlatforms,
  nodes: serverFormNodes,
  editHints: serverFormEditHints,
  filesElevationItems,
  openAdd: openAddServer,
  openEdit: openEditServerDialog,
  close: closeServerForm,
  save: saveServerForm,
} = useServerFormDialog(() => {
  loadServers()
  loadStats()
  loadCategories()
})

watch(serverFormVisible, (open, wasOpen) => {
  if (wasOpen && !open) closeServerForm()
})

// ── Batch selection (Vuetify returns IDs when item-value="id", objects when return-object) ──
const selectedItems = ref<Array<ServerApiItem | number>>([])
const selectedIds = computed(() => new Set(normalizeServerIds(selectedItems.value)))

// ── Table config ──
const headers = [
  { title: '状态', key: 'status', width: 100 },
  { title: '名称', key: 'name' },
  { title: '地址', key: 'domain' },
  { title: '分类', key: 'category', width: 100 },
  { title: '目标路径', key: 'target_path', width: 200 },
  { title: 'Agent', key: 'agent_version', width: 100 },
  { title: '心跳', key: 'last_heartbeat', width: 120 },
  { title: '操作', key: 'actions', width: 200, align: 'end' as const, sortable: false },
]

const statsBooting = ref(true)
const statsRefreshing = ref(false)

// ── Stats（与仪表盘共用 composable；本页可点击筛选）──
const { statItems } = useServerStatsCards()

const unsetPathFocus = ref(false)

const statItemsDisplay = computed(() =>
  statItems.value.map((item, i) => {
    const key = SERVER_STAT_CARD_KEYS[i]
    let active = false
    if (key === 'offline') active = isOnlineFilter.value === false
    else if (key === 'unset-path') active = unsetPathFocus.value
    return {
      ...item,
      key,
      clickable: key !== 'total' && key !== 'online',
      active,
    }
  }),
)

const statusFilterLabel = computed(() => {
  if (isOnlineFilter.value === true) return '筛选：在线服务器'
  if (isOnlineFilter.value === false) return '筛选：离线服务器'
  return ''
})

function scrollToUnsetPathPanel() {
  void nextTick(() => {
    document.getElementById('unset-path-panel')?.scrollIntoView({ behavior: 'smooth', block: 'start' })
  })
}

function syncDashboardStatQuery() {
  const status = route.query.status
  if (status === 'offline') {
    unsetPathFocus.value = false
    if (isOnlineFilter.value !== false) {
      isOnlineFilter.value = false
      viewMode.value = 'table'
      void loadServers()
    }
  } else if (status === 'online') {
    unsetPathFocus.value = false
    if (isOnlineFilter.value !== true) {
      isOnlineFilter.value = true
      viewMode.value = 'table'
      void loadServers()
    }
  }
  if (route.query.unset_path === '1') {
    unsetPathFocus.value = true
    scrollToUnsetPathPanel()
  }
}

watch(
  () => [route.query.status, route.query.unset_path] as const,
  () => syncDashboardStatQuery(),
  { immediate: true },
)

function onStatCardClick(key: string) {
  if (key === 'alerts') {
    unsetPathFocus.value = false
    router.push({ path: '/alerts' })
    return
  }
  if (key === 'unset-path') {
    unsetPathFocus.value = !unsetPathFocus.value
    const q = { ...route.query }
    if (unsetPathFocus.value) {
      q.unset_path = '1'
      scrollToUnsetPathPanel()
    } else {
      delete q.unset_path
    }
    router.replace({ path: route.path, query: q })
    return
  }
  unsetPathFocus.value = false
  if (key === 'offline') {
    const next = isOnlineFilter.value === false ? null : false
    onOnlineFilterChange(next)
    viewMode.value = 'table'
    const q = { ...route.query }
    delete q.unset_path
    if (next === false) q.status = 'offline'
    else delete q.status
    router.replace({ path: route.path, query: q })
  }
}

function clearStatusFilter() {
  clearOnlineFilter()
  if (route.query.status) {
    const q = { ...route.query }
    delete q.status
    router.replace({ path: route.path, query: q })
  }
}

function clearUnsetPathFocus() {
  unsetPathFocus.value = false
  if (route.query.unset_path) {
    const q = { ...route.query }
    delete q.unset_path
    router.replace({ path: route.path, query: q })
  }
}

async function loadStats(silent = false) {
  if (!silent) statsBooting.value = true
  try {
    const s = await fetchServerStats()
    applyServerStats(statItems, s)
  } catch { /* non-critical */ } finally {
    if (!silent) statsBooting.value = false
  }
}

async function refreshStatCards() {
  statsRefreshing.value = true
  try {
    await refreshServerStats(statItems)
  } catch {
    snackbar('刷新统计数据失败', 'error')
  } finally {
    statsRefreshing.value = false
  }
}

// ── Handlers ──
function openTerminal(item: ServerApiItem) {
  router.push({ path: '/terminal', query: { server_id: String(item.id) } })
}
function openBrowser(item: ServerApiItem) {
  const url = guessSiteUrlFromDomain(item.domain || '')
  if (!url) {
    snackbar('该服务器无有效域名', 'warning')
    return
  }
  globalBrowser.openUrl(url, { title: item.name || url })
}
function openFiles(item: ServerApiItem) {
  router.push({ path: '/files', query: { server_id: String(item.id) } })
}

const showAgentDiagnose = ref(false)
const agentDiagnoseLoading = ref(false)
const agentDiagnoseLoadingId = ref<number | null>(null)
const agentActionLoadingId = ref<number | null>(null)
const agentDiagnoseResult = ref<AgentDiagnoseResult | null>(null)
const agentDiagnoseTarget = ref<ServerApiItem | null>(null)

async function openAgentDiagnose(item: ServerApiItem) {
  agentDiagnoseTarget.value = item
  agentDiagnoseResult.value = null
  showAgentDiagnose.value = true
  agentDiagnoseLoading.value = true
  agentDiagnoseLoadingId.value = item.id
  try {
    agentDiagnoseResult.value = await http.post<AgentDiagnoseResult>(`/servers/${item.id}/agent-diagnose`, {})
  } catch (e: unknown) {
    showAgentDiagnose.value = false
    snackbar(formatApiError(e, 'Agent 诊断失败'), 'error')
  } finally {
    agentDiagnoseLoading.value = false
    agentDiagnoseLoadingId.value = null
  }
}

async function installAgentOne(item: ServerApiItem) {
  agentActionLoadingId.value = item.id
  try {
    await http.post(`/servers/${item.id}/install-agent`, {})
    snackbar(`Agent 安装完成：${item.name}`, 'success')
    await _load()
  } catch (e: unknown) {
    snackbar(formatApiError(e, '安装 Agent 失败'), 'error')
  } finally {
    agentActionLoadingId.value = null
  }
}

async function upgradeAgentOne(item: ServerApiItem) {
  agentActionLoadingId.value = item.id
  try {
    await submitServerBatchJob('upgrade-agent', [item.id])
    snackbar(`已提交 Agent 升级：${item.name}`, 'success')
  } catch (e: unknown) {
    snackbar(formatApiError(e, '提交升级失败'), 'error')
  } finally {
    agentActionLoadingId.value = null
  }
}

function editServer(item: ServerApiItem) {
  void openEditServerDialog(item)
}

function confirmDelete(item: ServerApiItem) {
  deletingServer.value = item
  showDelete.value = true
}

// ── Batch category ──
const showBatchCategory = ref(false)
const batchCategoryValue = ref('')
const batchCategoryLoading = ref(false)

const batchCategoryOptions = computed(() => {
  const names = categoryList.value
    .filter(c => c.name !== 'uncategorized')
    .map(c => c.label)
  if (categoryOptions.value.length) {
    for (const c of categoryOptions.value) {
      if (c && !names.includes(c)) names.push(c)
    }
  }
  return names.sort((a, b) => a.localeCompare(b, 'zh'))
})

function openBatchCategory(scope: BatchScope) {
  batchScope.value = scope
  const { ids } = batchSelection(scope)
  if (ids.value.size === 0) {
    snackbar('请先勾选至少一台服务器', 'error')
    return
  }
  batchCategoryValue.value = ''
  showBatchCategory.value = true
}

async function submitBatchCategory() {
  const { items, ids } = batchSelection(batchScope.value)
  const idList = [...ids.value]
  if (idList.length === 0) return
  batchCategoryLoading.value = true
  try {
    const category = (batchCategoryValue.value || '').trim()
    await submitServerBatchJob('category', idList, { category })
    showBatchCategory.value = false
    items.value = []
  } catch (e: unknown) {
    snackbar(formatApiError(e, '批量修改分类失败'), 'error')
  } finally {
    batchCategoryLoading.value = false
  }
}

// ── Batch operations (background queue, same as script library) ──
const showDetectPathConfirm = ref(false)

const editingPathId = ref<number | null>(null)
const editingPathValue = ref('')
const savingPathId = ref<number | null>(null)

function openDetectPathConfirm(scope: BatchScope) {
  batchScope.value = scope
  const { ids } = batchSelection(scope)
  if (!ids.value.size) {
    snackbar('请先选择服务器', 'warning')
    return
  }
  showDetectPathConfirm.value = true
}

async function runBatchDetectPath() {
  const { items, ids } = batchSelection(batchScope.value)
  const idList = [...ids.value]
  if (!idList.length) return
  showDetectPathConfirm.value = false
  try {
    await submitServerBatchJob('detect-path', idList)
    items.value = []
  } catch (e: unknown) {
    snackbar(formatApiError(e, '提交检测任务失败'), 'error')
  }
}

async function batchHealthCheck(scope: BatchScope) {
  batchScope.value = scope
  const { items } = batchSelection(scope)
  const ids = normalizeServerIds(items.value)
  if (!ids.length) {
    snackbar('请先选择服务器', 'warning')
    return
  }
  try {
    await submitServerBatchJob('health-check', ids)
  } catch (e: unknown) {
    snackbar(formatApiError(e, '提交健康检查失败'), 'error')
  }
}

async function batchInstallAgent(scope: BatchScope) {
  batchScope.value = scope
  const { items } = batchSelection(scope)
  const ids = normalizeServerIds(items.value)
  if (!ids.length) {
    snackbar('请先选择服务器', 'warning')
    return
  }
  try {
    await submitServerBatchJob('install-agent', ids)
  } catch (e: unknown) {
    snackbar(formatApiError(e, '提交安装任务失败'), 'error')
  }
}

async function batchUpgradeAgent(scope: BatchScope) {
  batchScope.value = scope
  const { items } = batchSelection(scope)
  const ids = normalizeServerIds(items.value)
  if (!ids.length) {
    snackbar('请先选择服务器', 'warning')
    return
  }
  if (!confirm(`确定对 ${ids.length} 台服务器升级 Agent？`)) return
  try {
    await submitServerBatchJob('upgrade-agent', ids)
  } catch (e: unknown) {
    snackbar(formatApiError(e, '提交升级任务失败'), 'error')
  }
}

async function batchUninstallAgent(scope: BatchScope) {
  batchScope.value = scope
  const { items } = batchSelection(scope)
  const ids = normalizeServerIds(items.value)
  if (!ids.length) {
    snackbar('请先选择服务器', 'warning')
    return
  }
  if (!confirm(
    `确定对 ${ids.length} 台已安装 Agent 的服务器执行卸载？\n将停止服务、删除 /opt/nexus-agent 并清除 Agent 状态。`,
  )) return
  try {
    await submitServerBatchJob('uninstall-agent', ids)
  } catch (e: unknown) {
    snackbar(formatApiError(e, '提交卸载任务失败'), 'error')
  }
}

function startEditPath(item: ServerApiItem) {
  editingPathId.value = item.id
  editingPathValue.value = item.target_path?.trim() || '/www/wwwroot'
}

function cancelEditPath() {
  editingPathId.value = null
  editingPathValue.value = ''
  savingPathId.value = null
}

async function saveTargetPath(serverId: number) {
  const tp = editingPathValue.value.trim()
  if (!tp) {
    snackbar('目标路径不能为空', 'warning')
    return
  }
  if (!tp.startsWith('/')) {
    snackbar('目标路径须为绝对路径（以 / 开头）', 'warning')
    return
  }
  savingPathId.value = serverId
  try {
    await http.put(`/servers/${serverId}`, { target_path: tp })
    snackbar('目标路径已更新')
    cancelEditPath()
    await Promise.all([loadServers(), loadUnsetPathServers()])
    clearExpandedForPathMigration(serverId, tp)
  } catch (e: unknown) {
    snackbar(formatApiError(e, '保存失败'), 'error')
  } finally {
    savingPathId.value = null
  }
}

async function confirmBatchDelete(scope: BatchScope) {
  batchScope.value = scope
  const { items, ids } = batchSelection(scope)
  if (!ids.value.size) return
  if (!confirm(`确定删除选中的 ${ids.value.size} 台服务器？`)) return
  for (const id of ids.value) {
    try { await http.delete(`/servers/${id}`) } catch { /* skip individual errors */ }
  }
  items.value = []
  snackbar('批量删除完成')
  await Promise.all([loadServers(), loadUnsetPathServers()])
  loadStats()
}

// ── Batch add by IP ──
const showBatchAdd = ref(false)
const batchAddText = ref('')
const batchAddPort = ref(22)
const batchAddAgentPort = ref(8601)
const batchAddLoading = ref(false)
const batchAddResult = ref<AddByIpsBatchResult | null>(null)

function parseBatchLineDomain(line: string): string {
  const trimmed = line.trim()
  if (!trimmed) return ''
  const tab = trimmed.indexOf('\t')
  if (tab >= 0) return trimmed.slice(tab + 1).trim()
  const comma = trimmed.lastIndexOf(',')
  if (comma >= 0) {
    const domain = trimmed.slice(comma + 1).trim()
    const name = trimmed.slice(0, comma).trim()
    if (domain && name && (domain.includes('.') || domain.includes(':'))) return domain
  }
  return trimmed
}

function parseBatchIpPreview(text: string) {
  const seen = new Set<string>()
  const unique: string[] = []
  let raw = 0
  for (const line of text.split('\n')) {
    const domain = parseBatchLineDomain(line)
    if (!domain) continue
    raw += 1
    const key = domain.toLowerCase()
    if (seen.has(key)) continue
    seen.add(key)
    unique.push(domain)
  }
  return { unique: unique.length, raw, deduped: Math.max(0, raw - unique.length) }
}

const batchAddPreview = computed(() => parseBatchIpPreview(batchAddText.value))

function openBatchAdd() {
  batchAddText.value = ''
  batchAddPort.value = 22
  batchAddAgentPort.value = 8601
  batchAddResult.value = null
  showBatchAdd.value = true
}

async function submitBatchAdd() {
  const text = batchAddText.value.trim()
  if (!text) {
    snackbar('请填写至少一个 IP 或域名', 'error')
    return
  }
  batchAddLoading.value = true
  batchAddResult.value = null
  try {
    const res = await http.post<AddByIpsBatchResult>('/servers/add-by-ips-batch', {
      text,
      port: batchAddPort.value || 22,
      agent_port: batchAddAgentPort.value || 8601,
    })
    batchAddResult.value = res
    if (res.created > 0) {
      registerOnboardingJob(res.onboarding_job_id, '新服务器初始化', res.created)
      loadServers()
      loadStats()
    }
    if (res.pending > 0) {
      loadPendingServers()
    }
    const msg = res.pending > 0
      ? `添加完成：${res.created} 台成功，${res.pending} 台进入连接失败列表`
      : `添加完成：${res.created} 台成功`
    snackbar(msg, res.pending > 0 ? 'warning' : 'success')
  } catch (e: unknown) {
    snackbar(formatApiError(e, '批量添加失败'), 'error')
  } finally {
    batchAddLoading.value = false
  }
}

async function doDelete() {
  if (!deletingServer.value) return
  const deletedId = deletingServer.value.id
  deleting.value = true
  try {
    await http.delete(`/servers/${deletedId}`)
    showDelete.value = false
    expandedMainIds.value = expandedMainIds.value.filter(id => id !== deletedId)
    expandedUnsetIds.value = expandedUnsetIds.value.filter(id => id !== deletedId)
    await Promise.all([loadServers(), loadUnsetPathServers()])
    loadStats()
    snackbar('已删除')
  } catch (e: unknown) {
    snackbar(formatApiError(e, '删除失败'), 'error')
  } finally {
    deleting.value = false
  }
}

async function refreshServersPage(silent = false) {
  await loadServerSearchHistory()
  search.value = ''
  searchDraft.value = ''
  await Promise.all([
    loadStats(silent),
    loadCategories(silent),
    loadServers(silent),
  ])
  if (!silent) await loadPendingServers()
}

usePageActivateRefresh((silent) => refreshServersPage(silent))
</script>

<style scoped>
.server-main-table :deep(.v-data-table__td--expand-column),
.server-main-table :deep(.v-data-table-column--expand),
.server-main-table :deep(th.v-data-table__th--expand) {
  display: none !important;
  width: 0 !important;
  padding: 0 !important;
}

.server-main-table :deep(tr.v-data-table__expanded__row > td) {
  background: transparent !important;
  border-bottom: none;
}

.servers-category-panels :deep(.v-expansion-panel-text__wrapper) {
  background: color-mix(in srgb, rgb(var(--v-theme-surface)) 95%, rgb(var(--v-theme-on-surface)));
  color: rgb(var(--v-theme-on-surface));
}

.servers-category-panels :deep(.v-table) {
  background: transparent;
  color: rgb(var(--v-theme-on-surface));
}
</style>
