package port

import (
	"errors"
	"net/http"

	"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
	"gitea.dwysokinski.me/twhelp/corev3/internal/port/internal/apimodel"
	"github.com/ettle/strcase"
	"github.com/oapi-codegen/runtime"
)

type apiError struct {
	status  int
	code    apimodel.ErrorCode
	path    []string
	params  map[string]any
	message string
}

func (e apiError) Error() string {
	return e.message
}

func (e apiError) toResponse() apimodel.ErrorResponse {
	return apimodel.ErrorResponse{
		Errors: []apimodel.Error{
			{
				Message: e.message,
				Params:  e.params,
				Path:    e.path,
				Code:    e.code,
			},
		},
	}
}

type apiErrors []apiError

func (es apiErrors) status() int {
	if len(es) == 0 {
		return http.StatusOK
	}

	status := es[0].status

	for _, e := range es {
		if e.status > status {
			status = e.status
		}
	}

	return status
}

func (es apiErrors) toResponse() apimodel.ErrorResponse {
	resp := apimodel.ErrorResponse{
		Errors: make([]apimodel.Error, 0, len(es)),
	}

	for _, e := range es {
		resp.Errors = append(resp.Errors, e.toResponse().Errors...)
	}

	return resp
}

type errorPathFormatter func(segments []domain.ErrorPathSegment) []string

type apiErrorRenderer struct {
	// errorPathFormatter allows to override the default path formatter
	// for domain.ValidationError and domain.SliceElementValidationError.
	// If errorPathFormatter returns an empty slice, an internal server error is rendered.
	errorPathFormatter errorPathFormatter
}

func (re apiErrorRenderer) withErrorPathFormatter(formatter errorPathFormatter) apiErrorRenderer {
	// this assignment is done on purpose
	//nolint:revive
	re.errorPathFormatter = formatter
	return re
}

var errAPIInternalServerError = apiError{
	status:  http.StatusInternalServerError,
	code:    apimodel.ErrorCodeInternalServerError,
	message: "internal server error",
}

func (re apiErrorRenderer) render(w http.ResponseWriter, r *http.Request, errs ...error) {
	apiErrs := make(apiErrors, 0, len(errs))

	for _, err := range errs {
		if err == nil {
			continue
		}

		var apiErr apiError
		var domainErr domain.Error
		var paramFormatErr *apimodel.InvalidParamFormatError

		switch {
		case errors.As(err, &apiErr):
		case errors.As(err, &paramFormatErr):
			apiErr = re.invalidParamFormatErrorToAPIError(paramFormatErr)
		case errors.As(err, &domainErr):
			apiErr = re.domainErrorToAPIError(domainErr)
		default:
			apiErr = errAPIInternalServerError
		}

		apiErrs = append(apiErrs, apiErr)
	}

	renderJSON(w, r, apiErrs.status(), apiErrs.toResponse())
}

func (re apiErrorRenderer) invalidParamFormatErrorToAPIError(
	paramFormatErr *apimodel.InvalidParamFormatError,
) apiError {
	location := "$unknown"

	switch paramFormatErr.Location {
	case runtime.ParamLocationPath:
		location = "$path"
	case runtime.ParamLocationHeader:
		location = "$header"
	case runtime.ParamLocationQuery:
		location = "$query"
	case runtime.ParamLocationCookie:
		location = "$cookie"
	case runtime.ParamLocationUndefined:
		// do nothing
	}

	return apiError{
		status:  http.StatusBadRequest,
		code:    apimodel.ErrorCodeInvalidParamFormat,
		path:    []string{location, paramFormatErr.ParamName},
		message: paramFormatErr.Err.Error(),
	}
}

func (re apiErrorRenderer) domainErrorToAPIError(domainErr domain.Error) apiError {
	message := domainErr.Error()
	var pathSegments []domain.ErrorPathSegment

	var err error = domainErr
	for {
		var withPath domain.ErrorWithPath

		if !errors.As(err, &withPath) {
			break
		}

		pathSegments = append(pathSegments, withPath.Path())

		u, ok := withPath.(interface {
			Unwrap() error
		})
		if ok {
			err = u.Unwrap()
			message = err.Error()
		} else {
			err = nil
		}
	}

	var path []string

	if len(pathSegments) > 0 {
		if re.errorPathFormatter == nil {
			return errAPIInternalServerError
		}

		path = re.errorPathFormatter(pathSegments)

		if len(path) == 0 {
			return errAPIInternalServerError
		}
	}

	var params map[string]any
	if withParams, ok := domainErr.(domain.ErrorWithParams); ok {
		params = withParams.Params()
	}

	cloned := make(map[string]any, len(params))
	for k, v := range params {
		cloned[strcase.ToCamel(k)] = v
	}

	return apiError{
		status:  re.domainErrorTypeToStatusCode(domainErr.Type()),
		code:    apimodel.ErrorCode(domainErr.Code()),
		path:    path,
		params:  cloned,
		message: message,
	}
}

func (re apiErrorRenderer) domainErrorTypeToStatusCode(code domain.ErrorType) int {
	switch code {
	case domain.ErrorTypeIncorrectInput:
		return http.StatusBadRequest
	case domain.ErrorTypeNotFound:
		return http.StatusNotFound
	case domain.ErrorTypeUnknown:
		fallthrough
	default:
		return http.StatusInternalServerError
	}
}
