// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

package model

import (
	"encoding/json"
	"io"
	"net/url"
	"path"
	"reflect"
	"strings"

	"github.com/pkg/errors"
)

// AutocompleteArgType describes autocomplete argument type
type AutocompleteArgType string

// Argument types
const (
	AutocompleteArgTypeText        AutocompleteArgType = "TextInput"
	AutocompleteArgTypeStaticList  AutocompleteArgType = "StaticList"
	AutocompleteArgTypeDynamicList AutocompleteArgType = "DynamicList"
)

// AutocompleteData describes slash command autocomplete information.
type AutocompleteData struct {
	// Trigger of the command
	Trigger string
	// Hint of a command
	Hint string
	// Text displayed to the user to help with the autocomplete description
	HelpText string
	// Role of the user who should be able to see the autocomplete info of this command
	RoleID string
	// Arguments of the command. Arguments can be named or positional.
	// If they are positional order in the list matters, if they are named order does not matter.
	// All arguments should be either named or positional, no mixing allowed.
	Arguments []*AutocompleteArg
	// Subcommands of the command
	SubCommands []*AutocompleteData
}

// AutocompleteArg describes an argument of the command. Arguments can be named or positional.
// If Name is empty string Argument is positional otherwise it is named argument.
// Named arguments are passed as --Name Argument_Value.
type AutocompleteArg struct {
	// Name of the argument
	Name string
	// Text displayed to the user to help with the autocomplete
	HelpText string
	// Type of the argument
	Type AutocompleteArgType
	// Required determins if argument is optional or not.
	Required bool
	// Actual data of the argument (depends on the Type)
	Data interface{}
}

// AutocompleteTextArg describes text user can input as an argument.
type AutocompleteTextArg struct {
	// Hint of the input text
	Hint string
	// Regex pattern to match
	Pattern string
}

// AutocompleteListItem describes an item in the AutocompleteStaticListArg.
type AutocompleteListItem struct {
	Item     string
	Hint     string
	HelpText string
}

// AutocompleteStaticListArg is used to input one of the arguments from the list,
// for example [yes, no], [on, off], and so on.
type AutocompleteStaticListArg struct {
	PossibleArguments []AutocompleteListItem
}

// AutocompleteDynamicListArg is used when user wants to download possible argument list from the URL.
type AutocompleteDynamicListArg struct {
	FetchURL string
}

// AutocompleteSuggestion describes a single suggestion item sent to the front-end
// Example: for user input `/jira cre` -
// Complete might be `/jira create`
// Suggestion might be `create`,
// Hint might be `[issue text]`,
// Description might be `Create a new Issue`
type AutocompleteSuggestion struct {
	// Complete describes completed suggestion
	Complete string
	// Suggestion describes what user might want to input next
	Suggestion string
	// Hint describes a hint about the suggested input
	Hint string
	// Description of the command or a suggestion
	Description string
	// IconData is base64 encoded svg image
	IconData string
}

// NewAutocompleteData returns new Autocomplete data.
func NewAutocompleteData(trigger, hint, helpText string) *AutocompleteData {
	return &AutocompleteData{
		Trigger:     trigger,
		Hint:        hint,
		HelpText:    helpText,
		RoleID:      SYSTEM_USER_ROLE_ID,
		Arguments:   []*AutocompleteArg{},
		SubCommands: []*AutocompleteData{},
	}
}

// AddCommand adds a subcommand to the autocomplete data.
func (ad *AutocompleteData) AddCommand(command *AutocompleteData) {
	ad.SubCommands = append(ad.SubCommands, command)
}

// AddTextArgument adds positional AutocompleteArgTypeText argument to the command.
func (ad *AutocompleteData) AddTextArgument(helpText, hint, pattern string) {
	ad.AddNamedTextArgument("", helpText, hint, pattern, true)
}

