<script setup lang="ts">
import { IotDeviceApi } from '@/api/pms/device'
import { cancelAllRequests, IotStatApi } from '@/api/pms/stat'
import { useTableComponents } from '@/components/ZmTable/useTableComponents'
import { rangeShortcuts } from '@/utils/formatTime'
import { useMqtt } from '@/utils/useMqtt'
import {
  Dimensions,
  formatChartValue,
  HeaderItem,
  IotTdItem,
  mapToDimension,
  normalizeRangeValue,
  RangeSettingDraft,
  RawDimension,
  withDisplayStyle
} from '@/utils/useSocketBus'
import { useFullscreen } from '@vueuse/core'
import { snapdom } from '@zumer/snapdom'
import { Crop, DataLine, FullScreen, Odometer, Setting } from '@element-plus/icons-vue'
import dayjs from 'dayjs'
import * as echarts from 'echarts'

const { query } = useRoute()

const data = ref({
  deviceCode: query.code || '',
  deviceName: query.name || '',
  lastInlineTime: query.time || '',
  ifInline: query.ifInline === '3',
  dept: query.dept || '',
  vehicle: query.vehicle || '',
  carOnline: query.carOnline === 'true',
  mqttUrl: query.mqttUrl || ''
})

const headerCenterContent: HeaderItem[] = [
  { label: '设备名称', key: 'deviceName' },
  { label: '所属部门', key: 'dept' },
  { label: '车牌号码', key: 'vehicle', judgment: true },
  { label: '最后上报时间', key: 'lastInlineTime' }
]

const tagProps = { size: 'default', round: true } as const

const headerTagContent: HeaderItem[] = [
  { label: '网关', key: 'ifInline' },
  { label: '北斗', key: 'carOnline', judgment: true }
]

const dimensions = ref<Dimensions[]>([])
const gatewayDimensions = ref<Dimensions[]>([])
// const carDimensions = ref<Dimensions[]>([])

const dimensionsContent = computed(() => [
  {
    label: '网关数采',
    icon: DataLine,
    value: gatewayDimensions.value,
    judgment: false
  }
  // {
  //   label: '中航北斗',
  //   icon: TrendCharts,
  //   value: carDimensions.value,
  //   countColor: 'text-indigo-600',
  //   countBg: 'bg-indigo-50',
  //   judgment: true
  // }
])

const disabledDimensions = ref<string[]>(['online', 'vehicle_name'])
const selectedDimension = ref<Record<string, boolean>>({})
const dimensionLoading = ref(false)
const message = useMessage()
const { connect, destroy, isConnected, subscribe } = useMqtt()

const DEFAULT_CHART_WINDOW_MINUTES = 10
const MIN_CHART_WINDOW_MINUTES = 1
const MAX_CHART_WINDOW_MINUTES = 120
const chartWindowMinutes = ref(DEFAULT_CHART_WINDOW_MINUTES)
const chartWindowMs = computed(() => chartWindowMinutes.value * 60 * 1000)

function normalizeChartWindowMinutes(value?: number | null) {
  const numberValue = Number(value)

  if (!Number.isFinite(numberValue)) return DEFAULT_CHART_WINDOW_MINUTES

  return Math.min(
    MAX_CHART_WINDOW_MINUTES,
    Math.max(MIN_CHART_WINDOW_MINUTES, Math.round(numberValue))
  )
}

function getDateRangeByWindow(endTime = dayjs()) {
  const end = dayjs(endTime).isValid() ? dayjs(endTime) : dayjs()

  return [
    end.subtract(chartWindowMinutes.value, 'minute').format('YYYY-MM-DD HH:mm:ss'),
    end.format('YYYY-MM-DD HH:mm:ss')
  ]
}

function getInitialDateRange() {
  return getDateRangeByWindow()
}

function isRangeGreaterThan120Minutes(range: string[]) {
  const begin = dayjs(range[0])
  const end = dayjs(range[1])

  return begin.isValid() && end.isValid() && end.diff(begin, 'minute', true) > 120
}

const REALTIME_RIGHT_BLANK_MS = 60 * 1000
const selectedDate = ref<string[]>(getInitialDateRange())
const chartRef = ref<HTMLDivElement | null>(null)
const targetArea = ref<HTMLElement | null>(null)
const downloadRef = ref<HTMLElement | null>(null)
const chartLoading = ref(false)
const chartRealtimeMode = ref(true)
const token = ref('')
let chart: echarts.ECharts | null = null
let chartLoadVersion = 0
let realtimeUpdateFrame: number | null = null
const pendingRealtimeSeriesNames = new Set<string>()
const { toggle, isFullscreen } = useFullscreen(targetArea)

interface ChartPoint {
  ts: number
  value: number
}

interface ChartData {
  [key: Dimensions['name']]: ChartPoint[]
}

const chartData = ref<ChartData>({})
const resizeChart = () => chart?.resize()

watch(isConnected, (newVal) => {
  if (newVal && data.value.mqttUrl) {
    subscribe(data.value.mqttUrl as string)
  }
})

