package gitlab

import (
	"fmt"
	"net/http"
	"slices"

	"buf.build/go/protovalidate"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/errz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/httpz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/ioz"
	"google.golang.org/grpc/mem"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
)

type ResponseHandlerStruct struct {
	AcceptHeader string
	HandleFunc   func(protovalidate.Validator, *http.Response, error) error
}

type ErrHandler func(*http.Response) error

func (r ResponseHandlerStruct) Handle(v protovalidate.Validator, resp *http.Response, err error) error {
	return r.HandleFunc(v, resp, err)
}

func (r ResponseHandlerStruct) Accept() string {
	return r.AcceptHeader
}

func NakedResponseHandler(response **http.Response) ResponseHandler {
	return ResponseHandlerStruct{
		HandleFunc: func(v protovalidate.Validator, r *http.Response, err error) error {
			if err != nil {
				return err
			}
			*response = r
			return nil
		},
	}
}

func ProtoJSONResponseHandler(response proto.Message) ResponseHandler {
	return ProtoJSONResponseHandlerWithErr(response, DefaultErrorHandler)
}

func ProtoJSONResponseHandlerWithStructuredErrReason(response proto.Message) ResponseHandler {
	return ProtoJSONResponseHandlerWithErr(response, DefaultErrorHandlerWithReason)
}

func ProtoJSONResponseHandlerWithErr(response proto.Message, errHandler ErrHandler) ResponseHandler {
	return ResponseHandlerStruct{
		AcceptHeader: "application/json",
		HandleFunc: hCheckErrCloseBody( //nolint:bodyclose
			hCheckStatusCode(
				[]int{http.StatusOK, http.StatusCreated},
				hCheckContentType(
					"application/json",
					hReadBodySlice(func(v protovalidate.Validator, resp *http.Response, body []byte) error {
						err := protojson.UnmarshalOptions{
							DiscardUnknown: true,
						}.Unmarshal(body, response)
						if err != nil {
							return fmt.Errorf("protojson.Unmarshal: %w", err)
						}
						if err = v.Validate(response); err != nil {
							return fmt.Errorf("response body: %w", err)
						}
						return nil
					}),
				),
				errHandler,
			),
		),
	}
}

func DefaultErrorHandler(resp *http.Response) error {
	return DefaultErrorHandlerTyped(resp)
}

func DefaultErrorHandlerTyped(resp *http.Response) *ClientError {
	path := ""
	if resp.Request != nil && resp.Request.URL != nil {
		path = resp.Request.URL.Path
	}

	return &ClientError{
		StatusCode: int32(resp.StatusCode), //nolint: gosec
		Path:       path,
	}
}

