<template>
  <v-container fluid class="pa-6">
    <v-row>
      <!-- System Settings -->
      <v-col cols="12" lg="6">
        <v-card elevation="0" border rounded="lg" class="mb-4">
          <v-card-title class="d-flex align-center">
            <v-icon class="mr-2">mdi-cog</v-icon>
            系统设置
          </v-card-title>
          <v-progress-linear v-if="settingsLoading" indeterminate color="primary" />
          <v-divider />
          <v-form ref="systemSettingsFormRef" @submit.prevent="saveSettings">
            <v-card-text>
              <v-text-field v-model="settings.system_name" label="系统名称" variant="outlined" density="compact" class="mb-3" :rules="[required('系统名称')]" />
              <v-text-field v-model="settings.system_title" label="系统标题" variant="outlined" density="compact" class="mb-3" :rules="[required('系统标题')]" />

              <v-divider class="my-4" />
              <div class="text-subtitle-2 mb-3">告警阈值</div>
              <v-row dense>
                <v-col cols="4">
                  <v-text-field v-model="settings.cpu_alert_threshold" label="CPU %" variant="outlined" density="compact" type="number" :rules="[required('阈值'), numberRange(1, 100, '阈值')]" />
                </v-col>
                <v-col cols="4">
                  <v-text-field v-model="settings.mem_alert_threshold" label="内存 %" variant="outlined" density="compact" type="number" :rules="[required('阈值'), numberRange(1, 100, '阈值')]" />
                </v-col>
                <v-col cols="4">
                  <v-text-field v-model="settings.disk_alert_threshold" label="磁盘 %" variant="outlined" density="compact" type="number" :rules="[required('阈值'), numberRange(1, 100, '阈值')]" />
                </v-col>
              </v-row>

              <v-divider class="my-4" />
              <div class="text-subtitle-2 mb-3">连接池</div>
              <v-row dense>
                <v-col cols="6">
                  <v-text-field v-model="settings.db_pool_size" label="连接池大小" variant="outlined" density="compact" type="number" :rules="[required('连接池大小'), numberRange(1, 1000, '连接池大小')]" />
                </v-col>
                <v-col cols="6">
                  <v-text-field v-model="settings.db_max_overflow" label="溢出连接数" variant="outlined" density="compact" type="number" :rules="[required('溢出连接数'), numberRange(1, 1000, '溢出连接数')]" />
                </v-col>
              </v-row>
            </v-card-text>
            <v-card-actions class="px-4 pb-4">
              <v-spacer />
              <v-btn type="submit" color="primary" variant="flat" :loading="saving">保存设置</v-btn>
            </v-card-actions>
          </v-form>
        </v-card>

        <TerminalQuickCommandsSettings />

        <v-card elevation="0" border rounded="lg" class="mb-4">
          <v-card-title class="d-flex align-center">
            <v-icon class="mr-2">mdi-bell-ring-outline</v-icon>
            界面提示
          </v-card-title>
          <v-divider />
          <v-card-text>
            <p class="text-caption text-medium-emphasis mb-3">
              脚本批量执行全部结束时，在浏览器播放提示音（仅当前登录的管理员会话，与 Telegram 推送无关）。
            </p>
            <v-select
              v-model="scriptExecCompleteSound"
              :items="scriptCompleteSoundOptions"
              item-title="title"
              item-value="value"
              label="脚本执行完成提示音"
              variant="outlined"
              density="compact"
              hide-details
              class="mb-3"
              :loading="scriptSoundSaving"
              :disabled="scriptSoundSaving"
              @update:model-value="onScriptExecSoundChange"
            />
            <v-btn
              size="small"
              variant="tonal"
              prepend-icon="mdi-volume-high"
              :disabled="scriptExecCompleteSound === 'off' || scriptSoundSaving"
              @click="previewScriptExecSound"
            >
              试听
            </v-btn>
            <v-divider class="my-4" />
            <p class="text-caption text-medium-emphasis mb-3">
              文件推送批次结束且至少一台成功时，在浏览器播放提示音（与脚本提示音独立配置）。
            </p>
            <v-select
              v-model="pushCompleteSound"
              :items="scriptCompleteSoundOptions"
              item-title="title"
              item-value="value"
              label="文件推送完成提示音"
              variant="outlined"
              density="compact"
              hide-details
              class="mb-3"
              :loading="pushSoundSaving"
              :disabled="pushSoundSaving"
              @update:model-value="onPushCompleteSoundChange"
            />
            <v-btn
              size="small"
              variant="tonal"
              prepend-icon="mdi-volume-high"
              :disabled="pushCompleteSound === 'off' || pushSoundSaving"
              @click="previewPushCompleteSound"
            >
              试听
            </v-btn>
          </v-card-text>
        </v-card>

        <!-- Telegram Settings -->
        <v-card elevation="0" border rounded="lg">
          <v-card-title class="d-flex align-center">
            <v-icon class="mr-2">mdi-send</v-icon>
            Telegram 推送
          </v-card-title>
          <v-divider />
          <v-card-text>
            <v-text-field
              v-model="telegramTokenDraft"
              label="Bot Token"
              variant="outlined"
              density="compact"
              class="mb-3"
              :placeholder="telegramTokenSet ? '已配置（留空保持不变）' : '输入 Bot Token'"
              :type="showToken ? 'text' : 'password'"
              :append-inner-icon="showToken ? 'mdi-eye-off' : 'mdi-eye'"
              @click:append-inner="toggleTelegramTokenVisibility"
            />
            <v-chip v-if="telegramTokenSet && !telegramTokenDraft" size="small" color="success" variant="tonal" class="mb-3">Token 已配置</v-chip>
            <v-text-field v-model="settings.telegram_chat_id" label="Chat ID" variant="outlined" density="compact" class="mb-3" />
            <p class="text-caption text-medium-emphasis mb-3">
              私聊：向 Bot 发送任意消息。群聊：将 Bot 拉入群后发送 <code>/start@你的Bot用户名</code>（或 @提及 Bot），再点击下方检测。
            </p>
            <div class="d-flex flex-wrap ga-2 mb-4">
              <v-btn size="small" variant="tonal" prepend-icon="mdi-radar" @click="detectTelegramChats" :loading="detectingTgChats">检测 Chat ID</v-btn>
              <v-btn size="small" variant="tonal" prepend-icon="mdi-message-text" @click="testTelegram" :loading="testingTg">发送测试消息</v-btn>
              <v-btn size="small" variant="tonal" color="primary" prepend-icon="mdi-content-save" @click="saveTelegramConfig" :loading="savingTelegram">保存 Telegram 配置</v-btn>
            </div>

            <v-divider class="my-3" />
            <div class="text-subtitle-2 mb-1">告警通知项目</div>
            <p class="text-caption text-medium-emphasis mb-3">
              关闭后该类告警仍记录日志，但不发送 Telegram 消息。Bot Token 未配置时全部静默。
            </p>
            <template v-for="(groupItems, groupName) in notifyToggleGroups" :key="groupName">
              <div class="text-caption text-medium-emphasis font-weight-medium mt-2 mb-1">{{ groupName }}</div>
              <v-switch
                v-for="item in groupItems"
                :key="item.key"
                :model-value="notifyToggles[item.key] ?? true"
                :label="item.label"
                :hint="item.desc"
                persistent-hint
                color="primary"
                density="compact"
                hide-details="auto"
                class="mb-1"
                :disabled="notifyToggleSaving === item.key"
                @update:model-value="(v: boolean | null) => toggleNotify(item.key, Boolean(v))"
              />
            </template>

            <v-divider class="my-3" />
            <div class="text-subtitle-2 mb-1">子机离线告警专用 Bot（可选）</div>
            <p class="text-caption text-medium-emphasis mb-3">
              与上方默认 Bot 分离：仅「子机离线告警」走此通道。Bot Token 与 Chat ID 均填写后生效；留空则离线告警仍走默认 Bot。
            </p>
            <v-text-field
              v-model="telegramOfflineTokenDraft"
              label="离线告警 Bot Token"
              variant="outlined"
              density="compact"
              class="mb-3"
              :placeholder="telegramOfflineTokenSet ? '已配置（留空保持不变）' : '输入专用 Bot Token'"
              :type="showOfflineToken ? 'text' : 'password'"
              :append-inner-icon="showOfflineToken ? 'mdi-eye-off' : 'mdi-eye'"
              @click:append-inner="toggleOfflineTelegramTokenVisibility"
            />
            <v-chip
              v-if="telegramOfflineTokenSet && !telegramOfflineTokenDraft"
              size="small"
              color="success"
              variant="tonal"
              class="mb-3"
            >
              离线 Token 已配置
            </v-chip>
            <v-text-field
              v-model="settings.telegram_offline_chat_id"
              label="离线告警 Chat ID"
              variant="outlined"
              density="compact"
              class="mb-3"
            />
            <p class="text-caption text-medium-emphasis mb-3">
              检测规则与默认 Bot 相同：群聊需拉入专用 Bot 后在群内发送 <code>/start@Bot用户名</code>。
            </p>
            <div class="d-flex flex-wrap ga-2 mb-2">
              <v-btn
                size="small"
                variant="tonal"
                prepend-icon="mdi-radar"
                :loading="detectingOfflineTgChats"
                @click="detectOfflineTelegramChats"
              >
                检测离线 Chat ID
              </v-btn>
              <v-btn
                size="small"
                variant="tonal"
                prepend-icon="mdi-message-text"
                :loading="testingOfflineTg"
                @click="testOfflineTelegram"
              >
                测试离线通道
              </v-btn>
              <v-btn
                size="small"
                variant="tonal"
                color="primary"
                prepend-icon="mdi-content-save"
                :loading="savingOfflineTelegram"
                @click="saveOfflineTelegramConfig"
              >
                保存离线 Bot
              </v-btn>
            </div>

            <v-divider class="my-4" />
            <div class="text-subtitle-2 mb-1">Layer 3 宿主机巡检</div>
            <p class="text-caption text-medium-emphasis mb-3">
              每分钟由宿主机 cron 执行 <code>health_monitor.sh</code>：连续 3 次 /health 失败则自动重启并推送 Telegram。
              配置保存后会自动同步至 .env；亦可手动同步。未同步时脚本会从数据库读取 Telegram 配置。
            </p>
            <div class="d-flex flex-wrap align-center ga-2 mb-3">
              <v-chip
                size="small"
                :color="patrolStatus?.telegram_layer3_ready ? 'success' : 'warning'"
                variant="tonal"
              >
                {{ patrolStatus?.telegram_layer3_ready ? '巡检 Telegram 就绪' : '请先配置 Telegram' }}
              </v-chip>
              <v-chip
                v-if="patrolStatus"
                size="small"
                :color="patrolStatus.telegram_env_synced ? 'success' : 'grey'"
                variant="tonal"
              >
                {{ patrolStatus.telegram_env_synced ? '.env 已同步' : '.env 未同步（可用 DB 回退）' }}
              </v-chip>
            </div>
            <div class="d-flex flex-wrap ga-2">
              <v-btn
                size="small"
                variant="tonal"
                prepend-icon="mdi-sync"
                @click="syncPatrolTelegram"
                :loading="syncingPatrol"
                :disabled="!patrolStatus?.telegram_app_configured"
              >
                同步巡检 Telegram
              </v-btn>
              <v-btn size="small" variant="text" prepend-icon="mdi-refresh" @click="loadPatrolStatus" :loading="patrolLoading">
                刷新状态
              </v-btn>
            </div>
            <p v-if="patrolStatus" class="text-caption text-medium-emphasis mt-2 mb-0">
              宿主机安装 cron：<code>{{ patrolStatus.cron_install_command }}</code> · 日志：<code>{{ patrolStatus.health_log_path }}</code>
            </p>
          </v-card-text>
        </v-card>
      </v-col>

      <!-- Security Settings -->
      <v-col cols="12" lg="6">
        <!-- Password Change -->
        <v-card elevation="0" border rounded="lg" class="mb-4">
          <v-card-title class="d-flex align-center">
            <v-icon class="mr-2">mdi-lock</v-icon>
            修改密码
          </v-card-title>
          <v-divider />
          <v-card-text>
            <v-text-field v-model="pwForm.current" label="当前密码" variant="outlined" density="compact" type="password" class="mb-3" :rules="[required('密码')]" />
            <v-text-field v-model="pwForm.new_pw" label="新密码" variant="outlined" density="compact" type="password" class="mb-3" :rules="[required('密码')]" />
            <v-text-field v-model="pwForm.confirm" label="确认新密码" variant="outlined" density="compact" type="password" class="mb-3" :rules="[required('确认密码'), matchOther(() => pwForm.new_pw)]" />
            <v-text-field
              v-if="auth.admin?.totp_enabled"
              v-model="pwForm.totp_code"
              label="TOTP 验证码"
              variant="outlined"
              density="compact"
              type="text"
              inputmode="numeric"
              maxlength="6"
              class="mb-3"
              :rules="auth.admin?.totp_enabled ? [required('验证码')] : []"
            />
          </v-card-text>
          <v-card-actions class="px-4 pb-4">
            <v-spacer />
            <v-btn color="primary" variant="flat" @click="changePassword" :loading="changingPw">修改密码</v-btn>
          </v-card-actions>
        </v-card>

        <!-- TOTP -->
        <v-card elevation="0" border rounded="lg" class="mb-4">
          <v-card-title class="d-flex align-center">
            <v-icon class="mr-2">mdi-shield-key</v-icon>
            双因素认证 (TOTP)
          </v-card-title>
          <v-divider />
          <v-card-text>
            <div class="d-flex align-center mb-3">
              <span class="text-body-2">状态：</span>
              <v-chip :color="auth.admin?.totp_enabled ? 'success' : 'grey'" size="small" variant="tonal" label class="ml-2">
                {{ auth.admin?.totp_enabled ? '已启用' : '未启用' }}
              </v-chip>
            </div>
            <div v-if="totpQr" class="text-center mb-3">
              <img :src="totpQr" alt="TOTP QR" style="max-width: 200px" />
              <div v-if="totpSecret" class="d-flex align-center justify-center ga-2 mt-2">
                <code class="text-caption pa-1 bg-grey-lighten-4 rounded" style="user-select: all">{{ totpSecret }}</code>
                <v-btn size="x-small" variant="text" icon="mdi-content-copy" @click="copyTotpSecret" />
              </div>
              <div class="text-caption text-medium-emphasis mt-1">用认证器 App 扫描二维码或手动输入密钥</div>
            </div>
            <div v-if="!auth.admin?.totp_enabled">
              <v-btn v-if="!totpQr" size="small" variant="tonal" prepend-icon="mdi-qrcode" @click="setupTotp">生成二维码</v-btn>
              <div v-else class="d-flex ga-2 align-center">
                <v-text-field v-model="totpCode" label="验证码" variant="outlined" density="compact" style="max-width: 160px" />
                <v-btn color="primary" variant="flat" size="small" @click="enableTotp" :loading="enablingTotp">启用</v-btn>
              </div>
            </div>
            <v-btn v-else size="small" variant="tonal" color="error" prepend-icon="mdi-shield-off" @click="disableTotp">禁用 TOTP</v-btn>
          </v-card-text>
        </v-card>

        <!-- API Key -->
        <v-card elevation="0" border rounded="lg" class="mb-4">
          <v-card-title class="d-flex align-center">
            <v-icon class="mr-2">mdi-key</v-icon>
            API Key
          </v-card-title>
          <v-divider />
          <v-card-text>
            <div class="d-flex align-center ga-2">
              <v-text-field :model-value="showApiKey ? apiKeyValue : '••••••••'" label="API Key" variant="outlined" density="compact" readonly :type="showApiKey ? 'text' : 'password'" style="max-width: 320px" />
              <v-btn variant="text" size="small" :icon="showApiKey ? 'mdi-eye-off' : 'mdi-eye'" @click="revealApiKey" :loading="revealingApiKey" />
              <v-btn variant="text" size="small" icon="mdi-content-copy" @click="copyApiKey" />
            </div>
          </v-card-text>
        </v-card>

        <!-- IP Allowlist -->
        <v-card elevation="0" border rounded="lg">
          <v-card-title class="d-flex align-center">
            <v-icon class="mr-2">mdi-ip-network</v-icon>
            登录 IP 白名单
          </v-card-title>
          <v-divider />
          <v-card-text>
            <v-switch
              v-model="ipAllowlistEnabled"
              label="启用 IP 白名单（仅白名单 IP 可登录）"
              color="primary"
              density="compact"
              hide-details
              class="mb-4"
              @update:model-value="toggleAllowlist"
            />

            <div class="text-subtitle-2 mb-2">代理订阅</div>
            <p class="text-caption text-medium-emphasis mb-2">
              填写 SS/VMess/VLESS 等订阅链接，保存后每 2 小时自动刷新节点 IP 并合并进白名单。
            </p>
            <v-text-field
              v-model="subscriptionUrl"
              label="订阅 URL"
              variant="outlined"
              density="compact"
              class="mb-2"
              placeholder="https://example.com/sub?token=..."
            />
            <div class="d-flex flex-wrap ga-2 mb-3">
              <v-btn size="small" variant="tonal" prepend-icon="mdi-cloud-download" @click="parseSubscription" :loading="parsingSubscription">解析预览</v-btn>
              <v-btn size="small" variant="tonal" color="primary" prepend-icon="mdi-content-save" @click="saveAllowlist" :loading="savingAllowlist">保存白名单</v-btn>
            </div>

            <div v-if="subscriptionIps.length" class="mb-4">
              <div class="text-caption text-medium-emphasis mb-2">
                订阅节点 {{ subscriptionIps.length }} 个
                <span v-if="allowlistLastRefresh"> · 上次刷新 {{ allowlistLastRefresh }}</span>
              </div>
              <div class="d-flex flex-wrap ga-1">
                <v-chip v-for="ip in subscriptionIpsPreview" :key="ip" size="x-small" variant="tonal" label>{{ ip }}</v-chip>
                <v-chip v-if="subscriptionIps.length > subscriptionIpsPreview.length" size="x-small" variant="text" label>
                  +{{ subscriptionIps.length - subscriptionIpsPreview.length }} 更多
                </v-chip>
              </div>
            </div>
            <div v-else-if="subscriptionUrl" class="text-caption text-medium-emphasis mb-4">
              {{ allowlistLastRefresh ? `上次刷新 ${allowlistLastRefresh}，暂无节点` : '等待首次刷新…保存订阅 URL 后将自动拉取' }}
            </div>

            <v-divider class="my-3" />

            <div class="text-subtitle-2 mb-2">手动 IP</div>
            <v-textarea
              v-model="manualIpList"
              label="手动 IP / CIDR（每行一个）"
              variant="outlined"
              density="compact"
              rows="4"
              class="mb-2"
              placeholder="1.2.3.4&#10;10.0.0.0/24"
            />
            <div class="text-caption text-medium-emphasis">
              手动 IP 与订阅节点合并生效；共 {{ allowlistTotalCount }} 条（去重后）。
            </div>
          </v-card-text>
        </v-card>
      </v-col>
    </v-row>

    <!-- Subscription parse preview -->
    <v-dialog v-model="subParseDialog" max-width="520">
      <v-card border>
        <v-card-title>订阅解析预览</v-card-title>
        <v-card-text>
          <div class="text-body-2 mb-2">共解析出 <strong>{{ subParseHosts.length }}</strong> 个节点 IP / 域名：</div>
          <div class="d-flex flex-wrap ga-1 mb-2" style="max-height: 240px; overflow-y: auto">
            <v-chip v-for="h in subParseHosts" :key="h" size="small" variant="tonal" label>{{ h }}</v-chip>
          </div>
          <div class="text-caption text-medium-emphasis">点击「保存白名单」后才会写入订阅 URL 并触发后台刷新。</div>
        </v-card-text>
        <v-card-actions>
          <v-spacer />
          <v-btn variant="text" @click="subParseDialog = false">关闭</v-btn>
          <v-btn color="primary" variant="flat" @click="subParseDialog = false; saveAllowlist()">保存白名单</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>

    <!-- Telegram Chat ID picker -->
    <v-dialog v-model="tgChatDialog" max-width="480">
      <v-card border>
        <v-card-title>选择 Chat ID</v-card-title>
        <v-card-text>
          <div v-if="!tgChats.length" class="text-body-2 text-medium-emphasis">
            未检测到对话。私聊请向 Bot 发消息；群聊请将 Bot 拉入群后在群内发送 <code>/start@Bot用户名</code>，然后重新检测。
          </div>
          <v-list v-else density="compact" lines="two">
            <v-list-item
              v-for="chat in tgChats"
              :key="chat.id"
              :title="chat.title || chat.username || `Chat ${chat.id}`"
              :subtitle="`${formatTelegramChatType(chat.type)}${chat.username ? ' · @' + chat.username : ''} · ID ${chat.id}`"
              @click="applyTelegramChat(chat)"
            />
          </v-list>
        </v-card-text>
        <v-card-actions>
          <v-spacer />
          <v-btn variant="text" @click="tgChatDialog = false">关闭</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>

    <!-- TOTP Disable Dialog -->
    <v-dialog v-model="showDisableTotp" max-width="400">
      <v-card border>
        <v-card-title>禁用 TOTP</v-card-title>
        <v-card-text>
          <v-text-field v-model="disableTotpPassword" label="当前密码" variant="outlined" density="compact" type="password" class="mb-2" :rules="[required()]" />
          <v-text-field v-model="disableTotpCode" label="验证码" variant="outlined" density="compact" :rules="[required()]" />
        </v-card-text>
        <v-card-actions>
          <v-spacer />
          <v-btn variant="text" @click="showDisableTotp = false">取消</v-btn>
          <v-btn color="error" variant="flat" @click="doDisableTotp" :loading="disablingTotp">禁用</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>
  </v-container>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'