async function loadDimensions() {
  if (!query.id) return
  dimensionLoading.value = true
  try {
    const gateway = [
      ...(((await IotDeviceApi.getIotDeviceTds(Number(query.id))) as IotTdItem[]) ?? [])
    ]
      .sort((a, b) => (b.modelOrder ?? 0) - (a.modelOrder ?? 0))
      .map(mapToDimension)
    // const car = [
    //   ...(((await IotDeviceApi.getIotDeviceZHBDTds(Number(query.id))) as IotTdItem[]) ?? [])
    // ]
    //   .sort((a, b) => (b.modelOrder ?? 0) - (a.modelOrder ?? 0))
    //   .map(mapToDimension)

    const uniqueDimensions = new Map<string, RawDimension>()

    gateway.forEach((item) => {
      uniqueDimensions.set(`${item.identifier}|${item.name}`, item)
    })
    // car.forEach((item) => {
    //   uniqueDimensions.set(`${item.identifier}|${item.name}`, item)
    // })

    dimensions.value = Array.from(uniqueDimensions.values()).map(withDisplayStyle)

    gatewayDimensions.value = dimensions.value.filter((d) =>
      gateway.some((g) => g.identifier === d.identifier)
    )
    // carDimensions.value = dimensions.value.filter((d) =>
    //   car.some((c) => c.identifier === d.identifier)
    // )

    selectedDimension.value = Object.fromEntries(
      dimensions.value.map((item, index) => [item.name, index === 0])
    )
  } catch (e) {
    console.error(e)
  } finally {
    dimensionLoading.value = false
  }
}

const { ZmTable, ZmTableColumn } = useTableComponents<RangeSettingDraft>()
const rangeDialogVisible = ref(false)
const rangeSettingDrafts = ref<RangeSettingDraft[]>([])

function getDimensionByName(name: string) {
  return dimensions.value.find((item) => item.name === name)
}

function getDatasetMaxValue(name: string) {
  const dataset = chartData.value[name] || []
  let maxValue = -Infinity

  dataset.forEach(({ value }) => {
    const numberValue = Number(value)
    if (Number.isFinite(numberValue) && numberValue > maxValue) {
      maxValue = numberValue
    }
  })

  const currentValue = normalizeRangeValue(getDimensionByName(name)?.value)
  if (currentValue !== undefined && currentValue > maxValue) {
    maxValue = currentValue
  }

  return Number.isFinite(maxValue) ? maxValue : 0
}

function getSeriesRange(name: string) {
  const dimension = getDimensionByName(name)
  const configuredMin = normalizeRangeValue(dimension?.minValue)
  const configuredMax = normalizeRangeValue(dimension?.maxValue)
  const min = configuredMin ?? 0
  const max = configuredMax ?? getDatasetMaxValue(name)
  const normalizedMax = max > min ? max : min + 1

  return {
    min,
    max: normalizedMax,
    labelMin: min,
    labelMax: normalizedMax,
    configured: configuredMin !== undefined && configuredMax !== undefined
  }
}

const chartLegendItems = computed(() =>
  dimensions.value
    .filter((item) => selectedDimension.value[item.name])
    .map((item) => {
      const range = getSeriesRange(item.name)

      return {
        name: item.name,
        color: item.color,
        bgHover: item.bgHover,
        selected: true,
        max: formatChartValue(range.labelMax),
        min: formatChartValue(range.labelMin)
      }
    })
)

function getTrendYAxisOptions() {
  return {
    type: 'value',
    min: 0,
    max: 100,
    interval: 10,
    show: true,
    axisLabel: {
      show: false
    },
    axisLine: {
      show: false
    },
    axisTick: {
      show: false
    },
    splitLine: {
      show: true,
      lineStyle: {
        color: 'rgba(148, 163, 184, 0.22)',
        width: 1
      }
    }
  }
}

function getPointTimestamp(value: unknown) {
  if (value === null || value === undefined || value === '') {
    return Date.now()
  }

  const numberValue = Number(value)

  if (Number.isFinite(numberValue) && numberValue > 0) {
    return numberValue > 1_000_000_000_000 ? numberValue : dayjs.unix(numberValue).valueOf()
  }

  const parsedValue = dayjs(value as string).valueOf()

  return Number.isFinite(parsedValue) ? parsedValue : Date.now()
}

function mapChartPoint({ ts, value }: ChartPoint, name: string) {
  const numberValue = Number(value)

  if (!Number.isFinite(numberValue)) return [ts, null, null]

  const { min, max } = getSeriesRange(name)
  const denominator = max - min || 1
  const mappedValue = ((numberValue - min) / denominator) * 100
  const displayValue = Math.min(100, Math.max(0, mappedValue))

  return [ts, Number(displayValue.toFixed(2)), numberValue]
}

function getLatestTimestamp() {
  let latest: number | null = null

  Object.values(chartData.value).forEach((series) => {
    series.forEach((point) => {
      if (latest === null || point.ts > latest) {
        latest = point.ts
      }
    })
  })

  return latest
}

function getSelectedDateRangeTimestamps() {
  const fallbackEnd = Date.now()
  const startTs = dayjs(selectedDate.value[0]).valueOf()
  const endTs = dayjs(selectedDate.value[1]).valueOf()

  if (Number.isFinite(startTs) && Number.isFinite(endTs) && startTs < endTs) {
    return { startTs, endTs }
  }

  return {
    startTs: fallbackEnd - chartWindowMs.value,
    endTs: fallbackEnd
  }
}

function getRealtimeXAxisMax(anchorTs = Date.now()) {
  return anchorTs + REALTIME_RIGHT_BLANK_MS
}

function applyRealtimeWindow(anchorTs = Date.now()) {
  chart?.setOption({
    xAxis: {
      min: anchorTs - chartWindowMs.value,
      max: getRealtimeXAxisMax(anchorTs)
    }
  })
}

