package webui

import (
	"fmt"
	"math"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"

	"bee/audit/internal/platform"
)

type chartTimelineSegment struct {
	Start  time.Time
	End    time.Time
	Active bool
}

type chartScale struct {
	Min   float64
	Max   float64
	Ticks []float64
}

type chartLayout struct {
	Width      int
	Height     int
	PlotLeft   int
	PlotRight  int
	PlotTop    int
	PlotBottom int
}

type metricChartSeries struct {
	Name      string
	AxisTitle string
	Color     string
	Values    []float64
}

var metricChartPalette = []string{
	"#5794f2",
	"#73bf69",
	"#f2cc0c",
	"#ff9830",
	"#f2495c",
	"#b877d9",
	"#56d2f7",
	"#8ab8ff",
	"#9adf8f",
	"#ffbe5c",
}

var gpuLabelCache struct {
	mu       sync.Mutex
	loadedAt time.Time
	byIndex  map[int]string
}

func renderMetricChartSVG(title string, labels []string, times []time.Time, datasets [][]float64, names []string, yMin, yMax *float64, canvasHeight int, timeline []chartTimelineSegment) ([]byte, error) {
	pointCount := len(labels)
	if len(times) > pointCount {
		pointCount = len(times)
	}
	if pointCount == 0 {
		pointCount = 1
		labels = []string{""}
		times = []time.Time{time.Time{}}
	}
	if len(labels) < pointCount {
		padded := make([]string, pointCount)
		copy(padded, labels)
		labels = padded
	}
	if len(times) < pointCount {
		times = synthesizeChartTimes(times, pointCount)
	}
	for i := range datasets {
		if len(datasets[i]) == 0 {
			datasets[i] = make([]float64, pointCount)
		}
	}

	// Downsample to at most ~1400 points (one per pixel) before building SVG.
	times, datasets = downsampleTimeSeries(times, datasets, 1400)
	pointCount = len(times)

	statsLabel := chartStatsLabel(datasets)

	legendItems := []metricChartSeries{}
	for i, name := range names {
		color := metricChartPalette[i%len(metricChartPalette)]
		values := make([]float64, pointCount)
		if i < len(datasets) {
			copy(values, coalesceDataset(datasets[i], pointCount))
		}
		legendItems = append(legendItems, metricChartSeries{
			Name:   name,
			Color:  color,
			Values: values,
		})
	}

	scale := singleAxisChartScale(datasets, yMin, yMax)
	layout := singleAxisChartLayout(canvasHeight, len(legendItems))
	start, end := chartTimeBounds(times)

	var b strings.Builder
	writeSVGOpen(&b, layout.Width, layout.Height)
	writeChartFrame(&b, title, statsLabel, layout.Width, layout.Height)
	writeTimelineIdleSpans(&b, layout, start, end, timeline)
	writeVerticalGrid(&b, layout, times, pointCount, 8)
	writeHorizontalGrid(&b, layout, scale)
	writeTimelineBoundaries(&b, layout, start, end, timeline)
	writePlotBorder(&b, layout)
	writeSingleAxisY(&b, layout, scale)
	writeXAxisLabels(&b, layout, times, labels, start, end, 8)
	for _, item := range legendItems {
		writeSeriesPolyline(&b, layout, times, start, end, item.Values, scale, item.Color)
	}
	writeLegend(&b, layout, legendItems)
	writeSVGClose(&b)
	return []byte(b.String()), nil
}

