package service

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

	kithttp "github.com/go-kit/kit/transport/http"
	"golang.org/x/net/context"
)

// erroer interface is implemented by response structs to encode business logic errors
type errorer interface {
	error() error
}

type jsonError struct {
	Message string              `json:"message"`
	Errors  []map[string]string `json:"errors,omitempty"`
}

// use baseError to encode an jsonError.Errors field with an error that has
// a generic "name" field. The frontend client always expects errors in a
// []map[string]string format.
func baseError(err string) []map[string]string {
	return []map[string]string{map[string]string{
		"name":   "base",
		"reason": err},
	}
}

// same as baseError, but replaces "base" with different name.
func namedError(name string, err string) []map[string]string {
	return []map[string]string{map[string]string{
		"name":   name,
		"reason": err},
	}
}

// encode error and status header to the client
func encodeError(ctx context.Context, err error, w http.ResponseWriter) {
	// Unwrap Go-Kit Error
	domain := "service"
	if e, ok := err.(kithttp.Error); ok {
		err = e.Err
		domain = e.Domain
	}

	enc := json.NewEncoder(w)
	enc.SetIndent("", "  ")

	type licensingError interface {
		LicensingError() string
	}
	if e, ok := err.(licensingError); ok {
		le := jsonError{
			Message: "Licensing Error",
			Errors:  namedError("license", e.LicensingError()),
		}
		w.WriteHeader(http.StatusPaymentRequired)
		enc.Encode(le)
		return
	}

	type validationError interface {
		error
		Invalid() []map[string]string
	}
	if e, ok := err.(validationError); ok {
		ve := jsonError{
			Message: "Validation Failed",
			Errors:  e.Invalid(),
		}
		w.WriteHeader(http.StatusUnprocessableEntity)
		enc.Encode(ve)
		return
	}

	type authenticationError interface {
		error
		AuthError() string
	}
	if e, ok := err.(authenticationError); ok {
		ae := jsonError{
			Message: "Authentication Failed",
			Errors:  baseError(e.AuthError()),
		}
		w.WriteHeader(http.StatusUnauthorized)
		enc.Encode(ae)
		return
	}

	type permissionError interface {
		PermissionError() []map[string]string
	}
	if e, ok := err.(permissionError); ok {
		pe := jsonError{
			Message: "Permission Denied",
			Errors:  e.PermissionError(),
		}
		w.WriteHeader(http.StatusForbidden)
		enc.Encode(pe)
		return
	}

	type mailError interface {
		MailError() []map[string]string
	}
	if e, ok := err.(mailError); ok {
		me := jsonError{
			Message: "Mail Error",
			Errors:  e.MailError(),
		}
		w.WriteHeader(http.StatusInternalServerError)
		enc.Encode(me)
		return
	}

	type osqueryError interface {
		error
		NodeInvalid() bool
	}
	if e, ok := err.(osqueryError); ok {
		// osquery expects to receive the node_invalid key when a TLS
		// request provides an invalid node_key for authentication. It
		// doesn't use the error message provided, but we provide this
		// for debugging purposes (and perhaps osquery will use this
		// error message in the future).

		errMap := map[string]interface{}{"error": e.Error()}
		if e.NodeInvalid() {
			w.WriteHeader(http.StatusUnauthorized)
			errMap["node_invalid"] = true
		} else {
			w.WriteHeader(http.StatusInternalServerError)
		}

		enc.Encode(errMap)
		return
	}

	type notFoundError interface {
		error
		IsNotFound() bool
	}
	if e, ok := err.(notFoundError); ok {
		je := jsonError{
			Message: "Resource Not Found",
			Errors:  baseError(e.Error()),
		}
		w.WriteHeader(http.StatusNotFound)
		enc.Encode(je)
		return
	}

	type existsError interface {
		error
		IsExists() bool
	}
	if e, ok := err.(existsError); ok {
		je := jsonError{
			Message: "Resource Already Exists",
			Errors:  baseError(e.Error()),
		}
		w.WriteHeader(http.StatusConflict)
		enc.Encode(je)
		return
	}

	// Other errors
	switch domain {
	case kithttp.DomainDecode:
		w.WriteHeader(http.StatusBadRequest)
	case kithttp.DomainDo:
		w.WriteHeader(http.StatusServiceUnavailable)
	default:
		w.WriteHeader(http.StatusInternalServerError)
	}
	je := jsonError{
		Message: "Unknown Error",
		Errors:  baseError(err.Error()),
	}
	enc.Encode(je)
}
