<template>
  <div class="engineering-analysis-panel">
    <section class="analysis-card analysis-workbench" :class="{ 'is-running': analysisRunning }">
      <header class="analysis-card-head">
        <div>
          <span>生态工程实施分析</span>
          <small>基础候选区 + 动态业务条件</small>
        </div>
        <strong>{{ statusText }}</strong>
      </header>

      <div class="analysis-section-title">
        <span>01</span>
        <strong>工程类型</strong>
        <em>单选</em>
      </div>
      <div class="analysis-type-list" role="radiogroup" aria-label="工程类型">
        <button
          v-for="item in analysisRows"
          :key="item.key"
          type="button"
          class="analysis-type-row"
          :class="{ 'is-selected': selectedTypes.includes(item.key) }"
          :disabled="analysisRunning"
          role="radio"
          :aria-checked="selectedTypes.includes(item.key)"
          @click="toggleType(item.key)"
        >
          <i class="analysis-type-check" :style="{ '--type-color': item.color }"></i>
          <div class="analysis-type-main">
            <strong>{{ item.name }}</strong>
          </div>
        </button>
      </div>

      <div class="analysis-section-title is-filter">
        <span>02</span>
        <strong>动态筛选条件</strong>
        <em>3 项条件</em>
      </div>
      <div class="analysis-filter-grid" aria-label="动态筛选条件">
        <label class="is-wide">
          <span>所属乡镇</span>
          <select v-model="selectedTown" data-testid="analysis-town" :disabled="analysisRunning">
            <option value="">全县</option>
            <option v-for="town in townshipOptions" :key="town" :value="town">{{ town }}</option>
          </select>
        </label>
        <label>
          <span>已有项目</span>
          <select v-model="projectFilterMode" data-testid="analysis-project-filter" :disabled="analysisRunning">
            <option value="same">排除同类工程</option>
            <option value="all">排除全部工程</option>
            <option value="none">不排除</option>
          </select>
        </label>
        <label>
          <span>未实施年限</span>
          <select v-model.number="recentYears" data-testid="analysis-recent-years" :disabled="analysisRunning || projectFilterMode === 'none'">
            <option :value="1">最近 1 年</option>
            <option :value="3">最近 3 年</option>
            <option :value="5">最近 5 年</option>
          </select>
        </label>
      </div>
      <div class="analysis-filter-note">
        <i></i>
        <span v-if="projectFilterMode === 'none'">保留与已有工程相交的候选地块</span>
        <span v-else-if="projectCoverageState === 'loading'">正在读取项目库最新实施年份…</span>
        <span v-else-if="projectCoverageState === 'error'">项目库年份读取失败，暂不能执行历史项目排除</span>
        <span v-else>{{ exclusionNote }}</span>
      </div>

      <button
        type="button"
        class="analysis-submit"
        data-testid="analysis-submit"
        :disabled="!analysisCanStart"
        :aria-busy="analysisRunning"
        @click="startAnalysis"
      >
        <span v-if="analysisRunning" class="analysis-submit-spinner"></span>
        <span>{{ analysisRunning ? '空间分析进行中' : analysisDone ? '重新分析' : '开始分析' }}</span>
        <strong v-if="analysisRunning">逐批加载</strong>
        <strong v-else>{{ selectedTypeName }}</strong>
      </button>

      <div v-if="analysisRunning || analysisDone" class="analysis-progress" aria-live="polite">
        <div
          role="progressbar"
          aria-label="分析进度"
          :class="{ 'is-indeterminate': analysisRunning }"
          :aria-valuenow="analysisDone ? 100 : undefined"
          :aria-busy="analysisRunning"
          aria-valuemin="0"
          aria-valuemax="100"
        >
          <i :style="analysisDone ? { width: '100%' } : undefined"></i>
        </div>
        <span>{{ progressText }}</span>
      </div>
      <p v-if="errorText" class="analysis-error">{{ errorText }}</p>
    </section>

    <section v-if="analysisRunning || analysisDone" class="analysis-card analysis-result-card">
      <header class="analysis-card-head is-result">
        <div>
          <span>可实施区域</span>
          <small>{{ resultScopeText }}</small>
        </div>
        <strong>{{ formatCount(totalLoadedCount) }} 个{{ analysisRunning ? '已加载' : '' }}</strong>
      </header>

      <div v-if="analysisRunning" class="analysis-result-summary is-loading">
        <div>
          <span>已加载图斑</span>
          <strong>{{ formatCount(totalLoadedCount) }}</strong>
          <small>个</small>
        </div>
        <div>
          <span>已加载面积</span>
          <strong>{{ formatArea(totalLoadedAreaWanMu) }}</strong>
          <small>万亩</small>
        </div>
        <div class="is-final">
          <span>当前批次</span>
          <strong>{{ currentLoadedPage || '—' }}</strong>
          <small>{{ currentLoadedPage ? '页' : '准备中' }}</small>
        </div>
      </div>

      <div v-else class="analysis-result-summary">
        <div>
          <span>基础候选</span>
          <strong>{{ formatCount(totalBaseCount) }}</strong>
          <small>个图斑</small>
        </div>
        <div class="is-excluded">
          <span>项目排除</span>
          <strong>{{ formatCount(totalExcludedCount) }}</strong>
          <small>个图斑</small>
        </div>
        <div class="is-final">
          <span>最终可实施</span>
          <strong>{{ formatArea(totalLoadedAreaWanMu) }}</strong>
          <small>万亩</small>
        </div>
      </div>

      <div class="analysis-result-list">
        <button
          v-for="item in visibleResultRows"
          :key="item.key"
          type="button"
          class="analysis-result-row"
          :class="[`is-${item.status}`, { 'is-hidden': item.visible === false }]"
          :aria-pressed="item.visible !== false"
          @click="toggleResultVisibility(item)"
        >
          <i :style="{ '--type-color': item.color }"></i>
          <div>
            <strong>{{ item.name }}</strong>
            <span v-if="item.status === 'loading'">正在读取第 {{ item.loadedPage }} 页</span>
            <span v-else-if="item.status === 'error'">加载失败</span>
            <span v-else>{{ formatArea(item.loadedAreaMu / 10000) }} 万亩</span>
          </div>
          <b v-if="item.status === 'loading'">已加载 {{ formatCount(item.loadedCount) }} 个</b>
          <b v-else>{{ formatCount(item.loadedCount) }} 个图斑</b>
          <em>{{ item.visible === false ? '已隐藏' : resultStatusLabel(item) }}</em>
        </button>
      </div>

      <section v-if="analysisDone && excludedProjectRows.length" class="analysis-excluded-projects">
        <header>
          <strong>排除项目与候选面积</strong>
          <em>合计 {{ formatArea(excludedProjectAreaWanMu) }} 万亩</em>
        </header>
        <div class="analysis-excluded-project-list">
          <div v-for="(item, index) in excludedProjectRows" :key="item.name" class="analysis-excluded-project-row">
            <span>{{ index + 1 }}</span>
            <div>
              <strong :title="item.name">{{ item.name }}</strong>
              <small>{{ item.latestYear ? `${item.latestYear} 年 · ` : '' }}排除 {{ formatCount(item.featureCount) }} 个相交图斑</small>
            </div>
            <b>{{ formatArea(item.areaMu / 10000) }} 万亩</b>
          </div>
        </div>
      </section>

      <button
        v-if="analysisDone"
        type="button"
        class="analysis-export"
        data-testid="analysis-export-shp"
        :disabled="!completedCqlFilter || !totalLoadedCount"
        @click="exportShapefile"
      >
        <span>导出本次分析结果</span>
        <strong>SHP</strong>
      </button>

      <footer class="analysis-result-foot">
        {{ analysisRunning
          ? '正在逐批叠加候选图斑，最终统计将在加载完成后生成。'
          : '结果已排除初筛禁入范围；工程立项前仍需结合连片度、权属和现场踏勘复核。' }}
      </footer>
    </section>
  </div>