import { usePageActivateRefresh } from '@/composables/usePageActivateRefresh'
import { http } from '@/api'
import { useAuthStore } from '@/stores/auth'
import { useSnackbar } from '@/composables/useSnackbar'
import { required, numberRange, matchOther } from '@/utils/validation'
import TerminalQuickCommandsSettings from '@/components/TerminalQuickCommandsSettings.vue'
import type {
  SettingsResponse,
  AllowlistResponse,
  SettingItem,
  TelegramChatItem,
  TelegramChatsResponse,
  OpsPatrolStatusResponse,
  OpsPatrolSyncResponse,
  SubscriptionParseResponse,
  IpAllowlistSaveRequest,
  IpAllowlistSaveResponse,
} from '@/types/api'
import { NOTIFY_TOGGLE_ITEMS } from '@/types/api'
import {
  SCRIPT_COMPLETE_SOUND_OPTIONS,
  DEFAULT_SCRIPT_COMPLETE_SOUND,
  normalizeScriptCompleteSound,
  type ScriptCompleteSoundId,
} from '@/utils/scriptCompleteSound'
import { useScriptCompleteSound } from '@/composables/useScriptCompleteSound'
import { usePushCompleteSound } from '@/composables/usePushCompleteSound'

defineOptions({ name: 'SettingsPage' })

