<template>
  <div class="kra-table-box integration-page">
    <header class="page-heading">
      <div>
        <h2>通信集成</h2>
        <p>消息队列与实时连接</p>
      </div>
      <el-button :icon="Refresh" :loading="loading" @click="load">
        刷新
      </el-button>
    </header>

    <div v-loading="loading" class="integration-layout">
      <aside class="provider-panel" aria-label="通信集成列表">
        <div class="panel-heading">
          <span>服务</span>
          <span>{{ integrations.length }}</span>
        </div>
        <button
          v-for="item in integrations"
          :key="integrationKey(item)"
          type="button"
          class="provider-item"
          :class="{ active: integrationKey(item) === selectedKey }"
          @click="selectedKey = integrationKey(item)"
        >
          <span class="provider-icon" aria-hidden="true">
            <el-icon><component :is="providerMeta(item).icon" /></el-icon>
          </span>
          <span class="provider-copy">
            <strong>{{ item.name || providerMeta(item).name }}</strong>
            <small>{{ providerMeta(item).protocol }}</small>
          </span>
          <span class="provider-state" :class="{ enabled: item.enabled }">
            {{ item.enabled ? '已启用' : '已停用' }}
          </span>
        </button>
      </aside>

      <section v-if="selected" class="editor-panel">
        <header class="editor-heading">
          <div class="editor-title-group">
            <div class="editor-title-row">
              <h3>{{ selected.name || providerMeta(selected).name }}</h3>
              <el-tag v-if="isDirty(selected)" type="warning" effect="plain">
                未保存
              </el-tag>
            </div>
            <p>{{ selected.description || providerMeta(selected).description }}</p>
          </div>
          <div class="enable-control">
            <span>{{ selected.enabled ? '已启用' : '已停用' }}</span>
            <el-switch
              :model-value="selected.enabled"
              :loading="isToggling(selected)"
              :disabled="isBusy(selected)"
              aria-label="启用服务"
              @change="(value) => toggleIntegration(selected, value)"
            />
          </div>
        </header>

        <el-alert
          v-if="hasMaskedSecret(selected)"
          type="info"
          :closable="false"
          show-icon
          class="secret-alert"
          title="密码已脱敏；保留 ****** 将继续使用当前密码。"
        />

        <el-form
          :model="selected.config"
          label-position="top"
          class="config-form"
          @submit.prevent
        >
          <div class="field-grid">
            <el-form-item
              v-for="field in selected.fields || []"
              :key="field.key"
              :required="field.required"
              :error="fieldError(selected, field.key)"
            >
              <template #label>
                <span class="field-label">
                  <span>{{ field.label }}</span>
                  <small>{{ field.key }}</small>
                </span>
              </template>

              <el-select
                v-if="field.type === 'select'"
                v-model="selected.config[field.key]"
                class="field-control"
                :placeholder="field.placeholder || '请选择'"
                @update:model-value="clearFieldError(selected, field.key)"
              >
                <el-option
                  v-for="option in field.options || []"
                  :key="String(option.value)"
                  :label="option.label"
                  :value="option.value"
                />
              </el-select>

              <el-switch
                v-else-if="field.type === 'switch'"
                v-model="selected.config[field.key]"
                @update:model-value="clearFieldError(selected, field.key)"
              />

              <el-input-number
                v-else-if="field.type === 'number'"
                v-model="selected.config[field.key]"
                class="field-control"
                :min="numberConstraint(field.key).min"
                :max="numberConstraint(field.key).max"
                :precision="numberConstraint(field.key).integer ? 0 : undefined"
                :step="1"
                controls-position="right"
                @update:model-value="clearFieldError(selected, field.key)"
              />

              <el-input
                v-else-if="field.type === 'string-list'"
                :model-value="stringListDraft(selected, field.key)"
                class="field-control"
                type="textarea"
                :rows="4"
                :placeholder="field.placeholder || '每行一个值'"
                spellcheck="false"
                @input="(value) => updateStringList(selected, field.key, value)"
              />

              <el-input
                v-else-if="field.type === 'textarea'"
                v-model="selected.config[field.key]"
                class="field-control"
                type="textarea"
                :rows="4"
                :placeholder="field.placeholder"
                :show-password="field.secret"
                spellcheck="false"
                @update:model-value="clearFieldError(selected, field.key)"
              />

              <el-input
                v-else
                v-model="selected.config[field.key]"
                class="field-control"
                :type="field.secret ? 'password' : 'text'"
                :show-password="field.secret"
                :placeholder="field.placeholder"
                spellcheck="false"
                @update:model-value="clearFieldError(selected, field.key)"
              />

              <p v-if="field.description" class="field-description">
                {{ field.description }}
              </p>
            </el-form-item>
          </div>
        </el-form>

        <footer class="editor-actions">
          <span class="save-state">
            {{ selected.configured ? '配置已创建' : '尚未保存配置' }}
          </span>
          <el-button
            :icon="Connection"
            :loading="isTesting(selected)"
            :disabled="isBusy(selected)"
            @click="testSelected"
          >
            测试连接
          </el-button>
          <el-button
            type="primary"
            :icon="Check"
            :loading="isSaving(selected)"
            :disabled="isBusy(selected) || !isDirty(selected)"
            @click="saveSelected"
          >
            保存配置
          </el-button>
        </footer>
      </section>

      <el-empty v-else description="暂无通信集成配置" />
    </div>
  </div>
