package util

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"math"
	"os"
	"sort"
	"strconv"
	"strings"
	"unicode"

	"github.com/stretchr/objx"
	"github.com/urfave/cli/v2"
	"go-micro.dev/v5/client"
	"go-micro.dev/v5/metadata"
	"go-micro.dev/v5/registry"
)

// AddMetadataToContext parses metadata strings in the format "Key:Value" and adds them to the context
func AddMetadataToContext(ctx context.Context, metadataStrings []string) context.Context {
	if len(metadataStrings) == 0 {
		return ctx
	}

	md := make(metadata.Metadata)
	for _, m := range metadataStrings {
		parts := strings.SplitN(m, ":", 2)
		if len(parts) != 2 {
			continue
		}
		key := strings.TrimSpace(parts[0])
		value := strings.TrimSpace(parts[1])
		md[key] = value
	}

	return metadata.MergeContext(ctx, md, true)
}

// LookupService queries the service for a service with the given alias. If
// no services are found for a given alias, the registry will return nil and
// the error will also be nil. An error is only returned if there was an issue
// listing from the registry.
func LookupService(name string) (*registry.Service, error) {
	// return a lookup in the default domain as a catch all
	return serviceWithName(name)
}

// FormatServiceUsage returns a string containing the service usage.
func FormatServiceUsage(srv *registry.Service, c *cli.Context) string {
	alias := c.Args().First()
	subcommand := c.Args().Get(1)

	commands := make([]string, len(srv.Endpoints))
	endpoints := make([]*registry.Endpoint, len(srv.Endpoints))
	for i, e := range srv.Endpoints {
		// map "Helloworld.Call" to "helloworld.call"
		parts := strings.Split(e.Name, ".")
		for i, part := range parts {
			parts[i] = lowercaseInitial(part)
		}
		name := strings.Join(parts, ".")

		// remove the prefix if it is the service name, e.g. rather than
		// "micro run helloworld helloworld call", it would be
		// "micro run helloworld call".
		name = strings.TrimPrefix(name, alias+".")

		// instead of "micro run helloworld foo.bar", the command should
		// be "micro run helloworld foo bar".
		commands[i] = strings.Replace(name, ".", " ", 1)
		endpoints[i] = e
	}

	result := ""
	if len(subcommand) > 0 && subcommand != "--help" {
		result += fmt.Sprintf("NAME:\n\tmicro %v %v\n\n", alias, subcommand)
		result += fmt.Sprintf("USAGE:\n\tmicro %v %v [flags]\n\n", alias, subcommand)
		result += fmt.Sprintf("FLAGS:\n")

		for i, command := range commands {
			if command == subcommand {
				result += renderFlags(endpoints[i])
			}
		}
	} else {
		// sort the command names alphabetically
		sort.Strings(commands)

		result += fmt.Sprintf("NAME:\n\tmicro %v\n\n", alias)
		result += fmt.Sprintf("VERSION:\n\t%v\n\n", srv.Version)
		result += fmt.Sprintf("USAGE:\n\tmicro %v [command]\n\n", alias)
		result += fmt.Sprintf("COMMANDS:\n\t%v\n", strings.Join(commands, "\n\t"))

	}

	return result
}

func lowercaseInitial(str string) string {
	for i, v := range str {
		return string(unicode.ToLower(v)) + str[i+1:]
	}
	return ""
}

func renderFlags(endpoint *registry.Endpoint) string {
	ret := ""
	for _, value := range endpoint.Request.Values {
		ret += renderValue([]string{}, value) + "\n"
	}
	return ret
}

func renderValue(path []string, value *registry.Value) string {
	if len(value.Values) > 0 {
		renders := []string{}
		for _, v := range value.Values {
			renders = append(renders, renderValue(append(path, value.Name), v))
		}
		return strings.Join(renders, "\n")
	}
	return fmt.Sprintf("\t--%v %v", strings.Join(append(path, value.Name), "_"), value.Type)
}

