package restclient

import (
	"encoding/base64"
	"fmt"
	"net/url"
	"reflect"
	"strings"

	"code.justin.tv/common/alvin/internal/protomsg"
	"github.com/golang/protobuf/jsonpb"
	"github.com/golang/protobuf/proto"
)

// leafFieldValues visits each primitive field reachable from the provided
// message, i.e. including transitive submessages. Each field is converted
// into its string format and added to a url.Values map, using the dot-
// delimited field identifier as a key.
//
// The resulting url.Values map can be used to send query parameters to APIs
// described with google.api.http annotations: any fields not present in the
// URI template or designated for inclusion in the request body will be sent
// in the query portion of the request URI.
//
// The field identifiers are the original names used in the .proto source
// files. It is safe to call on cyclic datastructures.
func leafFieldValues(message proto.Message) (url.Values, error) {
	v := make(url.Values)
	seen := make(map[proto.Message]struct{})
	err := protomsg.WalkStruct(message, func(path string, prop *proto.Properties, fv reflect.Value) error {
		if fv.Kind() == reflect.Ptr {
			fv = fv.Elem()
		}
		if fv.Kind() == reflect.Struct {
			pb, _ := fv.Addr().Interface().(proto.Message)
			if _, ok := seen[pb]; ok || pb == nil {
				return protomsg.ErrSkipStruct
			}
			seen[pb] = struct{}{}

			if str, ok, err := wellKnownStringFormat(pb); err != nil {
				return err
			} else if ok {
				v.Add(path, str)
				return protomsg.ErrSkipStruct
			}
			return nil
		}
		if fv.Kind() == reflect.Invalid {
			// filter out unset slices and unset proto2 fields
			return nil
		}
		if !prop.Repeated {
			// filter out unset proto3 fields
			if fv.Kind() == reflect.Slice || fv.Interface() != reflect.Zero(fv.Type()).Interface() {
				v.Add(path, stringFormat(fv))
			}
			return nil
		}

		if fv.Len() == 0 {
			return nil
		}

		elemType := fv.Type().Elem()

		switch wkt := wellKnownType(reflect.Zero(elemType).Interface()); wkt {
		default:
			return fmt.Errorf("unknown well-known type %q (%s)", wkt, elemType)
		case "":
			if elemType.Kind() == reflect.Ptr && elemType.Elem().Kind() == reflect.Struct {
				return fmt.Errorf("repeated messages are not valid leaf fields")
			}
		case "Any", "ListValue", "Struct", "Value":
			return fmt.Errorf("well-known type %q may not be repeated", wkt)
		case "Duration", "Timestamp",
			"DoubleValue", "FloatValue",
			"Int64Value", "UInt64Value",
			"Int32Value", "UInt32Value",
			"StringValue", "BytesValue", "BoolValue",
			"Empty":
		}

		err := eachRepeat(fv, func(keySuffix, value string) {
			v.Add(path+keySuffix, value)
		})
		if err != nil {
			return err
		}

		return nil
	})
	if err != nil {
		return nil, err
	}
	return v, nil
}

func wellKnownStringFormat(pb proto.Message) (string, bool, error) {
	wkt := wellKnownType(pb)
	switch wkt {
	default:
		return "", false, fmt.Errorf("unknown well-known type %q (%T)", wkt, pb)
	case "", "Empty":
		return "", false, nil
	case "Any", "ListValue", "Struct", "Value":
		return "", false, fmt.Errorf("cannot format well-known type %q as string", wkt)
	case "Duration", "Timestamp",
		"DoubleValue", "FloatValue",
		"Int64Value", "UInt64Value",
		"Int32Value", "UInt32Value",
		"StringValue", "BytesValue", "BoolValue":
	}

	jsonStr, err := (&jsonpb.Marshaler{}).MarshalToString(pb)
	if err != nil {
		return "", false, err
	}

	str := ""
	switch {
	case jsonStr == `true` || jsonStr == `false` || jsonStr == `null`:
		str = jsonStr
		if wkt == "BytesValue" {
			return "", true, nil
		}
	case strings.IndexAny(jsonStr, `"`) == 0:
		str = jsonStr[1 : len(jsonStr)-1]
	case strings.IndexAny(jsonStr, `-0123456789`) == 0:
		str = jsonStr
	}
	if wkt == "BytesValue" {
		// JSON encoding instructions for protobuf "bytes" fields are to
		// base64-encode the value. Since this function is used
		// exclusively to generate URIs (paths and query parameters), use
		// the alternate alphabet that RFC 4648 §5 defines for use with
		// URLs.
		str = strings.NewReplacer(
			"+", "-",
			"/", "_",
			"=", "",
		).Replace(str)
	}
	return str, true, nil
}

