package main

import (
	"bytes"
	"embed"
	"encoding/json"
	"fmt"
	"net"
	"os"
	"os/exec"
	"runtime"
	"strings"
	"text/template"
	"time"

	_ "embed"

	cors "github.com/AdhityaRamadhanus/fasthttpcors"
	"github.com/go-echarts/go-echarts/v2/charts"
	"github.com/go-echarts/go-echarts/v2/components"
	"github.com/go-echarts/go-echarts/v2/opts"
	"github.com/go-echarts/go-echarts/v2/templates"
	"github.com/valyala/fasthttp"
)

//go:embed echarts.min.js
//go:embed jquery.min.js
var assetsFS embed.FS

var (
	assetsPath      = "/echarts/statics/"
	apiPath         = "/data/"
	latencyView     = "latency"
	rpsView         = "rps"
	codeView        = "code"
	timeFormat      = "15:04:05"
	refreshInterval = time.Second

	templateRegistry = map[string]string{
		rpsView:     ViewTpl,
		latencyView: ViewTpl,
		codeView:    CodeViewTpl,
	}
)

const (
	ViewTpl = `
$(function () { setInterval({{ .ViewID }}_sync, {{ .Interval }}); });
function {{ .ViewID }}_sync() {
    $.ajax({
        type: "GET",
        url: "{{ .APIPath }}{{ .Route }}",
        dataType: "json",
        success: function (result) {
            let opt = goecharts_{{ .ViewID }}.getOption();
            let x = opt.xAxis[0].data;
            x.push(result.time);
            opt.xAxis[0].data = x;
            for (let i = 0; i < result.values.length; i++) {
                let y = opt.series[i].data;
                y.push({ value: result.values[i] });
                opt.series[i].data = y;
                goecharts_{{ .ViewID }}.setOption(opt);
            }
        }
    });
}`
	PageTpl = `
{{- define "page" }}
<!DOCTYPE html>
<html>
    {{- template "header" . }}
<body>
<p align="center">🚀 <a href="https://github.com/six-ddc/plow"><b>Plow</b></a> %s</p>
<style> .box { justify-content:center; display:flex; flex-wrap:wrap } </style>
<div class="box"> {{- range .Charts }} {{ template "base" . }} {{- end }} </div>
</body>
</html>
{{ end }}
`
	CodeViewTpl = `
$(function () { setInterval({{ .ViewID }}_sync, {{ .Interval }}); });
function {{ .ViewID }}_sync() {
    $.ajax({
        type: "GET",
        url: "{{ .APIPath }}{{ .Route }}",
        dataType: "json",
        success: function (result) {
            let opt = goecharts_{{ .ViewID }}.getOption();
            let x = opt.xAxis[0].data;
            x.push(result.time);
            opt.xAxis[0].data = x;
							
			let nameAndSeriesMapping = {};
			for (let i = 0; i < opt.series.length; i++) {
				nameAndSeriesMapping[opt.series[i].name] = opt.series[i];
			}
			
			let code200Count = nameAndSeriesMapping['200'].data.length;
			
			let codes = result.values[0];
			if (codes === null){ 
				for (let key in nameAndSeriesMapping) {
					let series = nameAndSeriesMapping[key];
  					series.data.push({value:null});
				}
			}else{
				if (!('200' in codes)) {
					codes['200'] = null;
				}
			
				for (let code in codes) {
					let count = codes[code];
					if (code in nameAndSeriesMapping){
						let series = nameAndSeriesMapping[code];
						series.data.push({value:count});
					}else{
						let data = [];
						for (let i = 0; i < code200Count; i++) {
							data.push[null];
						}
						var newSeries = {
							name:  code,
							type: 'line',
							data:  data
						};
						opt.series.push(newSeries);
					}
				}
			}
			
			goecharts_{{ .ViewID }}.setOption(opt);
        }
    });
}`
)

func (c *Charts) genViewTemplate(vid, route string) string {
	tpl, err := template.New("view").Parse(templateRegistry[route])
	if err != nil {
		panic("failed to parse template " + err.Error())
	}

	var d = struct {
		Interval int
		APIPath  string
		Route    string
		ViewID   string
	}{
		Interval: int(refreshInterval.Milliseconds()),
		APIPath:  apiPath,
		Route:    route,
		ViewID:   vid,
	}

	buf := bytes.Buffer{}
	if err := tpl.Execute(&buf, d); err != nil {
		panic("failed to execute template " + err.Error())
	}

	return buf.String()
}

func (c *Charts) newBasicView(route string) *charts.Line {
	graph := charts.NewLine()
	graph.SetGlobalOptions(
		charts.WithTooltipOpts(opts.Tooltip{Show: true, Trigger: "axis"}),
		charts.WithXAxisOpts(opts.XAxis{Name: "Time"}),
		charts.WithInitializationOpts(opts.Initialization{
			Width:  "700px",
			Height: "400px",
		}),
		charts.WithDataZoomOpts(opts.DataZoom{
			Type:       "slider",
			XAxisIndex: []int{0},
		}),
	)
	graph.SetXAxis([]string{}).SetSeriesOptions(charts.WithLineChartOpts(opts.LineChart{Smooth: true}))
	graph.AddJSFuncs(c.genViewTemplate(graph.ChartID, route))
	return graph
}