// DefaultErrorHandlerWithReason tries to add an error reason from the response body.
// If no reason can be found, none is added to the response
func DefaultErrorHandlerWithReason(resp *http.Response) error {
	e := DefaultErrorHandlerTyped(resp)

	contentTypes := resp.Header[httpz.ContentTypeHeader]
	if len(contentTypes) == 0 {
		e.Reason = "<unknown reason: missing content type header to read reason>"
		return e
	}

	contentType := contentTypes[0]
	if !httpz.IsContentType(contentType, "application/json") {
		e.Reason = fmt.Sprintf("<unknown reason: expected application/json content type, but got %s>", contentType)
		return e
	}

	var message DefaultApiError
	err := ioz.ReadAllFunc(resp.Body, func(body []byte) error {
		err := protojson.UnmarshalOptions{DiscardUnknown: true}.Unmarshal(body, &message)
		if err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		e.Reason = fmt.Sprintf("<unknown reason: %s>", err)
		return e
	}
	e.Reason = message.Message
	return e
}

// NoContentResponseHandler can be used when no response is expected or response must be discarded.
func NoContentResponseHandler() ResponseHandler {
	return ResponseHandlerStruct{
		HandleFunc: hCheckErrCloseBody( //nolint:bodyclose
			hCheckStatusCode(
				[]int{http.StatusOK, http.StatusNoContent},
				func(v protovalidate.Validator, resp *http.Response) error {
					return nil
				},
				DefaultErrorHandler,
			),
		),
	}
}

// RawBodyResponseHandler allows the caller to get the raw response as a memory buffer.
// The caller is responsible for freeing the buffer when it's no longer needed.
func RawBodyResponseHandler(expectedStatusCodes []int, expectedContentType string, body *mem.BufferSlice) ResponseHandler {
	return ResponseHandlerStruct{
		AcceptHeader: expectedContentType,
		HandleFunc: hCheckErrCloseBody( //nolint:bodyclose
			hCheckStatusCode(
				expectedStatusCodes,
				hCheckContentType(
					expectedContentType,
					hReadBodyBuffer(func(v protovalidate.Validator, resp *http.Response, b mem.BufferSlice) error {
						// This callback is supposed to call b.Free() to free to buffer once done with it.
						// We are "leaking" it here to the caller, so not freeing the buffer. The caller must do it.
						*body = b
						return nil
					}),
				),
				DefaultErrorHandlerWithReason,
			),
		),
	}
}

// MultiResponseHandler joins multiple response handlers.
// First non-empty accept header is used.
func MultiResponseHandler(handlers ...ResponseHandler) ResponseHandler {
	acceptHeader := ""
	for _, h := range handlers {
		hAccept := h.Accept()
		if hAccept != "" {
			acceptHeader = hAccept
			break
		}
	}
	return ResponseHandlerStruct{
		AcceptHeader: acceptHeader,
		HandleFunc: func(v protovalidate.Validator, resp *http.Response, err error) error {
			for _, h := range handlers {
				hErr := h.Handle(v, resp, err)
				if hErr != nil {
					return hErr
				}
			}
			return nil
		},
	}
}

type handler func(v protovalidate.Validator, resp *http.Response) error

func hCheckErrCloseBody(delegate handler) func(protovalidate.Validator, *http.Response, error) error {
	return func(v protovalidate.Validator, resp *http.Response, err error) (retErr error) {
		if err != nil {
			return err
		}
		defer errz.DiscardAndClose(resp.Body, &retErr)
		return delegate(v, resp)
	}
}

func hCheckStatusCode(expectedStatusCodes []int, ok handler, errHandler ErrHandler) handler {
	return func(v protovalidate.Validator, resp *http.Response) error {
		if slices.Contains(expectedStatusCodes, resp.StatusCode) {
			return ok(v, resp)
		} else { // Unexpected status
			return errHandler(resp)
		}
	}
}

func hCheckContentType(expectedContentType string, ok handler) handler {
	return func(v protovalidate.Validator, resp *http.Response) error {
		if resp.StatusCode != http.StatusNoContent {
			contentTypes := resp.Header.Values(httpz.ContentTypeHeader)
			if len(contentTypes) != 1 {
				return fmt.Errorf("expecting single %s in response, got: %q", httpz.ContentTypeHeader, contentTypes)
			}
			if !httpz.IsContentType(contentTypes[0], expectedContentType) {
				return fmt.Errorf("unexpected %s in response: %q", httpz.ContentTypeHeader, contentTypes[0])
			}
		}
		return ok(v, resp)
	}
}

// ok MUST call Free() on the passed body ALWAYS and ASAP.
func hReadBodyBuffer(ok func(v protovalidate.Validator, resp *http.Response, body mem.BufferSlice) error) handler {
	return func(v protovalidate.Validator, resp *http.Response) error {
		body, err := mem.ReadAll(resp.Body, mem.DefaultBufferPool())
		if err != nil {
			body.Free() // must be freed even on error
			return fmt.Errorf("read all: %w", err)
		}
		return ok(v, resp, body)
	}
}

func hReadBodySlice(ok func(v protovalidate.Validator, resp *http.Response, body []byte) error) handler {
	return hReadBodyBuffer(
		func(v protovalidate.Validator, resp *http.Response, body mem.BufferSlice) error {
			data := body.MaterializeToBuffer(mem.DefaultBufferPool())
			body.Free()
			defer data.Free()
			return ok(v, resp, data.ReadOnlyData())
		},
	)
}