const snackbar = useSnackbar()
const { saveToServer: saveScriptSoundToServer, previewSound: previewScriptSound } = useScriptCompleteSound()
const { saveToServer: savePushSoundToServer, previewSound: previewPushSound } = usePushCompleteSound()

const auth = useAuthStore()

const scriptCompleteSoundOptions = SCRIPT_COMPLETE_SOUND_OPTIONS
const scriptExecCompleteSound = ref<ScriptCompleteSoundId>(DEFAULT_SCRIPT_COMPLETE_SOUND)
const scriptSoundSaving = ref(false)
const pushCompleteSound = ref<ScriptCompleteSoundId>(DEFAULT_SCRIPT_COMPLETE_SOUND)
const pushSoundSaving = ref(false)

// ── Settings ──
const settingsLoading = ref(false)
const settings = ref({
  system_name: '', system_title: '',
  cpu_alert_threshold: 80, mem_alert_threshold: 80, disk_alert_threshold: 80,
  db_pool_size: 10, db_max_overflow: 20,
  telegram_bot_token: '', telegram_chat_id: '',
  telegram_offline_chat_id: '',
})
const telegramTokenDraft = ref('')
const telegramTokenSet = ref(false)
const telegramOfflineTokenDraft = ref('')
const telegramOfflineTokenSet = ref(false)
const showOfflineToken = ref(false)
const savingOfflineTelegram = ref(false)
const testingOfflineTg = ref(false)
const detectingOfflineTgChats = ref(false)
const tgChatTarget = ref<'default' | 'offline'>('default')
const saving = ref(false)
const systemSettingsFormRef = ref<{ validate: () => Promise<{ valid: boolean }> } | null>(null)
const testingTg = ref(false)
const detectingTgChats = ref(false)
const tgChatDialog = ref(false)
const tgChats = ref<TelegramChatItem[]>([])
const showToken = ref(false)
const savingTelegram = ref(false)
const notifyToggles = ref<Record<string, boolean>>({})
const notifyToggleSaving = ref<string | null>(null)
const patrolStatus = ref<OpsPatrolStatusResponse | null>(null)
const patrolLoading = ref(false)
const syncingPatrol = ref(false)

