<template>
  <div class="edit-property-panel">
    <el-tabs v-model="tab" class="prop-tabs">
      <el-tab-pane label="对象" name="object">
        <div v-if="selectionCount === 0" class="empty-hint">
          <el-icon class="empty-icon"><InfoFilled /></el-icon>
          <span class="empty-title">未选中对象</span>
          <span class="empty-desc">在画布上点击或框选以编辑属性。</span>
        </div>

        <div v-else-if="loading" class="loading-hint">
          <el-icon class="is-loading"><Loading /></el-icon>
          <span>加载属性…</span>
        </div>

        <div v-else class="object-pane">
          <!-- Header card：ID / 类型 / 名称 / 图层 / 删除按钮 -->
          <section class="prop-card prop-card--header">
            <header class="prop-card-header">
              <span class="prop-card-title">基础属性</span>
              <div class="prop-header-actions">
                <el-tag
                  v-if="primary?.status"
                  :type="statusTagType"
                  size="small"
                  effect="dark"
                  class="prop-status-tag"
                >
                  {{ primary.status }}
                </el-tag>
                <el-tooltip
                  :content="selectionCount > 1 ? `删除选中的 ${selectionCount} 个对象（可 Ctrl+Z 撤销）` : '删除该对象（可 Ctrl+Z 撤销）'"
                  placement="top"
                  :show-after="220"
                >
                  <button
                    type="button"
                    class="prop-delete-btn"
                    :disabled="deleting"
                    @click="onDelete"
                  >
                    <span class="prop-delete-icon">{{ deleting ? '⟳' : '×' }}</span>
                    <span>删除</span>
                  </button>
                </el-tooltip>
              </div>
            </header>

            <div class="prop-card-row prop-id-row">
              <div class="prop-id-block">
                <span class="prop-id-tag">{{ selectionCount > 1 ? `多选 ${selectionCount}` : `#${primary?.id ?? '-'}` }}</span>
                <span class="prop-type-tag">{{ primary?.typeName ?? '-' }}</span>
              </div>
            </div>

            <div class="prop-card-row prop-name-row">
              <label class="prop-name-label">名称</label>
              <el-input
                v-if="primary"
                :model-value="editingName"
                size="small"
                placeholder="对象名"
                @update:model-value="(v: string) => editingName = v"
                @blur="onPrimaryNameBlur"
                @change="onPrimaryNameSubmit"
              />
            </div>

            <div class="prop-card-row prop-layer-row">
              <label class="prop-name-label">图层</label>
              <el-select
                v-if="primary"
                :model-value="primary.layer ?? ''"
                size="small"
                placeholder="—"
                class="prop-layer-select"
                @update:model-value="(v: any) => onPrimaryLayerChange(v)"
              >
                <el-option label="（无）" value="" />
                <el-option v-for="l in layers" :key="l" :label="l" :value="l" />
              </el-select>
            </div>

            <div v-if="directionField" class="prop-card-row prop-direction-row">
              <label class="prop-name-label">方向</label>
              <el-input
                :model-value="directionField.value"
                size="small"
                spellcheck="false"
                @change="(v: string) => onFieldChange('direction', v)"
              />
            </div>
          </section>

          <!-- 几何 / 位置：x, y, z, size, angle, rotation, scale, radius, siteA, siteB, ... -->
          <section v-if="positionFields.length > 0" class="prop-card prop-card--geom">
            <header class="prop-card-header">
              <span class="prop-card-title">位置 / 几何</span>
              <span class="prop-card-flag" :class="{ 'is-synced': syncFlash }" :title="syncFlash ? '已同步到画布' : '改动会立即同步到 SimpleLite 画布'">
                <span class="prop-card-flag-dot" />
                <span class="prop-card-flag-text">{{ syncFlash ? '已同步' : '同步到画布' }}</span>
              </span>
              <span class="prop-card-meta">{{ positionFields.length }} 项</span>
            </header>
            <div class="prop-grid">
              <div v-for="f in positionFields" :key="f.key" class="prop-grid-item">
                <label class="prop-field-label">
                  {{ fieldDisplayLabel(f.key) }}
                  <span v-if="fieldUnit(f.key)" class="prop-field-unit">{{ fieldUnit(f.key) }}</span>
                </label>
                <el-input-number
                  v-if="isNumberField(f.key)"
                  :model-value="numericValue(f.value)"
                  :controls="false"
                  :step="numericStep(f.key)"
                  size="small"
                  class="prop-field-num"
                  @change="(v: number | undefined) => onFieldChange(f.key, v == null ? '' : String(v))"
                />
                <el-input
                  v-else
                  :model-value="f.value"
                  size="small"
                  spellcheck="false"
                  @change="(v: string) => onFieldChange(f.key, v)"
                />
              </div>
            </div>
          </section>

          <!-- 扩展字段：上行 key - 中文，下行整行输入框（站点 / 路段 / 小车共用样式） -->
          <section
            v-if="selectionKind === 'site' || selectionKind === 'track' || selectionKind === 'car'"
            class="prop-card prop-card--custom"
          >
            <header class="prop-card-header">
              <span class="prop-card-title">扩展字段</span>
              <span class="prop-card-meta">{{ customFieldRows.length }} 项</span>
            </header>

            <div v-if="fieldCatalogLoading" class="prop-empty-row">加载字段定义…</div>
            <template v-else>
              <div v-if="customFieldRows.length > 0" class="custom-field-list">
                <div v-for="row in customFieldRows" :key="row.key" class="custom-field-item">
                  <label class="prop-field-label custom-field-key" :title="row.label">{{ row.label }}</label>
                  <div class="prop-field-row">
                    <el-input
                      v-model="customFieldDrafts[row.key]"
                      size="small"
                      spellcheck="false"
                      class="custom-field-input"
                      @focus="customFieldFocusKey = row.key"
                      @blur="customFieldFocusKey = null"
                      @change="(v: string) => onCustomFieldCommit(row.key, v)"
                    />
                    <button
                      type="button"
                      class="prop-field-icon-btn"
                      title="删除该字段"
                      @click="onDeleteCustomField(row.key)"
                    >×</button>
                  </div>
                </div>
              </div>
              <div v-else class="prop-empty-row">暂无扩展字段</div>

              <button
                class="prop-add-field-btn"
                :disabled="!addableFieldOptions.length"
                @click="openAddFieldDialog"
              >
                ＋ 添加字段
              </button>
            </template>
          </section>

          <el-dialog
            v-model="addFieldDialogOpen"
            title="添加扩展字段"
            width="420px"
            append-to-body
            destroy-on-close
          >
            <el-select
              v-model="addFieldPickKey"
              filterable
              placeholder="选择要添加的字段"
              class="add-field-select"
            >
              <el-option
                v-for="opt in addableFieldOptions"
                :key="opt.key"
                :label="opt.label"
                :value="opt.key"
              />
            </el-select>
            <template #footer>
              <el-button @click="addFieldDialogOpen = false">取消</el-button>
              <el-button type="primary" :disabled="!addFieldPickKey" @click="confirmAddField">
                确定
              </el-button>
            </template>
          </el-dialog>

          <!-- 运行状态（只读） -->
          <section v-if="status.length > 0" class="prop-card prop-card--status">
            <header class="prop-card-header">
              <span class="prop-card-title">运行状态</span>
              <span class="prop-card-meta">{{ status.length }} 项</span>
            </header>
            <div class="prop-status-list">
              <div v-for="s in status" :key="s.key" class="prop-status-item">
                <span class="prop-status-key">{{ s.key }}</span>
                <span class="prop-status-value" :title="s.value">{{ s.value }}</span>
              </div>
            </div>
          </section>

          <!-- 操作 -->
          <section v-if="methods.length > 0" class="prop-card prop-card--actions">
            <header class="prop-card-header">
              <span class="prop-card-title">操作</span>
              <span class="prop-card-meta">{{ methods.length }} 项</span>
            </header>
            <div class="prop-actions">
              <el-tooltip
                v-for="m in methods"
                :key="m.methodName"
                :content="m.description || m.label || m.methodName"
                placement="top"
                :show-after="220"
              >
                <button
                  class="prop-action-btn"
                  :disabled="actionBusy === m.methodName"
                  @click="onAction(m.methodName)"
                >
                  <span v-if="actionBusy === m.methodName" class="prop-action-spinner">⟳</span>
                  <span class="prop-action-icon">▶</span>
                  <span class="prop-action-label">{{ m.label || m.methodName }}</span>
                </button>
              </el-tooltip>
            </div>
          </section>
        </div>
      </el-tab-pane>

      <el-tab-pane label="类型默认" name="defaults">
        <div class="defaults-pane">
          <el-alert type="info" :closable="false" class="defaults-alert">
            视口样式与 SimpleLite「视口样式」对话框同步，修改后即时重绘画布；下方「新建默认」仅影响下次创建对象。
          </el-alert>

          <div v-if="!selectionKind" class="empty-hint defaults-empty">
            <el-icon class="empty-icon"><InfoFilled /></el-icon>
            <span class="empty-title">请选中站点或路径</span>
            <span class="empty-desc">在画布上选中一个站点或路径后，可在此调整对应视口样式。</span>
          </div>

          <template v-else-if="selectionKind === 'site' && localViewportSite">
            <section class="prop-card prop-card--style">
              <header class="prop-card-header">
                <span class="prop-card-title">站点视口样式</span>
                <span class="prop-card-flag" :class="{ 'is-synced': viewportSynced }">
                  <span class="prop-card-flag-dot" />
                  <span class="prop-card-flag-text">{{ viewportSyncing ? '同步中…' : viewportSynced ? '已同步' : '实时同步' }}</span>
                </span>
              </header>
              <el-form label-width="132px" size="small" class="defaults-form">
                <el-form-item label="整体大小倍率">
                  <el-input-number
                    :model-value="localViewportSite.drawScale"
                    :min="0.25"
                    :max="12"
                    :step="0.1"
                    :controls="true"
                    @update:model-value="(v: number | undefined) => patchSite({ drawScale: v ?? localViewportSite!.drawScale })"
                  />
                </el-form-item>
                <el-form-item label="未选中圆点半径">
                  <el-input-number
                    :model-value="localViewportSite.dotRadiusNormal"
                    :min="1"
                    :max="24"
                    :step="0.5"
                    @update:model-value="(v: number | undefined) => patchSite({ dotRadiusNormal: v ?? localViewportSite!.dotRadiusNormal })"
                  />
                </el-form-item>
                <el-form-item label="选中圆点半径">
                  <el-input-number
                    :model-value="localViewportSite.dotRadiusSelected"
                    :min="1"
                    :max="32"
                    :step="0.5"
                    @update:model-value="(v: number | undefined) => patchSite({ dotRadiusSelected: v ?? localViewportSite!.dotRadiusSelected })"
                  />
                </el-form-item>
                <el-form-item label="选中环半径 (m)">
                  <el-input-number
                    :model-value="localViewportSite.selectionRingM"
                    :min="0.02"
                    :max="0.8"
                    :step="0.01"
                    @update:model-value="(v: number | undefined) => patchSite({ selectionRingM: v ?? localViewportSite!.selectionRingM })"
                  />
                </el-form-item>
                <el-form-item label="站点颜色（未选中）">
                  <el-color-picker
                    :model-value="argbToHex(localViewportSite.colorNormalArgb)"
                    @update:model-value="(v: string | null) => patchSite({ colorNormalArgb: hexToArgb(v ?? '#ffffff') })"
                  />
                </el-form-item>
                <el-form-item label="站点颜色（选中）">
                  <el-color-picker
                    :model-value="argbToHex(localViewportSite.colorSelectedArgb)"
                    @update:model-value="(v: string | null) => patchSite({ colorSelectedArgb: hexToArgb(v ?? '#ff0000') })"
                  />
                </el-form-item>
                <el-form-item label="标签颜色（未选中）">
                  <el-color-picker
                    :model-value="argbToHex(localViewportSite.labelColorNormalArgb)"
                    @update:model-value="(v: string | null) => patchSite({ labelColorNormalArgb: hexToArgb(v ?? '#ffffff') })"
                  />
                </el-form-item>
                <el-form-item label="标签颜色（选中）">
                  <el-color-picker
                    :model-value="argbToHex(localViewportSite.labelColorSelectedArgb)"
                    @update:model-value="(v: string | null) => patchSite({ labelColorSelectedArgb: hexToArgb(v ?? '#ff0000') })"
                  />
                </el-form-item>
              </el-form>
            </section>
            <section class="prop-card">
              <header class="prop-card-header">
                <span class="prop-card-title">新建站点默认</span>
              </header>
              <el-form label-width="132px" size="small" class="defaults-form">
                <el-form-item label="默认图层">
                  <el-input v-model="defaults.site.layer" />
                </el-form-item>
                <el-form-item label="名字前缀">
                  <el-input v-model="defaults.site.namePrefix" />
                </el-form-item>
                <el-button size="small" type="primary" plain @click="onSaveDefaults">保存新建默认</el-button>
              </el-form>
            </section>
          </template>

          <template v-else-if="selectionKind === 'track' && localViewportTrack">
            <section class="prop-card prop-card--style">
              <header class="prop-card-header">
                <span class="prop-card-title">路径视口样式</span>
                <span class="prop-card-flag" :class="{ 'is-synced': viewportSynced }">
                  <span class="prop-card-flag-dot" />
                  <span class="prop-card-flag-text">{{ viewportSyncing ? '同步中…' : viewportSynced ? '已同步' : '实时同步' }}</span>
                </span>
              </header>
              <el-form label-width="132px" size="small" class="defaults-form">
                <el-form-item label="线宽倍率">
                  <el-input-number
                    :model-value="localViewportTrack.drawScale"
                    :min="0.25"
                    :max="12"
                    :step="0.1"
                    @update:model-value="(v: number | undefined) => patchTrack({ drawScale: v ?? localViewportTrack!.drawScale })"
                  />
                </el-form-item>
                <el-form-item label="未选中线宽">
                  <el-input-number
                    :model-value="localViewportTrack.widthNormal"
                    :min="0.4"
                    :max="8"
                    :step="0.1"
                    @update:model-value="(v: number | undefined) => patchTrack({ widthNormal: v ?? localViewportTrack!.widthNormal })"
                  />
                </el-form-item>
                <el-form-item label="选中线宽">
                  <el-input-number
                    :model-value="localViewportTrack.widthSelected"
                    :min="0.4"
                    :max="12"
                    :step="0.1"
                    @update:model-value="(v: number | undefined) => patchTrack({ widthSelected: v ?? localViewportTrack!.widthSelected })"
                  />
                </el-form-item>
                <el-form-item label="显示方向箭头">
                  <el-switch
                    :model-value="localViewportTrack.showDirectionArrows"
                    @update:model-value="(v: string | number | boolean) => patchTrack({ showDirectionArrows: Boolean(v) })"
                  />
                </el-form-item>
                <el-form-item label="路径颜色（未选中）">
                  <el-color-picker
                    :model-value="argbToHex(localViewportTrack.colorNormalArgb)"
                    @update:model-value="(v: string | null) => patchTrack({ colorNormalArgb: hexToArgb(v ?? '#00ffff') })"
                  />
                </el-form-item>
                <el-form-item label="路径颜色（选中）">
                  <el-color-picker
                    :model-value="argbToHex(localViewportTrack.colorSelectedArgb)"
                    @update:model-value="(v: string | null) => patchTrack({ colorSelectedArgb: hexToArgb(v ?? '#ff0000') })"
                  />
                </el-form-item>
              </el-form>
            </section>
            <section class="prop-card">
              <header class="prop-card-header">
                <span class="prop-card-title">新建路径默认</span>
              </header>
              <el-form label-width="132px" size="small" class="defaults-form">
                <el-form-item label="默认图层">
                  <el-input v-model="defaults.track.layer" />
                </el-form-item>
                <el-button size="small" type="primary" plain @click="onSaveDefaults">保存新建默认</el-button>
              </el-form>
            </section>
          </template>

          <div v-else-if="viewportStyleLoading" class="loading-hint">
            <el-icon class="is-loading"><Loading /></el-icon>
            <span>加载视口样式…</span>
          </div>
        </div>
      </el-tab-pane>

      <el-tab-pane label="图层" name="layers">
        <el-table :data="layerRows" size="small" class="layer-table">
          <el-table-column prop="name" label="图层" width="120" />
          <el-table-column label="可见" width="60">
            <template #default="{ row }">
              <el-switch v-model="row.visible" @change="onLayerToggle(row)" />
            </template>
          </el-table-column>
          <el-table-column label="可选" width="60">
            <template #default="{ row }">
              <el-switch v-model="row.selectable" @change="onLayerToggle(row)" />
            </template>
          </el-table-column>
          <el-table-column label="颜色">
            <template #default="{ row }">
              <el-color-picker v-model="row.color" @change="onLayerToggle(row)" />
            </template>
          </el-table-column>
        </el-table>
        <el-input v-model="newLayerName" size="small" placeholder="新图层名" class="new-layer">
          <template #append>
            <el-button @click="onAddLayer">添加</el-button>
          </template>
        </el-input>
      </el-tab-pane>
    </el-tabs>
  </div>
