// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

package cli

import (
	"fmt"
	"sort"
	"strconv"
	"strings"
	"unicode"

	"github.com/apache/cloudstack-cloudmonkey/cmd"
	"github.com/apache/cloudstack-cloudmonkey/config"
)

var nameSupportingApis = []string{
	"configuration",
}

func buildAPICacheMap(apiMap map[string][]*config.API) map[string][]*config.API {
	for _, cmd := range cmd.AllCommands() {
		verb := cmd.Name
		if cmd.SubCommands != nil && len(cmd.SubCommands) > 0 {
			for command, opts := range cmd.SubCommands {
				var args []*config.APIArg
				options := opts
				if command == "profile" {
					options = config.GetProfiles()
				}
				for _, opt := range options {
					args = append(args, &config.APIArg{
						Name: opt,
					})
				}
				apiMap[verb] = append(apiMap[verb], &config.API{
					Name: command,
					Verb: verb,
					Noun: command,
					Args: args,
				})
			}
		} else {
			dummyAPI := &config.API{
				Name: "",
				Verb: verb,
			}
			apiMap[verb] = append(apiMap[verb], dummyAPI)
		}
	}
	return apiMap
}

func trimSpaceLeft(in []rune) []rune {
	firstIndex := len(in)
	for i, r := range in {
		if unicode.IsSpace(r) == false {
			firstIndex = i
			break
		}
	}
	return in[firstIndex:]
}

