package config

import (
	"strings"
	"text/template"

	"github.com/OliveTin/OliveTin/internal/env"
	"github.com/google/uuid"
	log "github.com/sirupsen/logrus"
)

// Sanitize will look for common configuration issues, and fix them. For example,
// populating undefined fields - name -> title, etc.
func (cfg *Config) Sanitize() {
	cfg.sanitizeLogLevel()
	cfg.sanitizeAuthRequireGuestsToLogin()
	cfg.sanitizeLogHistoryPageSize()
	cfg.sanitizeLocalUserPasswords()

	// log.Infof("cfg %p", cfg)

	for idx := range cfg.Actions {
		cfg.Actions[idx].sanitize(cfg)
	}

	cfg.sanitizeDashboardsForInlineActions()
}

func (cfg *Config) sanitizeDashboardsForInlineActions() {
	for _, dashboard := range cfg.Dashboards {
		cfg.sanitizeDashboardComponentForInlineActions(dashboard)
	}
}
func (cfg *Config) sanitizeDashboardComponentForInlineActions(component *DashboardComponent) {
	visited := make(map[*DashboardComponent]bool)
	cfg.sanitizeDashboardComponentForInlineActionsHelper(component, visited)
}

func (cfg *Config) sanitizeDashboardComponentForInlineActionsHelper(component *DashboardComponent, visited map[*DashboardComponent]bool) {
	if component == nil {
		return
	}

	if visited[component] {
		return
	}

	visited[component] = true

	cfg.sanitizeInlineAction(component)
	cfg.sanitizeChildDashboardComponents(component, visited)
}

func (cfg *Config) sanitizeInlineAction(component *DashboardComponent) {
	if component.InlineAction == nil {
		return
	}

	sanitizeInlineActionTitles(component)

	if component.Entity != "" && component.InlineAction.Entity == "" {
		component.InlineAction.Entity = component.Entity
	}

	component.InlineAction.sanitize(cfg)

	cfg.addInlineActionIfNotExists(component.InlineAction)
}

func (cfg *Config) addInlineActionIfNotExists(action *Action) {
	if cfg.inlineActionExists(action) {
		return
	}

	cfg.Actions = append(cfg.Actions, action)
}

func sanitizeInlineActionTitles(component *DashboardComponent) {
	if component.InlineAction.Title == "" {
		component.InlineAction.Title = component.Title
	}

	if component.Title == "" {
		component.Title = component.InlineAction.Title
	}
}

func (cfg *Config) inlineActionExists(action *Action) bool {
	if cfg.inlineActionPointerExists(action) {
		return true
	}

	if cfg.inlineActionIDExists(action) {
		return true
	}

	return false
}

func (cfg *Config) inlineActionPointerExists(action *Action) bool {
	for _, existingAction := range cfg.Actions {
		if existingAction == action {
			return true
		}
	}

	return false
}

func (cfg *Config) inlineActionIDExists(action *Action) bool {
	if action.ID == "" {
		return false
	}

	for _, existingAction := range cfg.Actions {
		if existingAction.ID == action.ID {
			return true
		}
	}

	return false
}

func (cfg *Config) sanitizeChildDashboardComponents(component *DashboardComponent, visited map[*DashboardComponent]bool) {
	for _, child := range component.Contents {
		if child.Entity == "" {
			child.Entity = component.Entity
		}

		cfg.sanitizeDashboardComponentForInlineActionsHelper(child, visited)
	}
}

func (cfg *Config) sanitizeLogLevel() {
	if logLevel, err := log.ParseLevel(cfg.LogLevel); err == nil {
		log.Info("Setting log level to ", logLevel)
		log.SetLevel(logLevel)
	}
}

func (action *Action) sanitize(cfg *Config) {
	if action.Timeout < 3 {
		action.Timeout = 3
	}

	action.ID = getActionID(action)
	action.Icon = lookupHTMLIcon(action.Icon, cfg.DefaultIconForActions)
	action.PopupOnStart = sanitizePopupOnStart(action.PopupOnStart, cfg)

	sanitizeActionExecutionMode(action)

	if action.MaxConcurrent < 1 {
		action.MaxConcurrent = 1
	}

	for idx := range action.Arguments {
		action.Arguments[idx].sanitize()
	}
}