</template>

<script setup>
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue"
import { hongyuanTownshipDisplayOrder } from "@/config/townshipBoundaries"

const emit = defineEmits([
  "analysis-reset",
  "analysis-batch",
  "analysis-complete",
  "result-visibility",
])

const WFS_ENDPOINT = "/geoserver/ne/ows"
const DETAIL_TYPE_NAME = "ne:t_ecological_engineering_suitability_layer"
const EXPORT_TYPE_NAME = "ne:t_ecological_engineering_suitability_export_layer"
const PROJECT_TYPE_NAME = "ne:t_ecological_restoration_project_layer"
const PAGE_SIZE = 750
const EXCLUDED_PAGE_SIZE = 5000
const BATCH_DELAY_MS = 120
const MIN_ANALYSIS_DURATION_MS = 4500
const RESULT_COLOR = "#FFD43B"

const definitions = [
  {
    key: "forest-conservation",
    name: "封山育林",
    color: RESULT_COLOR,
  },
  {
    key: "grassland-improvement",
    name: "草原改良",
    color: RESULT_COLOR,
  },
  {
    key: "artificial-grass",
    name: "人工种草",
    color: RESULT_COLOR,
  },
]

const selectedTypes = ref([definitions[0].key])
const selectedTown = ref("")
const projectFilterMode = ref("same")
const recentYears = ref(3)
const endYear = ref(null)
const analysisState = ref("idle")
const errorText = ref("")
const completedCqlFilter = ref("")
const projectDataLatestYear = ref(null)
const projectCoverageState = ref("loading")
const resultRows = reactive({})
const distributionStats = reactive({})
const excludedProjectStats = reactive({})
let activeController = null
let analysisRunId = 0

const townshipOptions = hongyuanTownshipDisplayOrder
const analysisRunning = computed(() => analysisState.value === "running")
const analysisDone = computed(() => analysisState.value === "done")
const analysisCanStart = computed(() => (
  selectedTypes.value.length > 0
  && !analysisRunning.value
  && (projectFilterMode.value === "none" || projectCoverageState.value === "ready")
))
const analysisRows = computed(() => definitions)
const selectedTypeName = computed(() => definitions.find((item) => item.key === selectedTypes.value[0])?.name || "请选择工程")
const visibleResultRows = computed(() => definitions
  .filter((item) => selectedTypes.value.includes(item.key))
  .map((item) => resultRows[item.key] || createResultRow(item)))
