package metricecharts

import (
	"bytes"
	"errors"
	"fmt"
	"html/template"
	"io"
	"math"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"sort"
	"strings"
	"time"

	"github.com/go-echarts/go-echarts/v2/charts"
	"github.com/go-echarts/go-echarts/v2/opts"
	"github.com/tidbcloud/clinic-cli/internal/cmd/metricstui"
	"github.com/tidbcloud/clinic-cli/internal/legendlabels"
)

const (
	axisTimeLabelFormat = "15:04:05"
	rangeInfoTimeFormat = "2006-01-02 15:04:05 MST"
	maxXAxisLabels      = 16
	minSparseXAxisTicks = 7
	sparseXAxisMinSpan  = 10 * 60
	chartWidth          = "96vw"
	chartHeight         = "68vh"
)

type RangeReportContext struct {
	ClusterID    string
	ClusterName  string
	DeployType   string
	Query        string
	TimeLocation *time.Location
}

func (c RangeReportContext) timeLocation() *time.Location {
	if c.TimeLocation == nil {
		return time.Local
	}
	return c.TimeLocation
}

// RunRangeECharts renders Prometheus query_range data as an interactive ECharts HTML report.
func RunRangeECharts(stdout io.Writer, rawResponse []byte) error {
	return RunRangeEChartsWithContext(stdout, rawResponse, RangeReportContext{})
}

// RunRangeEChartsWithContext renders Prometheus query_range data as an interactive
// ECharts HTML report with optional cluster metadata and explicit timezone.
func RunRangeEChartsWithContext(stdout io.Writer, rawResponse []byte, reportContext RangeReportContext) error {
	if stdout == nil {
		return errors.New("metrics range echarts requires stdout")
	}

	response, err := metricstui.DecodeQueryRangeResponse(rawResponse)
	if err != nil {
		return err
	}

	html, err := renderRangeChartHTML(response, reportContext)
	if err != nil {
		return err
	}

	reportPath, err := writeHTMLReport(html)
	if err != nil {
		return err
	}

	if _, err := fmt.Fprintf(stdout, "ECharts report: %s\n", reportPath); err != nil {
		return err
	}

	if err := openReportInBrowser(reportPath); err != nil {
		_, _ = fmt.Fprintf(stdout, "warning: unable to auto-open browser: %v\n", err)
	}

	return nil
}

func renderRangeChartHTML(response *metricstui.QueryRangeResponse, reportContext RangeReportContext) ([]byte, error) {
	location := reportContext.timeLocation()
	xAxis, seriesData := buildAlignedSeriesDataInLocation(response, location)
	subtitle := buildReportSubtitle(response, reportContext)
	copyableHeader := buildCopyableMetadataHeader(subtitle)
	labelStep := axisLabelStep(len(xAxis), maxXAxisLabels)
	lastLabelIndex := len(xAxis) - 1
	line := charts.NewLine()
	configureOfflineAssets(line)
	configureLegendPaging(line)

	line.SetGlobalOptions(
		charts.WithInitializationOpts(opts.Initialization{
			PageTitle: "clinic metrics report",
			Width:     chartWidth,
			Height:    chartHeight,
		}),
		charts.WithTitleOpts(opts.Title{
			Show:      opts.Bool(false),
			Subtitle:  subtitle,
			Left:      "2%",
			Right:     "2%",
			Top:       "2%",
			TextAlign: "left",
			TitleStyle: &opts.TextStyle{
				FontSize: 16,
			},
			SubtitleStyle: &opts.TextStyle{
				FontSize: 12,
			},
		}),
		charts.WithLegendOpts(opts.Legend{
			Show:         opts.Bool(true),
			Type:         "scroll",
			Orient:       "vertical",
			Right:        "1%",
			Top:          "14%",
			Bottom:       "8%",
			Width:        "22%",
			SelectedMode: "multiple",
		}),
		charts.WithTooltipOpts(opts.Tooltip{
			Show:     opts.Bool(true),
			Trigger:  "axis",
			Position: opts.FuncOpts(buildTooltipPositionFormatter()),
		}),
		charts.WithAnimation(false),
		charts.WithXAxisOpts(opts.XAxis{
			Name:        "time",
			BoundaryGap: opts.Bool(false),
			AxisLabel: &opts.AxisLabel{
				Show:   opts.Bool(true),
				Rotate: 0,
				Formatter: opts.FuncOpts(fmt.Sprintf(`
						function (value, index) {
							if (index === 0 || index === %d || index %% %d === 0) {
								return value;
							}
							return '';
						}
					`, lastLabelIndex, labelStep)),
				ShowMinLabel: opts.Bool(true),
				ShowMaxLabel: opts.Bool(true),
				HideOverlap:  opts.Bool(false),
			},
		}),
		charts.WithYAxisOpts(opts.YAxis{
			Name:  "value",
			Scale: opts.Bool(true),
		}),
		charts.WithGridOpts(opts.Grid{
			Left:         "70px",
			Top:          "90px",
			Right:        "20%",
			Bottom:       "80px",
			ContainLabel: opts.Bool(true),
		}),
	)

	line.SetXAxis(xAxis)
	for _, item := range seriesData {
		line.AddSeries(item.Label, item.Values,
			charts.WithLineChartOpts(opts.LineChart{
				ShowSymbol:   opts.Bool(false),
				ConnectNulls: opts.Bool(true),
			}),
		)
	}

	var output bytes.Buffer
	if err := line.Render(&output); err != nil {
		return nil, fmt.Errorf("render echarts html: %w", err)
	}
	return injectHTMLAfterBodyTag(output.Bytes(), copyableHeader), nil
}