func sanitizeActionExecutionMode(action *Action) {
	prioritizeExecToolOverShellAndExec(action)
	if len(action.Exec) > 0 && action.Shell != "" {
		log.Warnf("Action %q has both shell and exec; using exec only", action.Title)
		action.Shell = ""
	}
	clearIncompleteExecTool(action)
}

func prioritizeExecToolOverShellAndExec(action *Action) {
	if !execToolIsFullyConfigured(action) {
		return
	}
	if action.Shell == "" && len(action.Exec) == 0 {
		return
	}
	log.Warnf("Action %q has both execTool and shell/exec; using execTool only", action.Title)
	action.Shell = ""
	action.Exec = nil
}

func execToolIsFullyConfigured(action *Action) bool {
	t := action.ExecTool
	if t == nil {
		return false
	}
	if t.Name == "" {
		return false
	}
	return t.Config != nil
}

func clearIncompleteExecTool(action *Action) {
	if action.ExecTool == nil || (action.ExecTool.Name != "" && action.ExecTool.Config != nil) {
		return
	}
	log.Warnf("Action %q has execTool with missing name or config; clearing execTool", action.Title)
	action.ExecTool = nil
}

func (cfg *Config) sanitizeAuthRequireGuestsToLogin() {
	if cfg.AuthRequireGuestsToLogin {
		log.Infof("AuthRequireGuestsToLogin is enabled. All defaultPermissions will be set to false")

		cfg.DefaultPermissions.View = false
		cfg.DefaultPermissions.Exec = false
		cfg.DefaultPermissions.Logs = false
	}
}

func (cfg *Config) sanitizeLogHistoryPageSize() {
	if cfg.LogHistoryPageSize < 10 {
		log.Warnf("LogsHistoryLimit is too low, setting it to 10")
		cfg.LogHistoryPageSize = 10
	} else if cfg.LogHistoryPageSize > 100 {
		log.Warnf("LogsHistoryLimit is high, you can do this, but expect browser lag.")
	}
}

func (cfg *Config) sanitizeLocalUserPasswords() {
	for _, user := range cfg.AuthLocalUsers.Users {
		if user.Password != "" {
			user.Password = parsePasswordTemplate(user.Password)
		}
	}
}

// parsePasswordTemplate expands {{ .Env.VAR }} in local user password fields using the process environment.
func parsePasswordTemplate(source string) string {
	t, err := template.New("password").Option("missingkey=error").Parse(source)
	if err != nil {
		log.WithFields(log.Fields{"error": err}).Debug("Password template parse failed, using literal")
		return source
	}
	var b strings.Builder
	if err := t.Execute(&b, map[string]interface{}{"Env": env.BuildEnvMap()}); err != nil {
		log.WithFields(log.Fields{"error": err}).Debug("Password template execute failed, using literal")
		return source
	}
	return b.String()
}

func getActionID(action *Action) string {
	if action.ID == "" {
		return uuid.NewString()
	}

	if strings.Contains(action.ID, "{{") {
		log.Fatalf("Action IDs cannot contain variables")
	}

	return action.ID
}

//gocyclo:ignore
func sanitizePopupOnStart(raw string, cfg *Config) string {
	switch raw {
	case "execution-dialog":
		return raw
	case "execution-dialog-output-html":
		return raw
	case "execution-dialog-stdout-only":
		return raw
	case "execution-button":
		return raw
	default:
		return cfg.DefaultPopupOnStart
	}
}

func (arg *ActionArgument) sanitize() {
	if arg.Title == "" {
		arg.Title = arg.Name
	}

	for idx, choice := range arg.Choices {
		if choice.Title == "" {
			arg.Choices[idx].Title = choice.Value
		}
	}

	arg.sanitizeNoType()

	// TODO Validate the default against the type checker, but this creates a
	// import loop
}

func (arg *ActionArgument) sanitizeNoType() {
	if len(arg.Choices) == 0 && arg.Type == "" {
		log.WithFields(log.Fields{
			"arg": arg.Name,
		}).Warn("Argument type isn't set, will default to 'ascii' but this may not be safe. You should set a type specifically.")
		arg.Type = "ascii"
	}
}