const exclusionStartYear = computed(() => {
  const year = Number(endYear.value)
  return Number.isFinite(year) ? year - Number(recentYears.value) + 1 : null
})
const exclusionNote = computed(() => {
  const range = `${exclusionStartYear.value}—${endYear.value} 年`
  const coverage = projectDataLatestYear.value ? `项目库更新至 ${projectDataLatestYear.value} 年 · ` : ""
  const rule = projectFilterMode.value === "all"
    ? `排除 ${range} 内实施过任一工程的相交地块`
    : `排除 ${range} 内实施过同类工程的相交地块`
  return `${coverage}${rule}`
})
const statusText = computed(() => {
  if (analysisRunning.value) return "分析中"
  if (analysisDone.value) return "分析完成"
  return "等待分析"
})
const totalBaseCount = computed(() => visibleResultRows.value.reduce((sum, item) => sum + Number(item.baseCount || 0), 0))
const totalTargetCount = computed(() => visibleResultRows.value.reduce((sum, item) => sum + Number(item.targetCount || 0), 0))
const totalLoadedCount = computed(() => visibleResultRows.value.reduce((sum, item) => sum + Number(item.loadedCount || 0), 0))
const totalExcludedCount = computed(() => Math.max(0, totalBaseCount.value - totalTargetCount.value))
const totalLoadedAreaWanMu = computed(() => visibleResultRows.value.reduce((sum, item) => sum + Number(item.loadedAreaMu || 0), 0) / 10000)
const currentLoadedPage = computed(() => Math.max(0, ...visibleResultRows.value.map((item) => Number(item.loadedPage || 0))))
const excludedProjectRows = computed(() => Object.values(excludedProjectStats)
  .map((item) => ({ ...item, areaMu: Number(Number(item.areaMu || 0).toFixed(2)) }))
  .sort((a, b) => b.areaMu - a.areaMu))
const excludedProjectAreaWanMu = computed(() => excludedProjectRows.value.reduce((sum, item) => sum + Number(item.areaMu || 0), 0) / 10000)
const progressPercent = computed(() => {
  if (analysisDone.value) return 100
  if (!analysisRunning.value) return 0
  if (!totalTargetCount.value) return visibleResultRows.value.some((item) => item.status === "counting") ? 4 : 0
  return Math.min(99, Math.round((totalLoadedCount.value / totalTargetCount.value) * 100))
})
const progressText = computed(() => {
  const active = visibleResultRows.value.find((item) => ["counting", "loading"].includes(item.status))
  if (active?.status === "counting") return `正在核对${active.name}候选量与历史项目记录`
  if (active?.status === "loading") return `正在加载${active.name}可实施图斑，已读取 ${formatCount(active.loadedCount)} 个`
  if (analysisDone.value) return `分析完成，共形成 ${formatCount(totalLoadedCount.value)} 个可实施候选图斑`
  return "正在准备空间分析"
})
const resultScopeText = computed(() => {
  const town = selectedTown.value || "红原县全域"
  if (projectFilterMode.value === "none") return `${town} · 未排除历史项目`
  return `${town} · ${exclusionStartYear.value}—${endYear.value} 年未实施条件`
})

watch([selectedTypes, selectedTown, projectFilterMode, recentYears, endYear], () => {
  if (!analysisDone.value && analysisState.value !== "error") return
  analysisState.value = "idle"
  errorText.value = ""
  completedCqlFilter.value = ""
  Object.keys(resultRows).forEach((key) => delete resultRows[key])
  emit("analysis-reset")
})

onMounted(() => loadProjectDataCoverage())
onBeforeUnmount(() => cancelActiveRun())

function createResultRow(definition) {
  return {
    ...definition,
    status: "idle",
    baseCount: 0,
    targetCount: 0,
    loadedCount: 0,
    loadedAreaMu: 0,
    loadedPage: 0,
    visible: true,
  }
}

function toggleType(key) {
  if (analysisRunning.value) return
  if (selectedTypes.value[0] === key) return
  selectedTypes.value = [key]
}

async function startAnalysis() {
  if (!analysisCanStart.value) return
  cancelActiveRun()
  const runId = ++analysisRunId
  const startedAt = window.performance?.now?.() || Date.now()
  activeController = new AbortController()
  analysisState.value = "running"
  errorText.value = ""
  completedCqlFilter.value = ""
  const filterSnapshot = getFilterSnapshot()
  Object.keys(resultRows).forEach((key) => delete resultRows[key])
  Object.keys(distributionStats).forEach((key) => delete distributionStats[key])
  Object.keys(excludedProjectStats).forEach((key) => delete excludedProjectStats[key])
  selectedTypes.value.forEach((key) => {
    const definition = definitions.find((item) => item.key === key)
    resultRows[key] = createResultRow(definition)
    resultRows[key].status = "counting"
  })
  emit("analysis-reset")

  try {
    await Promise.all(selectedTypes.value.map(async (key) => {
      const row = resultRows[key]
      const [baseCount, targetCount] = await Promise.all([
        fetchFeatureCount(buildCqlFilter(key, false, filterSnapshot), activeController.signal),
        fetchFeatureCount(buildCqlFilter(key, true, filterSnapshot), activeController.signal),
      ])
      assertActiveRun(runId)
      row.baseCount = baseCount
      row.targetCount = targetCount
      row.status = "queued"
    }))

    await Promise.all([
      (async () => {
        for (const key of selectedTypes.value) {
          assertActiveRun(runId)
          await loadDatasetPages(resultRows[key], filterSnapshot, runId, activeController.signal)
        }
      })(),
      Promise.all(selectedTypes.value.map((key) =>
        loadExcludedProjectSummary(key, filterSnapshot, runId, activeController.signal))),
    ])
    assertActiveRun(runId)
    const elapsed = (window.performance?.now?.() || Date.now()) - startedAt
    await waitForDuration(Math.max(0, MIN_ANALYSIS_DURATION_MS - elapsed), activeController.signal)
    assertActiveRun(runId)
    analysisState.value = "done"
    completedCqlFilter.value = buildExportCqlFilter(filterSnapshot)
    emit("analysis-complete", {
      rows: visibleResultRows.value,
      filters: filterSnapshot,
      statistics: getStatisticsSnapshot(filterSnapshot),
    })
  } catch (error) {
    if (error?.name === "AbortError" || runId !== analysisRunId) return
    analysisState.value = "error"
    errorText.value = "工程适宜区服务暂时无法连接，请稍后重试。"
    completedCqlFilter.value = ""
    emit("analysis-reset")
  } finally {
    if (runId === analysisRunId) activeController = null
  }
}

