// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package logical

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"

	"github.com/hashicorp/errwrap"
	multierror "github.com/hashicorp/go-multierror"
	"github.com/hashicorp/vault/sdk/helper/consts"
)

// RespondErrorCommon pulls most of the functionality from http's
// respondErrorCommon and some of http's handleLogical and makes it available
// to both the http package and elsewhere.
func RespondErrorCommon(req *Request, resp *Response, err error) (int, error) {
	if err == nil && (resp == nil || !resp.IsError()) {
		switch {
		case req.Operation == ReadOperation || req.Operation == HeaderOperation:
			if resp == nil {
				return http.StatusNotFound, nil
			}

		// Basically: if we have empty "keys" or no keys at all, 404. This
		// provides consistency with GET.
		case req.Operation == ListOperation && (resp == nil || resp.WrapInfo == nil):
			if resp == nil {
				return http.StatusNotFound, nil
			}
			if len(resp.Data) == 0 {
				if len(resp.Warnings) > 0 {
					return 0, nil
				}
				return http.StatusNotFound, nil
			}
			keysRaw, ok := resp.Data["keys"]
			if !ok || keysRaw == nil {
				// If we don't have keys but have other data, return as-is
				if len(resp.Data) > 0 || len(resp.Warnings) > 0 {
					return 0, nil
				}
				return http.StatusNotFound, nil
			}

			var keys []string
			switch keysRaw.(type) {
			case []interface{}:
				keys = make([]string, len(keysRaw.([]interface{})))
				for i, el := range keysRaw.([]interface{}) {
					s, ok := el.(string)
					if !ok {
						return http.StatusInternalServerError, nil
					}
					keys[i] = s
				}

			case []string:
				keys = keysRaw.([]string)
			default:
				return http.StatusInternalServerError, nil
			}

			if len(keys) == 0 {
				return http.StatusNotFound, nil
			}
		}

		return 0, nil
	}

	if errwrap.ContainsType(err, new(ReplicationCodedError)) {
		var allErrors error
		var codedErr *ReplicationCodedError
		errwrap.Walk(err, func(inErr error) {
			// The Walk function does not just traverse leaves, and execute the
			// callback function on the entire error first. So, if the error is
			// of type multierror.Error, we may want to skip storing the entire
			// error first to avoid adding duplicate errors when walking down
			// the leaf errors
			if _, ok := inErr.(*multierror.Error); ok {
				return
			}
			newErr, ok := inErr.(*ReplicationCodedError)
			if ok {
				codedErr = newErr
			} else {
				// if the error is of type fmt.wrapError which is typically
				// made by calling fmt.Errorf("... %w", err), allErrors will
				// contain duplicated error messages
				allErrors = multierror.Append(allErrors, inErr)
			}
		})
		if allErrors != nil {
			return codedErr.Code, multierror.Append(fmt.Errorf("errors from both primary and secondary; primary error was %v; secondary errors follow", codedErr.Msg), allErrors)
		}
		return codedErr.Code, errors.New(codedErr.Msg)
	}

	// Start out with internal server error since in most of these cases there
	// won't be a response so this won't be overridden
	statusCode := http.StatusInternalServerError
	// If we actually have a response, start out with bad request
	if resp != nil {
		statusCode = http.StatusBadRequest
	}

	// Now, check the error itself; if it has a specific logical error, set the
	// appropriate code
	if err != nil {
		switch {
		case errwrap.ContainsType(err, new(StatusBadRequest)):
			statusCode = http.StatusBadRequest
		case errwrap.Contains(err, ErrPermissionDenied.Error()):
			statusCode = http.StatusForbidden
		case errwrap.Contains(err, consts.ErrInvalidWrappingToken.Error()):
			statusCode = http.StatusBadRequest
		case errwrap.Contains(err, ErrUnsupportedOperation.Error()):
			statusCode = http.StatusMethodNotAllowed
		case errwrap.Contains(err, ErrUnsupportedPath.Error()):
			statusCode = http.StatusNotFound
		case errwrap.Contains(err, ErrInvalidRequest.Error()):
			statusCode = http.StatusBadRequest
		case errwrap.Contains(err, ErrUpstreamRateLimited.Error()):
			statusCode = http.StatusBadGateway
		case errwrap.Contains(err, ErrRateLimitQuotaExceeded.Error()):
			statusCode = http.StatusTooManyRequests
		case errwrap.Contains(err, ErrLeaseCountQuotaExceeded.Error()):
			statusCode = http.StatusTooManyRequests
		case errwrap.Contains(err, ErrMissingRequiredState.Error()):
			statusCode = http.StatusPreconditionFailed
		case errwrap.Contains(err, ErrPathFunctionalityRemoved.Error()):
			statusCode = http.StatusNotFound
		case errwrap.Contains(err, ErrRelativePath.Error()):
			statusCode = http.StatusBadRequest
		case errwrap.Contains(err, ErrInvalidCredentials.Error()):
			statusCode = http.StatusBadRequest
		}
	}

	if resp != nil && resp.IsError() {
		err = fmt.Errorf("%s", resp.Data["error"].(string))
	}

	return statusCode, err
}

// AdjustErrorStatusCode adjusts the status that will be sent in error
// conditions in a way that can be shared across http's respondError and other
// locations.
func AdjustErrorStatusCode(status *int, err error) {
	// Handle nested errors
	if t, ok := err.(*multierror.Error); ok {
		for _, e := range t.Errors {
			AdjustErrorStatusCode(status, e)
		}
	}

	// Adjust status code when sealed
	if errwrap.Contains(err, consts.ErrSealed.Error()) {
		*status = http.StatusServiceUnavailable
	}

	if errwrap.Contains(err, consts.ErrAPILocked.Error()) {
		*status = http.StatusServiceUnavailable
	}

	// Adjust status code on
	if errwrap.Contains(err, "http: request body too large") {
		*status = http.StatusRequestEntityTooLarge
	}

	// Allow HTTPCoded error passthrough to specify a code
	if t, ok := err.(HTTPCodedError); ok {
		*status = t.Code()
	}
}

func RespondError(w http.ResponseWriter, status int, err error) {
	AdjustErrorStatusCode(&status, err)

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)

	type ErrorResponse struct {
		Errors []string `json:"errors"`
	}
	resp := &ErrorResponse{Errors: make([]string, 0, 1)}
	if err != nil {
		resp.Errors = append(resp.Errors, err.Error())
	}

	enc := json.NewEncoder(w)
	enc.Encode(resp)
}

func RespondErrorAndData(w http.ResponseWriter, status int, data interface{}, err error) {
	AdjustErrorStatusCode(&status, err)

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)

	type ErrorAndDataResponse struct {
		Errors []string    `json:"errors"`
		Data   interface{} `json:"data""`
	}
	resp := &ErrorAndDataResponse{Errors: make([]string, 0, 1)}
	if err != nil {
		resp.Errors = append(resp.Errors, err.Error())
	}
	resp.Data = data

	enc := json.NewEncoder(w)
	enc.Encode(resp)
}
