import { AfterViewInit, Component, OnDestroy, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

import { ApiService, ForecastPoint, WeatherPoint } from '../../../src/app/api.service';
import { ChartPanelComponent } from '../../../src/app/chart-panel.component';
import { TablePanelComponent } from '../../../src/app/table-panel.component';

type Station = {
  station_id: string;
  decimal_id?: string;
  latitude?: number;
  longitude?: number;
  location?: string;
  device_type?: string;
  last_update?: string;
  name?: string;
  station_alias?: string;
};

type AlertLevel = 'none' | 'orange' | 'red';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, FormsModule, ChartPanelComponent, TablePanelComponent],
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class BigscreenAppComponent implements OnInit, AfterViewInit, OnDestroy {
  constructor(private api: ApiService) {}

  private readonly DEFAULT_STATION_HEX = '29C3';
  private readonly DEFAULT_TITLE = '第七台气象站（宾阳县细塘村东南约112米）';

  onlineDevices = 0;
  serverTime = '';
  stations: Station[] = [];

  hexId = this.DEFAULT_STATION_HEX;
  interval: 'raw' | '10min' | '30min' | '1hour' = '1hour';
  provider = 'imdroid_mix';
  start = '';
  end = '';
  legendMode = 'combo_standard';
  tileProduct: 'none' | 'radar' | 'rain' = 'radar';

  history: WeatherPoint[] = [];
  forecast: ForecastPoint[] = [];

  isLoading = false;

  // Summary + presentation state
  selectedLocation = '';
  selectedTitle = this.DEFAULT_TITLE;
  futureRainText = '未来1~3小时降雨 -- 毫米';
  pastAccuracyText = '过去预报准确率  +1h: --  +2h: --  +3h: --';
  alertLevel: AlertLevel = 'none';

  tileTimes: string[] = [];
  tileIndex = -1;
  tileDt = '';
  tileZ = 7;
  tileY = 40;
  tileX = 102;

  private map: any;
  private layers: any = {};
  private stationSource: any;
  private clusterSource: any;
  private stationLayer: any;
  private clusterLayer: any;
  private readonly CLUSTER_THRESHOLD = 10;
  private tileOverlayGroup: any;
  private tileLastList: any[] = [];
  private mapReady = false;
  private stationsReady = false;
  private initialQueryScheduled = false;

  async ngOnInit() {
    this.applyChartDefaults();
    await Promise.all([this.loadStatus(), this.loadStations()]);
    this.initializeTimeRange();
    this.ensureDefaultTitle();
    this.tryInitialQuery();
  }

  ngAfterViewInit() {
    this.initMap();
    this.reloadTileTimesAndShow();
  }

  ngOnDestroy(): void {
    this.setBodyAlertClass('none');
  }

  get tileCountInfo(): string {
    if (!this.tileTimes.length || this.tileIndex < 0) {
      return '共0条，第0条';
    }
    return `共${this.tileTimes.length}条，第${this.tileIndex + 1}条`;
  }

  async query(auto = false) {
    const hex = this.hexId.trim().toUpperCase();
    if (!hex) return;
    const sid = this.makeStationIdFromHex(hex);
    if (!sid) return;
    this.hexId = hex;

    const toFmt = (s: string) => s.replace('T', ' ') + ':00';
    const from = toFmt(this.start);
    const to = toFmt(this.end);

    this.isLoading = true;
    try {
      const [history, forecast] = await Promise.all([
        this.api.getHistory(hex, from, to, this.interval),
        this.provider ? this.api.getForecast(sid, from, to, this.provider, 3) : Promise.resolve([]),
      ]);
      this.history = history;
      this.forecast = forecast;
    } finally {
      this.isLoading = false;
    }

    const station = this.stations.find((s) => s.station_id === sid);
    this.selectedLocation = station?.location || '';
    const titleName = station?.name || station?.station_alias || station?.station_id || '';
    this.selectedTitle = titleName ? `${titleName}${this.selectedLocation ? `（${this.selectedLocation}）` : ''}` : this.selectedLocation;
    if (!this.selectedTitle) {
      this.ensureDefaultTitle();
    }

    this.focusOnStation(station);
    this.updateSummaryPanel();
    if (!auto) {
      this.scrollToTop();
    }
  }

  onProductChange() {
    this.reloadTileTimesAndShow();
  }

  async reloadTileTimesAndShow() {
    if (this.tileProduct === 'none') {
      this.clearTileOverlays();
      this.tileTimes = [];
      this.tileDt = '';
      this.tileIndex = -1;
      return;
    }
    await this.loadTileTimes(this.tileProduct);
  }

  prevTile() {
    if (!this.tileTimes.length) return;
    if (this.tileIndex < this.tileTimes.length - 1) {
      this.tileIndex += 1;
      this.tileDt = this.tileTimes[this.tileIndex];
      this.renderTilesAt(this.tileDt);
    }
  }

  nextTile() {
    if (!this.tileTimes.length) return;
    if (this.tileIndex > 0) {
      this.tileIndex -= 1;
      this.tileDt = this.tileTimes[this.tileIndex];
      this.renderTilesAt(this.tileDt);
    }
  }

  private async loadStatus() {
    const s = await this.api.getStatus();
    if (s) {
      this.onlineDevices = s.online_devices || 0;
      this.serverTime = s.server_time || '';
    }
  }

  private async loadStations() {
    this.stations = await this.api.getStations();
    this.updateStationsOnMap();
    try {
      const expected = this.makeStationIdFromHex(this.hexId || this.DEFAULT_STATION_HEX);
      const exists = !!this.stations.find((s) => s.station_id === expected);
      if (!exists) {
        const fallback = this.stations.find((s) => s.station_id && s.station_id.length >= 6);
        if (fallback) {
          const hex = fallback.station_id.slice(-6).toUpperCase();
          if (hex) this.hexId = hex;
          if (fallback.location) this.selectedTitle = fallback.location;
        }
      }
    } catch {}
    this.stationsReady = true;
    this.tryInitialQuery();
  }

  private ensureDefaultTitle() {
    if (!this.selectedTitle) {
      this.selectedTitle = this.DEFAULT_TITLE;
    }
    if (!this.hexId) {
      this.hexId = this.DEFAULT_STATION_HEX;
    }
    this.hexId = (this.hexId || '').toUpperCase();
  }

  private tryInitialQuery() {
    if (this.initialQueryScheduled) return;
    if (!this.mapReady || !this.stationsReady) return;
    if (!this.hexId || !this.start || !this.end) return;
    this.initialQueryScheduled = true;
    setTimeout(() => {
      this.query(true).catch(() => {
        this.initialQueryScheduled = false;
      });
    }, 300);
  }

  private initializeTimeRange() {
    const now = new Date();
    const pad = (n: number) => String(n).padStart(2, '0');
    const toLocal = (d: Date) =>
      `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
    const end = new Date(now.getTime() + 4 * 3600 * 1000);
    const start = new Date(now.getTime() - 24 * 3600 * 1000);
    this.end = toLocal(end);
    this.start = toLocal(start);
  }

  private applyChartDefaults() {
    try {
      const Chart = (window as any).Chart;
      if (Chart && Chart.defaults) {
        Chart.defaults.color = '#d5e3ff';
        Chart.defaults.borderColor = 'rgba(213,227,255,0.25)';
        if (Chart.defaults.scale && Chart.defaults.scale.grid) {
          Chart.defaults.scale.grid.color = 'rgba(213,227,255,0.12)';
          Chart.defaults.scale.ticks = Chart.defaults.scale.ticks || {};
          Chart.defaults.scale.ticks.color = '#cbd9ff';
        }
      }
    } catch {
      /* noop */
    }
  }

  private makeStationIdFromHex(hexRaw: string): string | null {
    if (!hexRaw) return null;
    const hex = String(hexRaw).toUpperCase().replace(/[^0-9A-F]/g, '').padStart(6, '0').slice(-6);
    if (!hex) return null;
    return `RS485-${hex}`;
  }

  private initMap() {
    const ol: any = (window as any).ol;
    if (!ol) return;
    const tk = this.getTiandituKey();
    const mkLayer = (url: string) =>
      new ol.layer.Tile({
        source: new ol.source.XYZ({ url }),
      });
    this.layers = {
      satellite: new ol.layer.Group({
        layers: [
          mkLayer(
            `https://t{0-7}.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tk}`,
          ),
          mkLayer(
            `https://t{0-7}.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tk}`,
          ),
        ],
      }),
      vector: new ol.layer.Group({
        layers: [
          mkLayer(
            `https://t{0-7}.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tk}`,
          ),
          mkLayer(
            `https://t{0-7}.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tk}`,
          ),
        ],
        visible: false,
      }),
      terrain: new ol.layer.Group({
        layers: [
          mkLayer(
            `https://t{0-7}.tianditu.gov.cn/ter_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=ter&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tk}`,
          ),
          mkLayer(
            `https://t{0-7}.tianditu.gov.cn/cta_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cta&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tk}`,
          ),
        ],
        visible: false,
      }),
      hybrid: new ol.layer.Group({
        layers: [
          mkLayer(
            `https://t{0-7}.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tk}`,
          ),
          mkLayer(
            `https://t{0-7}.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tk}`,
          ),
        ],
        visible: false,
      }),
    };

    this.stationSource = new ol.source.Vector();
    this.clusterSource = new ol.source.Cluster({ distance: 60, minDistance: 20, source: this.stationSource });
    this.clusterLayer = new ol.layer.Vector({ source: this.clusterSource, style: (f: any) => this.createClusterStyle(f) });
    this.stationLayer = new ol.layer.Vector({ source: this.stationSource, visible: false, style: (f: any) => this.createStationStyle(f) });
    this.tileOverlayGroup = new ol.layer.Group({ layers: [], zIndex: 999, visible: true });

    this.map = new ol.Map({
      target: 'map',
      layers: [this.layers.satellite, this.layers.vector, this.layers.terrain, this.layers.hybrid, this.tileOverlayGroup, this.clusterLayer, this.stationLayer],
      view: new ol.View({ center: ol.proj.fromLonLat([108, 35]), zoom: 5, minZoom: 3, maxZoom: 18 }),
    });

    this.map.getView().on('change:resolution', () => {
      const z = this.map.getView().getZoom();
      this.updateClusterDistance(z);
      this.updateLayerVisibility(z);
    });

    this.map.on('singleclick', async (evt: any) => {
      const olAny: any = (window as any).ol;
      const features = this.map.getFeaturesAtPixel(evt.pixel, { layerFilter: (l: any) => l === this.stationLayer || l === this.clusterLayer });
      if (!features || features.length === 0) return;
      const feature = features[0];
      const subFeatures = feature.get('features');
      if (Array.isArray(subFeatures) && subFeatures.length > 0) {
        const extent = olAny.extent.createEmpty();
        subFeatures.forEach((sf: any) => {
          olAny.extent.extend(extent, sf.getGeometry().getExtent());
        });
        this.map.getView().fit(extent, { duration: 350, padding: [40, 40, 40, 40], maxZoom: 14 });
        return;
      }
      const hex = feature.get('stationHex');
      if (!hex) return;
      this.hexId = hex;
      await this.query();
    });

    this.map.on('pointermove', (evt: any) => {
      const features = this.map.getFeaturesAtPixel(evt.pixel, { layerFilter: (l: any) => l === this.stationLayer || l === this.clusterLayer });
      const el = this.map.getTargetElement();
      if (el) el.style.cursor = features && features.length > 0 ? 'pointer' : '';
      this.showTileTooltip(evt);
    });

    if (this.stations?.length) this.updateStationsOnMap();
    this.mapReady = true;
    this.tryInitialQuery();
  }

  private getTiandituKey(): string {
    const anyWin = window as any;
    return anyWin.TIANDITU_KEY || '0c260b8a094a4e0bc507808812cefdac';
  }

  private updateClusterDistance(zoom: number) {
    if (!this.clusterSource) return;
    const distance = zoom < this.CLUSTER_THRESHOLD ? 60 : 20;
    this.clusterSource.setDistance(distance);
  }

  private updateLayerVisibility(zoom: number) {
    if (!this.clusterLayer || !this.stationLayer) return;
    const showCluster = zoom < this.CLUSTER_THRESHOLD;
    this.clusterLayer.setVisible(showCluster);
    this.stationLayer.setVisible(!showCluster);
  }

  private markerIcon(isOnline: boolean) {
    const ol: any = (window as any).ol;
    const src = isOnline ? '/static/images/marker-online.svg' : '/static/images/marker-offline.svg';
    return new ol.style.Icon({ src, anchor: [0.5, 1], anchorXUnits: 'fraction', anchorYUnits: 'fraction', scale: 0.9 });
  }

  private createStationStyle(feature: any) {
    const ol: any = (window as any).ol;
    const last = feature.get('lastUpdate');
    const online = last ? new Date(last).getTime() > Date.now() - 5 * 60 * 1000 : false;
    const location = feature.get('location') || '';
    return new ol.style.Style({
      image: this.markerIcon(online),
      text: location
        ? new ol.style.Text({
            text: location,
            offsetY: -28,
            fill: new ol.style.Fill({ color: '#111' }),
            stroke: new ol.style.Stroke({ color: '#fff', width: 3 }),
            font: '12px sans-serif',
          })
        : undefined,
    });
  }

  private createClusterStyle(feature: any) {
    const ol: any = (window as any).ol;
    const features = feature.get('features') || [];
    const size = features.length;
    const zoom = this.map.getView().getZoom();
    if (zoom < this.CLUSTER_THRESHOLD) {
      if (size > 1) {
        const radius = Math.min(16 + size * 0.8, 32);
        const fontSize = Math.min(11 + size / 12, 16);
        return new ol.style.Style({
          image: new ol.style.Circle({
            radius,
            fill: new ol.style.Fill({ color: 'rgba(0,123,255,0.8)' }),
            stroke: new ol.style.Stroke({ color: '#fff', width: 2 }),
          }),
          text: new ol.style.Text({
            text: String(size),
            fill: new ol.style.Fill({ color: '#fff' }),
            font: `bold ${fontSize}px Arial`,
            offsetY: 1,
          }),
        });
      } else {
        const f0 = features[0];
        const last = f0?.get('lastUpdate');
        const online = last ? new Date(last).getTime() > Date.now() - 5 * 60 * 1000 : false;
        const color = online ? 'rgba(0,123,255,0.8)' : 'rgba(108,117,125,0.8)';
        return new ol.style.Style({
          image: new ol.style.Circle({
            radius: 6,
            fill: new ol.style.Fill({ color }),
            stroke: new ol.style.Stroke({ color: '#fff', width: 2 }),
          }),
        });
      }
    }
    const f0 = features[0];
    const last = f0?.get('lastUpdate');
    const online = last ? new Date(last).getTime() > Date.now() - 5 * 60 * 1000 : false;
    return new ol.style.Style({
      image: this.markerIcon(online),
    });
  }

  private updateStationsOnMap() {
    const ol: any = (window as any).ol;
    if (!ol || !this.stationSource) return;
    const features = (this.stations || []).map((s) => {
      if (typeof s.longitude !== 'number' || typeof s.latitude !== 'number') return null;
      const stationId = s.station_id || '';
      if (!stationId) return null;
      const hex = stationId ? stationId.slice(-6).toUpperCase() : '';
      if (!hex) return null;
      const f = new ol.Feature({ geometry: new ol.geom.Point(ol.proj.fromLonLat([s.longitude, s.latitude])) });
      f.set('stationId', stationId);
      f.set('stationHex', hex);
      f.set('location', s.location || '');
      f.set('lastUpdate', s.last_update || '');
      return f;
    });
    this.stationSource.clear();
    features.filter(Boolean).forEach((f: any) => this.stationSource.addFeature(f));
  }

  private async loadTileTimes(product: 'radar' | 'rain') {
    try {
      const params = new URLSearchParams({ z: String(this.tileZ), y: String(this.tileY), x: String(this.tileX), limit: '60' });
      const path = product === 'rain' ? '/api/rain/times' : '/api/radar/times';
      const r = await fetch(`${path}?${params.toString()}`);
      if (!r.ok) return;
      const j = await r.json();
      this.tileTimes = j.times || [];
      this.tileIndex = 0;
      this.tileDt = this.tileTimes[0] || '';
      if (this.tileDt) await this.renderTilesAt(this.tileDt);
    } catch {
      this.tileTimes = [];
      this.tileIndex = -1;
      this.tileDt = '';
    }
  }

  async renderTilesAt(dt: string) {
    if (!dt) {
      this.clearTileOverlays();
      return;
    }
    try {
      const params = new URLSearchParams({ z: String(this.tileZ), dt: dt });
      const path = this.tileProduct === 'rain' ? '/api/rain/tiles_at' : '/api/radar/tiles_at';
      const r = await fetch(`${path}?${params.toString()}`);
      if (!r.ok) {
        this.clearTileOverlays();
        return;
      }
      const j = await r.json();
      const tiles = Array.isArray(j.tiles) ? j.tiles : [];
      if (tiles.length === 0) {
        this.clearTileOverlays();
        return;
      }
      await this.renderTilesOnMap(this.tileProduct, tiles);
      const idx = this.tileTimes.findIndex((t) => t === dt);
      if (idx >= 0) {
        this.tileIndex = idx;
      }
    } catch {
      this.clearTileOverlays();
    }
  }

  private clearTileOverlays() {
    if (!this.tileOverlayGroup) return;
    const coll = this.tileOverlayGroup.getLayers();
    if (coll) coll.clear();
    this.tileLastList = [];
  }

  private addImageOverlayFromCanvas(canvas: HTMLCanvasElement, extent4326: [number, number, number, number]) {
    const ol: any = (window as any).ol;
    if (!ol || !this.map) return;
    const proj = this.map.getView().getProjection();
    const extentProj = ol.proj.transformExtent(extent4326, 'EPSG:4326', proj);
    const src = new ol.source.ImageStatic({ url: canvas.toDataURL('image/png'), imageExtent: extentProj, projection: proj });
    const layer = new ol.layer.Image({ source: src, opacity: 0.8, visible: true });
    this.tileOverlayGroup.getLayers().push(layer);
  }

  private async renderTilesOnMap(product: 'none' | 'radar' | 'rain', tiles: any[]) {
    this.clearTileOverlays();
    const lastList: any[] = [];
    for (const t of tiles) {
      const w = t.width,
        h = t.height;
      if (!w || !h || !t.values) continue;
      const canvas = document.createElement('canvas');
      canvas.width = w;
      canvas.height = h;
      const ctx = canvas.getContext('2d')!;
      const img = ctx.createImageData(w, h);
      const radarColors = [
        [0, 0, 255],
        [0, 191, 255],
        [0, 255, 255],
        [127, 255, 212],
        [124, 252, 0],
        [173, 255, 47],
        [255, 255, 0],
        [255, 215, 0],
        [255, 165, 0],
        [255, 140, 0],
        [255, 69, 0],
        [255, 0, 0],
        [220, 20, 60],
        [199, 21, 133],
        [139, 0, 139],
      ];
      const rainEdges = [0, 5, 7.5, 10, 12.5, 15, 17.5, 20, 25, 30, 40, 50, 75, 100, Infinity];
      const rainColors = [
        [126, 212, 121],
        [126, 212, 121],
        [110, 200, 109],
        [97, 169, 97],
        [81, 148, 76],
        [90, 158, 112],
        [143, 194, 254],
        [92, 134, 245],
        [66, 87, 240],
        [45, 48, 214],
        [26, 15, 166],
        [63, 22, 145],
        [191, 70, 148],
        [213, 1, 146],
        [213, 1, 146],
      ];
      for (let row = 0; row < h; row++) {
        const srcRow = t.values[row] as (number | null)[];
        const dstRow = h - 1 - row;
        for (let col = 0; col < w; col++) {
          const v = srcRow[col];
          const off = (dstRow * w + col) * 4;
          if (v == null || v === 0) {
            img.data[off + 3] = 0;
            continue;
          }
          if (product === 'rain') {
            let idx = 0;
            while (idx < rainEdges.length - 1 && !(v >= rainEdges[idx] && v < rainEdges[idx + 1])) idx++;
            const c = rainColors[Math.min(idx, rainColors.length - 1)];
            img.data[off] = c[0];
            img.data[off + 1] = c[1];
            img.data[off + 2] = c[2];
            img.data[off + 3] = 220;
          } else {
            let bin = Math.floor(Math.max(0, Math.min(75, v)) / 5);
            if (bin >= radarColors.length) bin = radarColors.length - 1;
            const c = radarColors[bin];
            img.data[off] = c[0];
            img.data[off + 1] = c[1];
            img.data[off + 2] = c[2];
            img.data[off + 3] = 220;
          }
        }
      }
      ctx.putImageData(img, 0, 0);
      this.addImageOverlayFromCanvas(canvas, [t.west, t.south, t.east, t.north]);
      lastList.push({
        product,
        meta: { west: t.west, south: t.south, east: t.east, north: t.north, width: w, height: h },
        values: t.values,
      });
    }
    this.tileLastList = lastList;
  }

  private focusOnStation(station?: Station) {
    const ol: any = (window as any).ol;
    if (!station || !ol || typeof station.longitude !== 'number' || typeof station.latitude !== 'number' || !this.map) return;
    this.map.getView().animate({
      center: ol.proj.fromLonLat([station.longitude, station.latitude]),
      zoom: 11,
      duration: 400,
    });
    setTimeout(() => {
      try {
        this.map.updateSize();
      } catch {
        /* noop */
      }
    }, 300);
  }

  private scrollToTop() {
    const el = document.querySelector('.screen');
    if (el && 'scrollIntoView' in el) {
      try {
        (el as HTMLElement).scrollIntoView({ behavior: 'smooth', block: 'start' });
      } catch {
        /* noop */
      }
    }
  }

  private showTileTooltip(evt: any) {
    const tip = document.getElementById('tileValueTooltip');
    if (!tip || !this.map || !this.tileLastList || this.tileLastList.length === 0) {
      if (tip) tip.style.display = 'none';
      return;
    }
    try {
      const coord = this.map.getEventCoordinate(evt.originalEvent);
      const lonlat = (window as any).ol.proj.transform(coord, this.map.getView().getProjection(), 'EPSG:4326');
      const lon = lonlat[0],
        lat = lonlat[1];
      let value: number | null = null;
      let unit = '';
      for (const it of this.tileLastList) {
        const { west, south, east, north, width, height } = it.meta;
        if (lon < west || lon > east || lat < south || lat > north) continue;
        const px = Math.floor((lon - west) / ((east - west) / width));
        const py = Math.floor((lat - south) / ((north - south) / height));
        if (px < 0 || px >= width || py < 0 || py >= height) continue;
        const v = it.values?.[py]?.[px];
        if (v != null) {
          value = Number(v);
          unit = it.product === 'rain' ? 'mm' : 'dBZ';
          break;
        }
      }
      if (value == null) {
        tip.style.display = 'none';
        return;
      }
      tip.textContent = `${value.toFixed(1)} ${unit}`;
      const px = evt.pixel[0] + 12;
      const py = evt.pixel[1] + 12;
      tip.style.left = `${px}px`;
      tip.style.top = `${py}px`;
      tip.style.display = 'block';
    } catch {
      if (tip) tip.style.display = 'none';
    }
  }

  private updateSummaryPanel() {
    const historyData = Array.isArray(this.history) ? this.history : [];
    const forecastData = Array.isArray(this.forecast) ? this.forecast : [];

    const fmt = (n: number | null | undefined) => {
      if (n == null || isNaN(Number(n))) return '--';
      return Number(n).toFixed(1);
    };
    const pad2 = (n: number) => String(n).padStart(2, '0');
    const fmtDT = (d: Date) => `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:00:00`;
    const ceilHour = (d: Date) => {
      const t = new Date(d);
      if (t.getMinutes() || t.getSeconds() || t.getMilliseconds()) {
        t.setHours(t.getHours() + 1);
      }
      t.setMinutes(0, 0, 0);
      return t;
    };

    const now = new Date();
    const t1 = ceilHour(now);
    const t2 = new Date(t1.getTime() + 1 * 3600 * 1000);
    const t3 = new Date(t1.getTime() + 2 * 3600 * 1000);
    const pickBestAt = (dtStr: string) => {
      const cand = forecastData.filter((x) => x && x.date_time === dtStr && x.rainfall != null);
      if (!cand.length) return null;
      cand.sort((a, b) => (a.lead_hours ?? 99) - (b.lead_hours ?? 99));
      return cand[0].rainfall ?? null;
    };

    const r1 = pickBestAt(fmtDT(t1));
    const r2 = pickBestAt(fmtDT(t2));
    const r3 = pickBestAt(fmtDT(t3));
    const safe = (v: number | null) => (v != null ? Number(v) : 0);
    const futureSum = safe(r1) + safe(r2) + safe(r3);
    this.futureRainText = `未来1~3小时降雨 ${fmt(futureSum)} 毫米`;

    let level: AlertLevel = 'none';
    if (futureSum > 1) {
      level = 'red';
    } else if (futureSum > 0) {
      level = 'orange';
    }
    this.alertLevel = level;
    this.setBodyAlertClass(level);

    const bucketOf = (mm: any): number | null => {
      if (mm == null || isNaN(Number(mm))) return null;
      const v = Math.max(0, Number(mm));
      if (v === 0) return 0;
      if (v > 0 && v <= 5) return 1;
      if (v > 5 && v <= 10) return 2;
      return 3;
    };

    const rainActual = new Map<string, number | null>();
    historyData.forEach((it) => {
      if (it && it.date_time) rainActual.set(it.date_time, it.rainfall ?? null);
    });
    const tally = (lead: number) => {
      let correct = 0;
      let total = 0;
      forecastData.forEach((f) => {
        if (f.lead_hours !== lead) return;
        const a = rainActual.get(f.date_time);
        if (a == null) return;
        const ba = bucketOf(a);
        const bf = bucketOf(f.rainfall);
        if (ba == null || bf == null) return;
        total += 1;
        if (ba === bf) correct += 1;
      });
      return { correct, total };
    };
    const rH1 = tally(1);
    const rH2 = tally(2);
    const rH3 = tally(3);
    const pct = (r: { correct: number; total: number }) => (r.total > 0 ? `${((r.correct / r.total) * 100).toFixed(1)}%` : '--');
    this.pastAccuracyText = `过去预报准确率  +1h: ${pct(rH1)}  +2h: ${pct(rH2)}  +3h: ${pct(rH3)}`;
  }

  private setBodyAlertClass(level: AlertLevel) {
    const body = document.body;
    if (!body) return;
    body.classList.toggle('alert-mode', level !== 'none');
    body.classList.toggle('alert-mode-red', level === 'red');
    body.classList.toggle('alert-mode-orange', level === 'orange');
  }
}