async function loadDatasetPages(row, filters, runId, signal) {
  row.status = "loading"
  if (!row.targetCount) {
    row.status = "done"
    return
  }
  let startIndex = 0
  while (startIndex < row.targetCount) {
    assertActiveRun(runId)
    const geojson = await fetchFeaturePage(buildCqlFilter(row.key, true, filters), startIndex, signal)
    assertActiveRun(runId)
    const features = Array.isArray(geojson.features) ? geojson.features : []
    if (!features.length) break
    row.loadedPage += 1
    row.loadedCount += features.length
    row.loadedAreaMu += features.reduce((sum, feature) => sum + Number(feature?.properties?.source_area_mu || 0), 0)
    updateDistributionStats(features, filters)
    emit("analysis-batch", {
      key: row.key,
      name: row.name,
      color: row.color,
      features,
      loadedCount: row.loadedCount,
      targetCount: row.targetCount,
    })
    startIndex += features.length
    await yieldForMap(signal)
  }
  row.status = "done"
}

function buildCqlFilter(engineeringType, applyProjectFilter, filters = getFilterSnapshot()) {
  const conditions = [`engineering_type='${escapeCql(engineeringType)}'`]
  conditions.push(...buildBusinessCqlConditions(filters, applyProjectFilter))
  return conditions.join(" AND ")
}

function buildExportCqlFilter(filters) {
  const types = filters.engineeringTypes.map((key) => `'${escapeCql(key)}'`).join(",")
  const conditions = [`eng_type IN (${types})`]
  if (filters.township) conditions.push(`town_name='${escapeCql(filters.township)}'`)
  if (filters.projectFilterMode !== "none") {
    const field = filters.projectFilterMode === "all" ? "last_year" : "same_year"
    conditions.push(`(${field} IS NULL OR ${field}<${Number(filters.startYear)})`)
  }
  return conditions.join(" AND ")
}

function buildBusinessCqlConditions(filters, applyProjectFilter) {
  const conditions = []
  if (filters.township) conditions.push(`township_name='${escapeCql(filters.township)}'`)
  if (applyProjectFilter && filters.projectFilterMode !== "none") {
    const field = filters.projectFilterMode === "all" ? "latest_project_year" : "latest_same_type_project_year"
    conditions.push(`(${field} IS NULL OR ${field}<${Number(filters.startYear)})`)
  }
  return conditions
}

function buildExcludedProjectCqlFilter(engineeringType, filters) {
  const conditions = [`engineering_type='${escapeCql(engineeringType)}'`]
  if (filters.township) conditions.push(`township_name='${escapeCql(filters.township)}'`)
  if (filters.projectFilterMode === "none") return "EXCLUDE"
  const field = filters.projectFilterMode === "all" ? "latest_project_year" : "latest_same_type_project_year"
  conditions.push(`${field}>=${Number(filters.startYear)}`)
  return conditions.join(" AND ")
}

async function fetchFeatureCount(cqlFilter, signal) {
  const params = new URLSearchParams({
    service: "WFS",
    version: "2.0.0",
    request: "GetFeature",
    typeNames: DETAIL_TYPE_NAME,
    resultType: "hits",
    CQL_FILTER: cqlFilter,
  })
  const response = await fetch(`${WFS_ENDPOINT}?${params}`, { credentials: "same-origin", signal })
  if (!response.ok) throw new Error(`HTTP ${response.status}`)
  const text = await response.text()
  const matched = text.match(/numberMatched="(\d+)"/i) || text.match(/numberOfFeatures="(\d+)"/i)
  return Number(matched?.[1] || 0)
}

async function fetchFeaturePage(cqlFilter, startIndex, signal) {
  const params = new URLSearchParams({
    service: "WFS",
    version: "2.0.0",
    request: "GetFeature",
    typeNames: DETAIL_TYPE_NAME,
    outputFormat: "application/json",
    srsName: "EPSG:4326",
    count: String(PAGE_SIZE),
    startIndex: String(startIndex),
    sortBy: "id",
    CQL_FILTER: cqlFilter,
  })
  const response = await fetch(`${WFS_ENDPOINT}?${params}`, { credentials: "same-origin", signal })
  if (!response.ok) throw new Error(`HTTP ${response.status}`)
  return response.json()
}