func equal(a, b []rune) bool {
	if len(a) != len(b) {
		return false
	}
	for i := 0; i < len(a); i++ {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}

func hasPrefix(r, prefix []rune) bool {
	if len(r) < len(prefix) {
		return false
	}
	return equal(r[:len(prefix)], prefix)
}

func inArray(s string, array []string) bool {
	for _, item := range array {
		if s == item {
			return true
		}
	}
	return false
}

func lastString(array []string) string {
	return array[len(array)-1]
}

type argOption struct {
	Value  string
	Detail string
}

func buildArgOptions(response map[string]interface{}, hasID bool) []argOption {
	argOptions := []argOption{}
	for _, v := range response {
		switch obj := v.(type) {
		case []interface{}:
			if obj == nil {
				break
			}
			for _, item := range obj {
				resource, ok := item.(map[string]interface{})
				if !ok {
					continue
				}
				var id, name, detail string
				if resource["id"] != nil {
					switch rawID := resource["id"].(type) {
					case string:
						id = rawID
					case float64:
						id = strconv.FormatFloat(rawID, 'f', -1, 64)
					default:
						panic(fmt.Errorf("detected an invalid type at path (%v:%T). This should have been caught during validation, indicating a bug in CloudMonkey. Please report this issue", rawID, rawID))
					}
				}
				if resource["name"] != nil {
					name = resource["name"].(string)
				} else if resource["username"] != nil {
					name = resource["username"].(string)
				} else if resource["hypervisor"] != nil && resource["hypervisorversion"] != nil {
					name = fmt.Sprintf("%s %s", resource["hypervisor"].(string), resource["hypervisorversion"].(string))
					if resource["osdisplayname"] != nil {
						name = fmt.Sprintf("%s; %s", resource["osdisplayname"].(string), name)
					}
				}
				if resource["displaytext"] != nil {
					detail = resource["displaytext"].(string)
				}
				if len(detail) == 0 && resource["description"] != nil {
					detail = resource["description"].(string)
				}
				if len(detail) == 0 && resource["ipaddress"] != nil {
					detail = resource["ipaddress"].(string)
				}
				var opt argOption
				if hasID {
					opt.Value = id
					opt.Detail = name
					if len(name) == 0 {
						opt.Detail = detail
					}
				} else {
					opt.Value = name
					opt.Detail = detail
					if len(name) == 0 {
						opt.Value = detail
					}
				}
				argOptions = append(argOptions, opt)
			}
			break
		}
	}
	return argOptions
}

func doInternal(line []rune, pos int, lineLen int, argName []rune) (newLine [][]rune, offset int) {
	offset = lineLen
	if lineLen >= len(argName) {
		if hasPrefix(line, argName) {
			if lineLen == len(argName) {
				newLine = append(newLine, []rune{' '})
			} else {
				newLine = append(newLine, argName)
			}
			offset = offset - len(argName) - 1
		}
	} else {
		if hasPrefix(argName, line) {
			newLine = append(newLine, argName[offset:])
		}
	}
	return
}

func findAPI(apiMap map[string][]*config.API, relatedNoun string) *config.API {
	var autocompleteAPI *config.API
	for _, listAPI := range apiMap["list"] {
		if relatedNoun == listAPI.Noun {
			autocompleteAPI = listAPI
			break
		}
	}
	return autocompleteAPI
}

func findAutocompleteAPI(arg *config.APIArg, apiFound *config.API, apiMap map[string][]*config.API) *config.API {
	if arg.Type == "map" {
		return nil
	}

	var autocompleteAPI *config.API
	argName := strings.Replace(arg.Name, "=", "", -1)
	relatedNoun := argName
	switch {
	case argName == "id" || argName == "ids":
		// Heuristic: user is trying to autocomplete for id/ids arg for a list API
		relatedNoun = apiFound.Noun
		if apiFound.Verb != "list" {
			relatedNoun += "s"
		}
	case argName == "account":
		// Heuristic: user is trying to autocomplete for accounts
		relatedNoun = "accounts"
	case argName == "ipaddressid":
		// Heuristic: user is trying to autocomplete for ip addresses
		relatedNoun = "publicipaddresses"
	case argName == "storageid":
		relatedNoun = "storagepools"
	case argName == "associatednetworkid":
		relatedNoun = "networks"
	default:
		// Heuristic: autocomplete for the arg for which a list<Arg without id/ids>s API exists
		// For example, for zoneid arg, listZones API exists
		base := argName
		if strings.HasSuffix(argName, "id") {
			base = strings.TrimSuffix(argName, "id")
		} else if strings.HasSuffix(argName, "ids") {
			base = strings.TrimSuffix(argName, "ids")
		} else if argName == "name" {
			for _, noun := range nameSupportingApis {
				if strings.HasPrefix(apiFound.Noun, noun) {
					base = noun
					break
				}
			}
		}
		// Handle common cases where base ends with a vowel and needs "es"
		if strings.HasSuffix(base, "s") || strings.HasSuffix(base, "x") || strings.HasSuffix(base, "z") || strings.HasSuffix(base, "ch") || strings.HasSuffix(base, "sh") {
			relatedNoun = base + "es"
		} else {
			relatedNoun = base + "s"
		}
	}

	config.Debug("Possible related noun for the arg: ", relatedNoun, " and type: ", arg.Type)
	autocompleteAPI = findAPI(apiMap, relatedNoun)

	if autocompleteAPI == nil {
		if strings.Contains(strings.ToLower(relatedNoun), "storage") {
			relatedNoun = "storagepools"
			autocompleteAPI = findAPI(apiMap, relatedNoun)
		}
	}

	if autocompleteAPI != nil {
		config.Debug("Autocomplete: API found using heuristics: ", autocompleteAPI.Name)
	}

	if strings.HasSuffix(relatedNoun, "s") {
		relatedNoun = relatedNoun[:len(relatedNoun)-1]
	}

	// Heuristic: find any list API that contains the arg name
	if autocompleteAPI == nil {
		config.Debug("Finding possible API that have: ", argName, " related APIs: ", arg.Related)
		possibleAPIs := []*config.API{}
		for _, listAPI := range apiMap["list"] {
			if strings.Contains(listAPI.Noun, argName) {
				config.Debug("Found possible API: ", listAPI.Name)
				possibleAPIs = append(possibleAPIs, listAPI)
			}
		}
		if len(possibleAPIs) == 1 {
			autocompleteAPI = possibleAPIs[0]
		}
	}

	return autocompleteAPI
}

type autoCompleter struct {
	Config *config.Config
}

func (t *autoCompleter) Do(line []rune, pos int) (options [][]rune, offset int) {
	apiMap := buildAPICacheMap(t.Config.GetAPIVerbMap())

	var verbs []string
	for verb := range apiMap {
		verbs = append(verbs, verb)
		sort.Slice(apiMap[verb], func(i, j int) bool {
			return apiMap[verb][i].Name < apiMap[verb][j].Name
		})
	}
	sort.Strings(verbs)

	line = trimSpaceLeft(line[:pos])

	// Auto-complete verb
	var verbFound string
	for _, verb := range verbs {
		search := verb + " "
		if !hasPrefix(line, []rune(search)) {
			sLine, sOffset := doInternal(line, pos, len(line), []rune(search))
			options = append(options, sLine...)
			offset = sOffset
		} else {
			verbFound = verb
			break
		}
	}
	if len(verbFound) == 0 {
		return
	}

	// Auto-complete noun
	var nounFound string
	line = trimSpaceLeft(line[len(verbFound):])
	for _, api := range apiMap[verbFound] {
		search := api.Noun + " "
		if !hasPrefix(line, []rune(search)) {
			sLine, sOffset := doInternal(line, pos, len(line), []rune(search))
			options = append(options, sLine...)
			offset = sOffset
		} else {
			nounFound = api.Noun
			break
		}
	}
	if len(nounFound) == 0 {
		return
	}

	// Find API
	var apiFound *config.API
	for _, api := range apiMap[verbFound] {
		if api.Noun == nounFound {
			apiFound = api
			break
		}
	}
	if apiFound == nil {
		return
	}

	// Auto-complete API arg
	splitLine := strings.Split(string(line), " ")
	line = trimSpaceLeft([]rune(splitLine[len(splitLine)-1]))
	for _, arg := range apiFound.Args {
		search := arg.Name
		if !hasPrefix(line, []rune(search)) {
			sLine, sOffset := doInternal(line, pos, len(line), []rune(search))
			options = append(options, sLine...)
			offset = sOffset
		} else {
			words := strings.Split(string(line), "=")
			argInput := lastString(words)
			if arg.Type == "boolean" {
				for _, search := range []string{"true ", "false "} {
					offset = 0
					if strings.HasPrefix(search, argInput) {
						options = append(options, []rune(search[len(argInput):]))
						offset = len(argInput)
					}
				}
				return
			}

			if arg.Type == config.FAKE && arg.Name == "filter=" {
				offset = 0
				filterInputs := strings.Split(strings.Replace(argInput, ",", ",|", -1), "|")
				lastFilterInput := lastString(filterInputs)
				for _, key := range apiFound.ResponseKeys {
					if inArray(key, filterInputs) {
						continue
					}
					if strings.HasPrefix(key, lastFilterInput) {
						options = append(options, []rune(key[len(lastFilterInput):]))
						offset = len(lastFilterInput)
					}
				}
				return
			}

			if arg.Type == config.FAKE && arg.Name == "exclude=" {
				offset = 0
				excludeFilterInputs := strings.Split(strings.Replace(argInput, ",", ",|", -1), "|")
				lastExcludeFilterInput := lastString(excludeFilterInputs)
				for _, key := range apiFound.ResponseKeys {
					if inArray(key, excludeFilterInputs) {
						continue
					}
					if strings.HasPrefix(key, lastExcludeFilterInput) {
						options = append(options, []rune(key[len(lastExcludeFilterInput):]))
						offset = len(lastExcludeFilterInput)
					}
				}
				return
			}

			autocompleteAPI := findAutocompleteAPI(arg, apiFound, apiMap)
			if autocompleteAPI == nil {
				return nil, 0
			}

			completeArgs := t.Config.Core.AutoComplete
			autocompleteAPIArgs := []string{}
			argOptions := []argOption{}
			if completeArgs {
				autocompleteAPIArgs = []string{"listall=true"}
				if autocompleteAPI.Noun == "templates" {
					autocompleteAPIArgs = append(autocompleteAPIArgs, "templatefilter=executable")
				}
				if autocompleteAPI.Noun == "isos" {
					autocompleteAPIArgs = append(autocompleteAPIArgs, "isofilter=executable")
				}

				if apiFound.Name != "provisionCertificate" && autocompleteAPI.Name == "listHosts" {
					autocompleteAPIArgs = append(autocompleteAPIArgs, "type=Routing")
				} else if apiFound.Name == "migrateSystemVm" && autocompleteAPI.Name == "listVirtualMachines" {
					autocompleteAPI.Name = "listSystemVms"
				} else if apiFound.Name == "associateIpAddress" {
					autocompleteAPIArgs = append(autocompleteAPIArgs, "state=Free")
				} else if apiFound.Name == "listServiceOfferings" {
					if arg.Name == "id=" && strings.Contains(strings.Join(splitLine, " "), "issystem=true") {
						autocompleteAPIArgs = append(autocompleteAPIArgs, "issystem=true")
					}
				}

				spinner := t.Config.StartSpinner("fetching options, please wait...")
				request := cmd.NewRequest(nil, completer.Config, nil, false)
				response, _ := cmd.NewAPIRequest(request, autocompleteAPI.Name, autocompleteAPIArgs, false)
				t.Config.StopSpinner(spinner)

				hasID := strings.HasSuffix(arg.Name, "id=") || strings.HasSuffix(arg.Name, "ids=") || autocompleteAPI.Name == "listUsageTypes"
				argOptions = buildArgOptions(response, hasID)
			}

			filteredOptions := []argOption{}
			if len(argOptions) > 0 {
				if autocompleteAPI.Name == "listUsageTypes" {
					sort.Slice(argOptions, func(i, j int) bool {
						i, _ = strconv.Atoi(argOptions[i].Value)
						j, _ = strconv.Atoi(argOptions[j].Value)
						return i < j
					})
				} else {
					sort.Slice(argOptions, func(i, j int) bool {
						return argOptions[i].Value < argOptions[j].Value
					})
				}
				for _, item := range argOptions {
					if strings.HasPrefix(item.Value, argInput) || (len(item.Detail) > 0 && strings.HasPrefix(item.Detail, argInput)) {
						filteredOptions = append(filteredOptions, item)
					}
				}
			}
			offset = 0
			if len(filteredOptions) == 0 {
				options = [][]rune{[]rune("")}
			}
			for _, item := range filteredOptions {
				option := item.Value + " "
				if len(filteredOptions) > 0 && len(item.Detail) > 0 {
					option += fmt.Sprintf("(%v)", item.Detail)
				}
				if strings.HasPrefix(option, argInput) {
					options = append(options, []rune(option[len(argInput):]))
					offset = len(argInput)
				} else if len(item.Detail) > 0 && strings.HasPrefix(item.Detail, argInput) {
					options = append(options, []rune(option))
				}
			}
			return
		}
	}

	return options, offset
}