func renderGPUOverviewChartSVG(idx int, samples []platform.LiveMetricSample, timeline []chartTimelineSegment) ([]byte, bool, error) {
	temp := gpuDatasetByIndex(samples, idx, func(g platform.GPUMetricRow) float64 { return g.TempC })
	power := gpuDatasetByIndex(samples, idx, func(g platform.GPUMetricRow) float64 { return g.PowerW })
	coreClock := gpuDatasetByIndex(samples, idx, func(g platform.GPUMetricRow) float64 { return g.ClockMHz })
	if temp == nil && power == nil && coreClock == nil {
		return nil, false, nil
	}
	labels := sampleTimeLabels(samples)
	times := sampleTimes(samples)
	svg, err := drawGPUOverviewChartSVG(
		gpuDisplayLabel(idx)+" Overview",
		labels,
		times,
		[]metricChartSeries{
			{Name: "Temp C", Values: coalesceDataset(temp, len(labels)), Color: "#f05a5a", AxisTitle: "Temp C"},
			{Name: "Power W", Values: coalesceDataset(power, len(labels)), Color: "#ffb357", AxisTitle: "Power W"},
			{Name: "Core Clock MHz", Values: coalesceDataset(coreClock, len(labels)), Color: "#73bf69", AxisTitle: "Core MHz"},
		},
		timeline,
	)
	if err != nil {
		return nil, false, err
	}
	return svg, true, nil
}

func drawGPUOverviewChartSVG(title string, labels []string, times []time.Time, series []metricChartSeries, timeline []chartTimelineSegment) ([]byte, error) {
	if len(series) != 3 {
		return nil, fmt.Errorf("gpu overview requires 3 series, got %d", len(series))
	}
	const (
		width      = 1400
		height     = 840
		plotLeft   = 180
		plotRight  = 1220
		plotTop    = 96
		plotBottom = 660
	)
	const (
		leftOuterAxis  = 72
		leftInnerAxis  = 132
		rightInnerAxis = 1268
	)
	layout := chartLayout{
		Width:      width,
		Height:     height,
		PlotLeft:   plotLeft,
		PlotRight:  plotRight,
		PlotTop:    plotTop,
		PlotBottom: plotBottom,
	}
	axisX := []int{leftOuterAxis, leftInnerAxis, rightInnerAxis}
	pointCount := len(labels)
	if len(times) > pointCount {
		pointCount = len(times)
	}
	if pointCount == 0 {
		pointCount = 1
		labels = []string{""}
		times = []time.Time{time.Time{}}
	}
	if len(labels) < pointCount {
		padded := make([]string, pointCount)
		copy(padded, labels)
		labels = padded
	}
	if len(times) < pointCount {
		times = synthesizeChartTimes(times, pointCount)
	}
	for i := range series {
		if len(series[i].Values) == 0 {
			series[i].Values = make([]float64, pointCount)
		}
	}

	// Downsample to at most ~1400 points before building SVG.
	{
		datasets := make([][]float64, len(series))
		for i := range series {
			datasets[i] = series[i].Values
		}
		times, datasets = downsampleTimeSeries(times, datasets, 1400)
		pointCount = len(times)
		for i := range series {
			series[i].Values = datasets[i]
		}
	}

	scales := make([]chartScale, len(series))
	for i := range series {
		min, max := chartSeriesBounds(series[i].Values)
		ticks := chartNiceTicks(min, max, 8)
		scales[i] = chartScale{
			Min:   ticks[0],
			Max:   ticks[len(ticks)-1],
			Ticks: ticks,
		}
	}
	start, end := chartTimeBounds(times)

	var b strings.Builder
	writeSVGOpen(&b, width, height)
	writeChartFrame(&b, title, "", width, height)
	writeTimelineIdleSpans(&b, layout, start, end, timeline)
	writeVerticalGrid(&b, layout, times, pointCount, 8)
	writeHorizontalGrid(&b, layout, scales[0])
	writeTimelineBoundaries(&b, layout, start, end, timeline)
	writePlotBorder(&b, layout)

	for i, axisLineX := range axisX {
		fmt.Fprintf(&b, `<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="%s" stroke-width="1"/>`+"\n",
			axisLineX, layout.PlotTop, axisLineX, layout.PlotBottom, series[i].Color)
		fmt.Fprintf(&b, `<text x="%d" y="%d" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="700" fill="%s">%s</text>`+"\n",
			axisLineX, 64, series[i].Color, sanitizeChartText(series[i].AxisTitle))
		for _, tick := range scales[i].Ticks {
			y := chartYForValue(valueClamp(tick, scales[i]), scales[i], layout.PlotTop, layout.PlotBottom)
			label := sanitizeChartText(chartYAxisNumber(tick))
			if i < 2 {
				fmt.Fprintf(&b, `<line x1="%d" y1="%.1f" x2="%d" y2="%.1f" stroke="%s" stroke-width="1"/>`+"\n",
					axisLineX, y, axisLineX+6, y, series[i].Color)
				fmt.Fprintf(&b, `<text x="%d" y="%.1f" text-anchor="end" dy="4" font-family="sans-serif" font-size="10" fill="%s">%s</text>`+"\n",
					axisLineX-8, y, series[i].Color, label)
				continue
			}
			fmt.Fprintf(&b, `<line x1="%d" y1="%.1f" x2="%d" y2="%.1f" stroke="%s" stroke-width="1"/>`+"\n",
				axisLineX, y, axisLineX-6, y, series[i].Color)
			fmt.Fprintf(&b, `<text x="%d" y="%.1f" text-anchor="start" dy="4" font-family="sans-serif" font-size="10" fill="%s">%s</text>`+"\n",
				axisLineX+8, y, series[i].Color, label)
		}
	}

	writeXAxisLabels(&b, layout, times, labels, start, end, 8)
	for i := range series {
		writeSeriesPolyline(&b, layout, times, start, end, series[i].Values, scales[i], series[i].Color)
	}
	writeLegend(&b, layout, series)
	writeSVGClose(&b)
	return []byte(b.String()), nil
}