async function loadExcludedProjectSummary(engineeringType, filters, runId, signal) {
  if (filters.projectFilterMode === "none") return
  const yearField = filters.projectFilterMode === "all" ? "latest_project_year" : "latest_same_type_project_year"
  const nameField = filters.projectFilterMode === "all" ? "latest_project_name" : "latest_same_type_project_name"
  const cqlFilter = buildExcludedProjectCqlFilter(engineeringType, filters)
  let startIndex = 0
  while (true) {
    assertActiveRun(runId)
    const geojson = await fetchExcludedProjectPage(cqlFilter, yearField, nameField, startIndex, signal)
    assertActiveRun(runId)
    const features = Array.isArray(geojson.features) ? geojson.features : []
    features.forEach((feature) => {
      const properties = feature?.properties || {}
      const projectName = String(properties[nameField] || "未标注项目").trim() || "未标注项目"
      const year = Number(properties[yearField]) || null
      const current = excludedProjectStats[projectName] || {
        name: projectName,
        latestYear: year,
        featureCount: 0,
        areaMu: 0,
      }
      current.latestYear = Math.max(Number(current.latestYear || 0), Number(year || 0)) || null
      current.featureCount += 1
      current.areaMu += Number(properties.source_area_mu || 0)
      excludedProjectStats[projectName] = current
    })
    if (features.length < EXCLUDED_PAGE_SIZE) break
    startIndex += features.length
  }
}

async function fetchExcludedProjectPage(cqlFilter, yearField, nameField, startIndex, signal) {
  const params = new URLSearchParams({
    service: "WFS",
    version: "2.0.0",
    request: "GetFeature",
    typeNames: DETAIL_TYPE_NAME,
    outputFormat: "application/json",
    count: String(EXCLUDED_PAGE_SIZE),
    startIndex: String(startIndex),
    sortBy: "id",
    propertyName: `id,source_area_mu,${yearField},${nameField}`,
    CQL_FILTER: cqlFilter,
  })
  const response = await fetch(`${WFS_ENDPOINT}?${params}`, { credentials: "same-origin", signal })
  if (!response.ok) throw new Error(`HTTP ${response.status}`)
  return response.json()
}

async function loadProjectDataCoverage() {
  projectCoverageState.value = "loading"
  try {
    const params = new URLSearchParams({
      service: "WFS",
      version: "2.0.0",
      request: "GetFeature",
      typeNames: PROJECT_TYPE_NAME,
      outputFormat: "application/json",
      propertyName: "year",
      sortBy: "year D",
      count: "1",
      CQL_FILTER: "year IS NOT NULL",
    })
    const response = await fetch(`${WFS_ENDPOINT}?${params}`, { credentials: "same-origin" })
    if (!response.ok) throw new Error(`HTTP ${response.status}`)
    const geojson = await response.json()
    const latestYear = Number(geojson.features?.[0]?.properties?.year)
    if (!Number.isFinite(latestYear)) throw new Error("项目库缺少有效实施年份")
    projectDataLatestYear.value = latestYear
    if (analysisState.value === "idle") endYear.value = latestYear
    projectCoverageState.value = "ready"
  } catch (error) {
    projectCoverageState.value = "error"
    console.warn("[engineering-analysis] project data coverage unavailable", error)
  }
}

function updateDistributionStats(features, filters) {
  const field = filters.township ? "location_unit_name" : "township_name"
  features.forEach((feature) => {
    const properties = feature?.properties || {}
    const name = String(properties[field] || "未标注").trim() || "未标注"
    const current = distributionStats[name] || { name, featureCount: 0, areaMu: 0 }
    current.featureCount += 1
    current.areaMu += Number(properties.source_area_mu || 0)
    distributionStats[name] = current
  })
}

function getStatisticsSnapshot(filters) {
  return {
    engineeringType: filters.engineeringTypes[0],
    engineeringName: selectedTypeName.value,
    scopeName: filters.township || "红原县全域",
    dimensionLabel: filters.township ? "村" : "乡镇",
    baseCount: totalBaseCount.value,
    excludedCount: totalExcludedCount.value,
    finalCount: totalLoadedCount.value,
    finalAreaMu: totalLoadedAreaWanMu.value * 10000,
    projectDataLatestYear: projectDataLatestYear.value,
    filters,
    excludedProjects: excludedProjectRows.value.map((item) => ({ ...item })),
    distribution: Object.values(distributionStats)
      .map((item) => ({ ...item, areaMu: Number(item.areaMu.toFixed(2)) }))
      .sort((a, b) => b.areaMu - a.areaMu),
  }
}

function toggleResultVisibility(item) {
  if (!item || item.status === "idle") return
  item.visible = item.visible === false
  emit("result-visibility", { key: item.key, visible: item.visible })
}

