package charts

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"sort"
	"time"

	"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/projectdiscovery/nuclei/v3/pkg/scan/events"
	sliceutil "github.com/projectdiscovery/utils/slice"
)

const (
	TopK         = 50
	SpacerHeight = "50px"
)

func (s *ScanEventsCharts) AllCharts(w http.ResponseWriter, r *http.Request) {
	page := s.allCharts(r)
	renderChart(w, page)
}

func (s *ScanEventsCharts) GenerateHTML(filePath string) error {
	page := s.allCharts(nil)
	output, err := os.Create(filePath)
	if err != nil {
		return err
	}
	defer func() {
		_ = output.Close()
	}()
	return page.Render(output)
}

// AllCharts generates all the charts for the scan events and returns a page component
func (s *ScanEventsCharts) allCharts(r *http.Request) *components.Page {
	page := components.NewPage()
	page.PageTitle = "Nuclei Charts"
	line1 := s.totalRequestsOverTime(r)
	// line1.SetSpacerHeight(SpacerHeight)
	kline := s.topSlowTemplates(r)
	// kline.SetSpacerHeight(SpacerHeight)
	line2 := s.requestsVSInterval(r)
	// line2.SetSpacerHeight(SpacerHeight)
	line3 := s.concurrencyVsTime(r)
	// line3.SetSpacerHeight(SpacerHeight)
	page.AddCharts(line1, kline, line2, line3)
	page.SetLayout(components.PageCenterLayout)
	// page.Theme = "dark"
	page.Validate()

	return page
}

func (s *ScanEventsCharts) TotalRequestsOverTime(w http.ResponseWriter, r *http.Request) {
	line := s.totalRequestsOverTime(r)
	renderChart(w, line)
}

// totalRequestsOverTime generates a line chart showing total requests count over time
func (s *ScanEventsCharts) totalRequestsOverTime(_ *http.Request) *charts.Line {
	line := charts.NewLine()
	line.SetGlobalOptions(
		charts.WithTitleOpts(opts.Title{
			Title:    "Nuclei: Total Requests vs Time",
			Subtitle: "Chart Shows Total Requests Count Over Time (for each/all Protocols)",
		}),
	)

	startTime := time.Now()
	var endTime time.Time

	for _, event := range s.data {
		if event.Time.Before(startTime) {
			startTime = event.Time
		}
		if event.Time.After(endTime) {
			endTime = event.Time
		}
	}
	data := getCategoryRequestCount(s.data)
	max := 0
	for _, v := range data {
		if len(v) > max {
			max = len(v)
		}
	}
	line.SetXAxis(time.Now().Format(time.RFC3339))
	for k, v := range data {
		lineData := make([]opts.LineData, 0)
		temp := 0
		for _, scanEvent := range v {
			temp += scanEvent.MaxRequests
			val := scanEvent.Time.Sub(startTime)
			lineData = append(lineData, opts.LineData{
				Value: []interface{}{val.Milliseconds(), temp},
				Name:  scanEvent.TemplateID,
			})
		}
		line.AddSeries(k, lineData, charts.WithLineChartOpts(opts.LineChart{Smooth: opts.Bool(false)}), charts.WithLabelOpts(opts.Label{Show: opts.Bool(true), Position: "top"}))
	}

	line.SetGlobalOptions(
		charts.WithTitleOpts(opts.Title{Title: "Nuclei: total-req vs time"}),
		charts.WithXAxisOpts(opts.XAxis{Name: "Time", Type: "time", AxisLabel: &opts.AxisLabel{Show: opts.Bool(true), ShowMaxLabel: opts.Bool(true), Formatter: opts.FuncOpts(`function (date) { return (date/1000)+'s'; }`)}}),
		charts.WithYAxisOpts(opts.YAxis{Name: "Requests Sent", Type: "value"}),
		charts.WithInitializationOpts(opts.Initialization{Theme: "dark"}),
		charts.WithDataZoomOpts(opts.DataZoom{Type: "slider", Start: 0, End: 100}),
		charts.WithGridOpts(opts.Grid{Left: "10%", Right: "10%", Bottom: "15%", Top: "20%"}),
		charts.WithToolboxOpts(opts.Toolbox{Show: opts.Bool(true), Feature: &opts.ToolBoxFeature{
			SaveAsImage: &opts.ToolBoxFeatureSaveAsImage{Show: opts.Bool(true), Name: "save", Title: "save"},
			DataZoom:    &opts.ToolBoxFeatureDataZoom{Show: opts.Bool(true), Title: map[string]string{"zoom": "zoom", "back": "back"}},
			DataView:    &opts.ToolBoxFeatureDataView{Show: opts.Bool(true), Title: "raw", Lang: []string{"raw", "exit", "refresh"}},
		}}),
	)

	line.Validate()
	return line
}

