package v2alpha1

import (
	"context"
	"fmt"

	"github.com/gogo/protobuf/proto"
	"google.golang.org/grpc"

	codectypes "github.com/cosmos/cosmos-sdk/codec/types"
	sdk "github.com/cosmos/cosmos-sdk/types"
	"github.com/cosmos/cosmos-sdk/types/tx"
)

type Config struct {
	SigningModes      map[string]int32
	ChainID           string
	SdkConfig         *sdk.Config
	InterfaceRegistry codectypes.InterfaceRegistry
}

// Register registers the cosmos sdk reflection service
// to the provided *grpc.Server given a Config
func Register(srv *grpc.Server, conf Config) error {
	reflectionServer, err := newReflectionServiceServer(srv, conf)
	if err != nil {
		return err
	}
	RegisterReflectionServiceServer(srv, reflectionServer)
	return nil
}

type reflectionServiceServer struct {
	desc *AppDescriptor
}

func (r reflectionServiceServer) GetAuthnDescriptor(_ context.Context, _ *GetAuthnDescriptorRequest) (*GetAuthnDescriptorResponse, error) {
	return &GetAuthnDescriptorResponse{Authn: r.desc.Authn}, nil
}

func (r reflectionServiceServer) GetChainDescriptor(_ context.Context, _ *GetChainDescriptorRequest) (*GetChainDescriptorResponse, error) {
	return &GetChainDescriptorResponse{Chain: r.desc.Chain}, nil
}

func (r reflectionServiceServer) GetCodecDescriptor(_ context.Context, _ *GetCodecDescriptorRequest) (*GetCodecDescriptorResponse, error) {
	return &GetCodecDescriptorResponse{Codec: r.desc.Codec}, nil
}

func (r reflectionServiceServer) GetConfigurationDescriptor(_ context.Context, _ *GetConfigurationDescriptorRequest) (*GetConfigurationDescriptorResponse, error) {
	return &GetConfigurationDescriptorResponse{Config: r.desc.Configuration}, nil
}

func (r reflectionServiceServer) GetQueryServicesDescriptor(_ context.Context, _ *GetQueryServicesDescriptorRequest) (*GetQueryServicesDescriptorResponse, error) {
	return &GetQueryServicesDescriptorResponse{Queries: r.desc.QueryServices}, nil
}

func (r reflectionServiceServer) GetTxDescriptor(_ context.Context, _ *GetTxDescriptorRequest) (*GetTxDescriptorResponse, error) {
	return &GetTxDescriptorResponse{Tx: r.desc.Tx}, nil
}

func newReflectionServiceServer(grpcSrv *grpc.Server, conf Config) (reflectionServiceServer, error) {
	// set chain descriptor
	chainDescriptor := &ChainDescriptor{Id: conf.ChainID}
	// set configuration descriptor
	configurationDescriptor := &ConfigurationDescriptor{
		Bech32AccountAddressPrefix: conf.SdkConfig.GetBech32AccountAddrPrefix(),
	}
	// set codec descriptor
	codecDescriptor, err := newCodecDescriptor(conf.InterfaceRegistry)
	if err != nil {
		return reflectionServiceServer{}, fmt.Errorf("unable to create codec descriptor: %w", err)
	}
	// set query service descriptor
	queryServiceDescriptor := newQueryServiceDescriptor(grpcSrv)
	// set deliver descriptor
	txDescriptor, err := newTxDescriptor(conf.InterfaceRegistry)
	if err != nil {
		return reflectionServiceServer{}, fmt.Errorf("unable to create deliver descriptor: %w", err)
	}
	authnDescriptor := newAuthnDescriptor(conf.SigningModes)
	desc := &AppDescriptor{
		Authn:         authnDescriptor,
		Chain:         chainDescriptor,
		Codec:         codecDescriptor,
		Configuration: configurationDescriptor,
		QueryServices: queryServiceDescriptor,
		Tx:            txDescriptor,
	}

	ifaceList := make([]string, len(desc.Codec.Interfaces))
	ifaceImplementers := make(map[string][]string, len(desc.Codec.Interfaces))
	for i, iface := range desc.Codec.Interfaces {
		ifaceList[i] = iface.Fullname
		impls := make([]string, len(iface.InterfaceImplementers))
		for j, impl := range iface.InterfaceImplementers {
			impls[j] = impl.TypeUrl
		}
		ifaceImplementers[iface.Fullname] = impls
	}
	return reflectionServiceServer{
		desc: desc,
	}, nil
}

