<script lang="ts" setup>
import { IotDeviceApi } from '@/api/pms/device'
import { cancelAllRequests, IotStatApi } from '@/api/pms/stat'
import { useMqtt } from '@/utils/useMqtt'
import { Dimensions, formatIotValue } from '@/utils/useSocketBus'
import { useDebounceFn } from '@vueuse/core'
import dayjs from 'dayjs'
import * as echarts from 'echarts'
import { neonColors } from '@/utils/td-color'

const props = defineProps({
  id: {
    type: Number,
    required: true
  },
  deviceCode: {
    type: String,
    required: true
  },
  deviceName: {
    type: String,
    required: true
  },
  mqttUrl: {
    type: String,
    required: true
  },
  ifInline: {
    type: String,
    required: true
  },
  lastInlineTime: {
    type: String,
    required: true
  },
  deptName: {
    type: String,
    required: true
  },
  vehicleName: {
    type: String,
    required: true
  },
  carOnline: {
    type: String,
    required: true
  },
  // isRealTime: {
  //   type: Boolean,
  //   default: true
  // },
  date: {
    type: Array as PropType<Array<string>>,
    required: true
  },
  token: {
    type: String,
    required: true
  }
})

const dimensions = ref<Omit<Dimensions, 'color' | 'bgHover' | 'bgActive'>[]>([])
const selectedDimension = ref<Record<string, boolean>>({})

const { connect, destroy, isConnected, subscribe } = useMqtt()

const handleMessageUpdate = (_topic: string, data: any) => {
  const valueMap = new Map<string, number>()

  for (const item of data) {
    const { id: identity, value: logValue, remark } = item

    const value = logValue ? Number(logValue) : 0

    if (identity) {
      valueMap.set(identity, value)
    }

    const modelName = dimensions.value.find((item) => item.identifier === identity)?.name

    if (modelName && chartData.value[modelName]) {
      chartData.value[modelName].push({
        ts: dayjs.unix(remark).valueOf(),
        value
      })

      if (isFollowingFullRange.value) {
        resetVisibleTimeRange()
      }

      updateSeriesByNames([modelName])
    }
  }
}

watch(isConnected, (newVal) => {
  if (newVal) {
    // subscribe(`/636/${props.deviceCode}/property/post`)

    // switch (props.deviceCode) {
    //   case 'YF1539':
    //     subscribe(`/656/${props.deviceCode}/property/post`)
    //   case 'YF325':
    //   case 'YF288':
    //   case 'YF671':
    //   case 'YF459':
    //     subscribe(`/635/${props.deviceCode}/property/post`)
    //   case 'YF649':
    //     subscribe(`/636/${props.deviceCode}/property/post`)
    //   default:
    //     subscribe(`/636/${props.deviceCode}/property/post`)
    // }

    subscribe(props.mqttUrl)
    // subscribe('/636/YF649/property/post')
  }
})

async function loadDimensions() {
  if (!props.id) return
  try {
    dimensions.value = (((await IotDeviceApi.getIotDeviceTds(Number(props.id))) as any[]) ?? [])
      .sort((a, b) => b.modelOrder - a.modelOrder)
      .map((item) => {
        const { value, suffix, isText } = formatIotValue(item.value)
        return {
          identifier: item.identifier,
          name: item.modelName,
          value: value,
          suffix: suffix,
          isText: isText,
          response: false
        }
      })
      .filter((item) => item.isText === false)

    selectedDimension.value = Object.fromEntries(dimensions.value.map((item) => [item.name, true]))
  } catch (error) {
    console.error(error)
  }
}

interface ChartData {
  [key: Dimensions['name']]: { ts: number; value: number }[]
}

const chartData = ref<ChartData>({})

const chartRef = ref<HTMLDivElement | null>(null)
let chart: echarts.ECharts | null = null

const visibleTimeRange = ref<{
  startTs: number | null
  endTs: number | null
}>({
  startTs: null,
  endTs: null
})

const isFollowingFullRange = ref(true)

const TREND_AXIS_MIN = 0
const TREND_AXIS_MAX = 100
const TREND_AXIS_CENTER = 50
const TREND_AXIS_PADDING = 4

interface TrendStats {
  mode: 'empty' | 'flat' | 'mixed' | 'single'
  min: number
  max: number
  positiveMax: number
  negativeMin: number
  fixedValue: number
}

const resizeChart = useDebounceFn(() => {
  chart?.resize()
}, 100)