type alignedSeries struct {
	Label  string
	Values []opts.LineData
}

func buildAlignedSeriesData(response *metricstui.QueryRangeResponse) ([]string, []alignedSeries) {
	return buildAlignedSeriesDataInLocation(response, time.Local)
}

func buildAlignedSeriesDataInLocation(response *metricstui.QueryRangeResponse, location *time.Location) ([]string, []alignedSeries) {
	if response == nil || len(response.Series) == 0 {
		return []string{}, []alignedSeries{}
	}

	timestamps := collectSortedTimestamps(response.Series)
	timestamps = expandSparseXAxisTicks(timestamps, minSparseXAxisTicks, sparseXAxisMinSpan)
	xAxis := buildTimeLabelsInLocation(timestamps, location)
	metrics := make([]map[string]string, 0, len(response.Series))
	for _, series := range response.Series {
		metrics = append(metrics, series.Metric)
	}
	legendLabels := legendlabels.ExtractDistinctLegendLabels(metrics)

	result := make([]alignedSeries, 0, len(response.Series))
	for seriesIndex, series := range response.Series {
		valueByTimestamp := make(map[int64]float64, len(series.Samples))
		for _, sample := range series.Samples {
			valueByTimestamp[sample.Timestamp.Unix()] = sample.Value
		}

		values := make([]opts.LineData, 0, len(timestamps))
		for _, ts := range timestamps {
			value, ok := valueByTimestamp[ts]
			if !ok {
				values = append(values, opts.LineData{Value: nil})
				continue
			}
			values = append(values, opts.LineData{Value: value})
		}

		result = append(result, alignedSeries{
			Label:  legendLabels[seriesIndex],
			Values: values,
		})
	}

	return xAxis, result
}

func buildTimeLabels(timestamps []int64) []string {
	return buildTimeLabelsInLocation(timestamps, time.Local)
}

func buildTimeLabelsInLocation(timestamps []int64, location *time.Location) []string {
	if len(timestamps) == 0 {
		return []string{}
	}
	if location == nil {
		location = time.Local
	}

	labels := make([]string, len(timestamps))
	for i, ts := range timestamps {
		labels[i] = time.Unix(ts, 0).In(location).Format(axisTimeLabelFormat)
	}
	return labels
}

func axisLabelStep(totalPoints, maxLabels int) int {
	if maxLabels <= 0 || totalPoints <= maxLabels {
		return 1
	}

	step := totalPoints / maxLabels
	if totalPoints%maxLabels != 0 {
		step++
	}
	if step < 1 {
		return 1
	}
	return step
}

func buildReportSubtitle(response *metricstui.QueryRangeResponse, reportContext RangeReportContext) string {
	clusterID := normalizeMetadataValue(reportContext.ClusterID)
	clusterName := normalizeMetadataValue(reportContext.ClusterName)
	deployType := normalizeMetadataValue(reportContext.DeployType)
	query := normalizeMetadataValue(reportContext.Query)
	baseText := fmt.Sprintf(
		"cluster id: %s | cluster name: %s | deploy type: %s",
		clusterID,
		clusterName,
		deployType,
	)
	queryText := fmt.Sprintf("query: %s", query)
	rangeText := buildRangeSubtitle(response, reportContext.timeLocation())
	if rangeText == "" {
		return fmt.Sprintf("%s\n%s", baseText, queryText)
	}
	return fmt.Sprintf(
		"%s\n%s\n%s",
		baseText,
		queryText,
		rangeText,
	)
}

func normalizeMetadataValue(value string) string {
	trimmed := strings.TrimSpace(value)
	if trimmed == "" {
		return "unknown"
	}
	return trimmed
}