</template>

<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { InfoFilled, Loading } from '@element-plus/icons-vue'
import { computed, ref, toRef, watch } from 'vue'
import { useSimpleFieldCatalog } from '@/composables/useSimpleFieldCatalog'
import {
  argbToHex,
  hexToArgb,
  type ReflectionMethod,
  type ReflectionObject,
  type ViewportStyleSite,
  type ViewportStyleTrack,
  type ViewportStylePayload
} from '@/api/reflection'

interface PropFieldKv { key: string; value: string }
interface LayerRow { name: string; visible: boolean; selectable: boolean; color: string }

const props = defineProps<{
  selectionCount: number
  primary: ReflectionObject | null
  fields: PropFieldKv[]
  status: PropFieldKv[]
  methods: ReflectionMethod[]
  loading: boolean
  layers: string[]
  defaults: {
    site: { layer: string; namePrefix: string; color: string }
    track: { layer: string; lineWidth: number; color: string }
  }
  layerRows: LayerRow[]
  selectionKind: 'site' | 'track' | 'car' | null
  /** 当前小车类型（assemblyName.shortName 或 typeName），用于过滤 carFields。 */
  carType?: string | null
  viewportStyle: ViewportStylePayload | null
  viewportStyleLoading?: boolean
  viewportSyncing?: boolean
}>()