func metricsTimelineSegments(samples []platform.LiveMetricSample, now time.Time) []chartTimelineSegment {
	if len(samples) == 0 {
		return nil
	}
	times := sampleTimes(samples)
	start, end := chartTimeBounds(times)
	if start.IsZero() || end.IsZero() {
		return nil
	}
	return chartTimelineSegmentsForRange(start, end, now, snapshotTaskHistory())
}

func snapshotTaskHistory() []Task {
	globalQueue.mu.Lock()
	defer globalQueue.mu.Unlock()
	out := make([]Task, len(globalQueue.tasks))
	for i, t := range globalQueue.tasks {
		out[i] = *t
	}
	return out
}

func chartTimelineSegmentsForRange(start, end, now time.Time, tasks []Task) []chartTimelineSegment {
	if start.IsZero() || end.IsZero() {
		return nil
	}
	if end.Before(start) {
		start, end = end, start
	}
	type interval struct {
		start time.Time
		end   time.Time
	}
	active := make([]interval, 0, len(tasks))
	for _, task := range tasks {
		if task.StartedAt == nil {
			continue
		}
		intervalStart := task.StartedAt.UTC()
		intervalEnd := now.UTC()
		if task.DoneAt != nil {
			intervalEnd = task.DoneAt.UTC()
		}
		if !intervalEnd.After(intervalStart) {
			continue
		}
		if intervalEnd.Before(start) || intervalStart.After(end) {
			continue
		}
		if intervalStart.Before(start) {
			intervalStart = start
		}
		if intervalEnd.After(end) {
			intervalEnd = end
		}
		active = append(active, interval{start: intervalStart, end: intervalEnd})
	}
	sort.Slice(active, func(i, j int) bool {
		if active[i].start.Equal(active[j].start) {
			return active[i].end.Before(active[j].end)
		}
		return active[i].start.Before(active[j].start)
	})
	merged := make([]interval, 0, len(active))
	for _, span := range active {
		if len(merged) == 0 {
			merged = append(merged, span)
			continue
		}
		last := &merged[len(merged)-1]
		if !span.start.After(last.end) {
			if span.end.After(last.end) {
				last.end = span.end
			}
			continue
		}
		merged = append(merged, span)
	}

	segments := make([]chartTimelineSegment, 0, len(merged)*2+1)
	cursor := start
	for _, span := range merged {
		if span.start.After(cursor) {
			segments = append(segments, chartTimelineSegment{Start: cursor, End: span.start, Active: false})
		}
		segments = append(segments, chartTimelineSegment{Start: span.start, End: span.end, Active: true})
		cursor = span.end
	}
	if cursor.Before(end) {
		segments = append(segments, chartTimelineSegment{Start: cursor, End: end, Active: false})
	}
	if len(segments) == 0 {
		segments = append(segments, chartTimelineSegment{Start: start, End: end, Active: false})
	}
	return segments
}