const getSelectedSeriesNames = () => {
  return dimensions.value
    .map((item) => item.name)
    .filter((name) => selectedDimension.value[name] !== false)
}

const getGlobalTimeExtent = () => {
  let minTs = Infinity
  let maxTs = -Infinity

  Object.values(chartData.value).forEach((dataset) => {
    dataset.forEach(({ ts }) => {
      if (!Number.isFinite(ts)) return
      if (ts < minTs) minTs = ts
      if (ts > maxTs) maxTs = ts
    })
  })

  return {
    minTs: Number.isFinite(minTs) ? minTs : null,
    maxTs: Number.isFinite(maxTs) ? maxTs : null
  }
}

function resetVisibleTimeRange() {
  const { minTs, maxTs } = getGlobalTimeExtent()

  visibleTimeRange.value = {
    startTs: minTs,
    endTs: maxTs
  }

  isFollowingFullRange.value = true
}

function syncVisibleTimeRangeFromChart() {
  const { minTs, maxTs } = getGlobalTimeExtent()

  if (!chart || minTs === null || maxTs === null) {
    visibleTimeRange.value = {
      startTs: minTs,
      endTs: maxTs
    }
    isFollowingFullRange.value = true
    return
  }

  const dataZoomOptions = chart.getOption().dataZoom
  const primaryDataZoom = Array.isArray(dataZoomOptions) ? (dataZoomOptions[0] as any) : undefined

  if (!primaryDataZoom) {
    resetVisibleTimeRange()
    return
  }

  const startValue = Number(primaryDataZoom.startValue)
  const endValue = Number(primaryDataZoom.endValue)

  let nextStartTs = minTs
  let nextEndTs = maxTs

  if (Number.isFinite(startValue) && Number.isFinite(endValue)) {
    nextStartTs = Math.min(startValue, endValue)
    nextEndTs = Math.max(startValue, endValue)
  } else {
    const startPercent = Number(primaryDataZoom.start ?? 0)
    const endPercent = Number(primaryDataZoom.end ?? 100)
    const range = maxTs - minTs

    nextStartTs = minTs + (range * Math.min(startPercent, endPercent)) / 100
    nextEndTs = minTs + (range * Math.max(startPercent, endPercent)) / 100
  }

  visibleTimeRange.value = {
    startTs: nextStartTs,
    endTs: nextEndTs
  }

  const tolerance = Math.max((maxTs - minTs) * 0.001, 1)
  isFollowingFullRange.value =
    Math.abs(nextStartTs - minTs) <= tolerance && Math.abs(nextEndTs - maxTs) <= tolerance
}

const getVisibleDataset = (dataset: { ts: number; value: number }[]) => {
  const { startTs, endTs } = visibleTimeRange.value

  if (startTs === null || endTs === null) {
    return dataset
  }

  return dataset.filter(({ ts }) => ts >= startTs && ts <= endTs)
}

const getTrendStats = (dataset: { ts: number; value: number }[]): TrendStats => {
  const values = getVisibleDataset(dataset)
    .map(({ value }) => Number(value))
    .filter((value) => Number.isFinite(value))

  if (!values.length) {
    return {
      mode: 'empty',
      min: 0,
      max: 0,
      positiveMax: 0,
      negativeMin: 0,
      fixedValue: TREND_AXIS_MIN
    }
  }

  const min = Math.min(...values)
  const max = Math.max(...values)

  if (min === max) {
    return {
      mode: 'flat',
      min,
      max,
      positiveMax: max > 0 ? max : 0,
      negativeMin: min < 0 ? min : 0,
      fixedValue: min === 0 ? TREND_AXIS_MIN : TREND_AXIS_CENTER
    }
  }

  if (min < 0 && max > 0) {
    return {
      mode: 'mixed',
      min,
      max,
      positiveMax: max,
      negativeMin: min,
      fixedValue: TREND_AXIS_CENTER
    }
  }

  return {
    mode: 'single',
    min,
    max,
    positiveMax: max > 0 ? max : 0,
    negativeMin: min < 0 ? min : 0,
    fixedValue: TREND_AXIS_CENTER
  }
}

