import { h } from 'preact';
import { useEffect, useRef, useState } from 'preact/hooks';
import uPlot from 'uplot';
import * as echarts from 'echarts';
import { getSignalStore } from '../lib/stores';
import { wsClient } from '../lib/ws';
import type { Widget, SignalRef } from '../lib/types';

interface Props {
  widget: Widget;
  onContextMenu?: (e: MouseEvent) => void;
  timeRange?: { start: string; end: string } | null;
}

interface SeriesBuffer {
  timestamps: number[];
  values: number[];
}

const RING_MAX = 4000;
const COLORS = ['#4a9eff', '#22c55e', '#f59e0b', '#ef4444', '#a78bfa', '#f472b6', '#34d399', '#fb923c'];

const ECHARTS_DARK = {
  backgroundColor: '#1a1f2e',
  textStyle: { color: '#94a3b8' },
  axisLineStyle: { color: '#475569' },
  axisLabelStyle: { color: '#64748b' },
  splitLineStyle: { color: '#2d3748' },
};

function pushSample(buf: SeriesBuffer, ts: number, val: number) {
  buf.timestamps.push(ts);
  buf.values.push(val);
  if (buf.timestamps.length > RING_MAX) {
    buf.timestamps.splice(0, buf.timestamps.length - RING_MAX);
    buf.values.splice(0, buf.values.length - RING_MAX);
  }
}

function windowedSlice(buf: SeriesBuffer, windowSec: number) {
  const cutoff = Date.now() / 1000 - windowSec;
  const start = buf.timestamps.findIndex(t => t >= cutoff);
  if (start === -1) return { ts: [] as number[], vals: [] as number[] };
  return { ts: buf.timestamps.slice(start), vals: buf.values.slice(start) };
}

function buildHistogram(allVals: number[][]): { labels: string[]; counts: number[][] } {
  const BUCKETS = 20;
  let lo = Infinity, hi = -Infinity;
  for (const vals of allVals) for (const v of vals) { if (v < lo) lo = v; if (v > hi) hi = v; }
  if (!isFinite(lo) || lo === hi) return { labels: [], counts: allVals.map(() => []) };
  const size = (hi - lo) / BUCKETS;
  const counts = allVals.map(vals => {
    const bins = new Array<number>(BUCKETS).fill(0);
    for (const v of vals) bins[Math.min(BUCKETS - 1, Math.floor((v - lo) / size))]++;
    return bins;
  });
  const labels = Array.from({ length: BUCKETS }, (_, i) => (lo + i * size).toPrecision(3));
  return { labels, counts };
}