func (s *ScanEventsCharts) TopSlowTemplates(w http.ResponseWriter, r *http.Request) {
	kline := s.topSlowTemplates(r)
	renderChart(w, kline)
}

// topSlowTemplates generates a Kline chart showing the top slow templates by time taken
func (s *ScanEventsCharts) topSlowTemplates(_ *http.Request) *charts.Kline {
	kline := charts.NewKLine()
	kline.SetGlobalOptions(
		charts.WithTitleOpts(opts.Title{
			Title:    "Nuclei: Top Slow Templates",
			Subtitle: fmt.Sprintf("Chart Shows Top Slow Templates (by time taken) (Top %v)", TopK),
		}),
	)
	ids := map[string][]int64{}
	startTime := time.Now()
	for _, event := range s.data {
		if event.Time.Before(startTime) {
			startTime = event.Time
		}
	}
	for _, event := range s.data {
		ids[event.TemplateID] = append(ids[event.TemplateID], event.Time.Sub(startTime).Milliseconds())
	}

	type entry struct {
		ID        string
		KlineData opts.KlineData
		start     int64
		end       int64
	}
	data := []entry{}

	for a, b := range ids {
		if len(b) < 2 {
			continue // Prevents index out of range error
		}
		d := entry{
			ID:        a,
			KlineData: opts.KlineData{Value: []int64{b[0], b[len(b)-1], b[0], b[len(b)-1]}}, // Adjusted to prevent index out of range error
			start:     b[0],
			end:       b[len(b)-1],
		}
		data = append(data, d)
	}

	sort.Slice(data, func(i, j int) bool {
		return data[i].end-data[i].start > data[j].end-data[j].start
	})

	// Ensure we don't try to access more elements than available
	limit := TopK
	if len(data) < TopK {
		limit = len(data)
	}

	x := make([]string, 0)
	y := make([]opts.KlineData, 0)
	for _, event := range data[:limit] {
		x = append(x, event.ID)
		y = append(y, event.KlineData)
	}

	kline.SetXAxis(x).AddSeries("templates", y)
	kline.SetGlobalOptions(
		charts.WithTitleOpts(opts.Title{Title: fmt.Sprintf("Nuclei: Top %v Slow Templates", limit)}),
		charts.WithXAxisOpts(opts.XAxis{
			Type:      "category",
			Show:      opts.Bool(true),
			AxisLabel: &opts.AxisLabel{Rotate: 90, Show: opts.Bool(true), ShowMinLabel: opts.Bool(true), ShowMaxLabel: opts.Bool(true), Formatter: opts.FuncOpts(`function (value) { return value; }`)},
		}),
		charts.WithYAxisOpts(opts.YAxis{
			Scale:     opts.Bool(true),
			Type:      "value",
			Show:      opts.Bool(true),
			AxisLabel: &opts.AxisLabel{Show: opts.Bool(true), Formatter: opts.FuncOpts(`function (ms) {  return Math.floor(ms/60000) + 'm' + Math.floor((ms/60000 - Math.floor(ms/60000))*60) + 's'; }`)},
		}),
		charts.WithDataZoomOpts(opts.DataZoom{Type: "slider", Start: 0, End: 100}),
		charts.WithGridOpts(opts.Grid{Left: "10%", Right: "10%", Bottom: "40%", Top: "10%"}),
		charts.WithTooltipOpts(opts.Tooltip{Show: opts.Bool(true), Trigger: "item", TriggerOn: "mousemove|click", Enterable: opts.Bool(true), Formatter: opts.FuncOpts(`function (params) { return params.name ; }`)}),
		charts.WithToolboxOpts(opts.Toolbox{Show: opts.Bool(true), Feature: &opts.ToolBoxFeature{
			SaveAsImage: &opts.ToolBoxFeatureSaveAsImage{Show: opts.Bool(true), Name: "save", Title: "save"},
			DataZoom:    &opts.ToolBoxFeatureDataZoom{Show: opts.Bool(true), Title: map[string]string{"zoom": "zoom", "back": "back"}},
			DataView:    &opts.ToolBoxFeatureDataView{Show: opts.Bool(true), Title: "raw", Lang: []string{"raw", "exit", "refresh"}},
		}}),
	)

	return kline
}