</template>

<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import {
  ChatLineRound,
  Check,
  Connection,
  Promotion,
  Refresh
} from '@element-plus/icons-vue'
import {
  getIntegrationConfigs,
  saveIntegrationConfig,
  testIntegrationConfig
} from '@/api/integration'

defineOptions({ name: 'IntegrationConfig' })

const TARGETS = {
  'mq/emqx': {
    name: 'EMQX',
    protocol: 'MQTT',
    description: 'EMQX MQTT 消息服务',
    icon: Connection
  },
  'mq/rabbitmq': {
    name: 'RabbitMQ',
    protocol: 'AMQP',
    description: 'RabbitMQ 消息队列',
    icon: Promotion
  },
  'websocket/melody': {
    name: 'WebSocket',
    protocol: 'WS',
    description: 'WebSocket 实时连接服务',
    icon: ChatLineRound
  }
}

const TARGET_ORDER = Object.keys(TARGETS)
const RECONNECT_INTERVAL_KEY = 'reconnect_interval'
const RECONNECT_DEFAULT_SECONDS = 5
const MQ_RECONNECT_TARGETS = new Set(['mq/emqx', 'mq/rabbitmq'])
const RECONNECT_INTERVAL_FIELD = {
  key: RECONNECT_INTERVAL_KEY,
  label: '重连间隔（秒）',
  type: 'number',
  required: true,
  description: '连接中断后再次尝试连接的等待时间，最小 1 秒。'
}
const NUMBER_CONSTRAINTS = {
  port: { min: 1, max: 65535 },
  keep_alive: { min: 1 },
  connect_timeout: { min: 1 },
  reconnect_interval: { min: 1, integer: true },
  prefetch_count: { min: 0 },
  heartbeat: { min: 0 },
  max_message_size: { min: 0 },
  message_buffer_size: { min: 0 }
}
const DURATION_FIELDS = new Set(['write_wait', 'pong_wait', 'ping_period'])
const DURATION_PATTERN = /^(?:\d+(?:\.\d+)?(?:ns|us|µs|ms|s|m|h))+$/i

const integrations = ref([])
const selectedKey = ref(TARGET_ORDER[0])
const loading = ref(false)
const pending = reactive({})
const errors = reactive({})
const listDrafts = reactive({})

const selected = computed(
  () =>
    integrations.value.find(
      (item) => integrationKey(item) === selectedKey.value
    ) || integrations.value[0]
)

const integrationKey = (item) => `${item.kind}/${item.provider}`
const providerMeta = (item) => TARGETS[integrationKey(item)] || TARGETS[TARGET_ORDER[0]]
const operationKey = (item) => integrationKey(item)
const errorKey = (item, fieldKey) => `${integrationKey(item)}:${fieldKey}`
const listKey = (item, fieldKey) => `${integrationKey(item)}:${fieldKey}`
const cloneConfig = (value) => JSON.parse(JSON.stringify(value || {}))

const communicationFields = (item) => {
  const fields = Array.isArray(item.fields)
    ? item.fields.map((field) =>
        field.key === RECONNECT_INTERVAL_KEY
          ? {
              ...field,
              label: field.label || RECONNECT_INTERVAL_FIELD.label,
              type: 'number',
              required: true,
              description: field.description || RECONNECT_INTERVAL_FIELD.description
            }
          : { ...field }
      )
    : []
  if (
    !MQ_RECONNECT_TARGETS.has(integrationKey(item)) ||
    fields.some((field) => field.key === RECONNECT_INTERVAL_KEY)
  ) {
    return fields
  }

  const connectTimeoutIndex = fields.findIndex(
    (field) => field.key === 'connect_timeout'
  )
  fields.splice(
    connectTimeoutIndex < 0 ? fields.length : connectTimeoutIndex + 1,
    0,
    { ...RECONNECT_INTERVAL_FIELD }
  )
  return fields
}

