package api

import (
	"sort"
	"strconv"

	apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
	acl "github.com/OliveTin/OliveTin/internal/acl"
	config "github.com/OliveTin/OliveTin/internal/config"
	entities "github.com/OliveTin/OliveTin/internal/entities"
	"github.com/OliveTin/OliveTin/internal/tpl"
	log "github.com/sirupsen/logrus"
	"golang.org/x/exp/slices"
)

func renderDashboard(rr *DashboardRenderRequest, dashboardTitle string) *apiv1.Dashboard {
	if dashboardTitle == "default" {
		return buildDefaultDashboard(rr)
	}

	return findAndRenderDashboard(rr, dashboardTitle)
}

func getEntityFromRequest(rr *DashboardRenderRequest) *entities.Entity {
	if rr.EntityType == "" || rr.EntityKey == "" {
		return nil
	}

	entityInstances := entities.GetEntityInstances(rr.EntityType)
	if entity, ok := entityInstances[rr.EntityKey]; ok {
		return entity
	}

	return nil
}

func findAndRenderDashboard(rr *DashboardRenderRequest, dashboardTitle string) *apiv1.Dashboard {
	if dashboard := findDashboardByTitle(rr, dashboardTitle); dashboard != nil {
		return renderDashboardIfValid(dashboard, rr)
	}

	return renderDirectoryDashboard(rr, dashboardTitle)
}

func findDashboardByTitle(rr *DashboardRenderRequest, dashboardTitle string) *config.DashboardComponent {
	for _, dashboard := range rr.cfg.Dashboards {
		if dashboard.Title == dashboardTitle {
			return dashboard
		}
	}
	return nil
}

func renderDashboardIfValid(dashboard *config.DashboardComponent, rr *DashboardRenderRequest) *apiv1.Dashboard {
	if len(dashboard.Contents) == 0 {
		logEmptyDashboard(dashboard.Title, rr.AuthenticatedUser.Username)
		return nil
	}
	return buildDashboardFromConfig(dashboard, rr)
}

func renderDirectoryDashboard(rr *DashboardRenderRequest, dashboardTitle string) *apiv1.Dashboard {
	directoryComponent := findDirectoryComponent(rr, dashboardTitle)
	if directoryComponent == nil {
		return nil
	}

	entity := getEntityFromRequest(rr)
	return buildDashboardFromConfigWithEntity(directoryComponent, rr, entity)
}

func findDirectoryComponent(rr *DashboardRenderRequest, title string) *config.DashboardComponent {
	for _, dashboard := range rr.cfg.Dashboards {
		if component := searchDirectoryInComponent(dashboard, title); component != nil {
			return component
		}
	}
	return nil
}

func searchDirectoryInComponent(component *config.DashboardComponent, title string) *config.DashboardComponent {
	if isMatchingDirectory(component, title) {
		return component
	}

	return searchDirectoryInSubcomponents(component.Contents, title)
}

func isMatchingDirectory(component *config.DashboardComponent, title string) bool {
	return component.Title == title && len(component.Contents) > 0 && component.Type != "fieldset"
}

func searchDirectoryInSubcomponents(contents []*config.DashboardComponent, title string) *config.DashboardComponent {
	for _, subitem := range contents {
		if found := searchDirectoryInComponent(subitem, title); found != nil {
			return found
		}
	}

	return nil
}

func logEmptyDashboard(dashboardTitle, username string) {
	log.WithFields(log.Fields{
		"dashboard": dashboardTitle,
		"username":  username,
	}).Debugf("Dashboard has no readable contents, so it will not be visible in the web ui")
}

func buildDashboardFromConfig(dashboard *config.DashboardComponent, rr *DashboardRenderRequest) *apiv1.Dashboard {
	return buildDashboardFromConfigWithEntity(dashboard, rr, nil)
}

func buildDashboardFromConfigWithEntity(dashboard *config.DashboardComponent, rr *DashboardRenderRequest, entity *entities.Entity) *apiv1.Dashboard {
	contents, root := getDashboardComponentContentsWithEntity(dashboard, rr, entity)
	return &apiv1.Dashboard{
		Title:    dashboard.Title,
		Contents: orderTopLevelDashboardComponents(removeNulls(contents), root),
	}
}

