package handler

import (
	"fmt"
	"net/http"
	"sort"
	"strings"

	"github.com/creasty/defaults"
	"github.com/gorilla/schema"
	"github.com/labstack/echo/v4"

	commM "github.com/pingcap/test-infra/common/model"
	"github.com/pingcap/test-infra/common/model/tcms/taskruns"
	serror "github.com/pingcap/test-infra/services/tcms/service/common/errors"
	"github.com/pingcap/test-infra/services/tcms/utils"
)

var decoder = schema.NewDecoder()

// ListTestbedSetup ...
// @Summary List testbed-setup tasks
// @ID list-testbed-setup
// @Tags testbed-setup
// @Param scheduleStatus query string false "Filter by schedule.status"
// @Success 200 {object} taskruns.TestbedSetupList
// @Failure default {object} HTTPError
// @Router /testbedsetups [get]
func (h *HTTPHandler) ListTestbedSetup(c echo.Context) error {
	ctx := c.(utils.Context)
	opts := taskruns.ListTestbedSetupOpts{}
	err := decoder.Decode(&opts, ctx.QueryParams())
	if err != nil {
		return err
	}
	if c.QueryParam("count") == "" {
		var defaultLimit uint = 100
		opts.Count = defaultLimit
	}
	list, err := h.service.TestbedSetup.List(ctx, opts)
	if err != nil {
		return err
	}
	return c.JSON(http.StatusOK, *list)
}

// GetTestbedSetup ...
// @Summary Get testbed-setup by ID
// @ID get-testbed-setup
// @Tags testbed-setup
// @Param id path integer true "Case ID"
// @Success 200 {object} taskruns.TestbedSetup
// @Failure default {object} HTTPError
// @Router /testbedsetups/{id} [get]
func (h *HTTPHandler) GetTestbedSetup(c echo.Context) error {
	ctx := c.(utils.Context)
	id, err := utils.ParseID(ctx.Param("id"))
	if err != nil {
		return serror.New(http.StatusBadRequest, err)
	}
	opts := taskruns.GetTestbedSetupOpts{}
	err = decoder.Decode(&opts, ctx.QueryParams())
	if err != nil {
		return err
	}
	obj, err := h.service.TestbedSetup.Get(ctx, id, opts)
	if err != nil {
		return err
	}
	if obj == nil {
		return serror.New(http.StatusNotFound, err)
	}
	return c.JSON(http.StatusOK, *obj)
}

// CreateTestbedSetup ...
// @Summary Create new testbed-setup
// @ID create-testbed-setup
// @Tags testbed-setup
// @Param dryrun query bool true "Dry run" default(false)
// @Param dryRun query bool false "Dry run" default(false)
// @Param testbedSetup body taskruns.TestbedSetup true "testbed setup to create"
// @Success 200 {object} taskruns.TestbedSetup
// @Failure default {object} HTTPError
// @Router /testbedsetups [post]
func (h *HTTPHandler) CreateTestbedSetup(c echo.Context) error {
	ctx := c.(utils.Context)
	opts := taskruns.CreateTestbedSetupOpts{}
	err := decoder.Decode(&opts, ctx.QueryParams())
	if err != nil {
		return err
	}
	if ctx.DryRun() {
		opts.DryRun = ctx.DryRun()
	}
	req, err := bindTestbedSetup(ctx)
	if err != nil {
		return err
	}
	created, err := h.service.TestbedSetup.Create(ctx, *req, opts)
	if err != nil {
		return err
	}
	return c.JSON(http.StatusOK, *created)
}

// UpdateTestbedSetupSchedule ...
// @Summary update testbed-setup schedule status
// @ID update-testbed-setup-schedule
// @Tags testbed-setup
// @Param id path integer true "ID"
// @Param dryrun query bool true "Dry run" default(false)
// @Param dryRun query bool false "Dry run" default(false)
// @Param testbedSetup body taskruns.TestbedSetup true "testbed-setup to update"
// @Success 200 {object} taskruns.TestbedSetup
// @Failure default {object} HTTPError
// @Router /testbedsetups/{id}/status/schedule [put]
func (h *HTTPHandler) UpdateTestbedSetupSchedule(c echo.Context) error {
	return updateTestbedSetupStatus(c, h.service.TestbedSetup.UpdateSchedule)
}