function applySelectedDateWindow() {
  const { startTs, endTs } = getSelectedDateRangeTimestamps()

  chart?.setOption({
    xAxis: {
      min: startTs,
      max: endTs
    }
  })
}

function trimChartDataToRealtimeWindow(anchorTs = Date.now()) {
  const startTs = anchorTs - chartWindowMs.value

  Object.keys(chartData.value).forEach((name) => {
    const series = chartData.value[name] || []
    const windowStartIndex = series.findIndex((point) => point.ts >= startTs)

    if (windowStartIndex === -1) {
      chartData.value[name] = series.slice(-1)
      return
    }

    if (windowStartIndex === 0) return

    chartData.value[name] = series.slice(windowStartIndex - 1)
  })
}

function appendChartPoint(name: string, point: ChartPoint) {
  const series = chartData.value[name] ?? []
  const samePointIndex = series.findIndex((item) => item.ts === point.ts)

  if (samePointIndex >= 0) {
    series[samePointIndex] = point
  } else {
    series.push(point)
  }

  series.sort((a, b) => a.ts - b.ts)
  chartData.value[name] = series
}

function getChartAnimationOptions() {
  return {
    animation: !chartRealtimeMode.value,
    animationDuration: chartRealtimeMode.value ? 0 : 200,
    animationEasing: 'linear',
    animationDurationUpdate: chartRealtimeMode.value ? 0 : 200,
    animationEasingUpdate: 'linear'
  }
}

function updateSingleSeries(name: string) {
  if (!chart) renderChart()
  if (!chart) return

  chart.setOption({
    ...getChartAnimationOptions(),
    legend: {
      selected: selectedDimension.value
    },
    series: [
      {
        id: name,
        name,
        data: chartData.value[name]?.map((item) => mapChartPoint(item, name)) ?? []
      }
    ]
  })
}

function updateSeriesByNames(names: string[]) {
  if (!chart) renderChart()
  if (!chart) return

  chart.setOption({
    ...getChartAnimationOptions(),
    legend: {
      selected: selectedDimension.value
    },
    yAxis: getTrendYAxisOptions(),
    series: names.map((name) => ({
      id: name,
      name,
      data: chartData.value[name]?.map((item) => mapChartPoint(item, name)) ?? []
    }))
  })
}

function flushRealtimeChartUpdate() {
  realtimeUpdateFrame = null

  const names = Array.from(pendingRealtimeSeriesNames)
  pendingRealtimeSeriesNames.clear()

  if (!chart) renderChart()
  if (!chart || !names.length) return

  const latestTs = getLatestTimestamp() ?? Date.now()
  trimChartDataToRealtimeWindow(latestTs)

  chart.setOption({
    ...getChartAnimationOptions(),
    xAxis: {
      min: latestTs - chartWindowMs.value,
      max: getRealtimeXAxisMax(latestTs)
    },
    legend: {
      selected: selectedDimension.value
    },
    yAxis: getTrendYAxisOptions(),
    series: names.map((name) => ({
      id: name,
      name,
      data: chartData.value[name]?.map((item) => mapChartPoint(item, name)) ?? []
    }))
  })
}

function scheduleRealtimeChartUpdate(names: string[]) {
  names.forEach((name) => pendingRealtimeSeriesNames.add(name))

  if (realtimeUpdateFrame !== null) return

  realtimeUpdateFrame = window.requestAnimationFrame(flushRealtimeChartUpdate)
}

function cancelRealtimeChartUpdate() {
  if (realtimeUpdateFrame !== null) {
    window.cancelAnimationFrame(realtimeUpdateFrame)
    realtimeUpdateFrame = null
  }

  pendingRealtimeSeriesNames.clear()
}

function renderChart() {
  if (!chartRef.value) return

  if (!chart) {
    chart = echarts.init(chartRef.value, undefined, { renderer: 'canvas' })
    window.addEventListener('resize', resizeChart)
  }

  const { startTs, endTs } = getSelectedDateRangeTimestamps()

  chart.setOption(
    {
      ...getChartAnimationOptions(),
      color: dimensions.value.map((item) => item.color),
      grid: {
        left: '2%',
        top: '5%',
        right: '2%',
        bottom: '12%'
      },
      tooltip: {
        trigger: 'axis',
        axisPointer: {
          type: 'line'
        },
        formatter: (params: any[]) => {
          const list = Array.isArray(params) ? params : [params]
          const firstPoint = list[0]
          const title = firstPoint?.axisValue
            ? dayjs(Number(firstPoint.axisValue)).format('YYYY-MM-DD HH:mm:ss')
            : ''
          const existed: string[] = []
          const rows = list
            .filter((item) => {
              if (existed.includes(item.seriesName)) return false
              existed.push(item.seriesName)
              return true
            })
            .map(
              (
                item
              ) => `<div style="display:grid;grid-template-columns:max-content max-content;column-gap:16px;align-items:center;margin-top:6px;">
                <span style="white-space:nowrap;">${item.marker} ${item.seriesName}</span>
                <span style="white-space:nowrap;text-align:right;color:#64748b;font-variant-numeric:tabular-nums;">${formatChartValue(item.value?.[2])}</span>
              </div>`
            )

          return [title ? `<div style="margin-bottom:2px;">${title}</div>` : '', ...rows]
            .filter(Boolean)
            .join('')
        }
      },
      xAxis: {
        type: 'time',
        min: chartRealtimeMode.value ? endTs - chartWindowMs.value : startTs,
        max: chartRealtimeMode.value ? getRealtimeXAxisMax(endTs) : endTs,
        boundaryGap: false,
        axisLabel: {
          color: '#64748b',
          formatter: (value: number) => dayjs(value).format('YYYY-MM-DD\nHH:mm:ss'),
          rotate: 0,
          align: 'left'
        },
        axisLine: {
          lineStyle: {
            color: 'rgba(148, 163, 184, 0.35)'
          }
        },
        axisTick: {
          show: false
        }
      },
      dataZoom: [
        {
          type: 'inside',
          xAxisIndex: 0,
          filterMode: 'none'
        },
        {
          type: 'slider',
          xAxisIndex: 0,
          height: 18,
          bottom: 8,
          filterMode: 'none',
          borderColor: 'rgba(148, 163, 184, 0.25)',
          fillerColor: 'rgba(59, 130, 246, 0.12)',
          handleSize: 12
        }
      ],
      yAxis: getTrendYAxisOptions(),
      legend: {
        data: dimensions.value.map((item) => item.name),
        selected: selectedDimension.value,
        show: false
      },
      series: dimensions.value.map((item) => ({
        id: item.name,
        name: item.name,
        type: 'line',
        smooth: 0.24,
        showSymbol: false,
        connectNulls: true,
        endLabel: {
          show: true,
          formatter: (params: any) => formatChartValue(params.value?.[2]),
          offset: [6, 0],
          color: item.color,
          fontSize: 12,
          fontWeight: 'bold'
        },
        emphasis: {
          focus: 'series',
          lineStyle: {
            width: 3
          }
        },
        lineStyle: {
          width: 2,
          shadowColor: 'rgba(15, 23, 42, 0.16)'
        },
        color: item.color,
        data: chartData.value[item.name]?.map((point) => mapChartPoint(point, item.name)) ?? []
      }))
    },
    true
  )
}