// AddNamedTextArgument adds named AutocompleteArgTypeText argument to the command.
func (ad *AutocompleteData) AddNamedTextArgument(name, helpText, hint, pattern string, required bool) {
	argument := AutocompleteArg{
		Name:     name,
		HelpText: helpText,
		Type:     AutocompleteArgTypeText,
		Required: required,
		Data:     &AutocompleteTextArg{Hint: hint, Pattern: pattern},
	}
	ad.Arguments = append(ad.Arguments, &argument)
}

// AddStaticListArgument adds positional AutocompleteArgTypeStaticList argument to the command.
func (ad *AutocompleteData) AddStaticListArgument(helpText string, required bool, items []AutocompleteListItem) {
	ad.AddNamedStaticListArgument("", helpText, required, items)
}

// AddNamedStaticListArgument adds named AutocompleteArgTypeStaticList argument to the command.
func (ad *AutocompleteData) AddNamedStaticListArgument(name, helpText string, required bool, items []AutocompleteListItem) {
	argument := AutocompleteArg{
		Name:     name,
		HelpText: helpText,
		Type:     AutocompleteArgTypeStaticList,
		Required: required,
		Data:     &AutocompleteStaticListArg{PossibleArguments: items},
	}
	ad.Arguments = append(ad.Arguments, &argument)
}

// AddDynamicListArgument adds positional AutocompleteArgTypeDynamicList argument to the command.
func (ad *AutocompleteData) AddDynamicListArgument(helpText, url string, required bool) {
	ad.AddNamedDynamicListArgument("", helpText, url, required)
}

// AddNamedDynamicListArgument adds named AutocompleteArgTypeDynamicList argument to the command.
func (ad *AutocompleteData) AddNamedDynamicListArgument(name, helpText, url string, required bool) {
	argument := AutocompleteArg{
		Name:     name,
		HelpText: helpText,
		Type:     AutocompleteArgTypeDynamicList,
		Required: required,
		Data:     &AutocompleteDynamicListArg{FetchURL: url},
	}
	ad.Arguments = append(ad.Arguments, &argument)
}

// Equals method checks if command is the same.
func (ad *AutocompleteData) Equals(command *AutocompleteData) bool {
	if !(ad.Trigger == command.Trigger && ad.HelpText == command.HelpText && ad.RoleID == command.RoleID && ad.Hint == command.Hint) {
		return false
	}
	if len(ad.Arguments) != len(command.Arguments) || len(ad.SubCommands) != len(command.SubCommands) {
		return false
	}
	for i := range ad.Arguments {
		if !ad.Arguments[i].Equals(command.Arguments[i]) {
			return false
		}
	}
	for i := range ad.SubCommands {
		if !ad.SubCommands[i].Equals(command.SubCommands[i]) {
			return false
		}
	}
	return true
}

// UpdateRelativeURLsForPluginCommands method updates relative urls for plugin commands
func (ad *AutocompleteData) UpdateRelativeURLsForPluginCommands(baseURL *url.URL) error {
	for _, arg := range ad.Arguments {
		if arg.Type != AutocompleteArgTypeDynamicList {
			continue
		}
		dynamicList, ok := arg.Data.(*AutocompleteDynamicListArg)
		if !ok {
			return errors.New("Not a proper DynamicList type argument")
		}
		dynamicListURL, err := url.Parse(dynamicList.FetchURL)
		if err != nil {
			return errors.Wrapf(err, "FetchURL is not a proper url")
		}
		if !dynamicListURL.IsAbs() {
			absURL := &url.URL{}
			*absURL = *baseURL
			absURL.Path = path.Join(absURL.Path, dynamicList.FetchURL)
			dynamicList.FetchURL = absURL.String()
		}

	}
	for _, command := range ad.SubCommands {
		err := command.UpdateRelativeURLsForPluginCommands(baseURL)
		if err != nil {
			return err
		}
	}
	return nil
}