// UpdateTestbedSetupReport ...
// @Summary update testbed-setup report status
// @ID update-testbed-setup-report
// @Tags testbed-setup
// @Param id path integer true "ID"
// @Param dryrun query bool true "Dry run" default(false)
// @Param dryRun query bool false "Dry run" default(false)
// @Param testbedSetup body taskruns.TestbedSetup true "testbed-setup to update"
// @Success 200 {object} taskruns.TestbedSetup
// @Failure default {object} HTTPError
// @Router /testbedsetups/{id}/status/report [put]
func (h *HTTPHandler) UpdateTestbedSetupReport(c echo.Context) error {
	return updateTestbedSetupStatus(c, h.service.TestbedSetup.UpdateReport)
}

// UpdateTestbedSetupRun ...
// @Summary update testbed-setup run status
// @ID update-testbed-setup-run
// @Tags testbed-setup
// @Param id path integer true "ID"
// @Param dryrun query bool true "Dry run" default(false)
// @Param dryRun query bool false "Dry run" default(false)
// @Param testbedSetup body taskruns.TestbedSetup true "testbed-setup to update"
// @Success 200 {object} taskruns.TestbedSetup
// @Failure default {object} HTTPError
// @Router /testbedsetups/{id}/status/run [put]
func (h *HTTPHandler) UpdateTestbedSetupRun(c echo.Context) error {
	return updateTestbedSetupStatus(c, h.service.TestbedSetup.UpdateRun)
}

func bindTestbedSetup(ctx utils.Context) (*taskruns.TestbedSetup, error) {
	req := &taskruns.TestbedSetup{}
	if err := ctx.Bind(req); err != nil {
		return nil, serror.New(http.StatusBadRequest, err)
	}
	defaults.MustSet(req)
	if err := req.Validate(); err != nil {
		return nil, serror.New(http.StatusBadRequest, err)
	}
	return req, nil
}

type testbedSetupStatusUpdater func(ctx utils.Context, run taskruns.TestbedSetup, opts taskruns.GeneralUpdateOpts) (*taskruns.TestbedSetup, error)

func updateTestbedSetupStatus(c echo.Context, handler testbedSetupStatusUpdater) error {
	ctx := c.(utils.Context)
	opts := taskruns.GeneralUpdateOpts{}
	err := decoder.Decode(&opts, ctx.QueryParams())
	if err != nil {
		return serror.New(http.StatusBadRequest, err)
	}
	if ctx.DryRun() {
		opts.DryRun = ctx.DryRun()
	}
	req, err := bindTestbedSetup(ctx)
	if err != nil {
		return err
	}
	created, err := handler(ctx, *req, opts)
	if err != nil {
		return err
	}
	return c.JSON(http.StatusOK, *created)
}