function exportShapefile() {
  if (!analysisDone.value || !completedCqlFilter.value || !totalLoadedCount.value) return
  const date = new Date()
  const stamp = [date.getFullYear(), String(date.getMonth() + 1).padStart(2, "0"), String(date.getDate()).padStart(2, "0")].join("")
  const filename = `ecological_engineering_suitability_${stamp}.zip`
  const params = new URLSearchParams({
    service: "WFS",
    version: "2.0.0",
    request: "GetFeature",
    typeNames: EXPORT_TYPE_NAME,
    outputFormat: "shape-zip",
    srsName: "EPSG:4326",
    CQL_FILTER: completedCqlFilter.value,
    format_options: `charset:UTF-8;filename:${filename}`,
  })
  const link = document.createElement("a")
  link.href = `${WFS_ENDPOINT}?${params}`
  link.download = filename
  document.body.appendChild(link)
  link.click()
  link.remove()
}

function resultStatusLabel(item) {
  if (item.status === "counting") return "核对中"
  if (item.status === "queued") return "等待加载"
  if (item.status === "loading") return "加载中"
  if (item.status === "error") return "异常"
  return item.visible === false ? "已隐藏" : "地图显示"
}

function getFilterSnapshot() {
  return {
    engineeringTypes: [...selectedTypes.value],
    township: selectedTown.value,
    projectFilterMode: projectFilterMode.value,
    recentYears: recentYears.value,
    startYear: exclusionStartYear.value,
    endYear: endYear.value,
  }
}

function cancelActiveRun() {
  if (activeController) activeController.abort()
  activeController = null
}

function assertActiveRun(runId) {
  if (runId !== analysisRunId) throw new DOMException("Analysis superseded", "AbortError")
}

function yieldForMap(signal) {
  return waitForDuration(BATCH_DELAY_MS, signal)
}

function waitForDuration(duration, signal) {
  if (!duration) return Promise.resolve()
  if (signal?.aborted) return Promise.reject(new DOMException("Analysis aborted", "AbortError"))
  return new Promise((resolve, reject) => {
    const timer = window.setTimeout(done, duration)
    signal?.addEventListener("abort", abort, { once: true })
    function done() {
      signal?.removeEventListener("abort", abort)
      resolve()
    }
    function abort() {
      window.clearTimeout(timer)
      reject(new DOMException("Analysis aborted", "AbortError"))
    }
  })
}