func buildRangeSubtitle(response *metricstui.QueryRangeResponse, location *time.Location) string {
	if response == nil || len(response.Series) == 0 {
		return ""
	}
	if location == nil {
		location = time.Local
	}

	timestamps := collectSortedTimestamps(response.Series)
	if len(timestamps) == 0 {
		return ""
	}

	start := time.Unix(timestamps[0], 0).In(location).Format(rangeInfoTimeFormat)
	end := time.Unix(timestamps[len(timestamps)-1], 0).In(location).Format(rangeInfoTimeFormat)
	return fmt.Sprintf("range: %s -> %s", start, end)
}

func configureOfflineAssets(line *charts.Line) {
	line.Assets.ClearPresetJSAssets()
	line.Assets.AddCustomizedHeaders(buildInlineJSScriptTag(embeddedEChartsJS))
}

func configureLegendPaging(line *charts.Line) {
	line.JSFunctions.AddJSFuncStrs(opts.FuncOpts(`
		(function () {
			var chart = %MY_ECHARTS%;
			var option = chart.getOption();
			if (!option.legend || option.legend.length === 0) {
				return;
			}

			var legend = option.legend[0];
			legend.pageIconSize = 0;
			legend.pageButtonGap = 0;
			legend.pageButtonItemGap = 0;
			legend.pageTextStyle = { color: "transparent", fontSize: 0 };
			legend.selectedMode = "multiple";

			var seriesNames = [];
			if (option.series) {
				for (var i = 0; i < option.series.length; i++) {
					if (option.series[i] && option.series[i].name) {
						seriesNames.push(option.series[i].name);
					}
				}
			}

			var selected = legend.selected || {};
			for (var i = 0; i < seriesNames.length; i++) {
				if (selected[seriesNames[i]] === undefined) {
					selected[seriesNames[i]] = true;
				}
			}
			legend.selected = selected;
			chart.setOption({ legend: [legend] });

			var lastPointerAdditiveSelection = false;
			var lastPointerAdditiveSelectionAt = 0;
			var zr = chart.getZr && chart.getZr();
			if (zr && zr.on) {
				zr.on("mousedown", function (zrEvent) {
					var pointerEvent = zrEvent && zrEvent.event;
					lastPointerAdditiveSelection = !!(pointerEvent && (pointerEvent.metaKey || pointerEvent.ctrlKey));
					lastPointerAdditiveSelectionAt = Date.now();
				});
			}

			var focusedSeriesName = "";
			var applyingSelection = false;
			chart.on("legendselectchanged", function (params) {
				if (applyingSelection || !params || !params.name) {
					return;
				}

				var additiveSelection = false;
				if (lastPointerAdditiveSelection && (Date.now() - lastPointerAdditiveSelectionAt) < 1000) {
					additiveSelection = true;
				}

				var originalEvent = params.event && params.event.event;
				if (originalEvent && (originalEvent.metaKey || originalEvent.ctrlKey)) {
					additiveSelection = true;
				}
				lastPointerAdditiveSelection = false;
				lastPointerAdditiveSelectionAt = 0;
				if (additiveSelection) {
					focusedSeriesName = "";
					return;
				}

				applyingSelection = true;
				if (focusedSeriesName === params.name) {
					for (var i = 0; i < seriesNames.length; i++) {
						chart.dispatchAction({
							type: "legendSelect",
							name: seriesNames[i]
						});
					}
					focusedSeriesName = "";
				} else {
					for (var i = 0; i < seriesNames.length; i++) {
						chart.dispatchAction({
							type: seriesNames[i] === params.name ? "legendSelect" : "legendUnSelect",
							name: seriesNames[i]
						});
					}
					focusedSeriesName = params.name;
				}
				applyingSelection = false;
			});
		})();
	`))
}

func buildTooltipPositionFormatter() string {
	return `
		function (point, params, dom, rect, size) {
			var margin = 10;
			var viewWidth = size.viewSize[0];
			var viewHeight = size.viewSize[1];
			var boxWidth = size.contentSize[0];
			var boxHeight = size.contentSize[1];

			var x = point[0] + margin;
			var y = point[1] - boxHeight - margin;

			if (x + boxWidth > viewWidth - margin) {
				x = viewWidth - boxWidth - margin;
			}
			if (x < margin) {
				x = margin;
			}

			if (y < margin) {
				y = point[1] + margin;
			}
			if (y + boxHeight > viewHeight - margin) {
				y = viewHeight - boxHeight - margin;
			}
			if (y < margin) {
				y = margin;
			}

			return [x, y];
		}
	`
}