func sampleTimes(samples []platform.LiveMetricSample) []time.Time {
	times := make([]time.Time, 0, len(samples))
	for _, sample := range samples {
		times = append(times, sample.Timestamp)
	}
	return times
}

func singleAxisChartScale(datasets [][]float64, yMin, yMax *float64) chartScale {
	min, max := 0.0, 1.0
	if yMin != nil && yMax != nil {
		min, max = *yMin, *yMax
	} else {
		min, max = chartSeriesBounds(flattenDatasets(datasets))
		if yMin != nil {
			min = *yMin
		}
		if yMax != nil {
			max = *yMax
		}
	}
	ticks := chartNiceTicks(min, max, 8)
	return chartScale{Min: ticks[0], Max: ticks[len(ticks)-1], Ticks: ticks}
}

func flattenDatasets(datasets [][]float64) []float64 {
	total := 0
	for _, ds := range datasets {
		total += len(ds)
	}
	out := make([]float64, 0, total)
	for _, ds := range datasets {
		out = append(out, ds...)
	}
	return out
}

func singleAxisChartLayout(canvasHeight int, seriesCount int) chartLayout {
	legendRows := 0
	if chartLegendVisible(seriesCount) && seriesCount > 0 {
		cols := 4
		if seriesCount < cols {
			cols = seriesCount
		}
		legendRows = (seriesCount + cols - 1) / cols
	}
	legendHeight := 0
	if legendRows > 0 {
		legendHeight = legendRows*24 + 24
	}
	return chartLayout{
		Width:      1400,
		Height:     canvasHeight,
		PlotLeft:   96,
		PlotRight:  1352,
		PlotTop:    72,
		PlotBottom: canvasHeight - 60 - legendHeight,
	}
}

func chartTimeBounds(times []time.Time) (time.Time, time.Time) {
	if len(times) == 0 {
		return time.Time{}, time.Time{}
	}
	start := times[0].UTC()
	end := start
	for _, ts := range times[1:] {
		t := ts.UTC()
		if t.Before(start) {
			start = t
		}
		if t.After(end) {
			end = t
		}
	}
	return start, end
}

func synthesizeChartTimes(times []time.Time, count int) []time.Time {
	if count <= 0 {
		return nil
	}
	if len(times) == count {
		return times
	}
	if len(times) == 1 {
		out := make([]time.Time, count)
		for i := range out {
			out[i] = times[0].Add(time.Duration(i) * time.Minute)
		}
		return out
	}
	base := time.Now().UTC().Add(-time.Duration(count-1) * time.Minute)
	out := make([]time.Time, count)
	for i := range out {
		out[i] = base.Add(time.Duration(i) * time.Minute)
	}
	return out
}

func writeSVGOpen(b *strings.Builder, width, height int) {
	fmt.Fprintf(b, `<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 %d %d">`+"\n", width, height, width, height)
}

func writeSVGClose(b *strings.Builder) {
	b.WriteString("</svg>\n")
}

func writeChartFrame(b *strings.Builder, title, subtitle string, width, height int) {
	fmt.Fprintf(b, `<rect width="%d" height="%d" rx="10" ry="10" fill="#ffffff" stroke="#d7e0ea"/>`+"\n", width, height)
	fmt.Fprintf(b, `<text x="%d" y="30" text-anchor="middle" font-family="sans-serif" font-size="16" font-weight="700" fill="#1f2937">%s</text>`+"\n",
		width/2, sanitizeChartText(title))
	if strings.TrimSpace(subtitle) != "" {
		fmt.Fprintf(b, `<text x="%d" y="50" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="#64748b">%s</text>`+"\n",
			width/2, sanitizeChartText(subtitle))
	}
}