const emit = defineEmits<{
  (e: 'rename', name: string): void
  (e: 'change-layer', layer: string): void
  (e: 'set-field', key: string, value: string): void
  (e: 'delete-field', key: string): void
  (e: 'exec-action', method: string): void
  (e: 'save-defaults'): void
  (e: 'layer-toggle', row: LayerRow): void
  (e: 'add-layer', name: string): void
  (e: 'delete'): void
  (e: 'patch-viewport', patch: { site?: ViewportStyleSite; track?: ViewportStyleTrack }): void
}>()

const tab = ref<'object' | 'defaults' | 'layers'>('object')
const viewportSynced = ref(false)
let viewportSyncedTimer: ReturnType<typeof setTimeout> | null = null
const localViewportSite = ref<ViewportStyleSite | null>(null)
const localViewportTrack = ref<ViewportStyleTrack | null>(null)

watch(
  () => props.viewportStyle,
  (v) => {
    if (!v) {
      localViewportSite.value = null
      localViewportTrack.value = null
      return
    }
    localViewportSite.value = { ...v.site }
    localViewportTrack.value = { ...v.track }
  },
  { immediate: true, deep: true }
)

watch(
  () => props.selectionKind,
  (k) => {
    if (k === 'site' || k === 'track') tab.value = 'defaults'
  }
)