func buildInlineJSScriptTag(js string) string {
	// Guard against accidental premature closure of the script element.
	safeJS := strings.ReplaceAll(js, "</script>", "<\\/script>")
	return "<script>\n" + safeJS + "\n</script>"
}

func expandSparseXAxisTicks(timestamps []int64, minTicks int, minSpanSeconds int64) []int64 {
	if len(timestamps) < 2 || minTicks < 2 || len(timestamps) >= minTicks {
		return timestamps
	}

	start := timestamps[0]
	end := timestamps[len(timestamps)-1]
	span := end - start
	if span <= 0 || span < minSpanSeconds {
		return timestamps
	}

	seen := make(map[int64]struct{}, len(timestamps)+minTicks)
	for _, ts := range timestamps {
		seen[ts] = struct{}{}
	}

	steps := float64(minTicks - 1)
	for i := 0; i < minTicks; i++ {
		candidate := start + int64(math.Round(float64(span)*float64(i)/steps))
		if candidate < start {
			candidate = start
		}
		if candidate > end {
			candidate = end
		}
		seen[candidate] = struct{}{}
	}

	expanded := make([]int64, 0, len(seen))
	for ts := range seen {
		expanded = append(expanded, ts)
	}
	sort.Slice(expanded, func(i, j int) bool {
		return expanded[i] < expanded[j]
	})
	return expanded
}

func buildCopyableMetadataHeader(subtitle string) string {
	lines := strings.Split(strings.TrimSpace(subtitle), "\n")
	if len(lines) == 0 || strings.TrimSpace(lines[0]) == "" {
		return ""
	}

	escapeText := func(text string) string {
		return template.HTMLEscapeString(strings.TrimSpace(text))
	}

	primaryLine := escapeText(lines[0])
	secondaryLines := make([]string, 0, len(lines)-1)
	for _, line := range lines[1:] {
		trimmed := strings.TrimSpace(line)
		if trimmed == "" {
			continue
		}
		secondaryLines = append(secondaryLines, escapeText(trimmed))
	}

	var b strings.Builder
	b.WriteString(`<div id="clinic-report-metadata" style="margin: 12px 2% 0; user-select: text; -webkit-user-select: text; cursor: text; color: #4b5563;">`)
	b.WriteString(`<div style="font-size: 22px; line-height: 1.35; font-weight: 600;">`)
	b.WriteString(primaryLine)
	b.WriteString(`</div>`)
	if len(secondaryLines) > 0 {
		b.WriteString(`<div style="margin-top: 6px; font-size: 16px; line-height: 1.35;">`)
		b.WriteString(strings.Join(secondaryLines, "<br>"))
		b.WriteString(`</div>`)
	}
	b.WriteString(`</div>`)
	return b.String()
}

func injectHTMLAfterBodyTag(content []byte, htmlToInsert string) []byte {
	if strings.TrimSpace(htmlToInsert) == "" {
		return content
	}

	document := string(content)
	const bodyTag = "<body>"
	if !strings.Contains(document, bodyTag) {
		return content
	}

	return []byte(strings.Replace(document, bodyTag, bodyTag+"\n"+htmlToInsert, 1))
}

func collectSortedTimestamps(series []metricstui.QueryRangeSeries) []int64 {
	seen := map[int64]struct{}{}
	for _, item := range series {
		for _, sample := range item.Samples {
			seen[sample.Timestamp.Unix()] = struct{}{}
		}
	}

	timestamps := make([]int64, 0, len(seen))
	for ts := range seen {
		timestamps = append(timestamps, ts)
	}
	sort.Slice(timestamps, func(i, j int) bool {
		return timestamps[i] < timestamps[j]
	})
	return timestamps
}

func writeHTMLReport(content []byte) (string, error) {
	report, err := os.CreateTemp("", "clinic-metrics-range-*.html")
	if err != nil {
		return "", fmt.Errorf("create echarts report: %w", err)
	}
	defer report.Close()

	if _, err := report.Write(content); err != nil {
		return "", fmt.Errorf("write echarts report: %w", err)
	}
	return report.Name(), nil
}

func openReportInBrowser(path string) error {
	reportURL := "file://" + filepath.ToSlash(path)

	var cmd *exec.Cmd
	switch runtime.GOOS {
	case "darwin":
		cmd = exec.Command("open", reportURL)
	case "linux":
		cmd = exec.Command("xdg-open", reportURL)
	case "windows":
		cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", reportURL)
	default:
		return fmt.Errorf("unsupported OS for browser auto-open: %s", runtime.GOOS)
	}

	if err := cmd.Start(); err != nil {
		return err
	}
	return nil
}