const mapTrendValue = (value: number, stats: TrendStats) => {
  const safeValue = Number(value)

  if (!Number.isFinite(safeValue)) return TREND_AXIS_MIN

  if (stats.mode === 'empty') return TREND_AXIS_MIN
  if (stats.mode === 'flat') return stats.fixedValue

  if (stats.mode === 'mixed') {
    if (safeValue === 0) return TREND_AXIS_CENTER

    if (safeValue > 0) {
      const positiveHeight = TREND_AXIS_MAX - TREND_AXIS_CENTER - TREND_AXIS_PADDING
      return Number(
        (TREND_AXIS_CENTER + (safeValue / (stats.positiveMax || 1)) * positiveHeight).toFixed(2)
      )
    }

    const negativeHeight = TREND_AXIS_CENTER - TREND_AXIS_MIN - TREND_AXIS_PADDING
    return Number(
      (
        TREND_AXIS_CENTER -
        (Math.abs(safeValue) / Math.abs(stats.negativeMin || -1)) * negativeHeight
      ).toFixed(2)
    )
  }

  const usableHeight = TREND_AXIS_MAX - TREND_AXIS_MIN - TREND_AXIS_PADDING * 2
  return Number(
    (
      TREND_AXIS_PADDING +
      ((safeValue - stats.min) / (stats.max - stats.min)) * usableHeight
    ).toFixed(2)
  )
}

function formatTrendAxisLabel(value: number) {
  if (value === TREND_AXIS_MIN) return '低'
  if (value === TREND_AXIS_CENTER) return '中'
  if (value === TREND_AXIS_MAX) return '高'
  return ''
}

function buildSeriesData(name: string) {
  const dataset = chartData.value[name] || []
  const stats = getTrendStats(dataset)

  return dataset.map(({ ts, value }) => [ts, mapTrendValue(value, stats), value])
}

function updateSeriesByNames(names: string[]) {
  if (!chart) render()
  if (!chart || !names.length) return

  chart.setOption({
    series: names.map((name) => ({
      name,
      data: buildSeriesData(name)
    }))
  })
}

const refreshVisibleSeries = useDebounceFn(() => {
  syncVisibleTimeRangeFromChart()
  updateSeriesByNames(getSelectedSeriesNames())
}, 50)

