package mcp

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	reflectionpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/reflect/protodesc"
	"google.golang.org/protobuf/reflect/protoreflect"
	"google.golang.org/protobuf/reflect/protoregistry"
	"google.golang.org/protobuf/types/descriptorpb"
	"google.golang.org/protobuf/types/dynamicpb"
)

// ReflectedGRPCTarget describes an external gRPC server whose reflection
// catalog should be exposed as MCP tools. It is intentionally opt-in: teams can
// bridge existing reflected gRPC services without changing their servers or
// registering them in go-micro.
type ReflectedGRPCTarget struct {
	// Name prefixes generated tools. When empty, Address is sanitized and used.
	Name string
	// Address is the host:port of the reflected gRPC server.
	Address string
	// DialOptions customize the connection. If none are supplied, an insecure
	// transport is used for local/dev interoperability.
	DialOptions []grpc.DialOption
	// Timeout bounds reflection discovery and individual tool calls.
	Timeout time.Duration
}

func (s *Server) discoverReflectedGRPC() error {
	for _, target := range s.opts.ReflectedGRPCTargets {
		if strings.TrimSpace(target.Address) == "" {
			continue
		}
		tools, err := s.reflectedGRPCTools(target)
		if err != nil {
			return err
		}
		for _, tool := range tools {
			s.tools[tool.Name] = tool
		}
	}
	return nil
}

func (s *Server) reflectedGRPCTools(target ReflectedGRPCTarget) ([]*Tool, error) {
	timeout := target.Timeout
	if timeout == 0 {
		timeout = 10 * time.Second
	}
	ctx, cancel := context.WithTimeout(s.opts.Context, timeout)
	defer cancel()

	dialOpts := target.DialOptions
	if len(dialOpts) == 0 {
		dialOpts = []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
	}
	conn, err := grpc.NewClient(target.Address, dialOpts...)
	if err != nil {
		return nil, fmt.Errorf("connect reflected grpc target %s: %w", target.Address, err)
	}
	defer conn.Close()

	files, services, err := loadReflectedFiles(ctx, conn)
	if err != nil {
		return nil, fmt.Errorf("reflect grpc target %s: %w", target.Address, err)
	}

	prefix := target.Name
	if prefix == "" {
		prefix = sanitizeToolPart(target.Address)
	}

	var out []*Tool
	for _, serviceName := range services {
		desc, err := files.FindDescriptorByName(protoreflect.FullName(serviceName))
		if err != nil {
			continue
		}
		svc, ok := desc.(protoreflect.ServiceDescriptor)
		if !ok {
			continue
		}
		for i := 0; i < svc.Methods().Len(); i++ {
			method := svc.Methods().Get(i)
			if method.IsStreamingClient() || method.IsStreamingServer() {
				continue
			}
			fullMethod := "/" + string(svc.FullName()) + "/" + string(method.Name())
			toolName := prefix + "." + strings.ReplaceAll(string(svc.FullName()), ".", "_") + "." + string(method.Name())
			input := method.Input()
			out = append(out, &Tool{
				Name:        toolName,
				Description: fmt.Sprintf("Call reflected gRPC method %s on %s", fullMethod, target.Address),
				InputSchema: protoMessageSchema(input),
				Handler:     reflectedGRPCHandler(target, fullMethod, input, method.Output()),
			})
		}
	}
	return out, nil
}

func loadReflectedFiles(ctx context.Context, conn *grpc.ClientConn) (*protoregistryFiles, []string, error) {
	client := reflectionpb.NewServerReflectionClient(conn)
	stream, err := client.ServerReflectionInfo(ctx)
	if err != nil {
		return nil, nil, err
	}
	if err := stream.Send(&reflectionpb.ServerReflectionRequest{MessageRequest: &reflectionpb.ServerReflectionRequest_ListServices{ListServices: ""}}); err != nil {
		return nil, nil, err
	}
	resp, err := stream.Recv()
	if err != nil {
		return nil, nil, err
	}
	list := resp.GetListServicesResponse()
	if list == nil {
		return nil, nil, fmt.Errorf("reflection list services returned %T", resp.MessageResponse)
	}

	set := &descriptorpb.FileDescriptorSet{}
	seen := map[string]bool{}
	var services []string
	for _, svc := range list.Service {
		name := svc.Name
		if strings.HasPrefix(name, "grpc.reflection.") {
			continue
		}
		services = append(services, name)
		if err := requestFileContainingSymbol(ctx, client, name, set, seen); err != nil {
			return nil, nil, err
		}
	}
	files, err := newProtoregistryFiles(set)
	if err != nil {
		return nil, nil, err
	}
	return files, services, nil
}