function updateDimensionValues(valueMap: Map<string, number>) {
  dimensions.value.forEach((item) => {
    const value = valueMap.get(item.identifier)
    if (value !== undefined) {
      item.value = value
    }
  })
}

function handleMessageUpdate(_topic: string, payload: any) {
  const list = Array.isArray(payload) ? payload : Array.isArray(payload?.data) ? payload.data : []
  const valueMap = new Map<string, number>()
  const updatedNames = new Set<string>()

  list.forEach((item) => {
    const identity = String(item.id ?? item.identity ?? '')
    const numberValue = Number(item.value ?? item.logValue ?? 0)

    if (!identity || !Number.isFinite(numberValue)) return

    valueMap.set(identity, numberValue)

    const dimension = dimensions.value.find((item) => item.identifier === identity)
    if (!dimension) return

    appendChartPoint(dimension.name, {
      ts: getPointTimestamp(item.remark),
      value: numberValue
    })
    updatedNames.add(dimension.name)
  })

  if (!updatedNames.size) return

  updateDimensionValues(valueMap)
  scheduleRealtimeChartUpdate(Array.from(updatedNames))
}

async function ensureToken() {
  if (token.value) return

  token.value = await IotDeviceApi.getToken()
}

async function startRealtimeMqtt() {
  if (!data.value.mqttUrl) return

  await ensureToken()
  connect(`wss://aims.deepoil.cc/mqtt`, { password: token.value }, handleMessageUpdate)
}

async function initLoadChartData({
  realtime = true,
  resetDate = realtime
}: { realtime?: boolean; resetDate?: boolean } = {}) {
  if (!dimensions.value.length) return

  const loadVersion = ++chartLoadVersion
  chartRealtimeMode.value = realtime

  if (resetDate) {
    selectedDate.value = getInitialDateRange()
  }

  chartData.value = Object.fromEntries(dimensions.value.map((item) => [item.name, []]))
  chartLoading.value = true

  try {
    await Promise.all(
      dimensions.value.map(async (item) => {
        item.response = true
        try {
          const getChartData = isRangeGreaterThan120Minutes(selectedDate.value)
            ? IotStatApi.getDeviceInfoChart
            : IotStatApi.getDeviceInfoChartly
          const res = await getChartData(
            data.value.deviceCode,
            item.identifier,
            selectedDate.value[0],
            selectedDate.value[1]
          )

          chartData.value[item.name] = (((res as any[]) ?? []) as any[])
            .sort((a, b) => a.ts - b.ts)
            .map((item) => ({ ts: item.ts, value: item.value }))
        } catch (error) {
          console.error(error)
        } finally {
          item.response = false
        }
      })
    )

    if (loadVersion !== chartLoadVersion) return

    const latestTs = getLatestTimestamp() ?? dayjs(selectedDate.value[1]).valueOf()
    if (realtime) {
      trimChartDataToRealtimeWindow(latestTs)
      applyRealtimeWindow(latestTs)
    } else {
      applySelectedDateWindow()
    }
    updateSeriesByNames(dimensions.value.map((item) => item.name))
  } finally {
    chartLoading.value = false
  }

  if (realtime) {
    await startRealtimeMqtt()
  }
}

async function initPage(load = true) {
  if (load) await loadDimensions()

  await nextTick()
  renderChart()
  await initLoadChartData()
}

async function resetChart() {
  chartLoadVersion += 1
  chartRealtimeMode.value = true
  selectedDate.value = getInitialDateRange()
  await cancelAllRequests()
  destroy()
  cancelRealtimeChartUpdate()
  chart?.clear()
  await initPage(false)
}

function getSelectedDateEnd() {
  const end = dayjs(selectedDate.value?.[1])

  return end.isValid() ? end : dayjs()
}