// newCodecDescriptor describes the codec given the codectypes.InterfaceRegistry
func newCodecDescriptor(ir codectypes.InterfaceRegistry) (*CodecDescriptor, error) {
	registeredInterfaces := ir.ListAllInterfaces()
	interfaceDescriptors := make([]*InterfaceDescriptor, len(registeredInterfaces))

	for i, iface := range registeredInterfaces {
		implementers := ir.ListImplementations(iface)
		interfaceImplementers := make([]*InterfaceImplementerDescriptor, len(implementers))
		for j, implementer := range implementers {
			pb, err := ir.Resolve(implementer)
			if err != nil {
				return nil, fmt.Errorf("unable to resolve implementing type %s for interface %s", implementer, iface)
			}
			pbName := proto.MessageName(pb)
			if pbName == "" {
				return nil, fmt.Errorf("unable to get proto name for implementing type %s for interface %s", implementer, iface)
			}
			interfaceImplementers[j] = &InterfaceImplementerDescriptor{
				Fullname: pbName,
				TypeUrl:  implementer,
			}
		}
		interfaceDescriptors[i] = &InterfaceDescriptor{
			Fullname: iface,
			// NOTE(fdymylja): this could be filled, but it won't be filled as of now
			// doing this would require us to fully rebuild in a (dependency) transitive way the proto
			// registry of the supported proto.Messages for the application, this could be easily
			// done if we weren't relying on gogoproto which does not allow us to iterate over the
			// registry. Achieving this right now would mean to start slowly building descriptors
			// getting their files dependencies, building those dependencies then rebuilding the
			// descriptor builder. It's too much work as of now.
			InterfaceAcceptingMessages: nil,
			InterfaceImplementers:      interfaceImplementers,
		}
	}

	return &CodecDescriptor{
		Interfaces: interfaceDescriptors,
	}, nil
}

func newQueryServiceDescriptor(srv *grpc.Server) *QueryServicesDescriptor {
	svcInfo := srv.GetServiceInfo()
	queryServices := make([]*QueryServiceDescriptor, 0, len(svcInfo))
	for name, info := range svcInfo {
		methods := make([]*QueryMethodDescriptor, len(info.Methods))
		for i, svcMethod := range info.Methods {
			methods[i] = &QueryMethodDescriptor{
				Name:          svcMethod.Name,
				FullQueryPath: fmt.Sprintf("/%s/%s", name, svcMethod.Name),
			}
		}
		queryServices = append(queryServices, &QueryServiceDescriptor{
			Fullname: name,
			Methods:  methods,
		})
	}
	return &QueryServicesDescriptor{QueryServices: queryServices}
}

func newTxDescriptor(ir codectypes.InterfaceRegistry) (*TxDescriptor, error) {
	// get base tx type name
	txPbName := proto.MessageName(&tx.Tx{})
	if txPbName == "" {
		return nil, fmt.Errorf("unable to get *tx.Tx protobuf name")
	}
	// get msgs
	svcMsgImplementers := ir.ListImplementations(sdk.ServiceMsgInterfaceProtoName)

	msgsDesc := make([]*MsgDescriptor, 0, len(svcMsgImplementers))

	// process sdk.ServiceMsg
	for _, svcMsg := range svcMsgImplementers {
		resolved, err := ir.Resolve(svcMsg)
		if err != nil {
			return nil, fmt.Errorf("unable to resolve sdk.ServiceMsg %s: %w", svcMsg, err)
		}
		pbName := proto.MessageName(resolved)
		if pbName == "" {
			return nil, fmt.Errorf("unable to get proto name for sdk.ServiceMsg %s", svcMsg)
		}

		msgsDesc = append(msgsDesc, &MsgDescriptor{Msg: &MsgDescriptor_ServiceMsg{
			ServiceMsg: &ServiceMsgDescriptor{
				RequestFullname: pbName,
				RequestRoute:    svcMsg,
				RequestTypeUrl:  svcMsg,
				// NOTE(fdymylja): this cannot be filled as of now, the Configurator is not held inside the *BaseApp type
				// but is local to specific applications, hence we have no way of getting the MsgServer's descriptors
				// which contain response information.
				ResponseFullname: "",
			},
		}})
	}
	return &TxDescriptor{
		Fullname: txPbName,
		Msgs:     msgsDesc,
	}, nil
}

func newAuthnDescriptor(signingModes map[string]int32) *AuthnDescriptor {
	signModesDesc := make([]*SigningModeDescriptor, 0, len(signingModes))
	for i, m := range signingModes {
		signModesDesc = append(signModesDesc, &SigningModeDescriptor{
			Name:   i,
			Number: m,
			// NOTE(fdymylja): this cannot be filled as of now, auth and the sdk itself don't support as of now
			// a service which allows to get authentication metadata for the provided sign mode.
			AuthnInfoProviderMethodFullname: "",
		})
	}
	return &AuthnDescriptor{SignModes: signModesDesc}
}
