package config

import (
	"errors"
	"fmt"
	"log"
	"reflect"
	"slices"
	"strings"

	"github.com/jesseduffield/lazygit/pkg/constants"
	"github.com/jesseduffield/lazygit/pkg/utils"
	"github.com/samber/lo"
)

func (config *UserConfig) Validate() error {
	if err := validateEnum("gui.statusPanelView", config.Gui.StatusPanelView,
		[]string{"dashboard", "allBranchesLog"}); err != nil {
		return err
	}
	if err := validateEnum("gui.showDivergenceFromBaseBranch", config.Gui.ShowDivergenceFromBaseBranch,
		[]string{"none", "onlyArrow", "arrowAndNumber"}); err != nil {
		return err
	}
	if err := validateEnum("gui.fileTreeSortOrder", config.Gui.FileTreeSortOrder,
		[]string{"mixed", "filesFirst", "foldersFirst"}); err != nil {
		return err
	}
	if err := validateEnum("git.autoForwardBranches", config.Git.AutoForwardBranches,
		[]string{"none", "onlyMainBranches", "allBranches"}); err != nil {
		return err
	}
	if err := validateEnum("git.localBranchSortOrder", config.Git.LocalBranchSortOrder,
		[]string{"date", "recency", "alphabetical"}); err != nil {
		return err
	}
	if err := validateEnum("git.remoteBranchSortOrder", config.Git.RemoteBranchSortOrder,
		[]string{"date", "alphabetical"}); err != nil {
		return err
	}
	if err := validateEnum("git.log.order", config.Git.Log.Order,
		[]string{"date-order", "author-date-order", "topo-order", "default"}); err != nil {
		return err
	}
	if err := validateEnum("git.log.showGraph", config.Git.Log.ShowGraph,
		[]string{"always", "never", "when-maximised"}); err != nil {
		return err
	}
	if err := validatePagers(config.Git.Pagers); err != nil {
		return err
	}
	if err := validateKeybindings(config.Keybinding); err != nil {
		return err
	}
	if err := validateCustomCommands(config.CustomCommands); err != nil {
		return err
	}
	if err := validateSpinner(config.Gui.Spinner); err != nil {
		return err
	}
	if err := validateSidePanels(config.Gui.SidePanels); err != nil {
		return err
	}
	return nil
}

func validateSidePanels(panels []SidePanel) error {
	seen := map[string]bool{}
	total := 0
	for _, panel := range panels {
		if len(panel) == 0 {
			return errors.New("gui.sidePanels: a side panel must have at least one tab.")
		}
		for _, name := range panel {
			if !slices.Contains(ValidSidePanelTabs, name) {
				return fmt.Errorf("gui.sidePanels: unknown side panel '%s'. Allowed values: %s",
					name, strings.Join(ValidSidePanelTabs, ", "))
			}
			if seen[name] {
				return fmt.Errorf("gui.sidePanels: '%s' is listed more than once; each side panel may appear only once.", name)
			}
			seen[name] = true
			total++
		}
	}
	if total == 0 {
		return errors.New("gui.sidePanels must not be empty.")
	}
	// A lot of code focuses these panels directly (e.g. after resolving a
	// conflict or popping a stash), so they must always be present; otherwise
	// that code would focus a hidden panel.
	for _, required := range []string{"files", "branches", "commits"} {
		if !seen[required] {
			return fmt.Errorf("gui.sidePanels: '%s' must be included; it can't be hidden.", required)
		}
	}
	return nil
}

func validateSpinner(spinner SpinnerConfig) error {
	if len(spinner.Frames) == 0 {
		return errors.New("gui.spinner.frames must not be empty.")
	}
	firstWidth := utils.StringWidth(spinner.Frames[0])
	if lo.SomeBy(spinner.Frames, func(frame string) bool {
		return utils.StringWidth(frame) != firstWidth
	}) {
		return errors.New("All gui.spinner.frames entries must have the same width.")
	}
	return nil
}