//gocyclo:ignore
func buildDefaultDashboard(rr *DashboardRenderRequest) *apiv1.Dashboard {
	db := &apiv1.Dashboard{
		Title:    "Actions",
		Contents: make([]*apiv1.DashboardComponent, 0),
	}

	fieldset := &apiv1.DashboardComponent{
		Type:     "fieldset",
		Title:    "Actions",
		Contents: make([]*apiv1.DashboardComponent, 0),
	}

	for _, binding := range rr.ex.MapActionBindings {
		if binding == nil || binding.Action == nil || binding.Action.Hidden {
			continue
		}

		if binding.IsOnConfiguredDashboard() {
			continue
		}

		if !acl.IsAllowedView(rr.cfg, rr.AuthenticatedUser, binding.Action) {
			continue
		}

		action := buildAction(binding, rr)
		if action == nil {
			continue
		}

		comp := &apiv1.DashboardComponent{
			Type:   "link",
			Title:  action.Title,
			Icon:   action.Icon,
			Action: action,
		}
		if binding.Entity != nil {
			comp.EntityKey = binding.Entity.UniqueKey
		}
		fieldset.Contents = append(fieldset.Contents, comp)
	}

	if len(fieldset.Contents) > 0 {
		fieldset.Contents = sortDashboardComponents(fieldset.Contents)
		db.Contents = append(db.Contents, fieldset)
	}

	return db
}

func entityKeyLess(a, b string) bool {
	ai, errA := strconv.ParseInt(a, 10, 64)
	bi, errB := strconv.ParseInt(b, 10, 64)
	if errA == nil && errB == nil {
		return ai < bi
	}
	return a < b
}

//gocyclo:ignore
func sortDashboardComponents(components []*apiv1.DashboardComponent) []*apiv1.DashboardComponent {
	sort.Slice(components, func(i, j int) bool {
		if components[i].Action == nil || components[j].Action == nil {
			if components[i].EntityKey != "" && components[j].EntityKey != "" &&
				components[i].EntityKey != components[j].EntityKey {
				return entityKeyLess(components[i].EntityKey, components[j].EntityKey)
			}

			return components[i].Title < components[j].Title
		}

		if components[i].Action.Order != components[j].Action.Order {
			return components[i].Action.Order < components[j].Action.Order
		}
		if components[i].EntityKey != components[j].EntityKey {
			return entityKeyLess(components[i].EntityKey, components[j].EntityKey)
		}
		return components[i].Action.Title < components[j].Action.Title
	})

	return components
}

func removeNulls(components []*apiv1.DashboardComponent) []*apiv1.DashboardComponent {
	ret := make([]*apiv1.DashboardComponent, 0)

	for _, component := range components {
		if component == nil {
			continue
		}

		ret = append(ret, component)
	}

	return ret
}

func isNonEntityFieldset(component *apiv1.DashboardComponent) bool {
	return component != nil && component.Type == "fieldset" && component.EntityType == ""
}

func isRegularFieldset(component *apiv1.DashboardComponent, root *apiv1.DashboardComponent) bool {
	if !isNonEntityFieldset(component) {
		return false
	}
	return root == nil || component != root
}

func partitionTopLevelComponents(components []*apiv1.DashboardComponent, root *apiv1.DashboardComponent) (regular, sortables []*apiv1.DashboardComponent, isRegular []bool) {
	regular = make([]*apiv1.DashboardComponent, 0)
	sortables = make([]*apiv1.DashboardComponent, 0)
	isRegular = make([]bool, len(components))
	for i, c := range components {
		anchor := isRegularFieldset(c, root)
		isRegular[i] = anchor
		if anchor {
			regular = append(regular, c)
		} else {
			sortables = append(sortables, c)
		}
	}
	return regular, sortables, isRegular
}

func mergeOrderedTopLevelComponents(regular, sortables []*apiv1.DashboardComponent, isRegular []bool) []*apiv1.DashboardComponent {
	out := make([]*apiv1.DashboardComponent, 0, len(isRegular))
	regIdx, sortIdx := 0, 0
	for _, anchor := range isRegular {
		if anchor {
			out = append(out, regular[regIdx])
			regIdx++
		} else {
			out = append(out, sortables[sortIdx])
			sortIdx++
		}
	}
	return out
}

