package grpctool

import (
	"context"
	"errors"
	"fmt"

	"buf.build/go/protovalidate"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/proto"
)

const (
	clientValidationError = "invalid server response"
)

// UnaryClientValidatingInterceptor is a unary client interceptor that performs response validation.
func UnaryClientValidatingInterceptor(v protovalidate.Validator) grpc.UnaryClientInterceptor {
	return func(parentCtx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
		err := invoker(parentCtx, method, req, reply, cc, opts...)
		if err != nil {
			return err
		}
		return maybeValidate(v, reply, clientValidationError)
	}
}

// StreamClientValidatingInterceptor is a stream client interceptor that performs response stream validation.
func StreamClientValidatingInterceptor(v protovalidate.Validator) grpc.StreamClientInterceptor {
	return func(parentCtx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
		stream, err := streamer(parentCtx, desc, cc, method, opts...)
		if err != nil {
			return nil, err
		}
		return &clientRecvValidator{
			ClientStream: stream,
			validator:    v,
		}, nil
	}
}

type clientRecvValidator struct {
	grpc.ClientStream
	validator protovalidate.Validator
}

func (w *clientRecvValidator) RecvMsg(m any) error {
	if err := w.ClientStream.RecvMsg(m); err != nil {
		return err
	}
	return maybeValidate(w.validator, m, clientValidationError)
}

func maybeValidate(v protovalidate.Validator, msg any, errMsg string) error {
	m, ok := msg.(proto.Message)
	if !ok {
		return nil
	}
	err := v.Validate(m)
	if err == nil {
		return nil
	}
	errStatus := status.New(codes.InvalidArgument, fmt.Sprintf("%s: %v", errMsg, err))
	var vErr *protovalidate.ValidationError
	if errors.As(err, &vErr) {
		errStatusWithDetails, withDetailsErr := errStatus.WithDetails(vErr.ToProto())
		if withDetailsErr == nil {
			errStatus = errStatusWithDetails
		}
	}
	return errStatus.Err()
}