// CallService will call a service using the arguments and flags provided
// in the context. It will print the result or error to stdout. If there
// was an error performing the call, it will be returned.
func CallService(srv *registry.Service, args []string) error {
	// parse the flags and args
	args, flags, err := splitCmdArgs(args)
	if err != nil {
		return err
	}

	// construct the endpoint
	endpoint, err := constructEndpoint(args)
	if err != nil {
		return err
	}

	// ensure the endpoint exists on the service
	var ep *registry.Endpoint
	for _, e := range srv.Endpoints {
		if e.Name == endpoint {
			ep = e
			break
		}
	}
	if ep == nil {
		return fmt.Errorf("Endpoint %v not found for service %v", endpoint, srv.Name)
	}

	// create a context for the call
	callCtx := context.TODO()

	// parse out --header or --metadata flags before parsing request body
	// Note: This is for dynamic service calls (e.g., 'micro helloworld call --header X:Y').
	// Direct 'micro call' commands are handled in cli.go.
	if headerFlags, ok := flags["header"]; ok {
		callCtx = AddMetadataToContext(callCtx, headerFlags)
		delete(flags, "header")
	}
	if metadataFlags, ok := flags["metadata"]; ok {
		callCtx = AddMetadataToContext(callCtx, metadataFlags)
		delete(flags, "metadata")
	}

	// parse the flags into request body
	body, err := FlagsToRequest(flags, ep.Request)
	if err != nil {
		return err
	}

	// construct and execute the request using the json content type
	req := client.DefaultClient.NewRequest(srv.Name, endpoint, body, client.WithContentType("application/json"))
	var rsp json.RawMessage

	if err := client.DefaultClient.Call(callCtx, req, &rsp); err != nil {
		return err
	}

	// format the response
	var out bytes.Buffer
	defer out.Reset()
	if err := json.Indent(&out, rsp, "", "\t"); err != nil {
		return err
	}
	out.Write([]byte("\n"))
	out.WriteTo(os.Stdout)

	return nil
}

// splitCmdArgs takes a cli context and parses out the args and flags, for
// example "micro helloworld --name=foo call apple" would result in "call",
// "apple" as args and {"name":"foo"} as the flags.
func splitCmdArgs(arguments []string) ([]string, map[string][]string, error) {
	args := []string{}
	flags := map[string][]string{}

	prev := ""
	for _, a := range arguments {
		if !strings.HasPrefix(a, "--") {
			if len(prev) == 0 {
				args = append(args, a)
				continue
			}
			_, exists := flags[prev]
			if !exists {
				flags[prev] = []string{}
			}

			flags[prev] = append(flags[prev], a)
			prev = ""
			continue
		}

		// comps would be "foo", "bar" for "--foo=bar"
		comps := strings.Split(strings.TrimPrefix(a, "--"), "=")
		_, exists := flags[comps[0]]
		if !exists {
			flags[comps[0]] = []string{}
		}
		switch len(comps) {
		case 1:
			prev = comps[0]
		case 2:
			flags[comps[0]] = append(flags[comps[0]], comps[1])
		default:
			return nil, nil, fmt.Errorf("Invalid flag: %v. Expected format: --foo=bar", a)
		}
	}

	return args, flags, nil
}

// constructEndpoint takes a slice of args and converts it into a valid endpoint
// such as Helloworld.Call or Foo.Bar, it will return an error if an invalid number
// of arguments were provided
func constructEndpoint(args []string) (string, error) {
	var epComps []string
	switch len(args) {
	case 1:
		epComps = append(args, "call")
	case 2:
		epComps = args
	case 3:
		epComps = args[1:3]
	default:
		return "", fmt.Errorf("Incorrect number of arguments")
	}

	// transform the endpoint components, e.g ["helloworld", "call"] to the
	// endpoint name: "Helloworld.Call".
	return fmt.Sprintf("%v.%v", strings.Title(epComps[0]), strings.Title(epComps[1])), nil
}

// ShouldRenderHelp returns true if the help flag was passed
func ShouldRenderHelp(args []string) bool {
	args, flags, _ := splitCmdArgs(args)

	// only 1 arg e.g micro helloworld
	if len(args) == 1 {
		return true
	}

	for key := range flags {
		if key == "help" {
			return true
		}
	}

	return false
}

