// Package render implements functionality related to response rendering.
package render

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

	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"

	"github.com/smallstep/certificates/api/log"
)

// JSON is shorthand for JSONStatus(w, v, http.StatusOK).
func JSON(w http.ResponseWriter, v interface{}) {
	JSONStatus(w, v, http.StatusOK)
}

// JSONStatus marshals v into w. It additionally sets the status code of
// w to the given one.
//
// JSONStatus sets the Content-Type of w to application/json unless one is
// specified.
func JSONStatus(w http.ResponseWriter, v interface{}, status int) {
	var b bytes.Buffer
	if err := json.NewEncoder(&b).Encode(v); err != nil {
		panic(err)
	}

	setContentTypeUnlessPresent(w, "application/json")
	w.WriteHeader(status)
	_, _ = b.WriteTo(w)

	log.EnabledResponse(w, v)
}

// ProtoJSON is shorthand for ProtoJSONStatus(w, m, http.StatusOK).
func ProtoJSON(w http.ResponseWriter, m proto.Message) {
	ProtoJSONStatus(w, m, http.StatusOK)
}

// ProtoJSONStatus writes the given value into the http.ResponseWriter and the
// given status is written as the status code of the response.
func ProtoJSONStatus(w http.ResponseWriter, m proto.Message, status int) {
	b, err := protojson.Marshal(m)
	if err != nil {
		panic(err)
	}

	setContentTypeUnlessPresent(w, "application/json")
	w.WriteHeader(status)
	_, _ = w.Write(b)
}

func setContentTypeUnlessPresent(w http.ResponseWriter, contentType string) {
	const header = "Content-Type"

	h := w.Header()
	if _, ok := h[header]; !ok {
		h.Set(header, contentType)
	}
}

// RenderableError is the set of errors that implement the basic Render method.
//
// Errors that implement this interface will use their own Render method when
// being rendered into responses.
type RenderableError interface {
	error

	Render(http.ResponseWriter)
}

// Error marshals the JSON representation of err to w. In case err implements
// RenderableError its own Render method will be called instead.
func Error(w http.ResponseWriter, err error) {
	log.Error(w, err)

	var r RenderableError
	if errors.As(err, &r) {
		r.Render(w)

		return
	}

	JSONStatus(w, err, statusCodeFromError(err))
}

// StatusCodedError is the set of errors that implement the basic StatusCode
// function.
//
// Errors that implement this interface will use the code reported by StatusCode
// as the HTTP response code when being rendered by this package.
type StatusCodedError interface {
	error

	StatusCode() int
}

func statusCodeFromError(err error) (code int) {
	code = http.StatusInternalServerError

	type causer interface {
		Cause() error
	}

	for err != nil {
		var sc StatusCodedError
		if errors.As(err, &sc) {
			code = sc.StatusCode()

			break
		}

		var c causer
		if !errors.As(err, &c) {
			break
		}
		err = c.Cause()
	}

	return
}