const normalizeIntegration = (item) => {
  const config = cloneConfig(item.config)
  const fields = communicationFields(item)
  if (
    MQ_RECONNECT_TARGETS.has(integrationKey(item)) &&
    (config[RECONNECT_INTERVAL_KEY] === null ||
      typeof config[RECONNECT_INTERVAL_KEY] === 'undefined' ||
      config[RECONNECT_INTERVAL_KEY] === '')
  ) {
    config[RECONNECT_INTERVAL_KEY] = RECONNECT_DEFAULT_SECONDS
  }

  const normalized = {
    ...item,
    enabled: Boolean(item.enabled),
    configured: Boolean(item.configured),
    config,
    fields
  }
  normalized._savedEnabled = normalized.enabled
  normalized._savedConfig = cloneConfig(normalized.config)
  for (const field of normalized.fields) {
    if (field.type === 'string-list') {
      const value = normalized.config[field.key]
      listDrafts[listKey(normalized, field.key)] = Array.isArray(value)
        ? value.join('\n')
        : ''
      normalized.config[field.key] = Array.isArray(value) ? value : []
    }
  }
  return normalized
}

const load = async () => {
  if (loading.value) return
  loading.value = true
  try {
    const [mq, websocket] = await Promise.all([
      getIntegrationConfigs('mq'),
      getIntegrationConfigs('websocket')
    ])
    if (mq.code !== 0 || websocket.code !== 0) return

    Object.keys(listDrafts).forEach((key) => delete listDrafts[key])
    Object.keys(errors).forEach((key) => delete errors[key])
    const loaded = [...(mq.data || []), ...(websocket.data || [])]
      .filter((item) => TARGETS[integrationKey(item)])
      .map(normalizeIntegration)
      .sort(
        (left, right) =>
          TARGET_ORDER.indexOf(integrationKey(left)) -
          TARGET_ORDER.indexOf(integrationKey(right))
      )
    integrations.value = loaded
    if (!loaded.some((item) => integrationKey(item) === selectedKey.value)) {
      selectedKey.value = integrationKey(loaded[0] || { kind: '', provider: '' })
    }
  } catch {
    // The request layer already presents transport errors.
  } finally {
    loading.value = false
  }
}

const isBusy = (item) => Boolean(pending[operationKey(item)])
const isSaving = (item) => pending[operationKey(item)] === 'save'
const isToggling = (item) => pending[operationKey(item)] === 'toggle'
const isTesting = (item) => pending[operationKey(item)] === 'test'
const numberConstraint = (fieldKey) =>
  NUMBER_CONSTRAINTS[fieldKey] || { min: undefined, max: undefined }

const fieldError = (item, fieldKey) => errors[errorKey(item, fieldKey)] || ''
const clearFieldError = (item, fieldKey) => {
  delete errors[errorKey(item, fieldKey)]
}

const stringListDraft = (item, fieldKey) => listDrafts[listKey(item, fieldKey)] || ''
const updateStringList = (item, fieldKey, value) => {
  listDrafts[listKey(item, fieldKey)] = value
  item.config[fieldKey] = String(value)
    .split('\n')
    .map((entry) => entry.trim())
    .filter(Boolean)
  clearFieldError(item, fieldKey)
}

const isMissing = (value) => {
  if (Array.isArray(value)) return value.length === 0
  if (typeof value === 'string') return value.trim() === ''
  return value === null || typeof value === 'undefined'
}