func (s *ScanEventsCharts) RequestsVSInterval(w http.ResponseWriter, r *http.Request) {
	line := s.requestsVSInterval(r)
	renderChart(w, line)
}

// requestsVSInterval generates a line chart showing requests per second over time
func (s *ScanEventsCharts) requestsVSInterval(r *http.Request) *charts.Line {
	line := charts.NewLine()
	line.SetGlobalOptions(
		charts.WithTitleOpts(opts.Title{
			Title:    "Nuclei: Requests Per Second vs Time",
			Subtitle: "Chart Shows RPS (Requests Per Second) Over Time",
		}),
	)

	sort.Slice(s.data, func(i, j int) bool {
		return s.data[i].Time.Before(s.data[j].Time)
	})

	var interval time.Duration

	if r != nil {
		interval, _ = time.ParseDuration(r.URL.Query().Get("interval"))
	}
	if interval <= 3 {
		interval = 5 * time.Second
	}

	data := []opts.LineData{}
	temp := 0
	if len(s.data) > 0 {
		orig := s.data[0].Time
		startTime := orig
		xaxisData := []int64{}
		for _, v := range s.data {
			if v.Time.Sub(startTime) > interval {
				millisec := v.Time.Sub(orig).Milliseconds()
				xaxisData = append(xaxisData, millisec)
				data = append(data, opts.LineData{Value: temp, Name: v.Time.Sub(orig).String()})
				temp = 0
				startTime = v.Time
			}
			temp += 1
		}
		// Handle last interval if exists
		if temp > 0 {
			millisec := s.data[len(s.data)-1].Time.Sub(orig).Milliseconds()
			xaxisData = append(xaxisData, millisec)
			data = append(data, opts.LineData{Value: temp, Name: s.data[len(s.data)-1].Time.Sub(orig).String()})
		}
		line.SetXAxis(xaxisData)
		line.AddSeries("RPS", data, charts.WithLineChartOpts(opts.LineChart{Smooth: opts.Bool(false)}), charts.WithLabelOpts(opts.Label{Show: opts.Bool(true), Position: "top"}))
	}

	line.SetGlobalOptions(
		charts.WithTitleOpts(opts.Title{Title: "Nuclei: Template Execution", Subtitle: "Time Interval: " + interval.String()}),
		charts.WithXAxisOpts(opts.XAxis{Name: "Time Intervals", Type: "category", AxisLabel: &opts.AxisLabel{Show: opts.Bool(true), ShowMaxLabel: opts.Bool(true), Formatter: opts.FuncOpts(`function (date) { return (date/1000)+'s'; }`)}}),
		charts.WithYAxisOpts(opts.YAxis{Name: "RPS Value", Type: "value", Show: opts.Bool(true)}),
		charts.WithInitializationOpts(opts.Initialization{Theme: "dark"}),
		charts.WithDataZoomOpts(opts.DataZoom{Type: "slider", Start: 0, End: 100}),
		charts.WithGridOpts(opts.Grid{Left: "10%", Right: "10%", Bottom: "15%", Top: "20%"}),
		charts.WithToolboxOpts(opts.Toolbox{Show: opts.Bool(true), Feature: &opts.ToolBoxFeature{
			SaveAsImage: &opts.ToolBoxFeatureSaveAsImage{Show: opts.Bool(true), Name: "save", Title: "save"},
			DataZoom:    &opts.ToolBoxFeatureDataZoom{Show: opts.Bool(true), Title: map[string]string{"zoom": "zoom", "back": "back"}},
			DataView:    &opts.ToolBoxFeatureDataView{Show: opts.Bool(true), Title: "raw", Lang: []string{"raw", "exit", "refresh"}},
		}}),
	)

	line.Validate()
	return line
}

func (s *ScanEventsCharts) ConcurrencyVsTime(w http.ResponseWriter, r *http.Request) {
	line := s.concurrencyVsTime(r)
	renderChart(w, line)
}