const notifyToggleGroups = computed(() => {
  const groups: Record<string, typeof NOTIFY_TOGGLE_ITEMS> = {}
  for (const item of NOTIFY_TOGGLE_ITEMS) {
    if (!groups[item.group]) groups[item.group] = []
    groups[item.group].push(item)
  }
  return groups
})

function formatTelegramChatType(type: string): string {
  const labels: Record<string, string> = {
    private: '私聊',
    group: '群组',
    supergroup: '群组',
    channel: '频道',
  }
  return labels[type] || type
}

function parseNotifyEnabled(val: unknown): boolean {
  const s = String(val ?? 'true').trim().toLowerCase()
  return !['false', '0', 'no', 'off'].includes(s)
}

// ── Password ──
const pwForm = ref({ current: '', new_pw: '', confirm: '', totp_code: '' })
const changingPw = ref(false)

// ── TOTP ──
const totpQr = ref('')
const totpSecret = ref('')
const totpCode = ref('')
const enablingTotp = ref(false)
const showDisableTotp = ref(false)
const disableTotpPassword = ref('')
const disableTotpCode = ref('')
const disablingTotp = ref(false)

// ── API Key ──
const showApiKey = ref(false)
const apiKeyValue = ref('')
const revealingApiKey = ref(false)
const revealingTelegramToken = ref(false)