function flashViewportSynced() {
  viewportSynced.value = true
  if (viewportSyncedTimer) clearTimeout(viewportSyncedTimer)
  viewportSyncedTimer = setTimeout(() => { viewportSynced.value = false }, 1400)
}

watch(() => props.viewportSyncing, (busy, wasBusy) => {
  if (wasBusy && !busy) flashViewportSynced()
})

function patchSite(partial: Partial<ViewportStyleSite>) {
  if (!localViewportSite.value) return
  localViewportSite.value = { ...localViewportSite.value, ...partial }
  emit('patch-viewport', { site: { ...localViewportSite.value } })
}

function patchTrack(partial: Partial<ViewportStyleTrack>) {
  if (!localViewportTrack.value) return
  localViewportTrack.value = { ...localViewportTrack.value, ...partial }
  emit('patch-viewport', { track: { ...localViewportTrack.value } })
}

const actionBusy = ref<string | null>(null)
const newLayerName = ref('')
const deleting = ref(false)
const syncFlash = ref(false)
let syncFlashTimer: ReturnType<typeof setTimeout> | null = null
function flashSync() {
  syncFlash.value = true
  if (syncFlashTimer) clearTimeout(syncFlashTimer)
  syncFlashTimer = setTimeout(() => { syncFlash.value = false }, 1200)
}

// ──────────────────────────────────────────────────────────────────────────
// 字段分组
//
// 后端 ReflectionApiController/MapEditApiController 对每个被 [FieldMember] 标的
// public 字段都吐出来（含 `id / name / layerName / x / y / z / color /
// displaySetting / radius / direction / typeInfo / siteA / siteB / mustFree / ...`）。
// 这里的策略：
//   - 顶部 Header 卡片已经覆盖 id / name / layerName / direction / status；
//   - 「位置 / 几何」吃掉所有跟坐标 / 角度 / 大小 / 半径 / 端点站点强相关的 key；
//   - 剩下的统统进「其它字段」（含 displaySetting / typeInfo / color 及用户自定义字段）。
// 这样不管后端塞什么字段进来都能露出来 + 可编辑。
// ──────────────────────────────────────────────────────────────────────────

const HEADER_KEYS = new Set(['id', 'name', 'layerName', 'direction'])

const POSITION_KEYS = new Set([
  'x', 'y', 'z',
  'cx', 'cy', 'cz',
  'dx', 'dy', 'dz',
  'sx', 'sy', 'ex', 'ey',
  'startX', 'startY', 'endX', 'endY',
  'size', 'width', 'height', 'depth',
  'angle', 'rotation', 'theta',
  'scale',
  'radius', 'r',
  'siteA', 'siteB'
])

const editableFields = computed(() => props.fields.filter((f) => !HEADER_KEYS.has(f.key)))

const directionField = computed(() => props.fields.find((f) => f.key === 'direction'))

const positionFields = computed(() => editableFields.value.filter((f) => POSITION_KEYS.has(f.key)))
const otherFields = computed(() =>
  editableFields.value.filter((f) => !POSITION_KEYS.has(f.key))
)

const selectionKindRef = toRef(props, 'selectionKind')
const carTypeRef = computed(() => props.carType ?? null)

const {
  loading: fieldCatalogLoading,
  records: fieldCatalogRecords,
  labelForKey,
  defaultValueForKey
} = useSimpleFieldCatalog(selectionKindRef, carTypeRef)

const catalogKeySet = computed(() => new Set(fieldCatalogRecords.value.map((r) => r.key)))
const catalogDataTypeByKey = computed(() => {
  const map = new Map<string, string>()
  for (const r of fieldCatalogRecords.value) {
    if (!map.has(r.key)) map.set(r.key, r.dataType || '')
  }
  return map
})

function customFieldDisplayLabel(key: string, chinese: string): string {
  const cn = chinese.trim()
  if (!cn || cn === '—') return key
  return `${key} - ${cn}`
}