func writePlotBorder(b *strings.Builder, layout chartLayout) {
	fmt.Fprintf(b, `<rect x="%d" y="%d" width="%d" height="%d" fill="none" stroke="#cbd5e1" stroke-width="1"/>`+"\n",
		layout.PlotLeft, layout.PlotTop, layout.PlotRight-layout.PlotLeft, layout.PlotBottom-layout.PlotTop)
}

func writeHorizontalGrid(b *strings.Builder, layout chartLayout, scale chartScale) {
	b.WriteString(`<g stroke="#e2e8f0" stroke-width="1">` + "\n")
	for _, tick := range scale.Ticks {
		y := chartYForValue(tick, scale, layout.PlotTop, layout.PlotBottom)
		fmt.Fprintf(b, `<line x1="%d" y1="%.1f" x2="%d" y2="%.1f"/>`+"\n",
			layout.PlotLeft, y, layout.PlotRight, y)
	}
	b.WriteString(`</g>` + "\n")
}

func writeVerticalGrid(b *strings.Builder, layout chartLayout, times []time.Time, pointCount, target int) {
	if pointCount <= 0 {
		return
	}
	start, end := chartTimeBounds(times)
	b.WriteString(`<g stroke="#edf2f7" stroke-width="1">` + "\n")
	for _, idx := range gpuChartLabelIndices(pointCount, target) {
		ts := chartPointTime(times, idx)
		x := chartXForTime(ts, start, end, layout.PlotLeft, layout.PlotRight)
		fmt.Fprintf(b, `<line x1="%.1f" y1="%d" x2="%.1f" y2="%d"/>`+"\n",
			x, layout.PlotTop, x, layout.PlotBottom)
	}
	b.WriteString(`</g>` + "\n")
}

func writeSingleAxisY(b *strings.Builder, layout chartLayout, scale chartScale) {
	fmt.Fprintf(b, `<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#64748b" stroke-width="1"/>`+"\n",
		layout.PlotLeft, layout.PlotTop, layout.PlotLeft, layout.PlotBottom)
	for _, tick := range scale.Ticks {
		y := chartYForValue(tick, scale, layout.PlotTop, layout.PlotBottom)
		fmt.Fprintf(b, `<line x1="%d" y1="%.1f" x2="%d" y2="%.1f" stroke="#64748b" stroke-width="1"/>`+"\n",
			layout.PlotLeft, y, layout.PlotLeft-6, y)
		fmt.Fprintf(b, `<text x="%d" y="%.1f" text-anchor="end" dy="4" font-family="sans-serif" font-size="10" fill="#475569">%s</text>`+"\n",
			layout.PlotLeft-10, y, sanitizeChartText(chartYAxisNumber(tick)))
	}
}

func writeXAxisLabels(b *strings.Builder, layout chartLayout, times []time.Time, labels []string, start, end time.Time, target int) {
	pointCount := len(labels)
	if len(times) > pointCount {
		pointCount = len(times)
	}
	b.WriteString(`<g font-family="sans-serif" font-size="11" fill="#64748b" text-anchor="middle">` + "\n")
	for _, idx := range gpuChartLabelIndices(pointCount, target) {
		x := chartXForTime(chartPointTime(times, idx), start, end, layout.PlotLeft, layout.PlotRight)
		label := ""
		if idx < len(labels) {
			label = labels[idx]
		}
		fmt.Fprintf(b, `<text x="%.1f" y="%d">%s</text>`+"\n", x, layout.PlotBottom+28, sanitizeChartText(label))
	}
	b.WriteString(`</g>` + "\n")
	fmt.Fprintf(b, `<text x="%d" y="%d" text-anchor="middle" font-family="sans-serif" font-size="12" fill="#64748b">Time</text>`+"\n",
		(layout.PlotLeft+layout.PlotRight)/2, layout.PlotBottom+48)
}