// ── IP Allowlist ──
const ipAllowlistEnabled = ref(false)
const subscriptionUrl = ref('')
const manualIpList = ref('')
const subscriptionIps = ref<string[]>([])
const allowlistLastRefresh = ref<string | null>(null)
const allowlistTotalCount = ref(0)
const savingAllowlist = ref(false)
const parsingSubscription = ref(false)
const subParseDialog = ref(false)
const subParseHosts = ref<string[]>([])

const subscriptionIpsPreview = computed(() => subscriptionIps.value.slice(0, 24))

// ── Data loading ──
async function loadSettings(silent = false) {
  if (!silent) settingsLoading.value = true
  try {
    const res = await http.get<SettingsResponse>('/settings/')
    const items: SettingItem[] = Array.isArray(res) ? res : []
    const knownKeys = Object.keys(settings.value)
    for (const item of items) {
      const key = item.key
      if (key === 'telegram_bot_token') {
        telegramTokenSet.value = Boolean(item.value_set)
        telegramTokenDraft.value = ''
        continue
      }
      if (key === 'telegram_offline_bot_token') {
        telegramOfflineTokenSet.value = Boolean(item.value_set)
        telegramOfflineTokenDraft.value = ''
        continue
      }
      if (key === 'script_exec_complete_sound') {
        scriptExecCompleteSound.value = normalizeScriptCompleteSound(item.value)
        continue
      }
      if (key === 'push_complete_sound') {
        pushCompleteSound.value = normalizeScriptCompleteSound(item.value)
        continue
      }
      if (key && NOTIFY_TOGGLE_ITEMS.some(n => n.key === key)) {
        notifyToggles.value[key] = parseNotifyEnabled(item.value)
        continue
      }
      if (key && knownKeys.includes(key)) {
        const val = item.value
        // Type-cast numeric strings back to numbers for number inputs
        if (typeof val === 'string' && /^\d+(\.\d+)?$/.test(val)) {
          (settings.value as Record<string, unknown>)[key] = Number(val)
        } else {
          (settings.value as Record<string, unknown>)[key] = val
        }
      }
    }
  } catch {
    snackbar('加载设置失败', 'error')
  } finally {
    if (!silent) settingsLoading.value = false
  }
}