/** 仅展示已在对象上存在、且在 simple_fields 目录中的扩展字段 */
const customFieldRows = computed(() =>
  otherFields.value
    .filter((f) => catalogKeySet.value.has(f.key))
    .map((f) => {
      const chinese = labelForKey(f.key)
      return {
        key: f.key,
        value: f.value,
        dataType: catalogDataTypeByKey.value.get(f.key) ?? '',
        chinese,
        label: customFieldDisplayLabel(f.key, chinese)
      }
    })
)

const addableFieldOptions = computed(() => {
  const existing = new Set(customFieldRows.value.map((r) => r.key))
  return fieldCatalogRecords.value
    .filter((r) => !existing.has(r.key))
    .map((r) => ({
      key: r.key,
      label: r.chinese?.trim()
        ? `${r.key} · ${r.chinese}`
        : (r.english?.trim() ? `${r.key} · ${r.english}` : r.key)
    }))
})

const addFieldDialogOpen = ref(false)
const addFieldPickKey = ref('')
/** 扩展字段本地草稿；避免受控 input 无 update 导致无法键入，并抵御 SSE 刷新打断编辑。 */
const customFieldDrafts = ref<Record<string, string>>({})
const customFieldFocusKey = ref<string | null>(null)

watch(
  customFieldRows,
  (rows) => {
    const next = { ...customFieldDrafts.value }
    for (const row of rows) {
      if (customFieldFocusKey.value !== row.key) next[row.key] = row.value
    }
    for (const key of Object.keys(next)) {
      if (!rows.some((r) => r.key === key)) delete next[key]
    }
    customFieldDrafts.value = next
  },
  { immediate: true, deep: true }
)

function validateCustomFieldValue(value: string, dataType: string): string | null {
  const t = dataType.trim().toLowerCase()
  if (!t || t.includes('string')) return null
  if (value.trim() === '') return null

  if (t.includes('bool')) {
    return /^(true|false|1|0)$/i.test(value.trim())
      ? null
      : '只允许 true / false / 1 / 0'
  }

  if (t.includes('int16') || t.includes('int32') || t.includes('int64') ||
      t.includes('uint16') || t.includes('uint32') || t.includes('uint64') ||
      t.endsWith('.int') || t.endsWith('.long') || t.endsWith('.short')) {
    return /^-?\d+$/.test(value.trim()) ? null : '只允许整数'
  }

  if (t.includes('single') || t.includes('double') || t.includes('float') || t.includes('decimal')) {
    const n = Number(value)
    return Number.isFinite(n) ? null : '只允许数字'
  }

  return null
}

function onCustomFieldCommit(key: string, value: string) {
  const row = customFieldRows.value.find((r) => r.key === key)
  if (row) {
    const err = validateCustomFieldValue(value, row.dataType)
    if (err) {
      customFieldDrafts.value[key] = row.value
      ElMessage.warning(`${row.label} 校验失败：${err}`)
      return
    }
  }
  customFieldDrafts.value[key] = value
  onFieldChange(key, value)
}

function openAddFieldDialog() {
  addFieldPickKey.value = addableFieldOptions.value[0]?.key ?? ''
  addFieldDialogOpen.value = true
}

function confirmAddField() {
  const key = addFieldPickKey.value
  if (!key) return
  const value = defaultValueForKey(key)
  customFieldDrafts.value = { ...customFieldDrafts.value, [key]: value }
  addFieldDialogOpen.value = false
  emit('set-field', key, value)
}

function onDeleteCustomField(key: string) {
  emit('delete-field', key)
}

const FIELD_LABELS: Record<string, string> = {
  x: 'X',
  y: 'Y',
  z: 'Z',
  size: '大小',
  width: '宽',
  height: '高',
  depth: '厚',
  angle: '角度',
  rotation: '旋转',
  theta: 'θ',
  scale: '缩放',
  radius: '半径',
  siteA: '起点站',
  siteB: '终点站',
  startX: '起点 X',
  startY: '起点 Y',
  endX: '终点 X',
  endY: '终点 Y',
  cx: '中心 X',
  cy: '中心 Y',
  color: '颜色',
  displaySetting: '显示设置',
  direction: '方向',
  typeInfo: '类型信息',
  mustFree: '必须互斥',
  imagePath: '图片路径',
  modelPath: '模型路径',
  fontSize: '字号',
  textColor: '文字色',
  lineWidth: '线宽',
  opacity: '透明度'
}

const FIELD_UNITS: Record<string, string> = {
  x: 'mm', y: 'mm', z: 'mm',
  cx: 'mm', cy: 'mm', cz: 'mm',
  dx: 'mm', dy: 'mm', dz: 'mm',
  startX: 'mm', startY: 'mm',
  endX: 'mm', endY: 'mm',
  width: 'mm', height: 'mm', depth: 'mm',
  size: 'mm', radius: 'mm',
  angle: '°', rotation: '°', theta: '°',
  fontSize: 'px', lineWidth: 'px'
}

function fieldDisplayLabel(key: string): string {
  return FIELD_LABELS[key] ?? key
}
function fieldUnit(key: string): string {
  return FIELD_UNITS[key] ?? ''
}

function numericValue(v: string): number | undefined {
  if (v == null || v === '') return undefined
  const n = Number(v)
  return Number.isFinite(n) ? n : undefined
}
function numericStep(key: string): number {
  if (key.toLowerCase().includes('angle') || key.toLowerCase().includes('rotation') || key === 'theta') return 1
  if (key === 'scale' || key === 'opacity') return 0.1
  return 10
}
function isNumberField(key: string): boolean {
  return POSITION_KEYS.has(key)
}
function isColorField(key: string, value: string): boolean {
  if (key === 'color' || key === 'textColor') return true
  return /^#[0-9a-fA-F]{3,8}$/.test(value)
}
function isBooleanField(_key: string, value: string): boolean {
  if (value === 'true' || value === 'false' || value === 'True' || value === 'False') return true
  return false
}
function parseBool(v: string): boolean {
  return v === 'true' || v === 'True' || v === '1'
}