async function handleChartWindowMinutesChange(value?: number) {
  const realtime = chartRealtimeMode.value

  chartWindowMinutes.value = normalizeChartWindowMinutes(value)
  selectedDate.value = getDateRangeByWindow(realtime ? dayjs() : getSelectedDateEnd())

  chartLoadVersion += 1
  await cancelAllRequests()
  destroy()
  cancelRealtimeChartUpdate()
  chart?.clear()
  await nextTick()
  renderChart()
  await initLoadChartData({ realtime, resetDate: false })
}

async function handleDateChange() {
  if (!selectedDate.value?.[0] || !selectedDate.value?.[1]) {
    await resetChart()
    return
  }

  chartLoadVersion += 1
  chartRealtimeMode.value = false
  await cancelAllRequests()
  destroy()
  cancelRealtimeChartUpdate()
  chart?.clear()
  await nextTick()
  renderChart()
  await initLoadChartData({ realtime: false, resetDate: false })
}

async function exportChart() {
  try {
    if (!downloadRef.value) return

    const result = await snapdom(downloadRef.value, {
      scale: 2,
      backgroundColor: '#fff'
    })

    await result.download({
      filename: `${data.value.deviceName || data.value.deviceCode}-设备监控`,
      type: 'png'
    })
  } catch (error) {
    console.error(error)
    message.error('导出图表失败')
  }
}

function handleClickSpec(modelName: string) {
  selectedDimension.value[modelName] = !selectedDimension.value[modelName]
  chart?.setOption({
    legend: {
      selected: selectedDimension.value
    }
  })

  if (selectedDimension.value[modelName]) {
    updateSingleSeries(modelName)
  }

  nextTick(() => {
    chart?.resize()
  })
}

function openRangeDialog(targetName?: string) {
  const draftSource = [...dimensions.value]

  if (targetName) {
    draftSource.sort((a, b) => {
      if (a.name === targetName) return -1
      if (b.name === targetName) return 1
      return 0
    })
  }

  rangeSettingDrafts.value = draftSource.map((item) => ({
    name: item.name,
    identifier: item.identifier,
    color: item.color,
    minValue: normalizeRangeValue(item.minValue),
    maxValue: normalizeRangeValue(item.maxValue)
  }))
  rangeDialogVisible.value = true
}

async function saveRangeSetting(item: Dimensions, minValue?: number, maxValue?: number) {
  const min = normalizeRangeValue(minValue)
  const max = normalizeRangeValue(maxValue)

  if (min === undefined && max === undefined) {
    if (item.id) {
      await IotDeviceApi.deleteMaxMin({ id: item.id })
    }

    item.minValue = undefined
    item.maxValue = undefined
    item.id = undefined
    return
  }

  if (min === undefined || max === undefined) {
    throw new Error(`${item.name} 的最大值和最小值需要同时填写`)
  }

  if (min > max) {
    throw new Error(`${item.name} 的最小值不能大于最大值`)
  }

  if (min === max) {
    throw new Error(`${item.name} 的最大值和最小值不能相等`)
  }
  12
  const body = {
    minValue: min,
    maxValue: max,
    deviceId: query.id,
    propertyCode: item.identifier,
    alarmProperty: item.name,
    deviceName: data.value.deviceName,
    id: item.id
  }

  const res = await IotDeviceApi.saveMaxMin(body)

  if (res.id) item.id = res.id
  item.minValue = min
  item.maxValue = max
}

async function handleRangeDialogSave() {
  try {
    for (const draft of rangeSettingDrafts.value) {
      const item = dimensions.value.find((dimension) => dimension.identifier === draft.identifier)
      if (!item) continue

      await saveRangeSetting(item, draft.minValue, draft.maxValue)
    }

    updateSeriesByNames(dimensions.value.map((item) => item.name))
    rangeDialogVisible.value = false
    message.success('设置成功')
  } catch (error: any) {
    message.warning(error?.message || '设置失败')
  }
}

function handleRangeDialogReset() {
  rangeSettingDrafts.value = rangeSettingDrafts.value.map((item) => ({
    ...item,
    minValue: undefined,
    maxValue: undefined
  }))
}

function clearRangeDraft(row: RangeSettingDraft) {
  row.minValue = undefined
  row.maxValue = undefined
}

onMounted(() => {
  initPage()
})

onUnmounted(() => {
  chartLoadVersion += 1
  destroy()
  cancelRealtimeChartUpdate()
  window.removeEventListener('resize', resizeChart)
  chart?.dispose()
  chart = null
})
</script>