function escapeCql(value) {
  return String(value ?? "").replace(/'/g, "''")
}

function formatCount(value) {
  const number = Number(value)
  return Number.isFinite(number) ? Math.round(number).toLocaleString("zh-CN") : "0"
}

function formatArea(value) {
  const number = Number(value)
  if (!Number.isFinite(number)) return "0"
  if (number >= 100) return number.toFixed(1)
  if (number >= 10) return number.toFixed(2)
  return number.toFixed(3)
}
</script>

<style scoped lang="scss">
.engineering-analysis-panel {
  position: absolute;
  z-index: 8;
  top: 168px;
  bottom: 116px;
  left: 32px;
  width: 456px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  overflow-y: auto;
  pointer-events: auto;
  scrollbar-width: thin;
  scrollbar-color: rgba(48, 220, 255, 0.5) transparent;
}

.analysis-card {
  position: relative;
  flex: 0 0 auto;
  overflow: hidden;
  padding: 15px 16px 16px;
  border: 1px solid var(--v22-border, rgba(91, 224, 244, 0.46));
  border-radius: 2px;
  background:
    radial-gradient(circle at 12% 0, rgba(74, 210, 233, 0.12), transparent 38%),
    linear-gradient(160deg, rgba(6, 38, 54, 0.95), rgba(2, 20, 32, 0.92));
  box-shadow:
    inset 0 1px 0 rgba(180, 252, 255, 0.12),
    inset 0 0 0 1px rgba(91, 224, 244, 0.08),
    0 12px 28px rgba(0, 8, 16, 0.3);
  backdrop-filter: blur(7px);
  box-sizing: border-box;

  &::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 2px;
    background: linear-gradient(90deg, #30dcff, rgba(48, 220, 255, 0.18), transparent);
  }
}

.analysis-workbench.is-running {
  border-color: rgba(48, 220, 255, 0.56);
  box-shadow: inset 0 0 28px rgba(48, 220, 255, 0.12), 0 18px 32px rgba(0, 0, 0, 0.36);
}

.analysis-card-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  min-height: 36px;

  > div {
    position: relative;
    display: grid;
    gap: 2px;
    padding-left: 10px;

    &::before {
      content: "";
      position: absolute;
      top: 3px;
      bottom: 3px;
      left: 0;
      width: 3px;
      background: #30dcff;
      box-shadow: 0 0 9px rgba(48, 220, 255, 0.72);
    }
  }

  span {
    color: #fff;
    font-size: 16px;
    font-weight: 800;
    line-height: 20px;
  }

  small {
    color: rgba(213, 247, 251, 0.62);
    font-size: 11px;
    line-height: 14px;
  }

  > strong {
    min-width: 58px;
    padding: 3px 7px;
    border: 1px solid rgba(126, 251, 246, 0.22);
    border-radius: 2px;
    background: rgba(48, 220, 255, 0.07);
    color: var(--v22-accent, #7efbf6);
    font-size: 11px;
    line-height: 16px;
    text-align: center;
  }
}

.analysis-section-title {
  display: grid;
  grid-template-columns: 28px minmax(0, 1fr) auto;
  align-items: center;
  gap: 7px;
  margin-top: 11px;
  color: rgba(235, 253, 255, 0.9);

  &.is-filter { margin-top: 14px; }
  span { color: #30dcff; font-family: var(--screen-font-number, D-DIN, Arial, sans-serif); font-size: 13px; }
  strong { font-size: 12px; }
  em { color: rgba(213, 247, 251, 0.48); font-size: 10px; font-style: normal; }
}

.analysis-type-list {
  display: grid;
  gap: 6px;
  margin-top: 8px;
}

.analysis-type-row {
  display: grid;
  grid-template-columns: 20px minmax(0, 1fr);
  align-items: center;
  gap: 9px;
  min-height: 40px;
  padding: 6px 10px;
  border: 1px solid rgba(48, 220, 255, 0.15);
  border-radius: 2px;
  background: rgba(3, 22, 33, 0.54);
  color: #fff;
  text-align: left;
  cursor: pointer;
  transition: 0.18s ease;

  &:hover:not(:disabled), &.is-selected {
    border-color: rgba(126, 251, 246, 0.56);
    background: linear-gradient(90deg, rgba(48, 220, 255, 0.14), rgba(48, 220, 255, 0.025));
  }
  &:disabled { cursor: default; }
}

.analysis-type-check {
  width: 14px;
  height: 14px;
  border: 1px solid rgba(191, 244, 250, 0.42);
  border-radius: 50%;
  box-sizing: border-box;
}

.analysis-type-row.is-selected .analysis-type-check {
  border-color: var(--type-color);
  background: transparent;
  box-shadow: 0 0 9px color-mix(in srgb, var(--type-color) 65%, transparent);

  &::after {
    content: "";
    display: block;
    width: 8px;
    height: 8px;
    margin: 2px;
    border-radius: 50%;
    background: var(--type-color);
    box-shadow: 0 0 6px color-mix(in srgb, var(--type-color) 80%, transparent);
  }
}

.analysis-type-main { min-width: 0; }
.analysis-type-main strong { color: var(--v22-text, #fff); font-size: 13px; font-weight: 650; }

.analysis-filter-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 7px 9px;
  margin-top: 8px;
}
.analysis-filter-grid label {
  display: grid;
  grid-template-columns: 62px minmax(0, 1fr);
  align-items: center;
  height: 36px;
  padding-left: 9px;
  border: 1px solid rgba(48, 220, 255, 0.16);
  border-radius: 2px;
  background: rgba(3, 22, 33, 0.5);
  box-sizing: border-box;

  &.is-wide { grid-column: 1 / -1; }

  &:focus-within { border-color: rgba(126, 251, 246, 0.5); background: rgba(9, 43, 57, 0.68); }
  > span { color: var(--v22-text-muted, rgba(220, 249, 252, 0.66)); font-size: 10px; }
  select {
    min-width: 0;
    height: 34px;
    padding: 0 22px 0 4px;
    border: 0;
    outline: 0;
    background: transparent;
    color: var(--v22-text, #fff);
    color-scheme: dark;
    font: inherit;
    font-size: 11px;
    cursor: pointer;
  }
}

.analysis-filter-note {
  display: flex;
  align-items: center;
  gap: 7px;
  min-height: 28px;
  margin-top: 7px;
  padding: 5px 8px;
  border: 1px solid rgba(242, 184, 75, 0.14);
  border-left: 2px solid rgba(242, 184, 75, 0.72);
  border-radius: 2px;
  background: linear-gradient(90deg, rgba(242, 184, 75, 0.09), rgba(242, 184, 75, 0.025));
  color: rgba(255, 239, 199, 0.82);
  font-size: 10px;
  line-height: 16px;
  box-sizing: border-box;
  i { width: 6px; height: 6px; border-radius: 50%; background: #f2b84b; box-shadow: 0 0 8px rgba(242, 184, 75, 0.65); }
}

.analysis-submit {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  width: 100%;
  min-height: 42px;
  margin-top: 10px;
  border: 1px solid rgba(48, 220, 255, 0.72);
  border-radius: 2px;
  background: linear-gradient(90deg, rgba(28, 155, 190, 0.38), rgba(48, 220, 255, 0.2), rgba(28, 155, 190, 0.3));
  color: #fff;
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
  box-shadow: inset 0 0 18px rgba(48, 220, 255, 0.1), 0 0 14px rgba(48, 220, 255, 0.09);

  strong { color: #bff8ff; font-family: var(--screen-font-number, D-DIN, Arial, sans-serif); font-size: 12px; }
  transition: border-color 160ms ease, background 160ms ease, box-shadow 160ms ease;
  &:hover:not(:disabled) {
    border-color: rgba(126, 251, 246, 0.92);
    background: linear-gradient(90deg, rgba(31, 177, 210, 0.46), rgba(48, 220, 255, 0.25), rgba(31, 177, 210, 0.38));
    box-shadow: inset 0 0 18px rgba(48, 220, 255, 0.14), 0 0 18px rgba(48, 220, 255, 0.12);
  }
  &:disabled { opacity: 0.58; cursor: not-allowed; }
}

.analysis-submit-spinner {
  width: 14px;
  height: 14px;
  border: 2px solid rgba(255, 255, 255, 0.3);
  border-top-color: #fff;
  border-radius: 50%;
  animation: analysisSpin 0.8s linear infinite;
}

.analysis-progress {
  display: grid;
  grid-template-columns: 112px minmax(0, 1fr);
  align-items: center;
  gap: 9px;
  margin-top: 8px;
  > div { height: 4px; overflow: hidden; border-radius: 2px; background: rgba(48, 220, 255, 0.12); }
  i { display: block; height: 100%; background: linear-gradient(90deg, #1b9fd0, #30dcff); transition: width 0.25s ease; }
  > div.is-indeterminate i { width: 34%; animation: analysisProgressSweep 1.25s ease-in-out infinite; }
  span { overflow: hidden; color: rgba(213, 247, 251, 0.64); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
}
.analysis-error { margin: 8px 0 0; color: #ffb2a7; font-size: 11px; }

.analysis-result-card { padding-bottom: 13px; }
.analysis-card-head.is-result { min-height: 32px; }
.analysis-result-summary {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 6px;
  margin-top: 9px;
  > div { padding: 9px 6px 8px; border: 1px solid rgba(48, 220, 255, 0.14); border-radius: 2px; background: rgba(3, 22, 33, 0.46); text-align: center; }
  span, small { display: block; color: var(--v22-text-muted, rgba(213, 247, 251, 0.6)); font-size: 9px; }
  strong { display: inline-block; margin: 3px 2px; color: #fff; font-family: var(--screen-font-number, D-DIN, Arial, sans-serif); font-size: 20px; }
  .is-excluded strong { color: #f2b84b; }
  .is-final strong { color: #30dcff; }
}

.analysis-result-list { display: grid; gap: 5px; margin-top: 8px; }
.analysis-result-row {
  display: grid;
  grid-template-columns: 6px minmax(0, 1fr) auto 48px;
  align-items: center;
  gap: 8px;
  min-height: 42px;
  padding: 5px 8px;
  border: 1px solid rgba(48, 220, 255, 0.14);
  border-radius: 2px;
  background: rgba(3, 22, 33, 0.46);
  color: #fff;
  text-align: left;
  cursor: pointer;
  > i { width: 4px; height: 28px; background: var(--type-color); box-shadow: 0 0 8px color-mix(in srgb, var(--type-color) 50%, transparent); }
  > div { display: grid; gap: 2px; }
  > div strong { font-size: 12px; }
  > div span { color: rgba(213, 247, 251, 0.58); font-size: 9px; }
  > b { color: #bff8ff; font-family: var(--screen-font-number, D-DIN, Arial, sans-serif); font-size: 11px; font-weight: 600; }
  > em { color: #30dcff; font-size: 9px; font-style: normal; text-align: right; }
  &.is-hidden { opacity: 0.45; }
  &.is-loading { border-color: rgba(48, 220, 255, 0.32); }
}

.analysis-excluded-projects {
  margin-top: 10px;
  padding-top: 9px;
  border-top: 1px solid rgba(242, 184, 75, 0.2);

  > header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 7px;
  }

  > header strong { color: #f2fbff; font-size: 11px; }
  > header em { color: #ffd56f; font-size: 9px; font-style: normal; }
}

.analysis-excluded-project-list { display: grid; gap: 5px; }
.analysis-excluded-project-row {
  display: grid;
  grid-template-columns: 18px minmax(0, 1fr) auto;
  align-items: center;
  gap: 7px;
  min-height: 40px;
  padding: 5px 7px;
  border: 1px solid rgba(242, 184, 75, 0.16);
  background: linear-gradient(90deg, rgba(242, 184, 75, 0.07), rgba(3, 22, 33, 0.28));
  box-sizing: border-box;

  > span { color: #ffd56f; font: 10px var(--screen-font-number, D-DIN, Arial, sans-serif); text-align: center; }
  > div { display: grid; gap: 1px; min-width: 0; }
  > div strong { overflow: hidden; color: rgba(244, 253, 255, 0.9); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
  > div small { color: rgba(213, 247, 251, 0.56); font-size: 9px; }
  > b { color: #ffd56f; font: 600 10px var(--screen-font-number, D-DIN, Arial, sans-serif); white-space: nowrap; }
}

.analysis-export {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  height: 36px;
  margin-top: 9px;
  border: 1px solid rgba(48, 220, 255, 0.48);
  border-radius: 2px;
  background: linear-gradient(90deg, rgba(48, 220, 255, 0.17), rgba(31, 144, 197, 0.13));
  color: #e9fdff;
  font-size: 11px;
  cursor: pointer;
  strong { color: #30dcff; font-size: 12px; }
  &:hover:not(:disabled) { border-color: rgba(48, 220, 255, 0.82); background-color: rgba(48, 220, 255, 0.12); }
  &:disabled { opacity: 0.4; cursor: not-allowed; }
}

.analysis-result-foot {
  margin-top: 8px;
  color: rgba(213, 247, 251, 0.48);
  font-size: 9px;
  line-height: 15px;
}

@keyframes analysisSpin { to { transform: rotate(360deg); } }
@keyframes analysisProgressSweep {
  0% { transform: translateX(-110%); }
  55% { transform: translateX(110%); }
  100% { transform: translateX(290%); }
}

@media (prefers-reduced-motion: reduce) {
  .analysis-submit-spinner { animation: none; }
  .analysis-progress > div.is-indeterminate i { width: 100%; animation: none; opacity: 0.55; }
  .analysis-progress i,
  .analysis-type-row,
  .analysis-submit { transition: none; }
}
</style>