const statusTagType = computed<'success' | 'warning' | 'danger' | 'info'>(() => {
  const s = (props.primary?.status ?? '').toLowerCase()
  if (!s) return 'info'
  if (s.includes('error') || s.includes('alarm') || s.includes('failed')) return 'danger'
  if (s.includes('warn') || s.includes('idle')) return 'warning'
  if (s.includes('online') || s.includes('running') || s.includes('ready') || s.includes('ok')) return 'success'
  return 'info'
})

// 本地名称缓冲：避免 v-model 直接 mutate 父级传下的 primary props（违反 vue/no-mutating-props）。
// 父级 primary 变化时同步进 editingName；用户改完按回车/失焦时 emit('rename') 向上抛新值。
const editingName = ref<string>('')
watch(() => props.primary?.name ?? '', (n) => { editingName.value = n }, { immediate: true })

function onPrimaryNameBlur() { onPrimaryNameSubmit() }
function onPrimaryNameSubmit() {
  if (!props.primary) return
  const next = editingName.value
  if (next === (props.primary.name ?? '')) return
  emit('rename', next)
}
function onPrimaryLayerChange(v: string) {
  emit('change-layer', v)
}
function onFieldChange(key: string, value: string) {
  emit('set-field', key, value)
  if (POSITION_KEYS.has(key)) flashSync()
}
async function onAction(method: string) {
  try {
    actionBusy.value = method
    emit('exec-action', method)
  } finally {
    actionBusy.value = null
  }
}
function onSaveDefaults() { emit('save-defaults') }
function onLayerToggle(row: LayerRow) { emit('layer-toggle', row) }
function onAddLayer() {
  const n = newLayerName.value.trim()
  if (!n) return
  emit('add-layer', n)
  newLayerName.value = ''
}

function onDelete() {
  if (props.selectionCount === 0 || deleting.value) return
  // 仅置 busy；真正的删除由父组件 deleteSelection 走 history.run，
  // SSE object-deleted 回来后 selection 自动清空、本面板会被父组件解除。
  // 这里 100ms 后自动放开按钮，避免父组件 await 期间用户疯狂双击导致重复入栈。
  deleting.value = true
  emit('delete')
  setTimeout(() => { deleting.value = false }, 600)
}
</script>

<style scoped>
/* Style 2 分组卡片 + Style 5 主题色相（--me-* / --mg-primary） */
.edit-property-panel {
  height: 100%;
  width: 360px;
  background: linear-gradient(180deg, var(--me-chrome-1) 0%, var(--me-chrome-2) 100%);
  border-left: 1px solid var(--me-border);
  padding: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  color: var(--me-text);
}
.prop-tabs {
  height: 100%;
  display: flex;
  flex-direction: column;
}
.prop-tabs :deep(.el-tabs__header) {
  margin: 0;
  padding: 6px 10px 0;
  border-bottom: 1px solid var(--me-border);
  background: var(--me-surface);
}
.prop-tabs :deep(.el-tabs__nav-wrap)::after { background: transparent; }
.prop-tabs :deep(.el-tabs__item) {
  color: var(--me-text-muted);
  font-weight: 500;
  font-size: 13px;
  height: 34px;
  line-height: 34px;
  padding: 0 14px !important;
}
.prop-tabs :deep(.el-tabs__item.is-active) {
  color: var(--mg-primary);
  font-weight: 700;
}
.prop-tabs :deep(.el-tabs__active-bar) {
  background: var(--mg-primary);
  height: 2px;
  border-radius: 2px;
}
.prop-tabs :deep(.el-tabs__content) {
  flex: 1;
  overflow: auto;
  padding: 12px;
}

.empty-hint {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 48px 16px;
  text-align: center;
  color: var(--me-text-muted);
  flex-direction: column;
}
.empty-icon { font-size: 28px; color: rgba(var(--mg-primary-rgb), 0.45); }
.empty-title { font-size: 13px; color: var(--me-text); font-weight: 600; }
.empty-desc { font-size: 11.5px; color: var(--me-text-muted); max-width: 220px; line-height: 1.45; }
.loading-hint {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 32px 16px;
  justify-content: center;
  color: var(--me-text-muted);
  font-size: 12.5px;
}

.object-pane,
.defaults-pane {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* 分组卡片：跟 --me-card-*（Harbor 浅壳 / 紫蓝深壳共用） */
.prop-card {
  position: relative;
  background: var(--me-card-bg);
  border: 1px solid var(--me-card-border);
  border-radius: 12px;
  padding: 12px 12px 10px 14px;
  box-shadow: 0 4px 14px rgba(var(--mg-shadow-rgb, 22, 58, 74), 0.08);
}
.prop-card::before {
  content: '';
  position: absolute;
  left: 0;
  top: 10px;
  bottom: 10px;
  width: 3px;
  border-radius: 0 3px 3px 0;
  background: rgba(var(--mg-primary-rgb), 0.55);
}
.prop-card--header {
  background: linear-gradient(
    135deg,
    rgba(var(--mg-primary-rgb), 0.12) 0%,
    var(--me-card-bg) 55%
  );
  border-color: rgba(var(--mg-primary-rgb), 0.28);
  box-shadow: 0 4px 16px rgba(var(--mg-primary-rgb), 0.08);
}
.prop-card--header::before {
  background: var(--mg-primary);
  top: 8px;
  bottom: 8px;
  width: 3px;
}
.prop-card--geom,
.prop-card--style,
.prop-card--custom,
.prop-card--status,
.prop-card--actions {
  background: var(--me-card-bg);
  border-color: var(--me-card-border);
}

.prop-card-header {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 12px;
  min-height: 22px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--me-card-border);
}
.prop-card-title {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--me-text);
}
.prop-card-meta {
  margin-left: auto;
  font-size: 11px;
  font-family: var(--mg-font-mono, ui-monospace, Menlo, Consolas, monospace);
  font-variant-numeric: tabular-nums;
  color: var(--me-text-muted);
  background: var(--me-inset);
  border: 1px solid var(--me-card-border);
  border-radius: 999px;
  padding: 1px 8px;
}
.prop-card-flag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 10.5px;
  color: var(--me-text-muted);
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--me-surface);
  border: 1px solid var(--me-card-border);
  margin-left: auto;
}
.prop-card-flag.is-synced {
  background: rgba(34, 197, 94, 0.12);
  border-color: rgba(34, 197, 94, 0.35);
  color: #15803d;
  box-shadow: none;
}
.prop-card-flag-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--me-text-muted);
}
.prop-card-flag.is-synced .prop-card-flag-dot {
  background: #4ade80;
  box-shadow: none;
}