// IsValid method checks if autocomplete data is valid.
func (ad *AutocompleteData) IsValid() error {
	if ad == nil {
		return errors.New("No nil commands are allowed in AutocompleteData")
	}
	if ad.Trigger == "" {
		return errors.New("An empty command name in the autocomplete data")
	}
	if strings.ToLower(ad.Trigger) != ad.Trigger {
		return errors.New("Command should be lowercase")
	}
	roles := []string{SYSTEM_ADMIN_ROLE_ID, SYSTEM_USER_ROLE_ID, ""}
	if stringNotInSlice(ad.RoleID, roles) {
		return errors.New("Wrong role in the autocomplete data")
	}
	if len(ad.Arguments) > 0 && len(ad.SubCommands) > 0 {
		return errors.New("Command can't have arguments and subcommands")
	}
	if len(ad.Arguments) > 0 {
		namedArgumentIndex := -1
		for i, arg := range ad.Arguments {
			if arg.Name != "" { // it's a named argument
				if namedArgumentIndex == -1 { // first named argument
					namedArgumentIndex = i
				}
			} else { // it's a positional argument
				if namedArgumentIndex != -1 {
					return errors.New("Named argument should not be before positional argument")
				}
			}
			if arg.Type == AutocompleteArgTypeDynamicList {
				dynamicList, ok := arg.Data.(*AutocompleteDynamicListArg)
				if !ok {
					return errors.New("Not a proper DynamicList type argument")
				}
				_, err := url.Parse(dynamicList.FetchURL)
				if err != nil {
					return errors.Wrapf(err, "FetchURL is not a proper url")
				}
			} else if arg.Type == AutocompleteArgTypeStaticList {
				staticList, ok := arg.Data.(*AutocompleteStaticListArg)
				if !ok {
					return errors.New("Not a proper StaticList type argument")
				}
				for _, arg := range staticList.PossibleArguments {
					if arg.Item == "" {
						return errors.New("Possible argument name not set in StaticList argument")
					}
				}
			} else if arg.Type == AutocompleteArgTypeText {
				if _, ok := arg.Data.(*AutocompleteTextArg); !ok {
					return errors.New("Not a proper TextInput type argument")
				}
				if arg.Name == "" && !arg.Required {
					return errors.New("Positional argument can not be optional")
				}
			}
		}
	}
	for _, command := range ad.SubCommands {
		err := command.IsValid()
		if err != nil {
			return err
		}
	}
	return nil
}

// ToJSON encodes AutocompleteData struct to the json
func (ad *AutocompleteData) ToJSON() ([]byte, error) {
	b, err := json.Marshal(ad)
	if err != nil {
		return nil, errors.Wrapf(err, "can't marshal slash command %s", ad.Trigger)
	}
	return b, nil
}

// AutocompleteDataFromJSON decodes AutocompleteData struct from the json
func AutocompleteDataFromJSON(data []byte) (*AutocompleteData, error) {
	var ad AutocompleteData
	if err := json.Unmarshal(data, &ad); err != nil {
		return nil, errors.Wrap(err, "can't unmarshal AutocompleteData")
	}
	return &ad, nil
}

// Equals method checks if argument is the same.
func (a *AutocompleteArg) Equals(arg *AutocompleteArg) bool {
	if a.Name != arg.Name ||
		a.HelpText != arg.HelpText ||
		a.Type != arg.Type ||
		a.Required != arg.Required ||
		!reflect.DeepEqual(a.Data, arg.Data) {
		return false
	}
	return true
}