// FlagsToRequest parses a set of flags, e.g {name:"Foo", "options_surname","Bar"} and
// converts it into a request body. If the key is not a valid object in the request, an
// error will be returned.
//
// This function constructs []interface{} slices
// as opposed to typed ([]string etc) slices for easier testing
func FlagsToRequest(flags map[string][]string, req *registry.Value) (map[string]interface{}, error) {
	coerceValue := func(valueType string, value []string) (interface{}, error) {
		switch valueType {
		case "bool":
			if len(value) == 0 || len(strings.TrimSpace(value[0])) == 0 {
				return true, nil
			}
			return strconv.ParseBool(value[0])
		case "int32":
			i, err := strconv.Atoi(value[0])
			if err != nil {
				return nil, err
			}
			if i < math.MinInt32 || i > math.MaxInt32 {
				return nil, fmt.Errorf("value out of range for int32: %d", i)
			}
			return int32(i), nil
		case "int64":
			return strconv.ParseInt(value[0], 0, 64)
		case "float64":
			return strconv.ParseFloat(value[0], 64)
		case "[]bool":
			// length is one if it's a `,` separated int slice
			if len(value) == 1 {
				value = strings.Split(value[0], ",")
			}
			ret := []interface{}{}
			for _, v := range value {
				i, err := strconv.ParseBool(v)
				if err != nil {
					return nil, err
				}
				ret = append(ret, i)
			}
			return ret, nil
		case "[]int32":
			// length is one if it's a `,` separated int slice
			if len(value) == 1 {
				value = strings.Split(value[0], ",")
			}
			ret := []interface{}{}
			for _, v := range value {
				i, err := strconv.Atoi(v)
				if err != nil {
					return nil, err
				}
				if i < math.MinInt32 || i > math.MaxInt32 {
					return nil, fmt.Errorf("value out of range for int32: %d", i)
				}
				ret = append(ret, int32(i))
			}
			return ret, nil
		case "[]int64":
			// length is one if it's a `,` separated int slice
			if len(value) == 1 {
				value = strings.Split(value[0], ",")
			}
			ret := []interface{}{}
			for _, v := range value {
				i, err := strconv.ParseInt(v, 0, 64)
				if err != nil {
					return nil, err
				}
				ret = append(ret, i)
			}
			return ret, nil
		case "[]float64":
			// length is one if it's a `,` separated float slice
			if len(value) == 1 {
				value = strings.Split(value[0], ",")
			}
			ret := []interface{}{}
			for _, v := range value {
				i, err := strconv.ParseFloat(v, 64)
				if err != nil {
					return nil, err
				}
				ret = append(ret, i)
			}
			return ret, nil
		case "[]string":
			// length is one it's a `,` separated string slice
			if len(value) == 1 {
				value = strings.Split(value[0], ",")
			}
			ret := []interface{}{}
			for _, v := range value {
				ret = append(ret, v)
			}
			return ret, nil
		case "string":
			return value[0], nil
		case "map[string]string":
			var val map[string]string
			if err := json.Unmarshal([]byte(value[0]), &val); err != nil {
				return value[0], nil
			}
			return val, nil
		default:
			return value, nil
		}
		return nil, nil
	}

	result := objx.MustFromJSON("{}")

	var flagType func(key string, values []*registry.Value, path ...string) (string, bool)

	flagType = func(key string, values []*registry.Value, path ...string) (string, bool) {
		for _, attr := range values {
			if strings.Join(append(path, attr.Name), "-") == key {
				return attr.Type, true
			}
			if attr.Values != nil {
				typ, found := flagType(key, attr.Values, append(path, attr.Name)...)
				if found {
					return typ, found
				}
			}
		}
		return "", false
	}

	for key, value := range flags {
		ty, found := flagType(key, req.Values)
		if !found {
			return nil, fmt.Errorf("Unknown flag: %v", key)
		}
		parsed, err := coerceValue(ty, value)
		if err != nil {
			return nil, err
		}
		// objx.Set does not create the path,
		// so we do that here
		if strings.Contains(key, "-") {
			parts := strings.Split(key, "-")
			for i := range parts {
				pToCreate := strings.Join(parts[0:i], ".")
				if i > 0 && i < len(parts) && !result.Has(pToCreate) {
					result.Set(pToCreate, map[string]interface{}{})
				}
			}
		}
		path := strings.Replace(key, "-", ".", -1)
		result.Set(path, parsed)
	}

	return result, nil
}

// find a service in a domain matching the name
func serviceWithName(name string) (*registry.Service, error) {
	srvs, err := registry.GetService(name)
	if err == registry.ErrNotFound {
		return nil, nil
	} else if err != nil {
		return nil, err
	}
	if len(srvs) == 0 {
		return nil, nil
	}
	return srvs[0], nil
}