.prop-card-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 8px;
  gap: 8px;
}
.prop-card-row:last-child { margin-bottom: 0; }
.prop-id-row { margin-bottom: 10px; }

.prop-id-block {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  flex-wrap: wrap;
}
.prop-id-tag {
  background: var(--mg-primary);
  color: #fff;
  padding: 3px 9px;
  font-size: 12px;
  font-weight: 700;
  border-radius: 6px;
  font-family: var(--mg-font-mono, ui-monospace, Menlo, Consolas, monospace);
  font-variant-numeric: tabular-nums;
}
.prop-type-tag {
  color: var(--me-text-muted);
  font-size: 12px;
  font-family: var(--mg-font-mono, ui-monospace, Menlo, Consolas, monospace);
  background: var(--me-inset);
  border: 1px solid var(--me-card-border);
  border-radius: 6px;
  padding: 2px 8px;
}
.prop-status-tag { font-weight: 600; }

.prop-header-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-left: auto;
}

.prop-delete-btn {
  appearance: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 10px;
  border-radius: 6px;
  font-size: 12px;
  font-weight: 550;
  color: #fecaca;
  background: rgba(239, 68, 68, 0.12);
  border: 1px solid rgba(239, 68, 68, 0.35);
  transition: background .12s ease, border-color .12s ease;
}
.prop-delete-btn:hover:not(:disabled) {
  background: rgba(239, 68, 68, 0.22);
  border-color: rgba(239, 68, 68, 0.55);
  color: #fff;
  box-shadow: none;
}
.prop-delete-btn:disabled { opacity: 0.55; cursor: progress; }
.prop-delete-icon { font-size: 12px; line-height: 1; }

.prop-name-row,
.prop-layer-row,
.prop-direction-row { gap: 10px; }
.prop-name-label {
  width: 36px;
  font-size: 12px;
  color: var(--me-text-muted);
  flex: none;
}
.prop-layer-select { flex: 1; }