// GenerateScheduleGraph ...
// @Summary generate pending task graph
// @ID pending-testbed-setup-graph
// @Tags testbed-setup
// @Produce html
// @Failure default {object} HTTPError
// @Router /testbedsetups/schedule-graph [get]
func (h *HTTPHandler) GenerateScheduleGraph(c echo.Context) error {
	ctx := c.(utils.Context)
	opts := taskruns.ListTestbedSetupOpts{}
	err := decoder.Decode(&opts, ctx.QueryParams())
	if err != nil {
		return serror.New(http.StatusBadRequest, err)
	}
	isEmptyQuery := opts.ScheduleStatus == nil && opts.PlanExecIDs == nil && opts.IDs == nil
	if isEmptyQuery {
		status := taskruns.ScheduleStatusPending
		opts.ScheduleStatus = &status
	}
	list, err := h.service.TestbedSetup.List(ctx, opts)
	if err != nil {
		return err
	}
	builder := graphBuilder{}
	builder.Merge(list.Items)
	const MaxDepth = 20
	for i := 0; i < MaxDepth; i++ {
		lacking := builder.Lacking()
		if len(lacking) == 0 {
			break
		}
		opts = taskruns.ListTestbedSetupOpts{IDs: lacking}
		list, err = h.service.TestbedSetup.List(ctx, opts)
		if err != nil {
			return err
		}
		builder.Merge(list.Items)
	}
	template := `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>ScheduleStatus</title>
</head>
<body>
<script src="https://fastly.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
<script>
    mermaid.initialize({ startOnLoad: true });
</script>
<div class="mermaid">
%s
</div>
</body>
</html>
`
	graph, err := builder.Compile()
	if err != nil {
		return err
	}
	html := fmt.Sprintf(template, graph)
	return ctx.HTML(http.StatusOK, html)
}

type graphBuilder struct {
	tasks map[commM.ID]*taskruns.TestbedSetup
}

func (b *graphBuilder) Merge(nodes []taskruns.TestbedSetup) {
	if b.tasks == nil {
		b.tasks = make(map[commM.ID]*taskruns.TestbedSetup, len(nodes))
	}
	for i := range nodes {
		item := &nodes[i]
		b.tasks[item.MetaData.ID] = item
		if item.Status.Schedule == nil {
			continue
		}
		for _, targetID := range item.Status.Schedule.ScheduleReason.RelatedSetupIds {
			if _, exist := b.tasks[targetID]; !exist {
				b.tasks[targetID] = nil
			}
		}
	}
}

func (b *graphBuilder) Lacking() (rt []commM.ID) {
	for id, setup := range b.tasks {
		if setup == nil {
			rt = append(rt, id)
		}
	}
	return rt
}

type Graph []*GraphNode

type GraphNode struct {
	ID          string
	Label       string
	Color       string
	Children    []*GraphNode
	Parent      *GraphNode
	DownStreams []*GraphNode
	UpStreams   []*GraphNode
}

func (n GraphNode) PrintNode() string {
	builder := strings.Builder{}
	if n.Children != nil {
		builder.WriteString("subgraph ")
		builder.WriteString(n.ID)
		builder.WriteString("[ ]")
		builder.WriteString("\n")
		builder.WriteString("direction LR\n")
		for _, child := range n.Children {
			builder.WriteString(child.PrintNode())
		}
		builder.WriteString("end")
	} else {
		builder.WriteString(fmt.Sprintf(`%s[%q]`, n.ID, n.Label))
	}
	builder.WriteString("\n")
	if n.Color != "" {
		builder.WriteString(fmt.Sprintf("style %s color: %s", n.ID, n.Color))
		builder.WriteString("\n")
	}
	return builder.String()
}

func (n GraphNode) PrintEdges() string {
	builder := strings.Builder{}
	for _, ds := range n.DownStreams {
		builder.WriteString(n.ID)
		builder.WriteString(" --> ")
		builder.WriteString(ds.ID)
		builder.WriteString("\n")
	}
	return builder.String()
}

func (g Graph) Print() string {
	builder := strings.Builder{}
	builder.WriteString("flowchart LR\n")
	if len(g) == 0 {
		builder.WriteString("nil[( there is no tasks )]\nstyle nil color: gray\n")
	}
	for _, node := range g {
		if node.Parent != nil {
			continue
		}
		builder.WriteString(node.PrintNode())
	}
	for _, node := range g {
		builder.WriteString(node.PrintEdges())
	}
	rt := builder.String()
	return rt
}

func (b *graphBuilder) Compile() (string, error) {
	nodesMap := generateNodes(b.tasks)
	nodes, err := connectNodes(b.tasks, nodesMap)
	if err != nil {
		return "", err
	}
	var graph Graph = nodes
	graph = optimize(graph)
	return graph.Print(), nil
}