// UnmarshalJSON will unmarshal argument
func (a *AutocompleteArg) UnmarshalJSON(b []byte) error {
	var arg map[string]interface{}
	if err := json.Unmarshal(b, &arg); err != nil {
		return errors.Wrapf(err, "Can't unmarshal argument %s", string(b))
	}
	var ok bool
	a.Name, ok = arg["Name"].(string)
	if !ok {
		return errors.Errorf("No field Name in the argument %s", string(b))
	}

	a.HelpText, ok = arg["HelpText"].(string)
	if !ok {
		return errors.Errorf("No field HelpText in the argument %s", string(b))
	}

	t, ok := arg["Type"].(string)
	if !ok {
		return errors.Errorf("No field Type in the argument %s", string(b))
	}
	a.Type = AutocompleteArgType(t)

	a.Required, ok = arg["Required"].(bool)
	if !ok {
		return errors.Errorf("No field Required in the argument %s", string(b))
	}

	data, ok := arg["Data"]
	if !ok {
		return errors.Errorf("No field Data in the argument %s", string(b))
	}

	if a.Type == AutocompleteArgTypeText {
		m, ok := data.(map[string]interface{})
		if !ok {
			return errors.Errorf("Wrong Data type in the TextInput argument %s", string(b))
		}
		pattern, ok := m["Pattern"].(string)
		if !ok {
			return errors.Errorf("No field Pattern in the TextInput argument %s", string(b))
		}
		hint, ok := m["Hint"].(string)
		if !ok {
			return errors.Errorf("No field Hint in the TextInput argument %s", string(b))
		}
		a.Data = &AutocompleteTextArg{Hint: hint, Pattern: pattern}
	} else if a.Type == AutocompleteArgTypeStaticList {
		m, ok := data.(map[string]interface{})
		if !ok {
			return errors.Errorf("Wrong Data type in the StaticList argument %s", string(b))
		}
		list, ok := m["PossibleArguments"].([]interface{})
		if !ok {
			return errors.Errorf("No field PossibleArguments in the StaticList argument %s", string(b))
		}

		possibleArguments := []AutocompleteListItem{}
		for i := range list {
			args, ok := list[i].(map[string]interface{})
			if !ok {
				return errors.Errorf("Wrong AutocompleteStaticListItem type in the StaticList argument %s", string(b))
			}
			item, ok := args["Item"].(string)
			if !ok {
				return errors.Errorf("No field Item in the StaticList's possible arguments %s", string(b))
			}

			hint, ok := args["Hint"].(string)
			if !ok {
				return errors.Errorf("No field Hint in the StaticList's possible arguments %s", string(b))
			}
			helpText, ok := args["HelpText"].(string)
			if !ok {
				return errors.Errorf("No field Hint in the StaticList's possible arguments %s", string(b))
			}

			possibleArguments = append(possibleArguments, AutocompleteListItem{
				Item:     item,
				Hint:     hint,
				HelpText: helpText,
			})
		}
		a.Data = &AutocompleteStaticListArg{PossibleArguments: possibleArguments}
	} else if a.Type == AutocompleteArgTypeDynamicList {
		m, ok := data.(map[string]interface{})
		if !ok {
			return errors.Errorf("Wrong type in the DynamicList argument %s", string(b))
		}
		url, ok := m["FetchURL"].(string)
		if !ok {
			return errors.Errorf("No field FetchURL in the DynamicList's argument %s", string(b))
		}
		a.Data = &AutocompleteDynamicListArg{FetchURL: url}
	}
	return nil
}

// AutocompleteSuggestionsToJSON returns json for a list of AutocompleteSuggestion objects
func AutocompleteSuggestionsToJSON(suggestions []AutocompleteSuggestion) []byte {
	b, _ := json.Marshal(suggestions)
	return b
}

// AutocompleteSuggestionsFromJSON returns list of AutocompleteSuggestions from json.
func AutocompleteSuggestionsFromJSON(data io.Reader) []AutocompleteSuggestion {
	var o []AutocompleteSuggestion
	json.NewDecoder(data).Decode(&o)
	return o
}

// AutocompleteStaticListItemsToJSON returns json for a list of AutocompleteStaticListItem objects
func AutocompleteStaticListItemsToJSON(items []AutocompleteListItem) []byte {
	b, _ := json.Marshal(items)
	return b
}

// AutocompleteStaticListItemsFromJSON returns list of AutocompleteStaticListItem from json.
func AutocompleteStaticListItemsFromJSON(data io.Reader) []AutocompleteListItem {
	var o []AutocompleteListItem
	json.NewDecoder(data).Decode(&o)
	return o
}

func stringNotInSlice(a string, slice []string) bool {
	for _, b := range slice {
		if b == a {
			return false
		}
	}
	return true
}