.prop-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: 10px;
  row-gap: 10px;
}
.prop-grid-item { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.prop-grid-item--wide { grid-column: 1 / -1; }
.prop-field-label {
  font-size: 11px;
  color: var(--me-text-muted);
  letter-spacing: 0.2px;
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 4px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.prop-field-unit {
  font-size: 10px;
  color: var(--me-text-muted);
  font-family: var(--mg-font-mono, ui-monospace, Menlo, Consolas, monospace);
  flex: none;
}
.prop-field-num :deep(.el-input-number) { width: 100%; }
.prop-field-num :deep(.el-input__wrapper),
.prop-card :deep(.el-input__wrapper),
.prop-card :deep(.el-select__wrapper),
.prop-card :deep(.el-textarea__inner) {
  background: var(--me-inset) !important;
  box-shadow: inset 0 0 0 1px var(--me-card-border) !important;
}
.prop-field-num :deep(.el-input__inner),
.prop-card :deep(.el-input__inner),
.prop-card :deep(.el-textarea__inner),
.prop-card :deep(.el-select__selected-item),
.prop-card :deep(.el-select__placeholder) {
  color: var(--me-text) !important;
}
.prop-field-num :deep(.el-input__wrapper:hover),
.prop-card :deep(.el-input__wrapper:hover),
.prop-card :deep(.el-select__wrapper:hover) {
  box-shadow: inset 0 0 0 1px rgba(var(--mg-primary-rgb), 0.55) !important;
}
.prop-field-num :deep(.el-input__wrapper.is-focus),
.prop-card :deep(.el-input__wrapper.is-focus),
.prop-card :deep(.el-select__wrapper.is-focused) {
  box-shadow:
    inset 0 0 0 1px var(--mg-primary),
    0 0 0 3px rgba(var(--mg-primary-rgb), 0.16) !important;
}
.prop-field-row { display: flex; align-items: center; gap: 6px; }
.prop-field-icon-btn {
  appearance: none;
  border: 1px solid rgba(239, 68, 68, 0.28);
  cursor: pointer;
  width: 28px;
  height: 28px;
  border-radius: 7px;
  background: rgba(239, 68, 68, 0.08);
  color: #dc2626;
  font-size: 15px;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  transition: background .12s ease, border-color .12s ease, color .12s ease;
}
.prop-field-icon-btn:hover {
  background: rgba(239, 68, 68, 0.16);
  border-color: rgba(239, 68, 68, 0.45);
  color: #b91c1c;
}

.prop-empty-row {
  font-size: 12px;
  color: var(--me-text-muted);
  text-align: center;
  padding: 14px 8px;
  border-radius: 8px;
  background: var(--me-inset);
  border: 1px dashed var(--me-card-border);
}
.prop-add-field-btn {
  appearance: none;
  cursor: pointer;
  width: 100%;
  margin-top: 10px;
  padding: 8px 10px;
  border-radius: 8px;
  background: var(--me-surface);
  border: 1px dashed rgba(var(--mg-primary-rgb), 0.42);
  color: var(--mg-primary);
  font-size: 12px;
  font-weight: 600;
  transition: background .12s ease, border-color .12s ease, color .12s ease;
}
.prop-add-field-btn:hover:not(:disabled) {
  background: var(--me-hover);
  border-color: rgba(var(--mg-primary-rgb), 0.7);
  border-style: solid;
  color: var(--mg-primary);
}
.prop-add-field-btn:disabled { opacity: 0.45; cursor: not-allowed; }

.custom-field-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.custom-field-item {
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-width: 0;
  padding: 9px 10px;
  border-radius: 9px;
  background: var(--me-inset);
  border: 1px solid var(--me-card-border);
}
.custom-field-key {
  margin: 0;
  font-size: 11px;
  font-weight: 650;
  letter-spacing: 0.02em;
  color: var(--me-text);
  white-space: normal;
  word-break: break-all;
  line-height: 1.35;
  cursor: default;
}
.custom-field-input { flex: 1; min-width: 0; width: 100%; }
.custom-field-input :deep(.el-input__wrapper) {
  background: var(--me-card-bg) !important;
  box-shadow: inset 0 0 0 1px var(--me-card-border) !important;
  min-height: 30px;
}
.custom-field-input :deep(.el-input__inner) {
  color: var(--me-text) !important;
  font-weight: 550;
  font-variant-numeric: tabular-nums;
}
.custom-field-input :deep(.el-input__wrapper:hover) {
  box-shadow: inset 0 0 0 1px rgba(var(--mg-primary-rgb), 0.45) !important;
}
.custom-field-input :deep(.el-input__wrapper.is-focus) {
  box-shadow:
    inset 0 0 0 1px var(--mg-primary),
    0 0 0 3px rgba(var(--mg-primary-rgb), 0.14) !important;
}
.add-field-select { width: 100%; }

/* 运行状态：胶囊行 */
.prop-status-list {
  display: flex;
  flex-direction: column;
  gap: 5px;
  max-height: 280px;
  overflow: auto;
  padding-right: 2px;
}
.prop-status-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  padding: 7px 10px;
  border-radius: 8px;
  background: var(--me-inset);
  border: 1px solid var(--me-card-border);
  font-size: 11.5px;
}
.prop-status-item:nth-child(even) {
  background: rgba(var(--mg-primary-rgb), 0.08);
}
.prop-status-key {
  color: var(--me-text-muted);
  font-weight: 600;
  flex: none;
  max-width: 46%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.prop-status-value {
  color: var(--me-text);
  font-family: var(--mg-font-mono, ui-monospace, Menlo, Consolas, monospace);
  font-variant-numeric: tabular-nums;
  max-width: 54%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: right;
}

.prop-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}
.prop-action-btn {
  appearance: none;
  cursor: pointer;
  padding: 9px 11px;
  border-radius: 8px;
  background: var(--mg-primary);
  border: 1px solid var(--mg-primary);
  color: #fff;
  font-size: 12px;
  font-weight: 600;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 6px;
  text-align: left;
  transition: background .12s ease, border-color .12s ease, box-shadow .12s ease, transform .12s ease;
}
.prop-action-btn:nth-child(2n) {
  background: var(--me-card-bg);
  border-color: rgba(var(--mg-primary-rgb), 0.38);
  color: var(--mg-primary);
}
.prop-action-btn:nth-child(2n) .prop-action-icon {
  color: var(--mg-primary);
}
.prop-action-btn:hover:not(:disabled) {
  background: var(--mg-primary-hover, var(--mg-primary));
  border-color: var(--mg-primary-hover, var(--mg-primary));
  color: #fff;
  box-shadow: 0 4px 12px var(--me-active-glow);
  transform: translateY(-1px);
}
.prop-action-btn:nth-child(2n):hover:not(:disabled) {
  background: rgba(var(--mg-primary-rgb), 0.1);
  border-color: var(--mg-primary);
  color: var(--mg-primary);
  box-shadow: 0 2px 8px var(--me-active-glow);
}
.prop-action-btn:nth-child(2n):hover:not(:disabled) .prop-action-icon {
  color: var(--mg-primary);
}
.prop-action-btn:disabled { opacity: 0.55; cursor: progress; transform: none; }
.prop-action-icon { font-size: 9px; color: rgba(255, 255, 255, 0.92); flex: none; }
.prop-action-spinner {
  display: inline-block;
  animation: prop-spin .8s linear infinite;
  font-size: 13px;
}
.prop-action-label {
  flex: 1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
@keyframes prop-spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

.defaults-alert { margin-bottom: 0; }
.defaults-empty { padding: 32px 12px; }
.defaults-form .el-button { margin-top: 8px; }
.defaults-form :deep(.el-form-item) { margin-bottom: 10px; }
.defaults-form :deep(.el-form-item__label) { color: var(--me-text-muted); }

.layer-table {
  margin: 0;
  border-radius: 12px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.10);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
}
.layer-table :deep(.el-table__inner-wrapper),
.layer-table :deep(.el-table),
.layer-table :deep(.el-table tr) {
  background: transparent !important;
}
.layer-table :deep(th.el-table__cell) {
  background: rgba(var(--mg-primary-rgb), 0.16) !important;
  color: var(--me-text) !important;
  font-weight: 650;
  border-color: rgba(255, 255, 255, 0.08) !important;
}
.layer-table :deep(td.el-table__cell) {
  background: transparent !important;
  color: var(--me-text) !important;
  border-color: rgba(255, 255, 255, 0.06) !important;
}
.layer-table :deep(.cell) { color: inherit !important; }
.layer-table :deep(tr:hover > td.el-table__cell) {
  background: rgba(var(--mg-primary-rgb), 0.12) !important;
}
.new-layer { margin-top: 10px; }
.new-layer :deep(.el-input__wrapper) {
  background: rgba(0, 0, 0, 0.22) !important;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.10) !important;
}
.new-layer :deep(.el-input__inner) {
  color: var(--me-text);
  font-weight: 500;
}
.new-layer :deep(.el-input__inner::placeholder) {
  color: var(--me-text-muted);
}
.new-layer :deep(.el-input-group__append .el-button) {
  color: var(--mg-accent);
  font-weight: 600;
}
</style>