// concurrencyVsTime generates a line chart showing concurrency (total workers) over time
func (s *ScanEventsCharts) concurrencyVsTime(r *http.Request) *charts.Line {
	line := charts.NewLine()
	line.SetGlobalOptions(
		charts.WithTitleOpts(opts.Title{
			Title:    "Nuclei: Concurrency vs Time",
			Subtitle: "Chart Shows Concurrency (Total Workers) Over Time",
		}),
	)

	dataset := sliceutil.Clone(s.data)

	sort.Slice(dataset, func(i, j int) bool {
		return dataset[i].Time.Before(dataset[j].Time)
	})

	var interval time.Duration
	if r != nil {
		interval, _ = time.ParseDuration(r.URL.Query().Get("interval"))
	}
	if interval <= 3 {
		interval = 5 * time.Second
	}

	// create array with time interval as x-axis and worker count as y-axis
	// entry is a struct with time and poolsize
	type entry struct {
		Time     time.Duration
		poolsize int
	}
	allEntries := []entry{}

	dataIndex := 0
	maxIndex := len(dataset) - 1
	currEntry := entry{}

	lastTime := dataset[0].Time
	for dataIndex <= maxIndex {
		currTime := dataset[dataIndex].Time
		if currTime.Sub(lastTime) > interval {
			// next batch
			currEntry.Time = interval
			allEntries = append(allEntries, currEntry)
			lastTime = dataset[dataIndex-1].Time
		}
		if dataset[dataIndex].EventType == events.ScanStarted {
			currEntry.poolsize += 1
		} else {
			currEntry.poolsize -= 1
		}
		dataIndex += 1
	}

	plotData := []opts.LineData{}
	xaxisData := []int64{}
	tempTime := time.Duration(0)
	for _, v := range allEntries {
		tempTime += v.Time
		plotData = append(plotData, opts.LineData{Value: v.poolsize, Name: tempTime.String()})
		xaxisData = append(xaxisData, tempTime.Milliseconds())
	}
	line.SetXAxis(xaxisData)
	line.AddSeries("Concurrency", plotData, charts.WithLineChartOpts(opts.LineChart{Smooth: opts.Bool(false)}), charts.WithLabelOpts(opts.Label{Show: opts.Bool(true), Position: "top"}))

	line.SetGlobalOptions(
		charts.WithTitleOpts(opts.Title{Title: "Nuclei: WorkerPool", Subtitle: "Time Interval: " + interval.String()}),
		charts.WithXAxisOpts(opts.XAxis{Name: "Time Intervals", Type: "category", AxisLabel: &opts.AxisLabel{Show: opts.Bool(true), ShowMaxLabel: opts.Bool(true), Formatter: opts.FuncOpts(`function (date) { return (date/1000)+'s'; }`)}}),
		charts.WithYAxisOpts(opts.YAxis{Name: "Total Workers", Type: "value", Show: opts.Bool(true)}),
		charts.WithInitializationOpts(opts.Initialization{Theme: "dark"}),
		charts.WithDataZoomOpts(opts.DataZoom{Type: "slider", Start: 0, End: 100}),
		charts.WithGridOpts(opts.Grid{Left: "10%", Right: "10%", Bottom: "15%", Top: "20%"}),
		charts.WithToolboxOpts(opts.Toolbox{Show: opts.Bool(true), Feature: &opts.ToolBoxFeature{
			SaveAsImage: &opts.ToolBoxFeatureSaveAsImage{Show: opts.Bool(true), Name: "save", Title: "save"},
			DataZoom:    &opts.ToolBoxFeatureDataZoom{Show: opts.Bool(true), Title: map[string]string{"zoom": "zoom", "back": "back"}},
			DataView:    &opts.ToolBoxFeatureDataView{Show: opts.Bool(true), Title: "raw", Lang: []string{"raw", "exit", "refresh"}},
		}}),
	)

	line.Validate()
	return line
}

// getCategoryRequestCount returns a map of category and request count
func getCategoryRequestCount(values []events.ScanEvent) map[string][]events.ScanEvent {
	mx := make(map[string][]events.ScanEvent)
	for _, event := range values {
		mx[event.TemplateType] = append(mx[event.TemplateType], event)
	}
	return mx
}

type chartRenderer interface {
	Render(io.Writer) error
}

func renderChart(w http.ResponseWriter, chart chartRenderer) {
	if err := chart.Render(w); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}