<template>
  <div class="content">
    <div
      class="rounded-xl shadow-sm border border-gray-100 border-solid px-6 flex items-center justify-between shrink-0 bg-gradient-to-r from-blue-100 to-white">
      <div class="flex items-center gap-4">
        <div
          class="size-12 rounded-lg bg-blue-50 text-blue-600 flex items-center justify-center shadow-inner">
          <el-icon :size="24"><Odometer /></el-icon>
        </div>
        <div>
          <div class="text-xs text-gray-400 font-medium tracking-wider">资产编码</div>
          <div class="text-xl font-bold font-mono text-gray-800">{{ data.deviceCode }}</div>
        </div>
      </div>
      <div class="flex-1 flex justify-center divide-x divide-gray-100">
        <template v-for="item in headerCenterContent" :key="item.key">
          <div
            class="px-8 flex flex-col items-center"
            v-if="item.judgment ? Boolean(query[item.key]) : true">
            <span class="text-xs text-gray-400 mb-1">{{ item.label }}</span>
            <span class="font-semibold text-gray-700">{{ data[item.key] }}</span>
          </div>
        </template>
      </div>
      <div class="flex items-center gap-6">
        <template v-for="item in headerTagContent" :key="item.key">
          <div class="text-center" v-if="item.judgment ? Boolean(query[item.key]) : true">
            <div class="text-xs text-gray-400 mb-1">{{ item.label }}</div>
            <el-tag v-if="data[item.key]" type="success" v-bind="tagProps">在线 </el-tag>
            <el-tag v-else type="danger" v-bind="tagProps">离线 </el-tag>
          </div>
        </template>
      </div>
    </div>
    <div class="dimension-area">
      <div class="dimension-stack">
        <template v-for="citem in dimensionsContent" :key="citem.label">
          <template v-if="citem.judgment ? Boolean(citem.value.length) : true">
            <section
              class="dimension-panel border border-gray-100 border-solid bg-gradient-to-b from-blue-100 to-white"
              v-loading="dimensionLoading">
              <header class="dimension-panel__header">
                <div class="dimension-panel__title">
                  <el-icon><component :is="citem.icon" /></el-icon>
                  <span>{{ citem.label }}</span>
                </div>
                <el-button :icon="Setting" size="small" text bg @click="openRangeDialog()">
                  设置
                </el-button>
              </header>

              <div class="dimension-grid">
                <button
                  v-for="item in citem.value"
                  :key="item.identifier"
                  type="button"
                  :disabled="disabledDimensions.includes(item.identifier)"
                  :aria-pressed="selectedDimension[item.name]"
                  @click="handleClickSpec(item.name)"
                  class="dimension-card"
                  :class="{ 'is-active': selectedDimension[item.name] }"
                  :style="{
                    '--theme-color': item.color,
                    '--theme-bg-hover': item.bgHover,
                    '--theme-bg-active': item.bgActive
                  }">
                  <span class="dimension-card__bar"></span>
                  <span class="dimension-card__status"></span>

                  <span class="dimension-card__name">{{ item.name }}</span>

                  <span class="dimension-card__body">
                    <animated-count-to
                      v-if="!item.isText"
                      :value="Number(item.value)"
                      :duration="500"
                      :suffix="item.suffix"
                      class="dimension-card__value" />
                    <span v-else class="dimension-card__value">
                      {{ item.value }}
                    </span>

                    <span
                      v-if="item.minValue !== undefined || item.maxValue !== undefined"
                      class="dimension-card__ranges">
                      <span v-if="item.maxValue !== undefined" class="range-pill range-pill--max">
                        <i class="i-material-symbols:arrow-upward-alt-rounded"></i>
                        <span>MAX</span>
                        <strong>{{ formatChartValue(item.maxValue) }}</strong>
                      </span>

                      <span v-if="item.minValue !== undefined" class="range-pill range-pill--min">
                        <i class="i-material-symbols:arrow-downward-alt-rounded"></i>
                        <span>MIN</span>
                        <strong>{{ formatChartValue(item.minValue) }}</strong>
                      </span>
                    </span>
                  </span>
                </button>
              </div>
            </section>
          </template>
        </template>
      </div>
    </div>

    <section
      ref="targetArea"
      class="chart-panel border border-gray-100 border-solid bg-gradient-to-b from-blue-100 to-white">
      <header class="chart-panel__header">
        <h3 class="chart-panel__title">
          <i class="i-material-symbols:area-chart-outline-rounded"></i>
          <span>数据趋势</span>
        </h3>
        <div class="chart-panel__actions">
          <el-button type="primary" size="default" @click="exportChart">导出为图片</el-button>
          <el-button size="default" @click="resetChart">重置</el-button>
          <el-date-picker
            v-model="selectedDate"
            value-format="YYYY-MM-DD HH:mm:ss"
            type="datetimerange"
            unlink-panels
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            :shortcuts="rangeShortcuts"
            size="default"
            class="chart-panel__date-picker"
            placement="bottom-end"
            @change="handleDateChange" />
          <div class="chart-window-control">
            <span>时间区间</span>
            <el-input-number
              v-model="chartWindowMinutes"
              :min="MIN_CHART_WINDOW_MINUTES"
              :max="MAX_CHART_WINDOW_MINUTES"
              :precision="0"
              :step="1"
              controls-position="right"
              size="default"
              class="chart-window-control__input"
              @change="handleChartWindowMinutesChange" />
            <span>分钟</span>
          </div>
          <el-button
            size="default"
            :type="isFullscreen ? 'info' : 'primary'"
            :icon="isFullscreen ? Crop : FullScreen"
            @click="toggle">
            {{ isFullscreen ? '退出全屏' : '全屏' }}
          </el-button>
        </div>
      </header>

      <div ref="downloadRef" class="chart-panel__body">
        <div class="chart-legend">
          <button
            v-for="item in chartLegendItems"
            :key="item.name"
            type="button"
            class="chart-legend__item"
            :class="{ 'is-active': item.selected }"
            :aria-pressed="item.selected"
            :style="{
              '--theme-color': item.color,
              '--theme-bg-hover': item.bgHover
            }"
            @click="handleClickSpec(item.name)">
            <span class="chart-legend__range">{{ item.max }}</span>
            <span class="chart-legend__line"></span>
            <span class="chart-legend__name">{{ item.name }}</span>
            <span class="chart-legend__line"></span>
            <span class="chart-legend__range">{{ item.min }}</span>
          </button>
        </div>

        <div v-loading="chartLoading" element-loading-background="transparent" class="chart-stage">
          <div ref="chartRef" class="chart-canvas"></div>
        </div>
      </div>
    </section>

    <el-dialog
      v-model="rangeDialogVisible"
      title="曲线量程设置"
      width="900px"
      append-to-body
      destroy-on-close>
      <ZmTable :data="rangeSettingDrafts" :loading="false" :max-height="520" :show-border="true">
        <ZmTableColumn label="曲线" min-width="180">
          <template #default="{ row }">
            <div class="flex items-center gap-2 min-w-0">
              <span
                class="inline-block size-2.5 rounded-full shrink-0"
                :style="{ backgroundColor: row.color }"></span>
              <span class="truncate font-medium text-slate-700">{{ row.name }}</span>
            </div>
          </template>
        </ZmTableColumn>
        <ZmTableColumn label="最小值" width="180">
          <template #default="{ row }">
            <el-input-number
              v-model="row.minValue"
              class="!w-full"
              :controls="false"
              :placeholder="formatChartValue(getSeriesRange(row.name).labelMin)" />
          </template>
        </ZmTableColumn>
        <ZmTableColumn label="最大值" width="180">
          <template #default="{ row }">
            <el-input-number
              v-model="row.maxValue"
              class="!w-full"
              :controls="false"
              :placeholder="formatChartValue(getSeriesRange(row.name).labelMax)" />
          </template>
        </ZmTableColumn>
        <ZmTableColumn label="当前使用范围" width="170">
          <template #default="{ row }">
            <span class="text-xs text-slate-500">
              {{ formatChartValue(getSeriesRange(row.name).labelMin) }}
              -
              {{ formatChartValue(getSeriesRange(row.name).labelMax) }}
            </span>
          </template>
        </ZmTableColumn>
        <ZmTableColumn label="操作" width="90">
          <template #default="{ row }">
            <el-button size="small" link type="primary" @click="clearRangeDraft(row)">
              清空
            </el-button>
          </template>
        </ZmTableColumn>
      </ZmTable>

      <template #footer>
        <div class="flex justify-between">
          <el-button @click="handleRangeDialogReset">全部清空</el-button>
          <div class="flex gap-2">
            <el-button @click="rangeDialogVisible = false">取消</el-button>
            <el-button type="primary" @click="handleRangeDialogSave">保存</el-button>
          </div>
        </div>
      </template>
    </el-dialog>
  </div>