function chartInit() {
  if (!chart) return

  chart.off('legendselectchanged')
  chart.on('legendselectchanged', (params: any) => {
    selectedDimension.value = params.selected
    const clickedModelName = params.name
    const isSelected = params.selected[clickedModelName]

    if (isSelected) {
      updateSeriesByNames([clickedModelName])
    }
  })

  chart.off('datazoom')
  chart.on('datazoom', () => {
    refreshVisibleSeries()
  })

  window.removeEventListener('resize', resizeChart)
  window.addEventListener('resize', resizeChart)
}

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

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

  chartInit()

  chart.setOption({
    color: neonColors,
    animation: true,
    animationDuration: 200,
    animationEasing: 'linear',
    animationDurationUpdate: 200,
    animationEasingUpdate: 'linear',
    grid: {
      left: '3%',
      top: '60px',
      right: '6%',
      bottom: '10%',
      containLabel: true,
      show: false
    },
    tooltip: {
      trigger: 'axis',
      confine: true,
      enterable: true,
      className: 'echarts-tooltip-scroll',
      extraCssText:
        'max-height: 300px; overflow-y: auto; pointer-events: auto; border-radius: 4px;',
      backgroundColor: 'rgba(11, 17, 33, 0.95)',
      borderColor: '#22d3ee',
      borderWidth: 1,
      textStyle: {
        color: '#e2e8f0'
      },
      axisPointer: {
        type: 'line',
        lineStyle: { color: 'rgba(255,255,255,0.3)', type: 'dashed' }
      },
      formatter: (params: any) => {
        let d = `<div style="font-weight:bold; border-bottom:1px solid rgba(255,255,255,0.1); padding-bottom:5px; margin-bottom:5px;">${params[0].axisValueLabel}</div>`
        const exist: string[] = []
        params = params.filter((el: any) => {
          if (exist.includes(el.seriesName)) return false
          exist.push(el.seriesName)
          return true
        })

        // 优化列表显示，圆点使用原本的颜色
        let item = params.map(
          (
            el: any
          ) => `<div class="flex items-center justify-between mt-1" style="font-size:12px; min-width: 180px;">
            <span style="display:flex; align-items:center;">
                <span style="display:inline-block;width:8px;height:8px;border-radius:50%;background-color:${el.color};margin-right:6px;"></span>
                <span style="color:#cbd5e1">${el.seriesName}</span>
            </span>
            <span style="color:#fff; font-weight:bold; margin-left:10px;">${el.value[2]?.toFixed(2)}</span>
          </div>`
        )

        return d + item.join('')
      }
    },
    xAxis: {
      type: 'time',
      boundaryGap: ['0%', '25%'],
      axisLabel: {
        formatter: (v) => dayjs(v).format('YYYY-MM-DD\nHH:mm:ss'),
        rotate: 0,
        align: 'center',
        color: '#94a3b8',
        fontSize: 11
      },
      splitLine: {
        show: false,
        lineStyle: { color: 'rgba(255,255,255,0.5)', type: 'dashed' }
      }
    },
    dataZoom: [
      { type: 'inside', xAxisIndex: 0, filterMode: 'none', throttle: 50 },
      {
        type: 'slider',
        xAxisIndex: 0,
        filterMode: 'none',
        height: 20,
        bottom: 10,
        borderColor: 'transparent',
        backgroundColor: 'rgba(255,255,255,0.05)',
        fillerColor: 'rgba(34,211,238,0.2)',
        handleStyle: {
          color: '#22d3ee',
          borderColor: '#22d3ee'
        },
        labelFormatter: (value: any) => {
          return dayjs(value).format('YYYY-MM-DD\nHH:mm:ss')
        },
        textStyle: {
          color: '#94a3b8',
          fontSize: 10,
          lineHeight: 12
        },
        throttle: 50
      }
    ],
    yAxis: {
      type: 'value',
      min: TREND_AXIS_MIN,
      max: TREND_AXIS_MAX,
      splitNumber: 4,
      name: '相对趋势',
      nameGap: 14,
      nameTextStyle: {
        color: '#94a3b8'
      },
      axisLabel: {
        color: '#94a3b8',
        formatter: formatTrendAxisLabel
      },
      show: true,
      splitLine: {
        show: true,
        lineStyle: {
          color: 'rgba(255,255,255,0.05)',
          type: 'dashed'
        }
      },
      axisPointer: {
        show: false
      }
    },
    legend: {
      type: 'scroll', // 开启滚动，防止遮挡
      top: 10,

      left: 'center',
      width: '90%',

      textStyle: {
        color: '#e2e8f0', // 亮白色
        fontSize: 12
      },
      pageIconColor: '#22d3ee',
      pageIconInactiveColor: '#475569',
      pageTextStyle: { color: '#fff' },
      data: dimensions.value.map((item) => item.name),
      selected: selectedDimension.value,
      show: true
    },
    // legend: {
    //   data: dimensions.value.map((item) => item.name),
    //   selected: selectedDimension.value,
    //   show: true
    // },
    series: dimensions.value.map((item) => ({
      name: item.name,
      type: 'line',
      smooth: 0.3,
      showSymbol: false,

      endLabel: {
        show: true,
        formatter: (params) => params.value[2]?.toFixed(2),
        offset: [4, 0],
        color: '#fff',
        backgroundColor: 'auto',
        padding: [2, 6],
        borderRadius: 4,
        fontSize: 11,
        fontWeight: 'bold'
      },

      emphasis: {
        focus: 'series',
        lineStyle: { width: 4 }
      },

      lineStyle: {
        width: 3,
        shadowColor: 'rgba(0, 0, 0, 0.5)',
        shadowBlur: 10,
        shadowOffsetY: 5
      },

      connectNulls: true,
      data: [] // 占位数组
    }))
  })
}

const lastTsMap = ref<Record<Dimensions['name'], number>>({})

const chartLoading = ref(false)

async function initLoadChartData(real_time: boolean = true) {
  if (!dimensions.value.length) return

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

  chartLoading.value = true

  dimensions.value = dimensions.value.map((item) => {
    item.response = true
    return item
  })

  for (const item of dimensions.value) {
    const { identifier, name } = item
    try {
      const res = await IotStatApi.getDeviceInfoChart(
        props.deviceCode,
        identifier,
        props.date[0],
        props.date[1]
      )

      const sorted = res
        .sort((a, b) => a.ts - b.ts)
        .map((item) => ({ ts: item.ts, value: item.value }))

      chartData.value[name] = sorted

      if (isFollowingFullRange.value) {
        resetVisibleTimeRange()
      }

      lastTsMap.value[name] = sorted.at(-1)?.ts ?? 0

      updateSeriesByNames([name])

      chartLoading.value = false
    } finally {
      item.response = false
    }
  }

  if (real_time) {
    connect(`wss://aims.deepoil.cc/mqtt`, { password: props.token }, handleMessageUpdate)
  }
}