func wellKnownType(v interface{}) string {
	type wkt interface {
		XXX_WellKnownType() string
	}
	if v, ok := v.(wkt); ok {
		return v.XXX_WellKnownType()
	}
	return ""
}

func eachRepeat(fv reflect.Value, each func(keySuffix, value string)) error {
	switch fv.Kind() {
	default:
		return fmt.Errorf("unhandled kind %q", fv.Kind())
	case reflect.Slice:
		for i := 0; i < fv.Len(); i++ {
			each("", stringFormat(fv.Index(i)))
		}
	case reflect.Map:
		// It's not clear how maps should be represented. At the very least
		// there are good arguments for more than one key format: "field.key"
		// and "field[key]". Until support is required and until it's clear
		// exactly how that support should look, do not allow maps in fields
		// that are formatted as URI query parameters.
		return fmt.Errorf("map fields are not supported")
	}
	return nil
}

func stringFormat(val reflect.Value) string {
	if !val.IsValid() {
		return ""
	}

	pb, _ := val.Interface().(proto.Message)
	if str, ok, err := wellKnownStringFormat(pb); err != nil {
		return ""
	} else if ok {
		return str
	}

	switch v := val.Interface(); v := v.(type) {
	default:
		return fmt.Sprint(v)
	case []byte:
		// JSON encoding instructions for protobuf "bytes" fields are to
		// base64-encode the value. Since this function is used exclusively to
		// generate URIs (paths and query parameters), use the alternate
		// alphabet that RFC 4648 §5 defines for use with URLs.
		return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(v)
	}
}

func findRepeatedProtoField(pb proto.Message) (*proto.Properties, error) {
	var prop *proto.Properties

	err := protomsg.WalkStruct(pb, func(path string, p *proto.Properties, val reflect.Value) error {
		if prop != nil {
			return fmt.Errorf("message %q used as a list holder, but has extra field %q",
				proto.MessageName(pb), path)
		}
		if !p.Repeated {
			return fmt.Errorf("message %q used as a list holder, but has non-repeated field %q",
				proto.MessageName(pb), path)
		}
		prop = p
		return nil
	})
	if err != nil {
		return nil, err
	}
	if prop == nil {
		return nil, fmt.Errorf("message %q used as a list holder, but it has no usable fields",
			proto.MessageName(pb))
	}

	return prop, nil
}

func getListAppender(pb proto.Message, prop *proto.Properties) (func() proto.Message, error) {
	mv := reflect.ValueOf(pb)
	if mv.Kind() == reflect.Ptr {
		mv = mv.Elem()
	}
	if mv.Kind() != reflect.Struct {
		return nil, fmt.Errorf("message %q used as a list holder, but is not a struct",
			proto.MessageName(pb))
	}

	fv := mv.FieldByName(prop.Name)
	if fv.Kind() != reflect.Slice {
		return nil, fmt.Errorf("message %q used as a list holder, but field %q is not a slice",
			proto.MessageName(pb), prop.Name)
	}

	elem := fv.Type().Elem()

	protoMessageType := reflect.ValueOf((func(proto.Message))(nil)).Type().In(0)
	if !elem.Implements(protoMessageType) {
		return nil, fmt.Errorf("message %q used as a list holder, but elements of field %q don't implement proto.Message",
			proto.MessageName(pb), prop.Name)
	}

	if elem.Kind() == reflect.Ptr {
		elem = elem.Elem()
	}

	next := func() proto.Message {
		holder := reflect.New(elem)
		fv.Set(reflect.Append(fv, holder))
		return holder.Interface().(proto.Message)
	}

	return next, nil
}