const validate = (item, enabled = item.enabled) => {
  for (const field of item.fields || []) clearFieldError(item, field.key)
  let firstInvalid = ''

  for (const field of item.fields || []) {
    const value = item.config[field.key]
    let message = ''
    if (enabled && field.required && isMissing(value)) {
      message = `请填写${field.label}`
    } else if (field.type === 'number' && !isMissing(value)) {
      const number = Number(value)
      const constraint = numberConstraint(field.key)
      if (!Number.isFinite(number)) {
        message = `${field.label}必须是数字`
      } else if (constraint.integer && !Number.isInteger(number)) {
        message = `${field.label}必须是整数`
      } else if (constraint.min !== undefined && number < constraint.min) {
        message = `${field.label}不能小于 ${constraint.min}`
      } else if (constraint.max !== undefined && number > constraint.max) {
        message = `${field.label}不能大于 ${constraint.max}`
      }
    } else if (
      DURATION_FIELDS.has(field.key) &&
      !isMissing(value) &&
      !DURATION_PATTERN.test(String(value).trim())
    ) {
      message = `${field.label}格式无效，例如 10s 或 1m30s`
    } else if (field.key === 'path' && value && !String(value).startsWith('/')) {
      message = '访问路径必须以 / 开头'
    }

    if (message) {
      errors[errorKey(item, field.key)] = message
      firstInvalid ||= field.label
    }
  }

  if (firstInvalid) {
    ElMessage.warning(`请检查${firstInvalid}等配置项`)
    return false
  }
  return true
}

const savedSnapshot = (item) =>
  JSON.stringify({ enabled: item._savedEnabled, config: item._savedConfig })
const currentSnapshot = (item) =>
  JSON.stringify({ enabled: item.enabled, config: item.config })
const isDirty = (item) => savedSnapshot(item) !== currentSnapshot(item)

const hasMaskedSecret = (item) =>
  (item.fields || []).some(
    (field) => field.secret && item.config[field.key] === '******'
  )

const markSaved = (item) => {
  item.configured = true
  for (const field of item.fields || []) {
    if (field.secret && !isMissing(item.config[field.key])) {
      item.config[field.key] = '******'
    }
  }
  item._savedEnabled = item.enabled
  item._savedConfig = cloneConfig(item.config)
}

const persist = async (item, operation) => {
  const key = operationKey(item)
  if (pending[key]) return false
  pending[key] = operation
  try {
    const res = await saveIntegrationConfig(item.kind, item.provider, {
      enabled: item.enabled,
      config: item.config
    })
    if (res.code !== 0) {
      ElMessage.error(res.msg || '保存失败')
      return false
    }
    markSaved(item)
    return true
  } catch {
    return false
  } finally {
    delete pending[key]
  }
}

const saveSelected = async () => {
  const item = selected.value
  if (!item || isBusy(item) || !validate(item)) return
  if (await persist(item, 'save')) {
    ElMessage.success(`${item.name || providerMeta(item).name} 配置已保存`)
  }
}

const testSelected = async () => {
  const item = selected.value
  if (!item || isBusy(item) || !validate(item, true)) return
  const key = operationKey(item)
  pending[key] = 'test'
  try {
    const res = await testIntegrationConfig(item.kind, item.provider, {
      enabled: true,
      config: item.config
    })
    if (res.code !== 0) {
      ElMessage.error(res.msg || '连接测试失败')
      return
    }
    ElMessage.success(`${item.name || providerMeta(item).name} 连接测试成功`)
  } catch {
    ElMessage.error('连接测试失败')
  } finally {
    delete pending[key]
  }
}

const toggleIntegration = async (item, enabled) => {
  if (isBusy(item)) return
  const previous = item.enabled
  item.enabled = Boolean(enabled)
  if (item.enabled && !validate(item, true)) {
    item.enabled = previous
    return
  }

  if (await persist(item, 'toggle')) {
    ElMessage.success(
      `${item.name || providerMeta(item).name} 已${item.enabled ? '启用' : '停用'}`
    )
    return
  }
  item.enabled = previous
  ElMessage.warning('状态未改变')
}

onMounted(load)
</script>

<style scoped>
.integration-page {
  min-height: 640px;
}

.page-heading {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 18px;
}

.page-heading h2 {
  margin: 0;
  color: var(--el-text-color-primary);
  font-size: 20px;
  font-weight: 600;
  letter-spacing: 0;
}

.page-heading p {
  margin: 5px 0 0;
  color: var(--el-text-color-secondary);
  font-size: 13px;
}

.integration-layout {
  display: grid;
  grid-template-columns: 248px minmax(0, 1fr);
  min-height: 560px;
  overflow: hidden;
  border: 1px solid var(--el-border-color-lighter);
  background: var(--el-bg-color);
}

.provider-panel {
  padding: 12px 9px;
  border-right: 1px solid var(--el-border-color-lighter);
  background: var(--el-fill-color-blank);
}

.panel-heading {
  display: flex;
  justify-content: space-between;
  padding: 4px 10px 11px;
  color: var(--el-text-color-secondary);
  font-size: 12px;
}

