package grpctool

import (
	"context"
	"fmt"
	"io"
	"net/http"

	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/httpz"
	"google.golang.org/protobuf/proto"
)

var (
	_ http.RoundTripper = (*HTTPRoundTripperToOutboundGRPC)(nil)
)

type HTTPRoundTripperToOutboundGRPC struct {
	HTTP2GRPC   HTTPToOutboundGRPC
	NewClient   func(context.Context) (HTTPRequestClient, error)
	HeaderExtra proto.Message
}

func (h *HTTPRoundTripperToOutboundGRPC) RoundTrip(r *http.Request) (*http.Response, error) {
	// 0. Construct the outbound client
	success := false
	ctx, cancel := context.WithCancel(r.Context())
	defer func() {
		if !success {
			cancel()
		}
	}()
	outboundClient, err := h.NewClient(ctx)
	if err != nil {
		return nil, fmt.Errorf("new outbound client: %w", err)
	}

	// 1. Pipe client -> remote
	eResp := h.HTTP2GRPC.PipeInboundToOutbound(outboundClient, r, h.HeaderExtra)
	if eResp != nil {
		return nil, eResp2error(eResp)
	}
	h.HTTP2GRPC.SendCloseSend(outboundClient)

	// 2. Pipe remote -> client
	rw := newRW2resp(r)
	go h.pipeOutboundToInbound(outboundClient, rw)
	select {
	case resp := <-rw.respCh:
		success = true
		return resp, nil
	case err = <-rw.errCh:
		return nil, err
	}
}

func (h *HTTPRoundTripperToOutboundGRPC) pipeOutboundToInbound(outboundClient HTTPRequestClient, w *rw2resp) {
	eResp := h.HTTP2GRPC.PipeOutboundToInbound(outboundClient, w)
	if eResp == nil {
		_ = w.pw.Close() // clean close
	} else {
		err := eResp2error(eResp)
		if w.headerSent {
			// we've returned the http.Response already. Propagate the error via its Body.Read().
			_ = w.pw.CloseWithError(err)
		} else {
			// We haven't returned the http.Response yet. Propagate the error via directly returning it from RoundTrip().
			w.errCh <- err
		}
	}
}

func eResp2error(eResp *ErrResp) error {
	if eResp.Err == nil {
		return fmt.Errorf("%d: %s", eResp.StatusCode, eResp.Msg)
	}
	return fmt.Errorf("%d: %s: %w", eResp.StatusCode, eResp.Msg, eResp.Err)
}

var (
	_ ResponseWriter = (*rw2resp)(nil)
)

// rw2resp is a ResponseWriter -> http.Response adapter.
type rw2resp struct {
	pw         *io.PipeWriter
	pr         io.ReadCloser
	respCh     chan *http.Response
	errCh      chan error
	r          *http.Request
	headerSent bool
}

func newRW2resp(r *http.Request) *rw2resp {
	pr, pw := io.Pipe()
	respSink := make(chan *http.Response)
	errSink := make(chan error)
	return &rw2resp{
		pw:     pw,
		pr:     pr,
		respCh: respSink,
		errCh:  errSink,
		r:      r,
	}
}

func (r *rw2resp) Write(p []byte) (int, error) {
	return r.pw.Write(p)
}

func (r *rw2resp) WriteHeader(statusCode int32, status string, header http.Header) (successfulUpgrade, hasContentTypeHeader bool, err error) {
	r.respCh <- &http.Response{
		Status:     status,
		StatusCode: int(statusCode),
		Header:     header,
		Body:       r.pr,
		//ContentLength: 0, // TODO support Content-Length?
		Request: r.r,
	}
	r.headerSent = true
	return false, httpz.HasContentType(header), nil
}