async function loadAllowlist() {
  try {
    const res = await http.get<AllowlistResponse>('/settings/ip-allowlist')
    ipAllowlistEnabled.value = res.enabled || false
    subscriptionUrl.value = res.subscription_url || ''
    manualIpList.value = (res.manual_ips || []).join('\n')
    subscriptionIps.value = res.subscription_ips || []
    allowlistLastRefresh.value = res.last_refresh || null
    allowlistTotalCount.value = res.total_count ?? (
      new Set([...(res.manual_ips || []), ...(res.subscription_ips || [])]).size
    )
  } catch {
    snackbar('加载白名单失败', 'error')
  }
}

// ── Actions ──
async function saveSettings() {
  const form = systemSettingsFormRef.value
  if (form) {
    const { valid } = await form.validate()
    if (!valid) return
  }
  saving.value = true
  try {
    const entries = Object.entries(settings.value).filter(([key]) =>
      ['system_name', 'system_title', 'cpu_alert_threshold', 'mem_alert_threshold', 'disk_alert_threshold', 'db_pool_size', 'db_max_overflow'].includes(key)
    )
    for (const [key, value] of entries) {
      await http.put(`/settings/${key}`, { value: String(value ?? '') })
    }
    snackbar('设置已保存')
  } catch (e: unknown) {
    const msg = e instanceof Error ? e.message : '保存失败'
    snackbar(msg, 'error')
  } finally { saving.value = false }
}

async function persistTelegramConfig(options?: { requireToken?: boolean; requireChatId?: boolean }): Promise<boolean> {
  const tokenUpdate = telegramTokenDraft.value.trim()
  const chatId = String(settings.value.telegram_chat_id || '').trim()

  if (options?.requireToken && !tokenUpdate && !telegramTokenSet.value) {
    snackbar('请先填写 Bot Token', 'error')
    return false
  }
  if (options?.requireChatId && !chatId) {
    snackbar('请先填写或检测 Chat ID', 'error')
    return false
  }

  if (tokenUpdate) {
    await http.put('/settings/telegram_bot_token', { value: tokenUpdate })
    telegramTokenSet.value = true
    telegramTokenDraft.value = ''
    showToken.value = false
  }
  if (chatId) {
    await http.put('/settings/telegram_chat_id', { value: chatId })
  }
  return true
}

async function loadPatrolStatus(silent = false) {
  if (!silent) patrolLoading.value = true
  try {
    patrolStatus.value = await http.get<OpsPatrolStatusResponse>('/settings/ops-patrol/status')
  } catch {
    patrolStatus.value = null
  } finally {
    if (!silent) patrolLoading.value = false
  }
}

async function syncPatrolTelegram() {
  syncingPatrol.value = true
  try {
    if (!await persistTelegramConfig({ requireToken: true, requireChatId: true })) return
    const res = await http.post<OpsPatrolSyncResponse>('/settings/ops-patrol/sync-telegram', {})
    patrolStatus.value = {
      ...(patrolStatus.value || {} as OpsPatrolStatusResponse),
      telegram_layer3_ready: res.telegram_layer3_ready,
      telegram_env_synced: res.env_status.env_synced,
      env_status: res.env_status,
      telegram_app_configured: true,
    }
    snackbar('巡检 Telegram 已同步至 .env')
    await loadPatrolStatus()
  } catch (e: unknown) {
    snackbar(e instanceof Error ? e.message : '同步失败', 'error')
  } finally {
    syncingPatrol.value = false
  }
}

async function saveTelegramConfig() {
  savingTelegram.value = true
  try {
    const tokenUpdate = telegramTokenDraft.value.trim()
    const chatId = String(settings.value.telegram_chat_id || '').trim()
    if (!tokenUpdate && !chatId && !telegramTokenSet.value) {
      snackbar('请先填写 Bot Token 或 Chat ID', 'warning')
      return
    }
    await persistTelegramConfig()
    await loadPatrolStatus()
    snackbar('Telegram 配置已保存')
  } catch (e: unknown) {
    snackbar(e instanceof Error ? e.message : '保存失败', 'error')
  } finally {
    savingTelegram.value = false
  }
}

async function persistOfflineTelegramConfig(options?: { requireToken?: boolean; requireChatId?: boolean }): Promise<boolean> {
  const tokenUpdate = telegramOfflineTokenDraft.value.trim()
  const chatId = String(settings.value.telegram_offline_chat_id || '').trim()

  if (options?.requireToken && !tokenUpdate && !telegramOfflineTokenSet.value) {
    snackbar('请先填写离线告警 Bot Token', 'error')
    return false
  }
  if (options?.requireChatId && !chatId) {
    snackbar('请先填写或检测离线告警 Chat ID', 'error')
    return false
  }

  if (tokenUpdate) {
    await http.put('/settings/telegram_offline_bot_token', { value: tokenUpdate })
    telegramOfflineTokenSet.value = true
    telegramOfflineTokenDraft.value = ''
    showOfflineToken.value = false
  }
  if (chatId) {
    await http.put('/settings/telegram_offline_chat_id', { value: chatId })
  }
  return true
}

async function saveOfflineTelegramConfig() {
  savingOfflineTelegram.value = true
  try {
    const tokenUpdate = telegramOfflineTokenDraft.value.trim()
    const chatId = String(settings.value.telegram_offline_chat_id || '').trim()
    if (!tokenUpdate && !chatId && !telegramOfflineTokenSet.value) {
      snackbar('请先填写离线 Bot Token 或 Chat ID', 'warning')
      return
    }
    await persistOfflineTelegramConfig()
    snackbar('离线告警 Bot 已保存')
  } catch (e: unknown) {
    snackbar(e instanceof Error ? e.message : '保存失败', 'error')
  } finally {
    savingOfflineTelegram.value = false
  }
}