func orderTopLevelDashboardComponents(components []*apiv1.DashboardComponent, root *apiv1.DashboardComponent) []*apiv1.DashboardComponent {
	if len(components) == 0 {
		return components
	}
	regular, sortables, isRegular := partitionTopLevelComponents(components, root)
	sortDashboardComponents(sortables)
	return mergeOrderedTopLevelComponents(regular, sortables, isRegular)
}

func getDashboardComponentContentsWithEntity(dashboard *config.DashboardComponent, rr *DashboardRenderRequest, entity *entities.Entity) ([]*apiv1.DashboardComponent, *apiv1.DashboardComponent) {
	ret := make([]*apiv1.DashboardComponent, 0)
	rootFieldset := createRootFieldset()

	for _, subitem := range dashboard.Contents {
		processDashboardSubitemWithEntity(subitem, rr, &ret, rootFieldset, entity)
	}

	if len(rootFieldset.Contents) > 0 {
		ret = append(ret, rootFieldset)
		return ret, rootFieldset
	}
	return ret, nil
}

func createRootFieldset() *apiv1.DashboardComponent {
	return &apiv1.DashboardComponent{
		Type:     "fieldset",
		Title:    "Actions",
		Contents: make([]*apiv1.DashboardComponent, 0),
	}
}

func appendComponentIfNotNil(components *[]*apiv1.DashboardComponent, comp *apiv1.DashboardComponent) {
	if comp != nil {
		*components = append(*components, comp)
	}
}

func getDashboardComponentOrNil(subitem *config.DashboardComponent, rr *DashboardRenderRequest, entity *entities.Entity) *apiv1.DashboardComponent {
	if len(subitem.Contents) == 0 && rr.findActionForEntity(subitem.Title, entity) == nil {
		if !isAllowedType(subitem.Type) {
			return nil
		}
	}
	return buildDashboardComponentSimpleWithEntity(subitem, rr, entity)
}

func processDashboardSubitemWithEntity(subitem *config.DashboardComponent, rr *DashboardRenderRequest, ret *[]*apiv1.DashboardComponent, rootFieldset *apiv1.DashboardComponent, entity *entities.Entity) {
	if subitem.Type != "fieldset" {
		appendComponentIfNotNil(&rootFieldset.Contents, getDashboardComponentOrNil(subitem, rr, entity))
		return
	}

	if subitem.Entity != "" {
		*ret = append(*ret, buildEntityFieldsets(subitem.Entity, subitem, rr)...)
	} else {
		appendComponentIfNotNil(ret, getDashboardComponentOrNil(subitem, rr, entity))
	}
}

func buildDashboardComponentSimpleWithEntity(subitem *config.DashboardComponent, rr *DashboardRenderRequest, entity *entities.Entity) *apiv1.DashboardComponent {
	var contents []*apiv1.DashboardComponent

	if len(subitem.Contents) > 0 {
		contents, _ = getDashboardComponentContentsWithEntity(subitem, rr, entity)
	}

	action := rr.findActionForEntity(subitem.Title, entity)
	componentType := getDashboardComponentType(subitem, action)

	title := subitem.Title
	if entity != nil {
		title = tpl.ParseTemplateOfActionBeforeExec(subitem.Title, entity)
	}

	newitem := &apiv1.DashboardComponent{
		Title:    title,
		Type:     componentType,
		Contents: contents,
		Icon:     getDashboardComponentIcon(subitem, rr.cfg),
		CssClass: subitem.CssClass,
		Action:   action,
	}

	return newitem
}

func getDashboardComponentIcon(item *config.DashboardComponent, cfg *config.Config) string {
	if item.Icon == "" {
		return cfg.DefaultIconForDirectories
	}

	return item.Icon
}

func getDashboardComponentType(item *config.DashboardComponent, action *apiv1.Action) string {
	if hasContents(item) {
		return getTypeForComponentWithContents(item)
	}

	if isAllowedType(item.Type) {
		return item.Type
	}

	return getDefaultType(action)
}

func hasContents(item *config.DashboardComponent) bool {
	return len(item.Contents) > 0
}

func getTypeForComponentWithContents(item *config.DashboardComponent) string {
	if item.Type != "fieldset" {
		return "directory"
	}
	return "fieldset"
}

func isAllowedType(itemType string) bool {
	allowedTypes := []string{
		"stdout-most-recent-execution",
		"display",
	}
	return slices.Contains(allowedTypes, itemType)
}

func getDefaultType(action *apiv1.Action) string {
	if action == nil {
		return "display"
	}
	return "link"
}
