package gitlab

import (
	"fmt"
	"net/http"

	"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/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
)

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

type ErrHandler func(resp *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, func(resp *http.Response) error {
		return DefaultErrorHandler(resp)
	})
}

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

func ProtoJSONResponseHandlerWithErr(response proto.Message, errHandler ErrHandler) ResponseHandler {
	return ResponseHandlerStruct{
		AcceptHeader: "application/json",
		HandleFunc: handleOkResponse(func(v protovalidate.Validator, body []byte) error { //nolint:bodyclose
			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) *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 := DefaultErrorHandler(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
}

func handleOkResponse(h func(v protovalidate.Validator, body []byte) error, errHandler ErrHandler) 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)
		switch resp.StatusCode {
		case http.StatusOK, http.StatusCreated:
			contentType := resp.Header.Get(httpz.ContentTypeHeader)
			if !httpz.IsContentType(contentType, "application/json") {
				return fmt.Errorf("unexpected %s in response: %q", httpz.ContentTypeHeader, contentType)
			}
			err = ioz.ReadAllFunc(resp.Body, func(body []byte) error {
				return h(v, body)
			})
			if err != nil {
				return fmt.Errorf("response body: %w", err)
			}
			return nil
		default: // Unexpected status
			return errHandler(resp)
		}
	}
}

// NoContentResponseHandler can be used when no response is expected or response must be discarded.
func NoContentResponseHandler() ResponseHandler {
	return ResponseHandlerStruct{
		HandleFunc: func(v protovalidate.Validator, resp *http.Response, err error) (retErr error) {
			if err != nil {
				return err
			}
			defer errz.DiscardAndClose(resp.Body, &retErr)
			switch resp.StatusCode {
			case http.StatusOK, http.StatusNoContent:
				return nil
			default: // Unexpected status
				path := ""
				if resp.Request != nil && resp.Request.URL != nil {
					path = resp.Request.URL.Path
				}
				return &ClientError{
					StatusCode: int32(resp.StatusCode), //nolint: gosec
					Path:       path,
				}
			}
		},
	}
}