func (c *Charts) newLatencyView() components.Charter {
	graph := c.newBasicView(latencyView)
	graph.SetGlobalOptions(
		charts.WithTitleOpts(opts.Title{Title: "Latency"}),
		charts.WithYAxisOpts(opts.YAxis{Scale: true, AxisLabel: &opts.AxisLabel{Formatter: "{value} ms"}}),
		charts.WithLegendOpts(opts.Legend{Show: true, Selected: map[string]bool{"Min": false, "Max": false}}),
	)
	graph.AddSeries("Min", []opts.LineData{}).
		AddSeries("Mean", []opts.LineData{}).
		AddSeries("Max", []opts.LineData{})
	return graph
}

func (c *Charts) newRPSView() components.Charter {
	graph := c.newBasicView(rpsView)
	graph.SetGlobalOptions(
		charts.WithTitleOpts(opts.Title{Title: "Reqs/sec"}),
		charts.WithYAxisOpts(opts.YAxis{Scale: true}),
	)
	graph.AddSeries("RPS", []opts.LineData{})
	return graph
}

func (c *Charts) newCodeView() components.Charter {
	graph := c.newBasicView(codeView)
	graph.SetGlobalOptions(
		charts.WithTitleOpts(opts.Title{Title: "Response Status"}),
		charts.WithYAxisOpts(opts.YAxis{Scale: true}),
		charts.WithLegendOpts(opts.Legend{Show: true}),
	)
	graph.AddSeries("200", []opts.LineData{})
	return graph
}

type Metrics struct {
	Values []interface{} `json:"values"`
	Time   string        `json:"time"`
}

type Charts struct {
	page     *components.Page
	ln       net.Listener
	dataFunc func() *ChartsReport
}

func NewCharts(ln net.Listener, dataFunc func() *ChartsReport, desc string) (*Charts, error) {
	templates.PageTpl = fmt.Sprintf(PageTpl, desc)

	c := &Charts{ln: ln, dataFunc: dataFunc}
	c.page = components.NewPage()
	c.page.PageTitle = "plow"
	c.page.AssetsHost = assetsPath
	c.page.Assets.JSAssets.Add("jquery.min.js")
	c.page.AddCharts(c.newLatencyView(), c.newRPSView(), c.newCodeView())

	return c, nil
}

func (c *Charts) Handler(ctx *fasthttp.RequestCtx) {
	path := string(ctx.Path())
	if strings.HasPrefix(path, apiPath) {
		view := path[len(apiPath):]
		var values []interface{}
		reportData := c.dataFunc()
		switch view {
		case latencyView:
			if reportData != nil {
				values = append(values, reportData.Latency.min/1e6)
				values = append(values, reportData.Latency.Mean()/1e6)
				values = append(values, reportData.Latency.max/1e6)
			} else {
				values = append(values, nil, nil, nil)
			}
		case rpsView:
			if reportData != nil {
				values = append(values, reportData.RPS)
			} else {
				values = append(values, nil)
			}
		case codeView:
			if reportData != nil {
				values = append(values, reportData.CodeMap)
			} else {
				values = append(values, nil)
			}
		}
		metrics := &Metrics{
			Time:   time.Now().Format(timeFormat),
			Values: values,
		}
		_ = json.NewEncoder(ctx).Encode(metrics)
	} else if path == "/" {
		ctx.SetContentType("text/html")
		_ = c.page.Render(ctx)
	} else if strings.HasPrefix(path, assetsPath) {
		ap := path[len(assetsPath):]
		f, err := assetsFS.Open(ap)
		if err != nil {
			ctx.Error(err.Error(), 404)
		} else {
			ctx.SetBodyStream(f, -1)
		}
	} else {
		ctx.Error("NotFound", fasthttp.StatusNotFound)
	}
}

func (c *Charts) Serve(open bool) {
	server := fasthttp.Server{
		Handler: cors.DefaultHandler().CorsMiddleware(c.Handler),
	}
	if open {
		go openBrowser("http://" + c.ln.Addr().String())
	}
	_ = server.Serve(c.ln)
}

// openBrowser go/src/cmd/internal/browser/browser.go
func openBrowser(url string) bool {
	var cmds [][]string
	if exe := os.Getenv("BROWSER"); exe != "" {
		cmds = append(cmds, []string{exe})
	}
	switch runtime.GOOS {
	case "darwin":
		cmds = append(cmds, []string{"/usr/bin/open"})
	case "windows":
		cmds = append(cmds, []string{"cmd", "/c", "start"})
	default:
		if os.Getenv("DISPLAY") != "" {
			// xdg-open is only for use in a desktop environment.
			cmds = append(cmds, []string{"xdg-open"})
		}
	}
	cmds = append(cmds,
		[]string{"chrome"},
		[]string{"google-chrome"},
		[]string{"chromium"},
		[]string{"firefox"},
	)
	for _, args := range cmds {
		cmd := exec.Command(args[0], append(args[1:], url)...)
		if cmd.Start() == nil && appearsSuccessful(cmd, 3*time.Second) {
			return true
		}
	}
	return false
}

// appearsSuccessful reports whether the command appears to have run successfully.
// If the command runs longer than the timeout, it's deemed successful.
// If the command runs within the timeout, it's deemed successful if it exited cleanly.
func appearsSuccessful(cmd *exec.Cmd, timeout time.Duration) bool {
	errc := make(chan error, 1)
	go func() {
		errc <- cmd.Wait()
	}()

	select {
	case <-time.After(timeout):
		return true
	case err := <-errc:
		return err == nil
	}
}