</template>

<style scoped>
.content {
  display: grid;
  height: calc(
    100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
  );
  gap: 12px;
  grid-template-rows: 80px auto minmax(0, 1fr);
}

.dimension-area {
  max-height: 260px;
  min-height: 0;
  overflow: auto;
}

.dimension-stack {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.dimension-panel {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 12px;
  overflow: hidden;
  border-radius: 8px;
  box-shadow: 0 8px 22px -18px rgb(15 23 42 / 42%);
}

.dimension-panel__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 24px;
}

.dimension-panel__title {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 14px;
  font-weight: 700;
  color: #334155;
}

.dimension-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 8px;
}

.dimension-card {
  position: relative;
  display: grid;
  min-height: 72px;
  padding: 8px;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  background: rgb(255 255 255 / 62%);
  border: 1px solid rgb(203 213 225 / 82%);
  border-radius: 8px;
  outline: none;
  transition:
    background-color 0.2s ease,
    border-color 0.2s ease,
    box-shadow 0.2s ease,
    transform 0.2s ease;
  appearance: none;
  grid-template-rows: auto 1fr;
}

.dimension-card:disabled {
  cursor: not-allowed;
  opacity: 0.52;
}

.dimension-card:focus-visible {
  box-shadow:
    0 0 0 2px #fff,
    0 0 0 4px var(--theme-bg-active);
}

.dimension-card:hover {
  background-color: var(--theme-bg-hover);
  border-color: var(--theme-bg-active);
  transform: translateY(-1px);
  box-shadow: 0 8px 18px -16px rgb(15 23 42 / 42%);
}

.dimension-card:disabled:hover {
  background: rgb(255 255 255 / 62%);
  border-color: rgb(203 213 225 / 82%);
  transform: none;
  box-shadow: none;
}

.dimension-card.is-active {
  background-color: var(--theme-bg-active);
  border-color: var(--theme-color);
  box-shadow:
    0 0 0 1px var(--theme-bg-active),
    0 4px 12px -2px var(--theme-bg-active);
}

.dimension-card__bar {
  position: absolute;
  top: 12px;
  bottom: 12px;
  left: 0;
  width: 3px;
  background-color: var(--theme-color);
  border-radius: 0 999px 999px 0;
  opacity: 0;
  transition: opacity 0.2s ease;
}

.dimension-card.is-active .dimension-card__bar {
  opacity: 1;
}

.dimension-card__status {
  position: absolute;
  top: 12px;
  right: 12px;
  width: 7px;
  height: 7px;
  background-color: var(--theme-color);
  border-radius: 999px;
  opacity: 0;
  transform: scale(0.6);
  box-shadow: 0 0 8px var(--theme-color);
  transition:
    opacity 0.2s ease,
    transform 0.2s ease;
}

.dimension-card.is-active .dimension-card__status {
  opacity: 1;
  transform: scale(1);
}

.dimension-card__name {
  min-width: 0;
  padding-right: 16px;
  overflow: hidden;
  font-size: 12px;
  font-weight: 700;
  line-height: 16px;
  color: #64748b;
  text-overflow: ellipsis;
  white-space: nowrap;
  transition: color 0.2s ease;
}