.provider-item {
  display: grid;
  grid-template-columns: 34px minmax(0, 1fr) auto;
  align-items: center;
  gap: 9px;
  width: 100%;
  min-height: 58px;
  padding: 8px 9px;
  border: 0;
  border-left: 3px solid transparent;
  border-radius: 4px;
  background: transparent;
  color: inherit;
  text-align: left;
  cursor: pointer;
}

.provider-item:hover {
  background: var(--el-fill-color-light);
}

.provider-item.active {
  border-left-color: var(--el-color-primary);
  background: var(--el-color-primary-light-9);
}

.provider-icon {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: 6px;
  background: var(--el-fill-color);
  color: var(--el-text-color-regular);
  font-size: 17px;
}

.provider-item.active .provider-icon {
  background: var(--el-color-primary-light-8);
  color: var(--el-color-primary);
}

.provider-copy {
  display: grid;
  min-width: 0;
  gap: 3px;
}

.provider-copy strong {
  overflow: hidden;
  color: var(--el-text-color-primary);
  font-size: 14px;
  font-weight: 500;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.provider-copy small {
  color: var(--el-text-color-secondary);
  font-size: 11px;
}

.provider-state {
  color: var(--el-text-color-placeholder);
  font-size: 11px;
  white-space: nowrap;
}

.provider-state.enabled {
  color: var(--el-color-success);
}

.editor-panel {
  display: flex;
  min-width: 0;
  flex-direction: column;
  padding: 22px 28px 20px;
}

.editor-heading {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 24px;
  padding-bottom: 18px;
  border-bottom: 1px solid var(--el-border-color-lighter);
}

.editor-title-group {
  min-width: 0;
}

.editor-title-row {
  display: flex;
  align-items: center;
  gap: 9px;
}

.editor-title-row h3 {
  margin: 0;
  color: var(--el-text-color-primary);
  font-size: 18px;
  font-weight: 600;
  letter-spacing: 0;
}

.editor-title-group p {
  margin: 6px 0 0;
  color: var(--el-text-color-secondary);
  font-size: 13px;
}

.enable-control {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 32px;
  color: var(--el-text-color-regular);
  font-size: 13px;
  white-space: nowrap;
}

.secret-alert {
  margin-top: 18px;
}

.config-form {
  flex: 1;
  padding-top: 20px;
}

.field-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0 22px;
}

.field-label {
  display: inline-flex;
  align-items: baseline;
  gap: 7px;
  min-width: 0;
}

.field-label small {
  overflow: hidden;
  color: var(--el-text-color-placeholder);
  font-size: 11px;
  font-weight: 400;
  text-overflow: ellipsis;
}

.field-control {
  width: 100%;
}

.field-description {
  width: 100%;
  margin: 5px 0 0;
  color: var(--el-text-color-secondary);
  font-size: 12px;
  line-height: 1.5;
}

.editor-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 14px;
  padding-top: 16px;
  border-top: 1px solid var(--el-border-color-lighter);
}

.save-state {
  margin-right: auto;
  color: var(--el-text-color-secondary);
  font-size: 12px;
}

@media (max-width: 900px) {
  .integration-layout {
    grid-template-columns: 1fr;
  }

  .provider-panel {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 6px;
    border-right: 0;
    border-bottom: 1px solid var(--el-border-color-lighter);
  }

  .panel-heading {
    display: none;
  }

  .provider-item {
    grid-template-columns: 30px minmax(0, 1fr);
    min-height: 54px;
  }

  .provider-icon {
    width: 28px;
    height: 28px;
  }

  .provider-state {
    display: none;
  }

  .editor-panel {
    padding: 20px;
  }

  .field-grid {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 560px) {
  .integration-page {
    min-height: 0;
  }

  .page-heading {
    align-items: flex-start;
  }

  .provider-panel {
    grid-template-columns: 1fr;
  }

  .provider-item {
    grid-template-columns: 30px minmax(0, 1fr) auto;
  }

  .provider-state {
    display: inline;
  }

  .editor-panel {
    padding: 18px 14px;
  }

  .editor-heading {
    align-items: stretch;
    flex-direction: column;
    gap: 14px;
  }

  .enable-control {
    justify-content: space-between;
  }

  .editor-actions {
    align-items: stretch;
    flex-direction: column;
  }

  .save-state {
    margin-right: 0;
  }
}
</style>