func writeSeriesPolyline(b *strings.Builder, layout chartLayout, times []time.Time, start, end time.Time, values []float64, scale chartScale, color string) {
	if len(values) == 0 {
		return
	}
	var points strings.Builder
	for idx, value := range values {
		if idx > 0 {
			points.WriteByte(' ')
		}
		x := chartXForTime(chartPointTime(times, idx), start, end, layout.PlotLeft, layout.PlotRight)
		y := chartYForValue(value, scale, layout.PlotTop, layout.PlotBottom)
		points.WriteString(strconv.FormatFloat(x, 'f', 1, 64))
		points.WriteByte(',')
		points.WriteString(strconv.FormatFloat(y, 'f', 1, 64))
	}
	fmt.Fprintf(b, `<polyline points="%s" fill="none" stroke="%s" stroke-width="2.2" stroke-linejoin="round" stroke-linecap="round"/>`+"\n",
		points.String(), color)
	if len(values) == 1 {
		x := chartXForTime(chartPointTime(times, 0), start, end, layout.PlotLeft, layout.PlotRight)
		y := chartYForValue(values[0], scale, layout.PlotTop, layout.PlotBottom)
		fmt.Fprintf(b, `<circle cx="%.1f" cy="%.1f" r="3.5" fill="%s"/>`+"\n", x, y, color)
		return
	}
	peakIdx := 0
	peakValue := values[0]
	for idx, value := range values[1:] {
		if value >= peakValue {
			peakIdx = idx + 1
			peakValue = value
		}
	}
	x := chartXForTime(chartPointTime(times, peakIdx), start, end, layout.PlotLeft, layout.PlotRight)
	y := chartYForValue(peakValue, scale, layout.PlotTop, layout.PlotBottom)
	fmt.Fprintf(b, `<circle cx="%.1f" cy="%.1f" r="4.2" fill="%s" stroke="#ffffff" stroke-width="1.6"/>`+"\n", x, y, color)
	fmt.Fprintf(b, `<path d="M %.1f %.1f L %.1f %.1f L %.1f %.1f Z" fill="%s" opacity="0.9"/>`+"\n",
		x, y-10, x-5, y-18, x+5, y-18, color)
}

func writeLegend(b *strings.Builder, layout chartLayout, series []metricChartSeries) {
	if !chartLegendVisible(len(series)) || len(series) == 0 {
		return
	}
	cols := 4
	if len(series) < cols {
		cols = len(series)
	}
	cellWidth := float64(layout.PlotRight-layout.PlotLeft) / float64(cols)
	baseY := layout.PlotBottom + 74
	for i, item := range series {
		row := i / cols
		col := i % cols
		x := float64(layout.PlotLeft) + cellWidth*float64(col) + 8
		y := float64(baseY + row*24)
		fmt.Fprintf(b, `<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="3"/>`+"\n",
			x, y, x+28, y, item.Color)
		fmt.Fprintf(b, `<text x="%.1f" y="%.1f" font-family="sans-serif" font-size="12" fill="#1f2937">%s</text>`+"\n",
			x+38, y+4, sanitizeChartText(item.Name))
	}
}

func writeTimelineIdleSpans(b *strings.Builder, layout chartLayout, start, end time.Time, segments []chartTimelineSegment) {
	if len(segments) == 0 {
		return
	}
	b.WriteString(`<g data-role="timeline-overlay">` + "\n")
	for _, segment := range segments {
		if segment.Active || !segment.End.After(segment.Start) {
			continue
		}
		x0 := chartXForTime(segment.Start, start, end, layout.PlotLeft, layout.PlotRight)
		x1 := chartXForTime(segment.End, start, end, layout.PlotLeft, layout.PlotRight)
		fmt.Fprintf(b, `<rect x="%.1f" y="%d" width="%.1f" height="%d" fill="#475569" opacity="0.10"/>`+"\n",
			x0, layout.PlotTop, math.Max(1, x1-x0), layout.PlotBottom-layout.PlotTop)
	}
	b.WriteString(`</g>` + "\n")
}