.dimension-card:hover .dimension-card__name,
.dimension-card.is-active .dimension-card__name {
  color: var(--theme-color);
}

.dimension-card__body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) max-content;
  align-items: center;
  gap: 8px;
  min-width: 0;
}

.dimension-card__value {
  min-width: 0;
  overflow: hidden;
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
  font-size: 20px;
  font-weight: 900;
  line-height: 1;
  color: #0f172a;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.dimension-card__ranges {
  display: flex;
  flex-direction: column;
  gap: 4px;
  align-items: flex-end;
  justify-content: center;
  max-width: 100%;
}

.range-pill {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  height: 20px;
  padding: 0 6px;
  font-size: 10px;
  font-weight: 800;
  line-height: 1;
  border: 1px solid;
  border-radius: 999px;
}

.range-pill i {
  width: 12px;
  height: 12px;
}

.range-pill strong {
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
  font-size: 11px;
}

.range-pill--max {
  color: #047857;
  background: rgb(236 253 245 / 78%);
  border-color: rgb(167 243 208 / 86%);
}

.range-pill--min {
  color: #be123c;
  background: rgb(255 241 242 / 78%);
  border-color: rgb(254 205 211 / 88%);
}

.chart-panel {
  display: flex;
  flex-direction: column;
  min-height: 0;
  padding: 12px;
  overflow: hidden;
  border-radius: 8px;
  box-shadow: 0 8px 22px -18px rgb(15 23 42 / 42%);
}

.chart-panel__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 10px;
}

.chart-panel__title {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  font-size: 15px;
  font-weight: 800;
  color: #334155;
}

.chart-panel__title i {
  width: 22px;
  height: 22px;
  color: #0284c7;
}

.chart-panel__actions {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  justify-content: flex-end;
}

.chart-panel__date-picker {
  width: 400px;
  max-width: 42vw;
}

.chart-window-control {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 32px;
  padding: 0 8px;
  font-size: 13px;
  font-weight: 700;
  color: #475569;
  white-space: nowrap;
  background: rgb(255 255 255 / 82%);
  border: 1px solid rgb(203 213 225 / 72%);
  border-radius: 4px;
}

.chart-window-control__input {
  width: 108px;
}

.chart-panel__body {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 10px;
  min-height: 0;
  flex: 1;
}

.chart-legend {
  display: flex;
  max-width: 132px;
  min-width: 34px;
  min-height: 0;
  overflow: auto;
  user-select: none;
  gap: 4px;
}

.chart-legend__item {
  position: relative;
  display: grid;
  grid-template-rows: auto 1fr auto 1fr auto;
  place-items: center;
  width: 32px;
  min-height: 128px;
  padding: 8px 4px;
  font: inherit;
  color: #94a3b8;
  cursor: pointer;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 999px;
  outline: none;
  transition:
    color 0.2s ease,
    background-color 0.2s ease,
    border-color 0.2s ease,
    box-shadow 0.2s ease,
    opacity 0.2s ease;
  appearance: none;
}

.chart-legend__item:hover {
  background: var(--theme-bg-hover);
  border-color: rgb(226 232 240 / 86%);
  box-shadow: 0 8px 18px -18px rgb(15 23 42 / 40%);
}

.chart-legend__item:not(.is-active) {
  opacity: 0.48;
}

.chart-legend__item.is-active {
  color: var(--theme-color);
}

.chart-legend__range {
  font-size: 10px;
  line-height: 1;
  color: #94a3b8;
  writing-mode: sideways-lr;
}

.chart-legend__name {
  max-width: 104px;
  overflow: hidden;
  font-size: 12px;
  font-weight: 800;
  line-height: 1;
  color: currentcolor;
  text-overflow: ellipsis;
  white-space: nowrap;
  writing-mode: sideways-lr;
}

.chart-legend__line {
  width: 2px;
  height: 100%;
  min-height: 18px;
  background: var(--theme-color);
  border-radius: 999px;
  opacity: 0.42;
}

.chart-stage {
  position: relative;
  min-width: 0;
  min-height: 240px;
  overflow: hidden;
  background: rgb(248 250 252 / 54%);
  border: 1px dashed rgb(203 213 225 / 90%);
  border-radius: 8px;
}

.chart-canvas {
  width: 100%;
  height: 100%;
  min-height: 240px;
}

.chart-panel:fullscreen {
  width: 100vw;
  height: 100vh;
  padding: 16px;
  background-color: #fff;
}

::backdrop {
  background-color: #fff;
}

@media (width <= 768px) {
  .dimension-grid {
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  }

  .dimension-card__value {
    font-size: 20px;
  }

  .chart-panel__body {
    grid-template-columns: minmax(0, 1fr);
  }

  .chart-panel__header {
    align-items: flex-start;
    flex-wrap: wrap;
  }

  .chart-panel__actions {
    width: 100%;
    justify-content: flex-start;
  }

  .chart-panel__date-picker {
    flex: 1 1 260px;
    width: 100%;
    max-width: none;
  }

  .chart-legend {
    max-width: none;
    min-height: auto;
    overflow-x: auto;
  }

  .chart-legend__item {
    grid-template-rows: none;
    grid-template-columns: auto 1fr auto;
    gap: 6px;
    width: auto;
    min-width: 132px;
    min-height: 32px;
  }

  .chart-legend__name,
  .chart-legend__range {
    writing-mode: horizontal-tb;
  }

  .chart-legend__line {
    display: none;
  }
}
</style>
