<template>
  <v-container fluid class="pa-6">
    <!-- Toolbar -->
    <v-card elevation="0" border rounded="lg" class="mb-4">
      <v-card-title class="d-flex flex-wrap align-center ga-2">
        脚本库
        <v-chip v-if="execConfig" size="small" variant="tonal" label class="ml-1">
          分批 {{ execConfig.batch_size }} 台 · 并发 {{ execConfig.concurrency }}
        </v-chip>
        <v-spacer />
        <v-text-field
          v-model="search"
          prepend-inner-icon="mdi-magnify"
          label="搜索..."
          density="compact"
          hide-details
          variant="outlined"
          rounded
          style="max-width: 240px"
        />
        <v-btn color="primary" variant="flat" class="ml-1" prepend-icon="mdi-plus" size="small" @click="openEditor(null)">
          新建脚本
        </v-btn>
      </v-card-title>
    </v-card>

    <!-- Quick Execute -->
    <v-card elevation="0" border rounded="lg" class="mb-4">
      <v-card-title class="d-flex align-center" style="cursor: pointer" @click="showQuickExec = !showQuickExec">
        <v-icon class="mr-2">mdi-console</v-icon>
        临时命令执行
        <v-spacer />
        <v-icon>{{ showQuickExec ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
      </v-card-title>
      <v-expand-transition>
        <div v-show="showQuickExec">
          <v-divider />
          <v-card-text>
            <v-textarea
              v-model="quickCommand"
              label="命令"
              variant="outlined"
              density="compact"
              rows="3"
              placeholder="输入要执行的命令... (Ctrl+Enter 执行)"
              class="mb-3 font-monospace"
              @keydown.ctrl.enter="doQuickExec"
              :rules="[required('命令')]"
            />
            <div class="text-caption text-medium-emphasis mb-1">目标服务器（按分类选择）</div>
            <ServerCategoryPicker
              v-model="quickServerIds"
              :servers="serverList"
              :loading="serversLoading"
              max-height="220px"
              class="mb-3"
            />
            <v-row dense>
              <v-col cols="6" md="2">
                <v-text-field
                  v-model.number="quickTimeout"
                  label="超时(秒)"
                  variant="outlined"
                  density="compact"
                  type="number"
                  :max="execConfig?.max_timeout ?? 600"
                />
              </v-col>
              <v-col cols="6" md="3">
                <v-select
                  v-model="quickCredentialId"
                  :items="credentialOptions"
                  item-title="title"
                  item-value="value"
                  label="DB 凭据（可选）"
                  variant="outlined"
                  density="compact"
                  clearable
                  hide-details
                />
              </v-col>
              <v-col cols="12" md="2" class="d-flex align-center ga-2">
                <v-switch v-model="quickLongTask" label="长任务" density="compact" hide-details color="primary" />
                <v-btn
                  color="primary"
                  variant="flat"
                  @click="doQuickExec"
                  :loading="quickExecuting"
                  :disabled="!quickCommand.trim() || quickServerIds.length === 0"
                >
                  执行
                </v-btn>
              </v-col>
            </v-row>
          </v-card-text>
        </div>
      </v-expand-transition>
    </v-card>

    <!-- Script Grid -->
    <v-progress-linear v-if="loading" indeterminate color="primary" />
    <v-row v-if="pagedScripts.length">
      <v-col cols="12" sm="6" md="4" v-for="script in pagedScripts" :key="script.id">
        <v-card elevation="0" border rounded="lg" class="fill-height">
          <v-card-title class="d-flex align-center">
            <v-icon color="primary" class="mr-2">mdi-code-braces</v-icon>
            <span class="text-body-1 font-weight-medium">{{ script.name }}</span>
            <v-spacer />
            <v-menu>
              <template #activator="{ props: menuProps }">
                <v-btn icon="mdi-dots-vertical" variant="text" size="small" v-bind="menuProps" />
              </template>
              <v-list density="compact">
                <v-list-item title="编辑" @click="openEditor(script)">
                  <template #prepend><v-icon>mdi-pencil</v-icon></template>
                </v-list-item>
                <v-list-item title="执行" @click="runScript(script)">
                  <template #prepend><v-icon>mdi-play</v-icon></template>
                </v-list-item>
                <v-list-item title="删除" @click="confirmDelete(script)" class="text-error">
                  <template #prepend><v-icon color="error">mdi-delete</v-icon></template>
                </v-list-item>
              </v-list>
            </v-menu>
          </v-card-title>
          <v-card-text>
            <v-chip v-if="script.category" size="small" variant="tonal" label class="mb-2">
              {{ categoryLabel(script.category) }}
            </v-chip>
            <div class="text-body-2 text-medium-emphasis">{{ script.description || '无描述' }}</div>
            <div class="text-caption text-medium-emphasis mt-2">
              更新于 {{ formatDateTimeBeijing(script.updated_at || script.created_at) }}
            </div>
          </v-card-text>
          <v-card-actions>
            <v-btn size="small" variant="tonal" prepend-icon="mdi-play" color="primary" @click="runScript(script)">
              执行
            </v-btn>
            <v-spacer />
            <v-btn size="small" variant="text" @click="filterHistoryByScript(script)">历史</v-btn>
          </v-card-actions>
        </v-card>
      </v-col>
    </v-row>
    <v-card v-else-if="!loading" elevation="0" border rounded="lg">
      <v-card-text class="text-center text-medium-emphasis py-10">暂无脚本，点击「新建脚本」添加</v-card-text>
    </v-card>

    <div v-if="totalItems > itemsPerPage" class="d-flex justify-center mt-4">
      <v-pagination
        v-model="page"
        :length="Math.ceil(totalItems / itemsPerPage)"
        rounded
        density="compact"
      />
    </div>

    <div ref="historySectionRef">
    <!-- Recent execution logs (latest 5) -->
    <v-card elevation="0" border rounded="lg" class="mt-4">
      <v-card-title class="d-flex align-center text-subtitle-1">
        <v-icon class="mr-2">mdi-history</v-icon>
        最近执行
        <v-spacer />
        <v-btn size="small" variant="text" icon="mdi-refresh" :loading="recentLoading" @click="loadRecentExecutions" />
      </v-card-title>
      <v-divider />
      <v-card-text class="pa-0">
        <v-progress-linear v-if="recentLoading" indeterminate color="primary" />
        <v-list v-if="recentExecutions.length" density="compact" class="py-0">
          <v-list-item
            v-for="item in recentExecutions"
            :key="item.id"
            :title="`#${item.id} · ${execLabel(item)}`"
            @click="openExecDetail(item.id)"
            style="cursor: pointer"
          >
            <template #subtitle>
              <div class="text-caption">
                {{ execStatusLabel(item.status) }} · {{ parseServerCount(item.server_ids) }} 台
                · {{ item.operator || '—' }} · {{ formatDateTimeBeijing(item.started_at) }}
              </div>
              <div v-for="(ev, idx) in execEventLines(item)" :key="idx" class="text-caption text-medium-emphasis font-monospace mt-1">
                {{ ev }}
              </div>
            </template>
            <template #append>
              <v-chip :color="execStatusColor(item.status)" size="x-small" variant="tonal" label>
                {{ execStatusLabel(item.status) }}
              </v-chip>
            </template>
          </v-list-item>
        </v-list>
        <div v-else-if="!recentLoading" class="text-center text-medium-emphasis py-6">暂无执行记录</div>
      </v-card-text>
    </v-card>

    <!-- All execution history -->
    <v-card elevation="0" border rounded="lg" class="mt-4">
      <v-card-title class="d-flex flex-wrap align-center ga-2 text-subtitle-1">
        <v-icon class="mr-1">mdi-format-list-bulleted</v-icon>
        全部执行历史
        <v-chip
          v-if="historyScriptFilter"
          size="small"
          variant="tonal"
          closable
          label
          @click:close="clearHistoryFilter"
        >
          {{ filteredScriptName }}
        </v-chip>
        <v-spacer />
        <v-btn size="small" variant="text" icon="mdi-refresh" :loading="allExecLoading" @click="loadAllExecutions" />
      </v-card-title>
      <v-divider />
      <v-data-table-server
        :items="allExecutions"
        :headers="allExecHeaders"
        :items-length="allExecTotal"
        :loading="allExecLoading"
        :page="allExecPage"
        :items-per-page="allExecPerPage"
        :items-per-page-options="dataTablePageOptions"
        density="compact"
        hover
        @update:page="onAllExecPageChange"
        @update:items-per-page="onAllExecPerPageChange"
      >
        <template #item.label="{ item }">
          <div class="text-body-2">{{ execLabel(item) }}</div>
          <code class="text-caption text-medium-emphasis">{{ truncateCommand(item.command) }}</code>
        </template>
        <template #item.status="{ item }">
          <v-chip :color="execStatusColor(item.status)" size="x-small" variant="tonal" label>
            {{ execStatusLabel(item.status) }}
          </v-chip>
        </template>
        <template #item.server_count="{ item }">
          {{ parseServerCount(item.server_ids) }}
        </template>
        <template #item.progress="{ item }">
          <div class="d-flex align-center flex-wrap ga-1">
            <span>{{ formatProgress(item.progress) }}</span>
            <v-chip
              v-if="execResultSummary(item).failed > 0"
              color="error"
              size="x-small"
              variant="tonal"
              label
            >
              {{ execResultSummary(item).failed }} 台失败
            </v-chip>
          </div>
        </template>
        <template #item.started_at="{ item }">
          {{ formatDateTimeBeijing(item.started_at) }}
        </template>
        <template #item.actions="{ item }">
          <v-btn size="x-small" variant="text" @click="openExecDetail(item.id)">详情</v-btn>
          <v-btn
            v-if="item.status === 'running' || item.status === 'partial'"
            size="x-small"
            variant="text"
            color="error"
            @click="stopHistoryExec(item)"
          >
            停止
          </v-btn>
          <v-btn
            v-if="item.status === 'failed' || item.status === 'partial'"
            size="x-small"
            variant="text"
            color="primary"
            @click="retryHistoryExec(item)"
          >
            重试
          </v-btn>
        </template>
        <template #no-data>
          <div class="text-center text-medium-emphasis py-6">暂无执行记录</div>
        </template>
      </v-data-table-server>
    </v-card>
    </div>

    <!-- Editor Dialog -->
    <v-dialog v-model="showEditor" max-width="900" scrollable>
      <v-card border>
        <v-card-title>{{ editingScript ? '编辑脚本' : '新建脚本' }}</v-card-title>
        <v-divider />
        <v-card-text>
          <v-text-field v-model="form.name" label="脚本名称" variant="outlined" density="compact" class="mb-2" :rules="[required('脚本名称')]" />
          <v-select
            v-model="form.category"
            :items="categoryOptions"
            item-title="label"
            item-value="value"
            label="分类"
            variant="outlined"
            density="compact"
            class="mb-2"
          />
          <v-textarea v-model="form.description" label="描述" variant="outlined" density="compact" rows="2" class="mb-2" />
          <div class="text-caption text-medium-emphasis mb-1">脚本内容</div>
          <ScriptContentEditor v-model="form.content" />
        </v-card-text>
        <v-divider />
        <v-card-actions>
          <v-spacer />
          <v-btn variant="text" @click="showEditor = false">取消</v-btn>
          <v-btn color="primary" variant="flat" @click="saveScript" :loading="saving" :disabled="!form.content.trim()">
            保存
          </v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>

    <!-- Run Dialog -->
    <v-dialog v-model="showRun" max-width="720" scrollable>
      <v-card border>
        <v-card-title>执行脚本: {{ runningScript?.name }}</v-card-title>
        <v-card-text>
          <div class="text-caption text-medium-emphasis mb-1">选择服务器（按分类勾选）</div>
          <ServerCategoryPicker
            v-model="runServerIds"
            :servers="serverList"
            :loading="serversLoading"
            max-height="360px"
            class="mb-2"
          />
          <v-row dense>
            <v-col cols="6">
              <v-text-field
                v-model.number="runTimeout"
                label="超时（秒）"
                variant="outlined"
                density="compact"
                type="number"
                :max="execConfig?.max_timeout ?? 600"
              />
            </v-col>
            <v-col cols="6">
              <v-select
                v-model="runCredentialId"
                :items="credentialOptions"
                item-title="title"
                item-value="value"
                label="DB 凭据（可选）"
                variant="outlined"
                density="compact"
                clearable
              />
            </v-col>
          </v-row>
          <v-switch v-model="runLongTask" label="长任务（nohup + Agent 回调，适合超时较长的脚本）" density="compact" color="primary" />
        </v-card-text>
        <v-card-actions>
          <v-spacer />
          <v-btn variant="text" @click="showRun = false">取消</v-btn>
          <v-btn color="primary" variant="flat" @click="doRun" :loading="executing" :disabled="!runServerIds.length">
            执行
          </v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>

    <!-- Delete Confirm -->
    <v-dialog v-model="showDelete" max-width="400">
      <v-card border>
        <v-card-title>确认删除</v-card-title>
        <v-card-text>确定要删除脚本 <strong>{{ deletingScript?.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">删除</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>

    <!-- Execution Detail Dialog -->
    <v-dialog v-model="showExecDetail" max-width="960" scrollable>
      <v-card border>
        <v-card-title class="d-flex align-center flex-wrap ga-2">
          <span>执行详情 #{{ execDetail?.id }}</span>
          <v-chip
            v-if="execDetail"
            :color="execStatusColor(execDetail.status)"
            size="small"
            variant="tonal"
            label
          >
            {{ execStatusLabel(execDetail.status) }}
          </v-chip>
          <v-spacer />
          <v-btn
            v-if="execDetail?.long_task"
            size="small"
            variant="tonal"
            :loading="detailLoading"
            @click="loadExecDetail(true)"
          >
            拉取日志
          </v-btn>
        </v-card-title>
        <v-divider />
        <v-card-text v-if="execDetail">
          <div class="text-caption text-medium-emphasis mb-2">
            {{ formatProgress(execDetail.progress) }} · 操作人 {{ execDetail.operator || '—' }}
          </div>
          <pre class="text-body-2 mb-3 pa-3 rounded bg-surface-variant overflow-auto" style="max-height: 100px">{{ execDetail.command }}</pre>

          <div v-if="execDetailStats.total" class="d-flex flex-wrap ga-2 mb-3">
            <v-chip size="small" variant="tonal" label>共 {{ execDetailStats.total }} 台</v-chip>
            <v-chip v-if="execDetailStats.success" color="success" size="small" variant="tonal" label>
              成功 {{ execDetailStats.success }}
            </v-chip>
            <v-chip v-if="execDetailStats.failed" color="error" size="small" variant="tonal" label>
              失败 {{ execDetailStats.failed }}
            </v-chip>
            <v-chip v-if="execDetailStats.pending" color="info" size="small" variant="tonal" label>
              进行中 {{ execDetailStats.pending }}
            </v-chip>
          </div>

          <v-alert
            v-if="execDetailFailedNames.length"
            type="error"
            variant="tonal"
            density="compact"
            class="mb-3"
            :title="`失败 ${execDetailFailedNames.length} 台`"
          >
            <div class="text-body-2">{{ execDetailFailedNames.join('、') }}</div>
          </v-alert>

          <div class="d-flex align-center flex-wrap ga-2 mb-3">
            <span class="text-caption text-medium-emphasis">筛选</span>
            <v-btn-toggle v-model="detailRowFilter" mandatory density="compact" color="primary" variant="outlined" divided>
              <v-btn value="all" size="small">全部</v-btn>
              <v-btn value="failed" size="small">仅失败</v-btn>
              <v-btn value="success" size="small">仅成功</v-btn>
            </v-btn-toggle>
            <template v-if="execDetailStats.failed > 0">
              <v-divider vertical class="mx-1" />
              <span class="text-caption text-medium-emphasis">视图</span>
              <v-btn-toggle v-model="detailViewMode" mandatory density="compact" color="primary" variant="outlined" divided>
                <v-btn value="list" size="small">列表</v-btn>
                <v-btn value="group" size="small">按错误分组</v-btn>
              </v-btn-toggle>
              <v-btn size="small" variant="tonal" prepend-icon="mdi-content-copy" @click="copyFailedList">
                复制失败清单
              </v-btn>
              <v-btn
                v-if="detailViewMode === 'group'"
                size="small"
                variant="tonal"
                prepend-icon="mdi-content-copy"
                @click="copyFailureGroupSummary"
              >
                复制分组摘要
              </v-btn>
            </template>
          </div>

          <v-table v-if="detailViewMode === 'list'" density="compact" class="exec-detail-table">
            <thead>
              <tr>
                <th>服务器</th>
                <th>状态</th>
                <th style="width: 72px">退出码</th>
                <th>错误摘要</th>
                <th style="width: 120px" class="text-end">操作</th>
              </tr>
            </thead>
            <tbody>
              <tr
                v-for="row in filteredExecDetailRows"
                :key="row.serverId"
                :class="{ 'exec-detail-row--failed': row.failed, 'exec-detail-row--active': expandedRowId === row.serverId }"
                class="exec-detail-row cursor-pointer"
                @click="toggleDetailRow(row.serverId)"
              >
                <td>
                  <div class="font-weight-medium">{{ row.serverName }}</div>
                  <div v-if="row.serverDomain" class="text-caption text-medium-emphasis">{{ row.serverDomain }}</div>
                </td>
                <td>
                  <v-chip
                    :color="execServerStatusColor(row.status, row.phase, row.exitCode)"
                    size="x-small"
                    variant="tonal"
                    label
                  >
                    {{ execServerStatusLabel(row.status, row.phase) }}
                  </v-chip>
                </td>
                <td :class="{ 'text-error font-weight-medium': row.failed }">{{ row.exitCode ?? '—' }}</td>
                <td class="text-truncate" style="max-width: 280px" :title="row.summary">{{ row.summary }}</td>
                <td class="text-end" @click.stop>
                  <v-btn
                    v-if="row.fullOutput && row.fullOutput !== '—'"
                    size="x-small"
                    variant="text"
                    icon="mdi-content-copy"
                    title="复制输出"
                    @click="copyExecOutput(row)"
                  />
                  <v-btn
                    size="x-small"
                    variant="text"
                    icon="mdi-console"
                    title="打开终端"
                    @click="openServerTerminal(row.serverIdNum)"
                  />
                </td>
              </tr>
              <tr v-if="filteredExecDetailRows.length === 0">
                <td colspan="5" class="text-center text-medium-emphasis py-4">当前筛选无记录</td>
              </tr>
            </tbody>
          </v-table>

          <v-expansion-panels
            v-else-if="execDetailFailureGroups.length"
            v-model="expandedFailureGroups"
            multiple
            variant="accordion"
            class="exec-detail-groups"
          >
            <v-expansion-panel v-for="(group, idx) in execDetailFailureGroups" :key="`${idx}-${group.errorKey}`">
              <v-expansion-panel-title>
                <div class="d-flex align-center ga-2 flex-grow-1 min-width-0">
                  <span class="text-body-2 text-truncate" :title="group.errorKey">{{ group.errorKey }}</span>
                  <v-chip color="error" size="x-small" variant="tonal" label>{{ group.count }} 台</v-chip>
                </div>
              </v-expansion-panel-title>
              <v-expansion-panel-text>
                <v-table density="compact">
                  <thead>
                    <tr>
                      <th>服务器</th>
                      <th style="width: 72px">退出码</th>
                      <th style="width: 120px" class="text-end">操作</th>
                    </tr>
                  </thead>
                  <tbody>
                    <tr
                      v-for="row in group.rows"
                      :key="row.serverId"
                      :class="{ 'exec-detail-row--active': expandedRowId === row.serverId }"
                      class="exec-detail-row cursor-pointer"
                      @click="toggleDetailRow(row.serverId)"
                    >
                      <td>
                        <div class="font-weight-medium">{{ row.serverName }}</div>
                        <div v-if="row.serverDomain" class="text-caption text-medium-emphasis">{{ row.serverDomain }}</div>
                      </td>
                      <td class="text-error font-weight-medium">{{ row.exitCode ?? '—' }}</td>
                      <td class="text-end" @click.stop>
                        <v-btn
                          v-if="row.fullOutput && row.fullOutput !== '—'"
                          size="x-small"
                          variant="text"
                          icon="mdi-content-copy"
                          title="复制输出"
                          @click="copyExecOutput(row)"
                        />
                        <v-btn
                          size="x-small"
                          variant="text"
                          icon="mdi-console"
                          title="打开终端"
                          @click="openServerTerminal(row.serverIdNum)"
                        />
                      </td>
                    </tr>
                  </tbody>
                </v-table>
              </v-expansion-panel-text>
            </v-expansion-panel>
          </v-expansion-panels>
          <div v-else class="text-center text-medium-emphasis py-4">无失败记录可分组</div>

          <v-card
            v-if="expandedDetailRow"
            variant="tonal"
            :color="expandedDetailRow.failed ? 'error' : undefined"
            class="mt-3 pa-3"
          >
            <div class="d-flex align-center flex-wrap ga-2 mb-2">
              <span class="text-subtitle-2">
                {{ expandedDetailRow.serverName }}
                <span v-if="expandedDetailRow.serverDomain" class="text-medium-emphasis">（{{ expandedDetailRow.serverDomain }}）</span>
              </span>
              <v-spacer />
              <v-btn size="x-small" variant="text" prepend-icon="mdi-content-copy" @click="copyExecOutput(expandedDetailRow)">
                复制
              </v-btn>
              <v-btn size="x-small" variant="text" prepend-icon="mdi-console" @click="openServerTerminal(expandedDetailRow.serverIdNum)">
                终端
              </v-btn>
            </div>
            <pre class="text-body-2 overflow-auto exec-detail-output">{{ expandedDetailRow.fullOutput }}</pre>
          </v-card>
        </v-card-text>
        <v-card-actions v-if="execDetail">
          <v-btn
            v-if="execDetail.status === 'failed' || execDetail.status === 'partial'"
            color="primary"
            variant="text"
            @click="retryFromDetail"
          >
            重试失败项
          </v-btn>
          <v-spacer />
          <v-btn variant="text" @click="showExecDetail = false">关闭</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>
  </v-container>
</template>

<script setup lang="ts">
import { ref, computed, watch, nextTick } from 'vue'
import { usePageActivateRefresh } from '@/composables/usePageActivateRefresh'
import { useRouter } from 'vue-router'
import ScriptContentEditor from '@/components/ScriptContentEditor.vue'
import ServerCategoryPicker from '@/components/servers/ServerCategoryPicker.vue'
import { http } from '@/api'
import { useServerList } from '@/composables/useServerList'
import { useSnackbar } from '@/composables/useSnackbar'
import { formatApiError } from '@/utils/apiError'
import { required } from '@/utils/validation'
import type { DbCredentialItem, ScriptExecItem, ScriptItem } from '@/types/api'
import { DATA_TABLE_ITEMS_PER_PAGE_OPTIONS, headersWithoutSort } from '@/constants/dataTable'
import { fetchLimitOffset } from '@/utils/paginatedFetch'
import {
  execStatusLabel,
  execStatusColor,
  execServerStatusLabel,
  execServerStatusColor,
  isExecServerFailed,
  isExecServerPending,
  isExecTerminalStatus,
  summarizeExecServerResults,
  resolveExecServerName,
} from '@/utils/scriptExecutionLabels'
import { groupFailedExecRows } from '@/utils/execDetailGrouping'
import { registerScriptExecution } from '@/composables/useScriptExecutionQueue'

defineOptions({ name: 'ScriptsPage' })
import { showScriptSubmitToast } from '@/composables/useScriptSubmitToast'
import { useScriptExecutionListRefresh } from '@/composables/useScriptExecutionListRefresh'
import { formatDateTimeBeijing } from '@/utils/datetime'

const dataTablePageOptions = [...DATA_TABLE_ITEMS_PER_PAGE_OPTIONS]
const snackbar = useSnackbar()
const router = useRouter()
const { servers: serverList, loading: serversLoading, loadServers } = useServerList()

const categoryOptions = [
  { label: '运维', value: 'ops' },
  { label: '部署', value: 'deploy' },
  { label: '检查', value: 'check' },
  { label: '清理', value: 'cleanup' },
]

interface ExecConfig {
  batch_size: number
  concurrency: number
  max_timeout: number
}

interface ExecDetailRow {
  serverId: string
  serverIdNum: number
  serverName: string
  serverDomain: string
  status: string
  phase?: string
  exitCode: number | null
  summary: string
  fullOutput: string
  failed: boolean
  pending: boolean
}

const loading = ref(false)
const search = ref('')
const scripts = ref<ScriptItem[]>([])
const execConfig = ref<ExecConfig | null>(null)
const credentials = ref<DbCredentialItem[]>([])

const page = ref(1)
const itemsPerPage = ref(20)

const showEditor = ref(false)
const editingScript = ref<ScriptItem | null>(null)
const saving = ref(false)
const form = ref({ name: '', category: 'ops', description: '', content: '' })

const showRun = ref(false)
const runningScript = ref<ScriptItem | null>(null)
const runServerIds = ref<number[]>([])
const runTimeout = ref(300)
const runLongTask = ref(false)
const runCredentialId = ref<number | null>(null)
const executing = ref(false)

const showDelete = ref(false)
const deletingScript = ref<ScriptItem | null>(null)

const historySectionRef = ref<HTMLElement | null>(null)
const recentExecutions = ref<ScriptExecItem[]>([])
const recentLoading = ref(false)
const allExecutions = ref<ScriptExecItem[]>([])
const allExecTotal = ref(0)
const allExecPage = ref(1)
const allExecPerPage = ref(20)
const allExecLoading = ref(false)
const historyScriptFilter = ref<number | null>(null)

const showQuickExec = ref(false)
const quickCommand = ref('')
const quickServerIds = ref<number[]>([])
const quickTimeout = ref(60)
const quickLongTask = ref(false)
const quickCredentialId = ref<number | null>(null)
const quickExecuting = ref(false)

const showExecDetail = ref(false)
const execDetail = ref<ScriptExecItem | null>(null)
const detailLoading = ref(false)
const detailExecId = ref<number | null>(null)
const detailRowFilter = ref<'all' | 'failed' | 'success'>('all')
const detailViewMode = ref<'list' | 'group'>('list')
const expandedRowId = ref<string | null>(null)
const expandedFailureGroups = ref<number[]>([])

const credentialOptions = computed(() =>
  credentials.value.map(c => ({ title: c.name, value: c.id })),
)

const filteredScripts = computed(() => {
  const q = search.value.trim().toLowerCase()
  if (!q) return scripts.value
  return scripts.value.filter(s =>
    s.name.toLowerCase().includes(q) ||
    (s.category || '').toLowerCase().includes(q) ||
    (s.description || '').toLowerCase().includes(q),
  )
})

const totalItems = computed(() => filteredScripts.value.length)

const pagedScripts = computed(() => {
  const start = (page.value - 1) * itemsPerPage.value
  return filteredScripts.value.slice(start, start + itemsPerPage.value)
})

watch(search, () => { page.value = 1 })

const allExecHeaders = headersWithoutSort([
  { title: 'ID', key: 'id', width: 72 },
  { title: '脚本/命令', key: 'label' },
  { title: '状态', key: 'status', width: 100 },
  { title: '进度', key: 'progress', width: 88 },
  { title: '台数', key: 'server_count', width: 72 },
  { title: '操作人', key: 'operator', width: 88 },
  { title: '开始时间', key: 'started_at', width: 168 },
  { title: '操作', key: 'actions', width: 180 },
])

const filteredScriptName = computed(() => {
  if (!historyScriptFilter.value) return ''
  return scripts.value.find(s => s.id === historyScriptFilter.value)?.name || `脚本 #${historyScriptFilter.value}`
})

const execEventActionLabels: Record<string, string> = {
  started: '开始',
  batch_progress: '批次进度',
  execution_finished: '结束',
  stopped: '停止',
  retry_requested: '重试',
  stuck_marked: '标记卡住',
  server_auto_stuck: '子机超时',
  auto_stuck: '自动卡住',
  job_completed: '回调完成',
}

const serverBriefById = computed(() => new Map(serverList.value.map(s => [s.id, s])))

function buildExecOutput(entry: ScriptExecItem['results'][string]): { summary: string; full: string } {
  const stderr = (entry.stderr || '').trim()
  const stdout = (entry.stdout || '').trim()
  const message = (entry.message || '').trim()
  const full = stderr || stdout || message || '—'
  const summary = (stderr || message || stdout || '').slice(0, 200) || '—'
  return { summary, full }
}

const execDetailRows = computed<ExecDetailRow[]>(() => {
  if (!execDetail.value?.results) return []
  const briefs = serverBriefById.value
  const rows = Object.entries(execDetail.value.results)
    .filter(([k]) => k !== '_meta' && /^\d+$/.test(k))
    .map(([serverId, entry]) => {
      const idNum = Number(serverId)
      const brief = briefs.get(idNum)
      const status = String(entry.status || '')
      const phase = entry.phase ? String(entry.phase) : undefined
      const exitCode = entry.exit_code ?? null
      const output = buildExecOutput(entry)
      const failed = isExecServerFailed(status, phase, exitCode)
      const pending = isExecServerPending(phase)
      return {
        serverId,
        serverIdNum: idNum,
        serverName: resolveExecServerName(
          serverId,
          execDetail.value?.server_names,
          brief?.name,
        ),
        serverDomain: brief?.domain || '',
        status,
        phase,
        exitCode,
        summary: output.summary,
        fullOutput: output.full,
        failed,
        pending,
      }
    })

  return rows.sort((a, b) => {
    if (a.failed !== b.failed) return a.failed ? -1 : 1
    if (a.pending !== b.pending) return a.pending ? 1 : -1
    return a.serverName.localeCompare(b.serverName, 'zh')
  })
})

const execDetailStats = computed(() =>
  summarizeExecServerResults(execDetail.value?.results, execDetail.value?.server_ids),
)

const execDetailFailedNames = computed(() =>
  execDetailRows.value.filter(r => r.failed).map(r => r.serverName),
)

const execDetailFailureGroups = computed(() =>
  groupFailedExecRows(execDetailRows.value),
)

const filteredExecDetailRows = computed(() => {
  if (detailRowFilter.value === 'failed') return execDetailRows.value.filter(r => r.failed)
  if (detailRowFilter.value === 'success') {
    return execDetailRows.value.filter(r => !r.failed && !r.pending)
  }
  return execDetailRows.value
})

const expandedDetailRow = computed(() =>
  execDetailRows.value.find(r => r.serverId === expandedRowId.value) || null,
)

function execResultSummary(item: ScriptExecItem) {
  return summarizeExecServerResults(item.results, item.server_ids)
}

function categoryLabel(v: string) {
  return categoryOptions.find(o => o.value === v)?.label || v
}

function truncateCommand(cmd: string) {
  if (!cmd) return '—'
  const plain = cmd.startsWith('[long_task] ') ? cmd.slice('[long_task] '.length) : cmd
  return plain.length > 60 ? `${plain.slice(0, 60)}…` : plain
}

function execLabel(item: ScriptExecItem): string {
  if (item.script_id) {
    return scripts.value.find(s => s.id === item.script_id)?.name || `脚本 #${item.script_id}`
  }
  return '临时命令'
}

function execEventLines(item: ScriptExecItem): string[] {
  const events = item.events || []
  if (!events.length) return []
  return events.slice(-3).map(ev => {
    const label = execEventActionLabels[ev.action || ''] || ev.action || '事件'
    const time = formatDateTimeBeijing(ev.at)
    const detail = ev.detail || ''
    return `${time} · ${label}${detail ? ` — ${detail}` : ''}`
  })
}

async function loadRecentExecutions(silent = false) {
  if (!silent) recentLoading.value = true
  try {
    const res = await http.get<{ items: ScriptExecItem[]; total: number }>('/scripts/executions', { limit: 5 })
    recentExecutions.value = res.items || []
  } catch {
    if (!silent) recentExecutions.value = []
  } finally {
    if (!silent) recentLoading.value = false
  }
}

async function loadAllExecutions(silent = false) {
  if (!silent) allExecLoading.value = true
  try {
    const res = await fetchLimitOffset<ScriptExecItem>(
      async (limit, offset) => {
        const params: Record<string, unknown> = { limit, offset }
        if (historyScriptFilter.value) params.script_id = historyScriptFilter.value
        const data = await http.get<{ items: ScriptExecItem[]; total: number }>('/scripts/executions', params)
        return { items: data.items || [], total: data.total || 0 }
      },
      allExecPage.value,
      allExecPerPage.value,
    )
    allExecutions.value = res.items
    allExecTotal.value = res.total
    if (allExecPerPage.value === -1) {
      allExecPage.value = 1
    }
  } catch {
    if (!silent) {
      allExecutions.value = []
      allExecTotal.value = 0
    }
  } finally {
    if (!silent) allExecLoading.value = false
  }
}

function refreshExecutionHistory(silent = false) {
  void loadRecentExecutions(silent)
  void loadAllExecutions(silent)
}

useScriptExecutionListRefresh(refreshExecutionHistory)

function onAllExecPageChange(p: number) {
  allExecPage.value = p
  loadAllExecutions()
}

function onAllExecPerPageChange(n: number) {
  allExecPerPage.value = n
  allExecPage.value = 1
  loadAllExecutions()
}

function clearHistoryFilter() {
  historyScriptFilter.value = null
  allExecPage.value = 1
  loadAllExecutions()
}

async function filterHistoryByScript(script: ScriptItem) {
  historyScriptFilter.value = script.id
  allExecPage.value = 1
  await loadAllExecutions()
  await nextTick()
  historySectionRef.value?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}

function formatProgress(p: string | undefined) {
  if (!p) return '—'
  return p
}

function parseServerCount(serverIds: string | undefined): number {
  if (!serverIds) return 0
  try {
    const arr = JSON.parse(serverIds) as unknown
    return Array.isArray(arr) ? arr.length : 0
  } catch {
    return 0
  }
}

function executionId(res: { id?: number; execution_id?: number }) {
  const id = res.id ?? res.execution_id
  if (id == null) throw new Error('执行响应缺少 id')
  return id
}

async function loadScripts() {
  loading.value = true
  try {
    const res = await http.getList<ScriptItem>('/scripts/')
    scripts.value = res.items
  } catch (e: unknown) {
    scripts.value = []
    snackbar(formatApiError(e, '加载脚本列表失败'), 'error')
  } finally {
    loading.value = false
  }
}

async function loadExecConfig() {
  try {
    execConfig.value = await http.get<ExecConfig>('/scripts/exec-config')
  } catch {
    execConfig.value = null
  }
}

async function loadCredentials() {
  try {
    credentials.value = await http.getList<DbCredentialItem>('/scripts/credentials').then(r => r.items)
  } catch {
    credentials.value = []
  }
}

function openEditor(script: ScriptItem | null) {
  editingScript.value = script
  if (script) {
    form.value = {
      name: script.name,
      category: script.category || 'ops',
      description: script.description || '',
      content: script.content || '',
    }
  } else {
    form.value = { name: '', category: 'ops', description: '', content: '' }
  }
  showEditor.value = true
}

async function saveScript() {
  if (!form.value.name.trim() || !form.value.content.trim()) {
    snackbar('请填写名称与脚本内容', 'warning')
    return
  }
  saving.value = true
  try {
    const body = {
      name: form.value.name.trim(),
      category: form.value.category,
      description: form.value.description.trim() || null,
      content: form.value.content,
    }
    if (editingScript.value) {
      await http.put(`/scripts/${editingScript.value.id}`, body)
    } else {
      await http.post('/scripts/', body)
    }
    showEditor.value = false
    await loadScripts()
    snackbar('保存成功')
  } catch (e: unknown) {
    snackbar(formatApiError(e, '保存失败'), 'error')
  } finally {
    saving.value = false
  }
}

function runScript(script: ScriptItem) {
  runningScript.value = script
  runServerIds.value = []
  runLongTask.value = false
  runCredentialId.value = null
  showRun.value = true
}

async function submitExec(payload: Record<string, unknown>, label: string, serverCount: number) {
  const res = await http.post<ScriptExecItem & { execution_id?: number }>('/scripts/exec', payload)
  const id = executionId(res)
  registerScriptExecution(
    id,
    label,
    serverCount,
    Boolean(payload.long_task),
    res.progress,
    res.status,
  )
  return res
}

async function doRun() {
  if (!runningScript.value || !runServerIds.value.length) {
    snackbar('请选择服务器', 'warning')
    return
  }
  executing.value = true
  try {
    await submitExec(
      {
        script_id: runningScript.value.id,
        server_ids: runServerIds.value,
        timeout: runTimeout.value,
        long_task: runLongTask.value,
        credential_id: runCredentialId.value ?? undefined,
      },
      runningScript.value.name,
      runServerIds.value.length,
    )
    showRun.value = false
    showScriptSubmitToast('脚本已提交执行')
    refreshExecutionHistory()
  } catch (e: unknown) {
    snackbar(formatApiError(e, '执行失败'), 'error')
  } finally {
    executing.value = false
  }
}

function confirmDelete(script: ScriptItem) {
  deletingScript.value = script
  showDelete.value = true
}

async function doDelete() {
  if (!deletingScript.value) return
  try {
    await http.delete(`/scripts/${deletingScript.value.id}`)
    showDelete.value = false
    await loadScripts()
    snackbar('已删除')
  } catch (e: unknown) {
    snackbar(formatApiError(e, '删除失败'), 'error')
  }
}

async function doQuickExec() {
  if (!quickCommand.value.trim() || quickServerIds.value.length === 0) return
  quickExecuting.value = true
  try {
    await submitExec(
      {
        command: quickCommand.value.trim(),
        server_ids: quickServerIds.value,
        timeout: quickTimeout.value,
        long_task: quickLongTask.value,
        credential_id: quickCredentialId.value ?? undefined,
      },
      '临时命令',
      quickServerIds.value.length,
    )
    showScriptSubmitToast(`已在 ${quickServerIds.value.length} 台服务器上执行`)
    refreshExecutionHistory()
  } catch (e: unknown) {
    snackbar(formatApiError(e, '执行失败'), 'error')
  } finally {
    quickExecuting.value = false
  }
}

async function stopHistoryExec(item: ScriptExecItem) {
  try {
    await http.post(`/scripts/executions/${item.id}/stop`)
    refreshExecutionHistory()
    snackbar('已发送停止指令')
  } catch (e: unknown) {
    snackbar(formatApiError(e, '停止失败'), 'error')
  }
}

async function retryHistoryExec(item: ScriptExecItem) {
  try {
    const res = await http.post<ScriptExecItem>(`/scripts/executions/${item.id}/retry`, {})
    registerScriptExecution(
      executionId(res),
      `${execLabel(item)}（重试）`,
      parseServerCount(item.server_ids) || parseServerCount(res.server_ids),
      Boolean(item.long_task),
      res.progress,
      res.status,
    )
    refreshExecutionHistory()
    showScriptSubmitToast('重试已提交')
  } catch (e: unknown) {
    snackbar(formatApiError(e, '重试失败'), 'error')
  }
}

async function openExecDetail(id: number) {
  detailExecId.value = id
  detailRowFilter.value = 'all'
  detailViewMode.value = 'list'
  expandedRowId.value = null
  expandedFailureGroups.value = []
  showExecDetail.value = true
  await loadExecDetail(false)
  const summary = summarizeExecServerResults(execDetail.value?.results, execDetail.value?.server_ids)
  if (
    summary.failed > 0 &&
    (execDetail.value?.status === 'failed' || execDetail.value?.status === 'partial')
  ) {
    detailRowFilter.value = 'failed'
    const firstFailed = execDetailRows.value.find(r => r.failed)
    if (firstFailed) expandedRowId.value = firstFailed.serverId
  }
}

function formatFailedRowLine(row: ExecDetailRow): string {
  const domain = row.serverDomain ? ` (${row.serverDomain})` : ''
  const code = row.exitCode != null ? ` · exit ${row.exitCode}` : ''
  const line = row.summary.split('\n')[0]?.trim() || row.summary
  const summary = line && line !== '—' ? ` · ${line.slice(0, 120)}` : ''
  return `- ${row.serverName}${domain}${code}${summary}`
}

async function copyTextToClipboard(text: string, successMsg: string) {
  try {
    await navigator.clipboard.writeText(text)
    snackbar(successMsg)
  } catch {
    snackbar('复制失败', 'error')
  }
}

async function copyFailedList() {
  const failed = execDetailRows.value.filter(r => r.failed)
  if (!failed.length) return
  const id = execDetail.value?.id ?? detailExecId.value
  const lines = [
    `执行 #${id} · 失败 ${failed.length} 台`,
    ...failed.map(formatFailedRowLine),
  ]
  await copyTextToClipboard(lines.join('\n'), '已复制失败清单')
}

async function copyFailureGroupSummary() {
  const groups = execDetailFailureGroups.value
  if (!groups.length) return
  const id = execDetail.value?.id ?? detailExecId.value
  const lines = [`执行 #${id} · 失败分组 ${groups.length} 类`]
  for (const g of groups) {
    const names = g.rows.map(r => r.serverName).join('、')
    lines.push(`[${g.errorKey.slice(0, 120)}] ${g.count} 台: ${names}`)
  }
  await copyTextToClipboard(lines.join('\n'), '已复制分组摘要')
}

function toggleDetailRow(serverId: string) {
  expandedRowId.value = expandedRowId.value === serverId ? null : serverId
}

async function copyExecOutput(row: ExecDetailRow) {
  if (!row.fullOutput || row.fullOutput === '—') return
  try {
    await navigator.clipboard.writeText(row.fullOutput)
    snackbar('已复制到剪贴板')
  } catch {
    snackbar('复制失败', 'error')
  }
}

function openServerTerminal(serverId: number) {
  router.push({ path: '/terminal', query: { server_id: String(serverId) } })
}

async function retryFromDetail() {
  if (!execDetail.value) return
  await retryHistoryExec(execDetail.value)
}

async function loadExecDetail(fetchLogs: boolean) {
  if (detailExecId.value == null) return
  detailLoading.value = true
  try {
    execDetail.value = await http.get<ScriptExecItem>(`/scripts/executions/${detailExecId.value}`, {
      fetch_logs: fetchLogs,
      log_lines: 100,
    })
  } catch (e: unknown) {
    snackbar(formatApiError(e, '加载详情失败'), 'error')
    showExecDetail.value = false
  } finally {
    detailLoading.value = false
  }
}

async function refreshScriptsPage(silent = false) {
  await Promise.all([
    loadScripts(),
    loadServers({ all: true }),
    loadExecConfig(),
    loadCredentials(),
    refreshExecutionHistory(silent),
  ])
}

usePageActivateRefresh((silent) => refreshScriptsPage(silent))

</script>

<style scoped>
.exec-detail-row--failed {
  background: rgba(var(--v-theme-error), 0.08);
}
.exec-detail-row--active {
  background: rgba(var(--v-theme-primary), 0.06);
}
.exec-detail-output {
  max-height: 240px;
  white-space: pre-wrap;
  word-break: break-word;
}
</style>