async function initfn(load: boolean = true, real_time: boolean = true) {
  if (load) await loadDimensions()
  render()
  initLoadChartData(real_time)
}

onMounted(() => {
  initfn()
})

watch(
  () => props.date,
  async (newDate, oldDate) => {
    if (!newDate || newDate.length !== 2) return

    if (oldDate && newDate[0] === oldDate[0] && newDate[1] === oldDate[1]) return

    await cancelAllRequests()

    destroy()

    const endTime = dayjs(newDate[1])
    const now = dayjs()
    const isRealTime = endTime.isAfter(now.subtract(1, 'minute'))

    if (chart) chart.clear()

    if (isRealTime) initfn(false)
    else initfn(false, false)
  }
)

onUnmounted(() => {
  destroy()

  window.removeEventListener('resize', resizeChart)
})

const router = useRouter()

function handleDetailClick() {
  router.push({
    name: 'MonitoringDetail',
    query: {
      id: props.id,
      ifInline: props.ifInline,
      carOnline: props.carOnline,
      time: props.lastInlineTime,
      name: props.deviceName,
      code: props.deviceCode,
      dept: props.deptName,
      vehicle: props.vehicleName
    }
  })
}
</script>
<template>
  <div class="h-100 rounded-lg chart-container flex flex-col">
    <header class="chart-header justify-between">
      <div class="flex items-center">
        <div class="title-icon"></div>
        <div>{{ `${props.deviceCode}-${props.deviceName}` }}</div>
      </div>
      <el-button link type="primary" class="group" @click="handleDetailClick">
        详情
        <div
          class="i-material-symbols:arrow-right-alt-rounded size-4 transition-transform group-hover:translate-x-1"
        ></div>
      </el-button>
    </header>
    <main
      class="flex-1 chart-main"
      ref="chartRef"
      v-loading="chartLoading"
      element-loading-background="transparent"
    ></main>
  </div>
</template>
<style scoped>
.chart-container {
  position: relative;
  overflow: hidden;
  background-color: rgb(11 17 33 / 90%);
  border: 2px solid rgb(34 211 238 / 30%);
  box-shadow:
    0 0 20px rgb(0 0 0 / 80%),
    inset 0 0 15px rgb(34 211 238 / 10%);
  transition:
    border-color 0.3s ease,
    transform 0.3s ease;
}

.chart-container::before {
  position: absolute;
  pointer-events: none;
  background: radial-gradient(
    400px circle at var(--mouse-x, 50%) var(--mouse-y, 50%),
    rgb(34 211 238 / 5%),
    transparent 40%
  );
  content: '';
  inset: 0;
}

.chart-container:hover {
  border-color: rgb(34 211 238 / 60%);
  transform: scale(1.005);
}

.chart-header {
  display: flex;
  padding: 12px 16px;
  font-size: 18px;
  font-weight: 600;
  letter-spacing: 1px;
  color: #e2e8f0;
  background: rgb(255 255 255 / 3%);
  border-bottom: 1px solid transparent;
  border-image: linear-gradient(to right, rgb(34 211 238 / 50%), transparent) 1;
  align-items: center;
}

.title-icon {
  width: 4px;
  height: 16px;
  margin-right: 10px;
  background: #22d3ee;
  box-shadow: 0 0 8px #22d3ee;
}

.chart-main {
  padding-top: 12px;
  background-image: radial-gradient(circle at 50% 50%, rgb(34 211 238 / 10%) 0%, transparent 80%),
    linear-gradient(to right, rgb(34 211 238 / 15%) 1px, transparent 1px),
    linear-gradient(to bottom, rgb(34 211 238 / 15%) 1px, transparent 1px),
    linear-gradient(135deg, rgb(11 17 33 / 90%) 0%, rgb(6 9 18 / 95%) 100%);
  background-size:
    100% 100%,
    40px 40px,
    40px 40px,
    100% 100%;
}

/* 针对 ECharts tooltip 的滚动条美化 */
.echarts-tooltip-scroll::-webkit-scrollbar {
  width: 6px;
}

.echarts-tooltip-scroll::-webkit-scrollbar-thumb {
  background: #22d3ee; /* 青色滑块 */
  border-radius: 3px;
}

.echarts-tooltip-scroll::-webkit-scrollbar-track {
  background: rgb(255 255 255 / 10%); /* 深色轨道 */
}
</style>