export default function PlotWidget({ widget, onContextMenu, timeRange }: Props) {
  const outerRef = useRef<HTMLDivElement>(null);
  const chartRef = useRef<HTMLDivElement>(null);
  const [histStatus, setHistStatus] = useState<'idle' | 'loading' | 'empty' | 'error'>('idle');

  // Stable primitive deps so the effect re-runs when plotType or signals change.
  const timeRangeKey = timeRange ? `${timeRange.start}|${timeRange.end}` : 'live';
  const plotType = widget.options['plotType'] ?? 'timeseries';
  const signalsKey = widget.signals.map(s => `${s.ds}:${s.name}${s.color ?? ''}`).join(',');

  useEffect(() => {
    if (!chartRef.current) return;

    const signals = widget.signals;
    const timeWindow = parseFloat(widget.options['timeWindow'] ?? '60');
    const yMin = widget.options['yMin'];
    const yMax = widget.options['yMax'];
    const legendPos = widget.options['legend'] ?? 'bottom';
    const showLegend = legendPos !== 'none';

    const buffers: SeriesBuffer[] = signals.map(() => ({ timestamps: [], values: [] }));
    const histValues: number[][] = signals.map(() => []);
    const unsubs: (() => void)[] = [];

    // ── uPlot (timeseries) ──────────────────────────────────────────────────
    let uplot: uPlot | null = null;

    // windowSec: apply rolling window for live mode; 0 = use full buffer (historical)
    function uplotData(windowSec = 0): uPlot.AlignedData {
      if (signals.length === 0) return [[]];
      const allTs = new Set<number>();
      buffers.forEach(b => {
        const pts = windowSec > 0 ? windowedSlice(b, windowSec).ts : b.timestamps;
        pts.forEach(t => allTs.add(t));
      });
      const tsSorted = Array.from(allTs).sort((a, b) => a - b);
      if (tsSorted.length === 0) {
        return [[], ...signals.map(() => [])] as uPlot.AlignedData;
      }
      const tsMap = buffers.map(b => {
        const m = new Map<number, number>();
        const pts = windowSec > 0 ? windowedSlice(b, windowSec) : { ts: b.timestamps, vals: b.values };
        pts.ts.forEach((t, i) => m.set(t, pts.vals[i]));
        return m;
      });
      return [
        tsSorted,
        ...tsMap.map(m => tsSorted.map(t => m.get(t) ?? null)),
      ] as uPlot.AlignedData;
    }

    // ── ECharts ────────────────────────────────────────────────────────────
    let echart: echarts.EChartsType | null = null;

    function echartsOption(): any {
      const axisLine = { lineStyle: { color: '#475569' } };
      const axisLabel = { color: '#64748b' };
      const splitLine = { lineStyle: { color: '#2d3748' } };
      const legendOpt = showLegend
        ? { top: legendPos === 'top' ? 0 : 'bottom', textStyle: { color: '#94a3b8' } }
        : undefined;

      switch (plotType) {
        case 'histogram': {
          const { labels, counts } = buildHistogram(histValues);
          return {
            backgroundColor: ECHARTS_DARK.backgroundColor,
            legend: legendOpt,
            xAxis: { type: 'category', data: labels, axisLine, axisLabel, splitLine },
            yAxis: { type: 'value', axisLine, axisLabel, splitLine },
            series: counts.map((d, i) => ({
              name: signals[i]?.name ?? `S${i}`,
              type: 'bar',
              data: d,
              itemStyle: { color: signals[i]?.color ?? COLORS[i % COLORS.length] },
            })),
          };
        }
        case 'bar': {
          return {
            backgroundColor: ECHARTS_DARK.backgroundColor,
            legend: legendOpt,
            xAxis: { type: 'category', data: signals.map(s => s.name), axisLine, axisLabel },
            yAxis: { type: 'value', axisLine, axisLabel, splitLine },
            series: [{
              type: 'bar',
              data: buffers.map((b, i) => ({
                value: b.values.length ? b.values[b.values.length - 1] : 0,
                itemStyle: { color: signals[i]?.color ?? COLORS[i % COLORS.length] },
              })),
            }],
          };
        }
        case 'fft': {
          return {
            backgroundColor: ECHARTS_DARK.backgroundColor,
            legend: legendOpt,
            xAxis: { type: 'category', name: 'Freq Index', axisLine, axisLabel },
            yAxis: { type: 'value', axisLine, axisLabel, splitLine },
            series: buffers.map((b, i) => ({
              name: signals[i]?.name ?? `S${i}`,
              type: 'line',
              data: b.values.length ? b.values[b.values.length - 1] : [],
              lineStyle: { color: signals[i]?.color ?? COLORS[i % COLORS.length] },
              showSymbol: false,
            })),
          };
        }
        case 'logic': {
          return {
            backgroundColor: ECHARTS_DARK.backgroundColor,
            legend: legendOpt,
            xAxis: { type: 'value', min: 'dataMin', max: 'dataMax', axisLine, axisLabel },
            yAxis: { type: 'value', min: -0.1, max: 1.1, axisLine, axisLabel, splitLine },
            series: buffers.map((b, i) => {
              const { ts, vals } = windowedSlice(b, timeWindow);
              return {
                name: signals[i]?.name ?? `S${i}`,
                type: 'line', step: 'end',
                data: ts.map((t, j) => [t, vals[j] > 0 ? 1 : 0]),
                lineStyle: { color: signals[i]?.color ?? COLORS[i % COLORS.length] },
                showSymbol: false,
              };
            }),
          };
        }
        case 'waterfall': {
          const ROWS = 32;
          const b = buffers[0];
          const step = Math.max(1, Math.floor(b.values.length / ROWS));
          const flatData: [number, number, number][] = [];
          for (let ri = 0; ri < ROWS; ri++) {
            const idx = b.values.length - 1 - (ROWS - 1 - ri) * step;
            if (idx < 0) continue;
            const row = b.values[idx];
            const arr = Array.isArray(row) ? row : [row];
            arr.forEach((val, ci) => flatData.push([ci, ri, val]));
          }
          return {
            backgroundColor: ECHARTS_DARK.backgroundColor,
            visualMap: { min: 0, max: 1, show: false, inRange: { color: ['#0f1117', '#4a9eff'] } },
            xAxis: { type: 'value', axisLabel },
            yAxis: { type: 'value', axisLabel },
            series: [{ type: 'heatmap', data: flatData }],
          };
        }
        default:
          return { backgroundColor: ECHARTS_DARK.backgroundColor };
      }
    }

    // ── Init chart ─────────────────────────────────────────────────────────
    const el = chartRef.current;
    const w = el.clientWidth || widget.w;
    const h = el.clientHeight || widget.h;

    const scaleY: uPlot.Scale = {};
    if (yMin !== undefined && yMin !== 'auto') scaleY.min = parseFloat(yMin);
    if (yMax !== undefined && yMax !== 'auto') scaleY.max = parseFloat(yMax);

    if (plotType === 'timeseries') {
      const seriesConf: uPlot.Series[] = [
        {},
        ...signals.map((s, i) => ({
          label: s.name,
          stroke: s.color ?? COLORS[i % COLORS.length],
          width: 1.5,
        })),
      ];

      uplot = new uPlot(
        {
          width: w,
          height: h,
          series: seriesConf,
          legend: { show: showLegend },
          axes: [
            { stroke: '#64748b', grid: { stroke: '#2d3748' }, ticks: { stroke: '#475569' } },
            { stroke: '#64748b', grid: { stroke: '#2d3748' }, ticks: { stroke: '#475569' } },
          ],
          scales: { x: { time: true }, y: scaleY },
        },
        uplotData(),
        el,
      );
    } else {
      echart = echarts.init(el);
      echart.setOption(echartsOption());
    }

    // ── Historical mode (timeseries only) ──────────────────────────────────
    if (timeRange && plotType === 'timeseries') {
      setHistStatus('loading');
      let cancelled = false;

      Promise.all(
        signals.map((sig: SignalRef & { color?: string }, i) =>
          wsClient.history(sig, timeRange.start, timeRange.end, 10_000).then(pts => {
            if (cancelled) return;
            for (const p of pts) {
              const ts = new Date(p.ts).getTime() / 1000;
              const v = typeof p.value === 'number' ? p.value : parseFloat(String(p.value));
              if (!isNaN(v)) {
                buffers[i].timestamps.push(ts);
                buffers[i].values.push(v);
              }
            }
          }),
        ),
      ).then(() => {
        if (cancelled) return;
        const totalPoints = buffers.reduce((s, b) => s + b.timestamps.length, 0);
        setHistStatus(totalPoints === 0 ? 'empty' : 'idle');
        if (uplot) uplot.setData(uplotData());
      }).catch(() => {
        if (!cancelled) setHistStatus('error');
      });

      return () => {
        cancelled = true;
        if (uplot) uplot.destroy();
        if (echart) echart.dispose();
      };
    }

    // ── Live streaming mode ────────────────────────────────────────────────
    setHistStatus('idle');

    signals.forEach((sig: SignalRef & { color?: string }, i) => {
      const unsub = getSignalStore(sig).subscribe(sv => {
        if (sv.value === null || sv.value === undefined || sv.ts === null) return;
        const ts = new Date(sv.ts).getTime() / 1000;

        if (plotType === 'timeseries') {
          const v = typeof sv.value === 'number' ? sv.value : parseFloat(String(sv.value));
          if (isNaN(v)) return;
          pushSample(buffers[i], ts, v);
          if (uplot) {
            uplot.setData(uplotData(timeWindow));
          }
        } else {
          const v = typeof sv.value === 'number' ? sv.value : parseFloat(String(sv.value));
          if (!isNaN(v)) {
            pushSample(buffers[i], ts, v);
            if (plotType === 'histogram') histValues[i].push(v);
          }
          if (echart) echart.setOption(echartsOption(), { notMerge: true });
        }
      });
      unsubs.push(unsub);
    });

    // ── Resize ─────────────────────────────────────────────────────────────
    const ro = new ResizeObserver(() => {
      if (!chartRef.current) return;
      const nw = chartRef.current.clientWidth;
      const nh = chartRef.current.clientHeight;
      if (nw > 0 && nh > 0) {
        if (uplot) uplot.setSize({ width: nw, height: nh });
        if (echart) echart.resize();
      }
    });
    if (outerRef.current) ro.observe(outerRef.current);

    return () => {
      ro.disconnect();
      unsubs.forEach(u => u());
      if (uplot) uplot.destroy();
      if (echart) echart.dispose();
    };
  }, [widget.id, timeRangeKey, plotType, signalsKey]);

  return (
    <div
      class="plot-widget"
      ref={outerRef}
      style={`left:${widget.x}px;top:${widget.y}px;width:${widget.w}px;height:${widget.h}px;`}
      onContextMenu={onContextMenu}
    >
      <div class="chart-area" ref={chartRef} style={`width:${widget.w}px;height:${widget.h}px;`} />
      {histStatus === 'loading' && (
        <div class="hist-overlay">Loading history…</div>
      )}
      {histStatus === 'empty' && (
        <div class="hist-overlay hist-overlay-warn">No archive data for this range</div>
      )}
      {histStatus === 'error' && (
        <div class="hist-overlay hist-overlay-warn">Archive unavailable</div>
      )}
    </div>
  );
}