// validatePagers rejects pager entries that combine more than one diff
// mechanism. A pager (GIT_PAGER) formats the diff that git produces, whereas
// externalDiffCommand and useExternalDiffGitConfig change how git produces the
// diff in the first place; piping one through the other almost always yields
// garbled output, so we treat the three as mutually exclusive.
func validatePagers(pagers []PagingConfig) error {
	for i, pager := range pagers {
		count := 0
		if pager.Pager != "" {
			count++
		}
		if pager.ExternalDiffCommand != "" {
			count++
		}
		if pager.UseExternalDiffGitConfig {
			count++
		}
		if count > 1 {
			return fmt.Errorf("git.pagers[%d]: at most one of 'pager', 'externalDiffCommand', and 'useExternalDiffGitConfig' may be set; they are mutually exclusive", i)
		}
	}
	return nil
}

func validateEnum(name string, value string, allowedValues []string) error {
	if slices.Contains(allowedValues, value) {
		return nil
	}
	allowedValuesStr := strings.Join(allowedValues, ", ")
	return fmt.Errorf("Unexpected value '%s' for '%s'. Allowed values: %s", value, name, allowedValuesStr)
}

func validateKeybindingsRecurse(path string, node any) error {
	value := reflect.ValueOf(node)
	if value.Kind() == reflect.Struct {
		for _, field := range reflect.VisibleFields(reflect.TypeOf(node)) {
			var newPath string
			if len(path) == 0 {
				newPath = field.Name
			} else {
				newPath = fmt.Sprintf("%s.%s", path, field.Name)
			}
			if err := validateKeybindingsRecurse(newPath,
				value.FieldByName(field.Name).Interface()); err != nil {
				return err
			}
		}
	} else if value.Kind() == reflect.Slice {
		for i := range value.Len() {
			if err := validateKeybindingsRecurse(
				fmt.Sprintf("%s[%d]", path, i), value.Index(i).Interface()); err != nil {
				return err
			}
		}
	} else if value.Kind() == reflect.String {
		key := node.(string)
		if !isValidKeybindingKey(key) {
			return fmt.Errorf("Unrecognized key '%s' for keybinding '%s'. For permitted values see %s",
				key, path, constants.Links.Docs.CustomKeybindings)
		}
	} else {
		log.Fatalf("Unexpected type for property '%s': %s", path, value.Kind())
	}
	return nil
}

func validateKeybindings(keybindingConfig KeybindingConfig) error {
	return validateKeybindingsRecurse("", keybindingConfig)
}

func validateCustomCommandKey(key Keybinding) error {
	for _, k := range key {
		if !isValidKeybindingKey(k) {
			return fmt.Errorf("Unrecognized key '%s' for custom command. For permitted values see %s",
				k, constants.Links.Docs.CustomKeybindings)
		}
	}
	return nil
}

func validateCustomCommands(customCommands []CustomCommand) error {
	for _, customCommand := range customCommands {
		if err := validateCustomCommandKey(customCommand.Key); err != nil {
			return err
		}

		if len(customCommand.CommandMenu) > 0 {
			if len(customCommand.Context) > 0 ||
				len(customCommand.Command) > 0 ||
				len(customCommand.Prompts) > 0 ||
				len(customCommand.LoadingText) > 0 ||
				len(customCommand.Output) > 0 ||
				len(customCommand.OutputTitle) > 0 ||
				customCommand.After != nil {
				commandRef := ""
				if len(customCommand.Key) > 0 {
					commandRef = fmt.Sprintf(" with key '%s'", customCommand.Key.String())
				}
				return fmt.Errorf("Error with custom command%s: it is not allowed to use both commandMenu and any of the other fields except key and description.", commandRef)
			}

			if err := validateCustomCommands(customCommand.CommandMenu); err != nil {
				return err
			}
		} else {
			for _, prompt := range customCommand.Prompts {
				if err := validateCustomCommandPrompt(prompt); err != nil {
					return err
				}
			}

			if err := validateEnum("customCommand.output", customCommand.Output,
				[]string{"", "none", "terminal", "log", "logWithPty", "popup"}); err != nil {
				return err
			}
		}
	}
	return nil
}

func validateCustomCommandPrompt(prompt CustomCommandPrompt) error {
	for _, option := range prompt.Options {
		for _, k := range option.Key {
			if !isValidKeybindingKey(k) {
				return fmt.Errorf("Unrecognized key '%s' for custom command prompt option. For permitted values see %s",
					k, constants.Links.Docs.CustomKeybindings)
			}
		}
	}

	return nil
}