func writeTimelineBoundaries(b *strings.Builder, layout chartLayout, start, end time.Time, segments []chartTimelineSegment) {
	if len(segments) == 0 {
		return
	}
	seen := map[int]bool{}
	b.WriteString(`<g data-role="timeline-boundaries" stroke="#94a3b8" stroke-width="1.2">` + "\n")
	for i, segment := range segments {
		if i > 0 {
			x := int(math.Round(chartXForTime(segment.Start, start, end, layout.PlotLeft, layout.PlotRight)))
			if !seen[x] {
				seen[x] = true
				fmt.Fprintf(b, `<line x1="%d" y1="%d" x2="%d" y2="%d"/>`+"\n", x, layout.PlotTop, x, layout.PlotBottom)
			}
		}
		if i < len(segments)-1 {
			x := int(math.Round(chartXForTime(segment.End, start, end, layout.PlotLeft, layout.PlotRight)))
			if !seen[x] {
				seen[x] = true
				fmt.Fprintf(b, `<line x1="%d" y1="%d" x2="%d" y2="%d"/>`+"\n", x, layout.PlotTop, x, layout.PlotBottom)
			}
		}
	}
	b.WriteString(`</g>` + "\n")
}

// downsampleTimeSeries reduces the time series to at most maxPts points using
// min-max bucketing. Each bucket contributes the index of its min and max value
// (using the first full-length dataset as the reference series). All parallel
// datasets are sampled at those same indices so all series stay aligned.
// If len(times) <= maxPts the inputs are returned unchanged.
func downsampleTimeSeries(times []time.Time, datasets [][]float64, maxPts int) ([]time.Time, [][]float64) {
	n := len(times)
	if n <= maxPts || maxPts <= 0 {
		return times, datasets
	}
	buckets := maxPts / 2
	if buckets < 1 {
		buckets = 1
	}
	// Use the first dataset that has the same length as times as the reference
	// for deciding which two indices to keep per bucket.
	var ref []float64
	for _, ds := range datasets {
		if len(ds) == n {
			ref = ds
			break
		}
	}
	selected := make([]int, 0, maxPts)
	bucketSize := float64(n) / float64(buckets)
	for b := 0; b < buckets; b++ {
		lo := int(math.Round(float64(b) * bucketSize))
		hi := int(math.Round(float64(b+1) * bucketSize))
		if hi > n {
			hi = n
		}
		if lo >= hi {
			continue
		}
		if ref == nil {
			selected = append(selected, lo)
			if hi-1 != lo {
				selected = append(selected, hi-1)
			}
			continue
		}
		minIdx, maxIdx := lo, lo
		for i := lo + 1; i < hi; i++ {
			if ref[i] < ref[minIdx] {
				minIdx = i
			}
			if ref[i] > ref[maxIdx] {
				maxIdx = i
			}
		}
		if minIdx <= maxIdx {
			selected = append(selected, minIdx)
			if maxIdx != minIdx {
				selected = append(selected, maxIdx)
			}
		} else {
			selected = append(selected, maxIdx)
			if minIdx != maxIdx {
				selected = append(selected, minIdx)
			}
		}
	}
	outTimes := make([]time.Time, len(selected))
	for i, idx := range selected {
		outTimes[i] = times[idx]
	}
	outDatasets := make([][]float64, len(datasets))
	for d, ds := range datasets {
		if len(ds) != n {
			outDatasets[d] = ds
			continue
		}
		out := make([]float64, len(selected))
		for i, idx := range selected {
			out[i] = ds[idx]
		}
		outDatasets[d] = out
	}
	return outTimes, outDatasets
}