func generateNodes(tasks map[commM.ID]*taskruns.TestbedSetup) map[commM.ID]*GraphNode {
	nodes := make(map[commM.ID]*GraphNode, len(tasks))
	for id, setup := range tasks {
		node := GraphNode{ID: fmt.Sprintf("n%d", int(id)), Label: labelForTask(id, setup)}
		if setup == nil || setup.Status.Schedule.ScheduleStatus != taskruns.ScheduleStatusPending {
			node.Color = "gray"
		}
		nodes[id] = &node
	}
	return nodes
}

func connectNodes(tasks map[commM.ID]*taskruns.TestbedSetup, nodes map[commM.ID]*GraphNode) ([]*GraphNode, error) {
	for id, setup := range tasks {
		if setup == nil {
			continue
		}
		for _, targetId := range setup.Status.Schedule.ScheduleReason.RelatedSetupIds {
			if nodes[targetId] == nil {
				return nil, fmt.Errorf("edge target node is nil")
			}
			nodes[id].DownStreams = append(nodes[id].DownStreams, nodes[targetId])
			nodes[targetId].UpStreams = append(nodes[targetId].UpStreams, nodes[id])
		}
	}
	var rt []*GraphNode
	for _, node := range nodes {
		rt = append(rt, node)
	}
	sort.Slice(rt, func(i, j int) bool {
		return rt[i].ID < rt[j].ID
	})
	return rt, nil
}

func optimize(graph Graph) Graph {
	nodes := []*GraphNode(graph)
	groups := make([]*GraphNode, 0)
	for i := 0; i < len(nodes); i++ {
		if nodes[i].Parent != nil {
			continue
		}
		for j := i + 1; j < len(nodes); j++ {
			left := nodes[i]
			if left.Parent != nil {
				left = left.Parent
			}
			if isEqual(left.DownStreams, nodes[j].DownStreams) && isEqual(left.UpStreams, nodes[j].UpStreams) {
				if left.Children != nil {
					left.Children = append(left.Children, nodes[j])
					nodes[j].Parent = left
				} else {
					group := &GraphNode{ID: fmt.Sprintf("g%d", i), Children: []*GraphNode{nodes[i], nodes[j]}, DownStreams: nodes[i].DownStreams, UpStreams: nodes[i].UpStreams}
					groups = append(groups, group)
					nodes[i].Parent = group
					nodes[j].Parent = group
				}
			}
		}
	}

	topNodes := groups
	for _, node := range nodes {
		if node.Parent == nil {
			topNodes = append(topNodes, node)
		}
	}
	for _, node := range topNodes {
		dss := make(map[*GraphNode]struct{})
		for _, ds := range node.DownStreams {
			if ds.Parent != nil {
				dss[ds.Parent] = struct{}{}
			} else {
				dss[ds] = struct{}{}
			}
		}
		node.DownStreams = nil
		for target := range dss {
			node.DownStreams = append(node.DownStreams, target)
		}
	}
	return topNodes
}

func isEqual(l, r []*GraphNode) bool {
	if len(l) != len(r) {
		return false
	}
	for i := 0; i < len(l); i++ {
		if l[i] != r[i] {
			return false
		}
	}
	return true
}

func labelForTask(id commM.ID, task *taskruns.TestbedSetup) string {
	var label string
	if task == nil || task.Spec.PlanExecID == nil {
		label = fmt.Sprintf("SetupID: %d", int(id))
	} else {
		label = fmt.Sprintf("PlanExecID: %d", int(*task.Spec.PlanExecID))
	}
	if task == nil || task.Status.Schedule == nil {
		return label
	}
	if task.Status.Schedule.ScheduleStatus == taskruns.ScheduleStatusPending {
		label += fmt.Sprintf("<br/>%s: %s",
			task.Status.Schedule.ScheduleReason.SchedulerName,
			task.Status.Schedule.ScheduleReason.Message,
		)
	} else {
		label += "(not Pending)"
	}
	return label
}