async function testOfflineTelegram() {
  testingOfflineTg.value = true
  try {
    if (!await persistOfflineTelegramConfig({ requireToken: true, requireChatId: true })) return
    await http.post('/settings/telegram/offline/test')
    snackbar('离线通道测试消息已发送')
  } catch (e: unknown) {
    snackbar(e instanceof Error ? e.message : '发送失败', 'error')
  } finally {
    testingOfflineTg.value = false
  }
}

async function detectOfflineTelegramChats() {
  detectingOfflineTgChats.value = true
  try {
    if (!await persistOfflineTelegramConfig({ requireToken: true })) return
    const res = await http.get<TelegramChatsResponse>('/settings/telegram/offline/chats')
    tgChats.value = res.chats || []
    if (!tgChats.value.length) {
      snackbar('未检测到对话：私聊发消息，或群聊拉入 Bot 后发送 /start@Bot用户名', 'warning')
      return
    }
    tgChatTarget.value = 'offline'
    tgChatDialog.value = true
  } catch (e: unknown) {
    snackbar(e instanceof Error ? e.message : '检测失败', 'error')
  } finally {
    detectingOfflineTgChats.value = false
  }
}

async function toggleOfflineTelegramTokenVisibility() {
  if (showOfflineToken.value) {
    showOfflineToken.value = false
    if (telegramOfflineTokenSet.value) {
      telegramOfflineTokenDraft.value = ''
    }
    return
  }
  if (telegramOfflineTokenDraft.value.trim()) {
    showOfflineToken.value = true
    return
  }
  if (!telegramOfflineTokenSet.value) {
    showOfflineToken.value = true
    return
  }
  revealingTelegramToken.value = true
  try {
    const res = await http.post<{ value: string }>('/settings/telegram/offline/reveal-token', {})
    telegramOfflineTokenDraft.value = res.value
    showOfflineToken.value = true
  } catch (e: unknown) {
    snackbar(e instanceof Error ? e.message : '获取离线 Bot Token 失败', 'error')
  } finally {
    revealingTelegramToken.value = false
  }
}

async function onScriptExecSoundChange(value: ScriptCompleteSoundId | null) {
  if (!value || scriptSoundSaving.value) return
  const prev = scriptExecCompleteSound.value
  scriptExecCompleteSound.value = value
  scriptSoundSaving.value = true
  try {
    await saveScriptSoundToServer(value)
    snackbar('提示音设置已保存')
  } catch (e: unknown) {
    scriptExecCompleteSound.value = prev
    snackbar(e instanceof Error ? e.message : '保存失败', 'error')
  } finally {
    scriptSoundSaving.value = false
  }
}

function previewScriptExecSound() {
  previewScriptSound(scriptExecCompleteSound.value)
}

async function onPushCompleteSoundChange(value: ScriptCompleteSoundId | null) {
  if (!value || pushSoundSaving.value) return
  const prev = pushCompleteSound.value
  pushCompleteSound.value = value
  pushSoundSaving.value = true
  try {
    await savePushSoundToServer(value)
    snackbar('推送提示音设置已保存')
  } catch (e: unknown) {
    pushCompleteSound.value = prev
    snackbar(e instanceof Error ? e.message : '保存失败', 'error')
  } finally {
    pushSoundSaving.value = false
  }
}

function previewPushCompleteSound() {
  previewPushSound(pushCompleteSound.value)
}

async function toggleNotify(key: string, enabled: boolean) {
  const prev = notifyToggles.value[key]
  notifyToggles.value[key] = enabled
  notifyToggleSaving.value = key
  try {
    await http.put(`/settings/${key}`, { value: enabled ? 'true' : 'false' })
  } catch (e: unknown) {
    notifyToggles.value[key] = prev
    snackbar(e instanceof Error ? e.message : '保存失败，已撤销', 'error')
  } finally {
    notifyToggleSaving.value = null
  }
}

async function testTelegram() {
  testingTg.value = true
  try {
    if (!await persistTelegramConfig({ requireToken: true, requireChatId: true })) return
    await http.post('/settings/telegram/test')
    snackbar('测试消息已发送')
  } catch (e: unknown) {
    snackbar(e instanceof Error ? e.message : '发送失败', 'error')
  } finally { testingTg.value = false }
}

async function detectTelegramChats() {
  detectingTgChats.value = true
  try {
    if (!await persistTelegramConfig({ requireToken: true })) return
    const res = await http.get<TelegramChatsResponse>('/settings/telegram/chats')
    tgChats.value = res.chats || []
    if (!tgChats.value.length) {
      snackbar('未检测到对话：私聊发消息，或群聊拉入 Bot 后发送 /start@Bot用户名', 'warning')
      return
    }
    tgChatTarget.value = 'default'
    tgChatDialog.value = true
  } catch (e: unknown) {
    snackbar(e instanceof Error ? e.message : '检测失败', 'error')
  } finally {
    detectingTgChats.value = false
  }
}

async function applyTelegramChat(chat: TelegramChatItem) {
  const chatId = String(chat.id)
  const isOffline = tgChatTarget.value === 'offline'
  if (isOffline) {
    settings.value.telegram_offline_chat_id = chatId
  } else {
    settings.value.telegram_chat_id = chatId
  }
  tgChatDialog.value = false
  try {
    const key = isOffline ? 'telegram_offline_chat_id' : 'telegram_chat_id'
    await http.put(`/settings/${key}`, { value: chatId })
    snackbar(`已选择 Chat ID: ${chatId}`)
  } catch (e: unknown) {
    snackbar(e instanceof Error ? e.message : '保存 Chat ID 失败', 'error')
  }
}

