package runner_controller //nolint:staticcheck

import (
	"context"
	"fmt"
	"io"

	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/api"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/runner_controller/rpc"
	"google.golang.org/grpc"
)

type RunnerControllerClient struct {
	agentConnPool func(agentKey api.AgentKey) grpc.ClientConnInterface
}

func NewRunnerControllerClient(agentConnPool func(agentKey api.AgentKey) grpc.ClientConnInterface) *RunnerControllerClient {
	return &RunnerControllerClient{
		agentConnPool: agentConnPool,
	}
}

// AdmitJob sends an AdmitJob RPC request to a matching agent.
//
// This method uses the simplified tunneling approach.
// A Runner Controller client for that agent key must have an open
// AdmitJob RPC call to any KAS. The req is routed through a gateway KAS (potentially itself)
// to that client.
// This method must send the RPC messages in reversed order - meaning that the actually
// defined RPC "resposne" becomes this Runner Controller clients request and the vice versa.
func (c *RunnerControllerClient) AdmitJob(ctx context.Context, agentKey api.AgentKey, req *rpc.AdmitJobRequest) (*rpc.AdmitJobResponse, error) {
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	client := rpc.NewRunnerControllerServiceClient(c.agentConnPool(agentKey))
	stream, err := client.AdmitJob(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to create stream for runner controller service client: %w", err)
	}

	err = stream.SendMsg(req)
	if err != nil {
		return nil, fmt.Errorf("failed to send request to client: %w", err)
	}

	// We can immediately send the close frame for the sending end after sending
	// the only message we ever intend to send.
	// NOTE: this method actually never returns an error.
	_ = stream.CloseSend()

	var resp rpc.AdmitJobResponse
	err = stream.RecvMsg(&resp)
	if err != nil {
		return nil, fmt.Errorf("failed to retrieve response from client: %w", err)
	}

	maybeUnexpectedMsg, err := stream.Recv()
	switch err {
	case io.EOF: //nolint:errorlint
	// we are expecting an EOF at this point, no more messages are expected.
	case nil:
		// strange, we didn't expect more messages
		return nil, fmt.Errorf("received unexpected message %T after receiving expected %T", maybeUnexpectedMsg, &resp)
	default:
		// we got an actual error from the client
		return nil, fmt.Errorf("got error while awaiting closing EOF: %w", err)
	}

	return &resp, nil
}
