package recon

import (
	"fmt"
	"sort"
	"strings"

	"code.justin.tv/eventbus/schema/cmd/internal/util"
	"code.justin.tv/eventbus/schema/pkg/eventbus/owner"
	"github.com/golang/protobuf/proto"
	descriptor "github.com/golang/protobuf/protoc-gen-go/descriptor"
	pbplugin "github.com/golang/protobuf/protoc-gen-go/plugin"
	"github.com/pkg/errors"
)

type Lookup struct {
	TypeNamesFull map[string]*MessageType
	Packages      map[string]*Package
	Files         map[string]*ProtoFile
}

func (l *Lookup) EnsurePackage(pkgName string) (pkg *Package) {
	if l.Packages[pkgName] == nil {
		pkg = &Package{
			Name:         pkgName,
			MessageTypes: make(map[string]*MessageType),
		}
		l.Packages[pkgName] = pkg
	} else {
		pkg = l.Packages[pkgName]
	}
	return pkg
}

type Package struct {
	Name         string
	MessageTypes map[string]*MessageType
}

type MessageType struct {
	*descriptor.DescriptorProto
	protoPackage *Package
	file         *ProtoFile
}

type EncryptedField struct {
	EventType   string
	MessageName string
	FieldName   string
	FieldType   string
	GoType      string
	ZeroValue   string
}

func (m MessageType) ShortName() string {
	return m.GetName()
}
func (m MessageType) FullName() string {
	return m.protoPackage.Name + "." + m.ShortName()
}

type ProtoFile struct {
	*descriptor.FileDescriptorProto
	MessageTypes map[string]*MessageType
}

func (pf *ProtoFile) ReplaceName(replacement string) string {
	return strings.Replace(pf.GetName(), ".proto", replacement, -1)
}

func Recon(req *pbplugin.CodeGeneratorRequest) *Lookup {
	lookup := &Lookup{
		TypeNamesFull: make(map[string]*MessageType),
		Packages:      make(map[string]*Package),
		Files:         make(map[string]*ProtoFile),
	}

	for _, fdesc := range req.ProtoFile {
		f := &ProtoFile{
			FileDescriptorProto: fdesc,
			MessageTypes:        make(map[string]*MessageType),
		}
		lookup.Files[fdesc.GetName()] = f
		pkgName := fdesc.GetPackage()
		protoPackage := lookup.EnsurePackage(pkgName)

		for _, msgdesc := range fdesc.MessageType {
			mt := &MessageType{
				DescriptorProto: msgdesc,
				protoPackage:    protoPackage,
				file:            f,
			}
			lookup.TypeNamesFull[mt.FullName()] = mt
			f.MessageTypes[mt.FullName()] = mt
			protoPackage.MessageTypes[mt.ShortName()] = mt
		}
	}
	return lookup
}

type byAlphaOrder []EncryptedField

func (s byAlphaOrder) Len() int {
	return len(s)
}
func (s byAlphaOrder) Swap(i, j int) {
	s[i], s[j] = s[j], s[i]
}
func (s byAlphaOrder) Less(i, j int) bool {
	k1 := fmt.Sprintf("%s|%s|%s", s[i].EventType, s[i].MessageName, s[i].FieldName)
	k2 := fmt.Sprintf("%s|%s|%s", s[j].EventType, s[j].MessageName, s[j].FieldName)
	return sort.StringsAreSorted([]string{k1, k2})
}

func GetEncryptedFields(eventType string, f *ProtoFile) []EncryptedField {
	encryptedFields := []EncryptedField{}
	for _, msg := range f.MessageTypes {
		msgName := msg.GetName()
		for _, field := range msg.GetField() {
			if util.IsAuthorizedField(field) {
				encryptedFields = append(encryptedFields, EncryptedField{
					EventType:   eventType,
					FieldName:   util.SnakeToUpperCamel(field.GetName()),
					FieldType:   field.GetTypeName(),
					MessageName: msgName,
					GoType:      util.AuthorizedFieldTypeToGoType(field.GetTypeName()),
					ZeroValue:   util.AuthorizedFieldTypeToZeroValue(field.GetTypeName()),
				})
			}
		}
	}
	sort.Sort(byAlphaOrder(encryptedFields))
	return encryptedFields
}

func GetOwnerLDAPGroup(d *descriptor.DescriptorProto) (string, error) {
	opts := d.GetOptions()
	if opts == nil {
		return "", nil
	}

	i, err := proto.GetExtension(opts, owner.E_LdapGroup)
	if err != nil {
		return "", errors.Wrap(err, "could not get options from proto message")
	}

	ldapGroup, ok := i.(*string)
	if !ok || ldapGroup == nil {
		return "", nil
	} else if !ok {
		return "", fmt.Errorf("found unexpected type for eventbus.owner.ldap_group option for event '%s'", d.GetName())
	}

	if !strings.HasPrefix(*ldapGroup, "team-") {
		return "", fmt.Errorf("eventbus.owner.ldap_group for event '%s' must be prefixed with 'team-'", d.GetName())
	}

	return *ldapGroup, nil
}