async function changePassword() {
  if (pwForm.value.new_pw !== pwForm.value.confirm) {
    snackbar('两次密码不一致', 'error')
    return
  }
  changingPw.value = true
  try {
    const body: Record<string, string> = {
      current_password: pwForm.value.current,
      new_password: pwForm.value.new_pw,
    }
    if (auth.admin?.totp_enabled && pwForm.value.totp_code) {
      body.totp_code = pwForm.value.totp_code
    }
    await http.put('/auth/password', body)
    pwForm.value = { current: '', new_pw: '', confirm: '', totp_code: '' }
    snackbar('密码已修改')
  } catch (e: unknown) {
    const msg = e instanceof Error ? e.message : '修改失败'
    snackbar(msg, 'error')
  } finally { changingPw.value = false }
}

async function setupTotp() {
  try {
    const res = await http.post<{ qr_code: string; secret?: string }>('/auth/totp/setup')
    totpQr.value = res.qr_code
    totpSecret.value = res.secret || ''
  } catch (e: unknown) {
    const msg = e instanceof Error ? e.message : '生成失败'
    snackbar(msg, 'error')
  }
}

async function enableTotp() {
  enablingTotp.value = true
  try {
    await http.post('/auth/totp/enable', { code: totpCode.value })
    await auth.fetchProfile()
    totpQr.value = ''
    totpCode.value = ''
    snackbar('TOTP 已启用')
  } catch (e: any) { snackbar(e.message || '启用失败', 'error') }
  finally { enablingTotp.value = false }
}

function disableTotp() {
  showDisableTotp.value = true
}

async function doDisableTotp() {
  disablingTotp.value = true
  try {
    await http.post('/auth/totp/disable', {
      admin_id: auth.admin?.id,
      current_password: disableTotpPassword.value,
      totp_code: disableTotpCode.value,
    })
    await auth.fetchProfile()
    showDisableTotp.value = false
    disableTotpPassword.value = ''
    disableTotpCode.value = ''
    snackbar('TOTP 已禁用')
  } catch (e: any) { snackbar(e.message || '禁用失败', 'error') }
  finally { disablingTotp.value = false }
}

function copyTotpSecret() {
  if (totpSecret.value) {
    navigator.clipboard.writeText(totpSecret.value)
    snackbar('密钥已复制')
  }
}

async function revealApiKey() {
  if (showApiKey.value) {
    showApiKey.value = false
    return
  }
  revealingApiKey.value = true
  try {
    const res = await http.post<{ value: string }>('/settings/api-key/reveal', {})
    apiKeyValue.value = res.value
    showApiKey.value = true
  } catch (e: unknown) {
    snackbar(e instanceof Error ? e.message : '获取 API Key 失败', 'error')
  } finally {
    revealingApiKey.value = false
  }
}

async function toggleTelegramTokenVisibility() {
  if (showToken.value) {
    showToken.value = false
    if (telegramTokenSet.value) {
      telegramTokenDraft.value = ''
    }
    return
  }
  if (telegramTokenDraft.value.trim()) {
    showToken.value = true
    return
  }
  if (!telegramTokenSet.value) {
    showToken.value = true
    return
  }
  revealingTelegramToken.value = true
  try {
    const res = await http.post<{ value: string }>('/settings/telegram/reveal-token', {})
    telegramTokenDraft.value = res.value
    showToken.value = true
  } catch (e: unknown) {
    snackbar(e instanceof Error ? e.message : '获取 Bot Token 失败', 'error')
  } finally {
    revealingTelegramToken.value = false
  }
}

function copyApiKey() {
  if (!showApiKey.value || !apiKeyValue.value) {
    snackbar('请先点击眼睛图标显示 API Key', 'warning')
    return
  }
  navigator.clipboard.writeText(apiKeyValue.value)
  snackbar('已复制到剪贴板')
}

async function toggleAllowlist() {
  try {
    await http.post(`/settings/ip-allowlist/toggle?enabled=${ipAllowlistEnabled.value ? 'true' : 'false'}`)
  } catch (e: unknown) {
    snackbar(e instanceof Error ? e.message : '操作失败', 'error')
  }
}

async function parseSubscription() {
  const url = subscriptionUrl.value.trim()
  if (!url) {
    snackbar('请先填写订阅 URL', 'error')
    return
  }
  parsingSubscription.value = true
  try {
    const res = await http.post<SubscriptionParseResponse>('/settings/ip-allowlist/parse-subscription', { url })
    subParseHosts.value = res.hosts || []
    if (!subParseHosts.value.length) {
      snackbar('未能解析出任何节点', 'warning')
      return
    }
    subParseDialog.value = true
  } catch (e: unknown) {
    snackbar(e instanceof Error ? e.message : '解析失败', 'error')
  } finally {
    parsingSubscription.value = false
  }
}

async function saveAllowlist() {
  savingAllowlist.value = true
  try {
    const manual_ips = manualIpList.value.split('\n').map(s => s.trim()).filter(Boolean)
    const payload: IpAllowlistSaveRequest = {
      manual_ips,
      subscription_url: subscriptionUrl.value.trim(),
      enabled: ipAllowlistEnabled.value,
    }
    const res = await http.post<IpAllowlistSaveResponse>('/settings/ip-allowlist', payload)
    subscriptionIps.value = res.subscription_ips || []
    allowlistLastRefresh.value = res.last_refresh || null
    allowlistTotalCount.value = new Set([
      ...(res.subscription_ips || []),
      ...manual_ips,
    ]).size
    if (res.refresh_ok === false && res.refresh_error) {
      snackbar(`白名单已保存，但订阅刷新失败：${res.refresh_error}`, 'warning')
    } else {
      snackbar('白名单已保存')
    }
    subParseDialog.value = false
  } catch (e: unknown) {
    snackbar(e instanceof Error ? e.message : '保存失败', 'error')
  } finally {
    savingAllowlist.value = false
  }
}


async function refreshSettingsPage(silent = false) {
  await Promise.all([
    loadSettings(silent),
    loadAllowlist(),
    loadPatrolStatus(silent),
    auth.fetchProfile(),
  ])
}

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