func chartXForTime(ts, start, end time.Time, left, right int) float64 {
	if !end.After(start) {
		return float64(left+right) / 2
	}
	if ts.Before(start) {
		ts = start
	}
	if ts.After(end) {
		ts = end
	}
	ratio := float64(ts.Sub(start)) / float64(end.Sub(start))
	return float64(left) + ratio*float64(right-left)
}

func chartPointTime(times []time.Time, idx int) time.Time {
	if idx >= 0 && idx < len(times) && !times[idx].IsZero() {
		return times[idx].UTC()
	}
	if len(times) > 0 && !times[0].IsZero() {
		return times[0].UTC().Add(time.Duration(idx) * time.Minute)
	}
	return time.Now().UTC().Add(time.Duration(idx) * time.Minute)
}

func chartYForValue(value float64, scale chartScale, plotTop, plotBottom int) float64 {
	if scale.Max <= scale.Min {
		return float64(plotTop+plotBottom) / 2
	}
	return float64(plotBottom) - (value-scale.Min)/(scale.Max-scale.Min)*float64(plotBottom-plotTop)
}

func chartSeriesBounds(values []float64) (float64, float64) {
	if len(values) == 0 {
		return 0, 1
	}
	min, max := values[0], values[0]
	for _, value := range values[1:] {
		if value < min {
			min = value
		}
		if value > max {
			max = value
		}
	}
	if min == max {
		if max == 0 {
			return 0, 1
		}
		pad := math.Abs(max) * 0.1
		if pad == 0 {
			pad = 1
		}
		min -= pad
		max += pad
	}
	if min > 0 {
		pad := (max - min) * 0.2
		if pad == 0 {
			pad = max * 0.1
		}
		min -= pad
		if min < 0 {
			min = 0
		}
		max += pad
	}
	return min, max
}

func chartNiceTicks(min, max float64, target int) []float64 {
	if min == max {
		max = min + 1
	}
	span := max - min
	step := math.Pow(10, math.Floor(math.Log10(span/float64(target))))
	for _, factor := range []float64{1, 2, 5, 10} {
		if span/(factor*step) <= float64(target)*1.5 {
			step = factor * step
			break
		}
	}
	low := math.Floor(min/step) * step
	high := math.Ceil(max/step) * step
	var ticks []float64
	for value := low; value <= high+step*0.001; value += step {
		ticks = append(ticks, math.Round(value*1e9)/1e9)
	}
	return ticks
}

func valueClamp(value float64, scale chartScale) float64 {
	if value < scale.Min {
		return scale.Min
	}
	if value > scale.Max {
		return scale.Max
	}
	return value
}

func chartStatsLabel(datasets [][]float64) string {
	mn, avg, mx := globalStats(datasets)
	if mx <= 0 && avg <= 0 && mn <= 0 {
		return ""
	}
	return fmt.Sprintf("min %s   avg %s   max %s",
		chartLegendNumber(mn),
		chartLegendNumber(avg),
		chartLegendNumber(mx),
	)
}

func gpuDisplayLabel(idx int) string {
	if name := gpuModelNameByIndex(idx); name != "" {
		return fmt.Sprintf("GPU %d — %s", idx, name)
	}
	return fmt.Sprintf("GPU %d", idx)
}

func gpuModelNameByIndex(idx int) string {
	now := time.Now()
	gpuLabelCache.mu.Lock()
	if now.Sub(gpuLabelCache.loadedAt) > 30*time.Second || gpuLabelCache.byIndex == nil {
		gpuLabelCache.loadedAt = now
		gpuLabelCache.byIndex = loadGPUModelNames()
	}
	name := strings.TrimSpace(gpuLabelCache.byIndex[idx])
	gpuLabelCache.mu.Unlock()
	return name
}

func loadGPUModelNames() map[int]string {
	out := map[int]string{}
	gpus, err := platform.New().ListNvidiaGPUs()
	if err != nil {
		return out
	}
	for _, gpu := range gpus {
		name := strings.TrimSpace(gpu.Name)
		if name != "" {
			out[gpu.Index] = name
		}
	}
	return out
}
