package health

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

var (
	handler = &healthHandler{
		useJSON: false,
		status:  Up,
	}
)

// responseBody is the body of the response returned by the health handler.
type responseBody struct {
	Status string `json:"status"`
	Reason string `json:"reason,omitempty"`
}

// healthHandler is the HTTP handler for serving the health endpoint
type healthHandler struct {
	useJSON bool

	status Status
	reason string

	mutex sync.RWMutex
}

// WithJSON configures whether the handler should output a response in JSON or in raw text
//
// Defaults to false
func (h *healthHandler) WithJSON(v bool) *healthHandler {
	h.useJSON = v
	return h
}

// ServeHTTP serves the HTTP request for the health handler
func (h *healthHandler) ServeHTTP(writer http.ResponseWriter, _ *http.Request) {
	statusCode, body, useJSON := h.getResponseStatusCodeAndBodyAndWhetherBodyUsesJSON()
	if useJSON {
		writer.Header().Set("Content-Type", "application/json")
	}
	writer.WriteHeader(statusCode)
	_, _ = writer.Write(body)
}

func (h *healthHandler) GetResponseStatusCodeAndBody() (statusCode int, body []byte) {
	statusCode, body, _ = h.getResponseStatusCodeAndBodyAndWhetherBodyUsesJSON()
	return statusCode, body
}

func (h *healthHandler) getResponseStatusCodeAndBodyAndWhetherBodyUsesJSON() (statusCode int, body []byte, useJSON bool) {
	var status Status
	var reason string
	h.mutex.RLock()
	status, reason, useJSON = h.status, h.reason, h.useJSON
	h.mutex.RUnlock()
	if status == Up {
		statusCode = http.StatusOK
	} else {
		statusCode = http.StatusInternalServerError
	}
	if useJSON {
		// We can safely ignore the error here because we know that both values are strings, therefore are supported encoders.
		body, _ = json.Marshal(responseBody{Status: string(status), Reason: reason})
	} else {
		if len(reason) == 0 {
			body = []byte(status)
		} else {
			body = []byte(string(status) + ": " + reason)
		}
	}
	return
}

// Handler retrieves the health handler
func Handler() *healthHandler {
	return handler
}

// GetStatus retrieves the current status returned by the health handler
func GetStatus() Status {
	handler.mutex.RLock()
	defer handler.mutex.RUnlock()
	return handler.status
}

// SetStatus sets the status to be returned by the health handler
func SetStatus(status Status) {
	handler.mutex.Lock()
	handler.status = status
	handler.mutex.Unlock()
}

// GetReason retrieves the current status returned by the health handler
func GetReason() string {
	handler.mutex.RLock()
	defer handler.mutex.RUnlock()
	return handler.reason
}

// SetReason sets a reason for the current status to be returned by the health handler
func SetReason(reason string) {
	handler.mutex.Lock()
	handler.reason = reason
	handler.mutex.Unlock()
}

// SetStatusAndReason sets the status and reason to be returned by the health handler
func SetStatusAndReason(status Status, reason string) {
	handler.mutex.Lock()
	handler.status = status
	handler.reason = reason
	handler.mutex.Unlock()
}

// SetStatusAndResetReason sets the status and resets the reason to a blank string
func SetStatusAndResetReason(status Status) {
	handler.mutex.Lock()
	handler.status = status
	handler.reason = ""
	handler.mutex.Unlock()
}

// SetHealthy sets the status to Up and the reason to a blank string
func SetHealthy() {
	SetStatusAndResetReason(Up)
}

// SetUnhealthy sets the status to Down and the reason to the string passed as parameter
//
// Unlike SetHealthy, this function enforces setting a reason, because it's good practice to give at least a bit
// of information as to why an application is unhealthy, and this library attempts to promote good practices.
func SetUnhealthy(reason string) {
	handler.mutex.Lock()
	handler.status = Down
	handler.reason = reason
	handler.mutex.Unlock()
}