func requestFileContainingSymbol(ctx context.Context, client reflectionpb.ServerReflectionClient, symbol string, set *descriptorpb.FileDescriptorSet, seen map[string]bool) error {
	stream, err := client.ServerReflectionInfo(ctx)
	if err != nil {
		return err
	}
	if err := stream.Send(&reflectionpb.ServerReflectionRequest{MessageRequest: &reflectionpb.ServerReflectionRequest_FileContainingSymbol{FileContainingSymbol: symbol}}); err != nil {
		return err
	}
	resp, err := stream.Recv()
	if err != nil {
		return err
	}
	fd := resp.GetFileDescriptorResponse()
	if fd == nil {
		return fmt.Errorf("reflection lookup for %s returned %T", symbol, resp.MessageResponse)
	}
	for _, raw := range fd.FileDescriptorProto {
		var file descriptorpb.FileDescriptorProto
		if err := proto.Unmarshal(raw, &file); err != nil {
			return err
		}
		name := file.GetName()
		if !seen[name] {
			seen[name] = true
			set.File = append(set.File, &file)
		}
	}
	return nil
}

// protoregistryFiles is a narrow wrapper that keeps imports local to this file.
type protoregistryFiles struct{ files *protoregistry.Files }

func newProtoregistryFiles(set *descriptorpb.FileDescriptorSet) (*protoregistryFiles, error) {
	files, err := protodesc.NewFiles(set)
	if err != nil {
		return nil, err
	}
	return &protoregistryFiles{files: files}, nil
}

func (p *protoregistryFiles) FindDescriptorByName(name protoreflect.FullName) (protoreflect.Descriptor, error) {
	return p.files.FindDescriptorByName(name)
}

func reflectedGRPCHandler(target ReflectedGRPCTarget, fullMethod string, input, output protoreflect.MessageDescriptor) func(map[string]interface{}) (interface{}, error) {
	return func(args map[string]interface{}) (interface{}, error) {
		timeout := target.Timeout
		if timeout == 0 {
			timeout = 10 * time.Second
		}
		ctx, cancel := context.WithTimeout(context.Background(), timeout)
		defer cancel()
		dialOpts := target.DialOptions
		if len(dialOpts) == 0 {
			dialOpts = []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
		}
		conn, err := grpc.NewClient(target.Address, dialOpts...)
		if err != nil {
			return nil, err
		}
		defer conn.Close()

		req := dynamicpb.NewMessage(input)
		raw, err := json.Marshal(args)
		if err != nil {
			return nil, err
		}
		if err := protojson.Unmarshal(raw, req); err != nil {
			return nil, err
		}
		rsp := dynamicpb.NewMessage(output)
		if err := conn.Invoke(ctx, fullMethod, req, rsp); err != nil {
			return nil, err
		}
		b, err := protojson.MarshalOptions{UseProtoNames: true, EmitUnpopulated: true}.Marshal(rsp)
		if err != nil {
			return nil, err
		}
		var out interface{}
		if err := json.Unmarshal(b, &out); err != nil {
			return nil, err
		}
		return out, nil
	}
}

func protoMessageSchema(msg protoreflect.MessageDescriptor) map[string]interface{} {
	schema := map[string]interface{}{"type": "object", "properties": map[string]interface{}{}}
	props := schema["properties"].(map[string]interface{})
	fields := msg.Fields()
	for i := 0; i < fields.Len(); i++ {
		field := fields.Get(i)
		props[field.JSONName()] = protoFieldSchema(field)
	}
	return schema
}

func protoFieldSchema(field protoreflect.FieldDescriptor) map[string]interface{} {
	schema := map[string]interface{}{"type": protoJSONType(field)}
	if field.IsList() {
		schema["items"] = map[string]interface{}{"type": protoJSONType(field)}
	}
	if field.Kind() == protoreflect.MessageKind || field.Kind() == protoreflect.GroupKind {
		schema = protoMessageSchema(field.Message())
	}
	return schema
}

func protoJSONType(field protoreflect.FieldDescriptor) string {
	if field.IsList() {
		return "array"
	}
	switch field.Kind() {
	case protoreflect.BoolKind:
		return "boolean"
	case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind,
		protoreflect.Uint32Kind, protoreflect.Fixed32Kind, protoreflect.Int64Kind,
		protoreflect.Sint64Kind, protoreflect.Sfixed64Kind, protoreflect.Uint64Kind,
		protoreflect.Fixed64Kind:
		return "integer"
	case protoreflect.FloatKind, protoreflect.DoubleKind:
		return "number"
	case protoreflect.MessageKind, protoreflect.GroupKind:
		return "object"
	default:
		return "string"
	}
}

func sanitizeToolPart(s string) string {
	r := strings.NewReplacer(":", "_", "/", "_", ".", "_", "-", "_")
	return r.Replace(s)
}
