package http

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"mime"
	"net/http"
	"strings"

	errors2 "github.com/influxdata/influxdb/v2/kit/platform/errors"
	"go.uber.org/zap"
)

// ErrorHandler is the error handler in http package.
type ErrorHandler struct {
	logger *zap.Logger
}

func NewErrorHandler(logger *zap.Logger) ErrorHandler {
	return ErrorHandler{logger: logger}
}

// HandleHTTPError encodes err with the appropriate status code and format,
// sets the X-Platform-Error-Code headers on the response.
// We're no longer using X-Influx-Error and X-Influx-Reference.
// and sets the response status to the corresponding status code.
func (h ErrorHandler) HandleHTTPError(ctx context.Context, err error, w http.ResponseWriter) {
	if err == nil {
		return
	}

	code := errors2.ErrorCode(err)
	var msg string
	if _, ok := err.(*errors2.Error); ok {
		msg = err.Error()
	} else {
		msg = "An internal error has occurred - check server logs"
		h.logger.Warn("internal error not returned to client", zap.Error(err))
	}

	WriteErrorResponse(ctx, w, code, msg)
}

func WriteErrorResponse(ctx context.Context, w http.ResponseWriter, code string, msg string) {
	w.Header().Set(PlatformErrorCodeHeader, code)
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(ErrorCodeToStatusCode(ctx, code))
	e := struct {
		Code    string `json:"code"`
		Message string `json:"message"`
	}{
		Code:    code,
		Message: msg,
	}
	b, _ := json.Marshal(e)
	_, _ = w.Write(b)
}

// StatusCodeToErrorCode maps a http status code integer to an
// influxdb error code string.
func StatusCodeToErrorCode(statusCode int) string {
	errorCode, ok := httpStatusCodeToInfluxDBError[statusCode]
	if ok {
		return errorCode
	}

	return errors2.EInternal
}

// ErrorCodeToStatusCode maps an influxdb error code string to a
// http status code integer.
func ErrorCodeToStatusCode(ctx context.Context, code string) int {
	// If the client disconnects early or times out then return a different
	// error than the passed in error code. Client timeouts return a 408
	// while disconnections return a non-standard Nginx HTTP 499 code.
	if err := ctx.Err(); err == context.DeadlineExceeded {
		return http.StatusRequestTimeout
	} else if err == context.Canceled {
		return 499 // https://httpstatuses.com/499
	}

	// Otherwise map internal error codes to HTTP status codes.
	statusCode, ok := influxDBErrorToStatusCode[code]
	if ok {
		return statusCode
	}
	return http.StatusInternalServerError
}

// influxDBErrorToStatusCode is a mapping of ErrorCode to http status code.
var influxDBErrorToStatusCode = map[string]int{
	errors2.EInternal:            http.StatusInternalServerError,
	errors2.ENotImplemented:      http.StatusNotImplemented,
	errors2.EInvalid:             http.StatusBadRequest,
	errors2.EUnprocessableEntity: http.StatusUnprocessableEntity,
	errors2.EEmptyValue:          http.StatusBadRequest,
	errors2.EConflict:            http.StatusUnprocessableEntity,
	errors2.ENotFound:            http.StatusNotFound,
	errors2.EUnavailable:         http.StatusServiceUnavailable,
	errors2.EForbidden:           http.StatusForbidden,
	errors2.ETooManyRequests:     http.StatusTooManyRequests,
	errors2.EUnauthorized:        http.StatusUnauthorized,
	errors2.EMethodNotAllowed:    http.StatusMethodNotAllowed,
	errors2.ETooLarge:            http.StatusRequestEntityTooLarge,
}

var httpStatusCodeToInfluxDBError = map[int]string{}

func init() {
	for k, v := range influxDBErrorToStatusCode {
		httpStatusCodeToInfluxDBError[v] = k
	}
}

// CheckErrorStatus for status and any error in the response.
func CheckErrorStatus(code int, res *http.Response) error {
	err := CheckError(res)
	if err != nil {
		return err
	}

	if res.StatusCode != code {
		return fmt.Errorf("unexpected status code: %s", res.Status)
	}

	return nil
}

// CheckError reads the http.Response and returns an error if one exists.
// It will automatically recognize the errors returned by Influx services
// and decode the error into an internal error type. If the error cannot
// be determined in that way, it will create a generic error message.
//
// If there is no error, then this returns nil.
func CheckError(resp *http.Response) (err error) {
	switch resp.StatusCode / 100 {
	case 4, 5:
		// We will attempt to parse this error outside of this block.
	case 2:
		return nil
	default:
		// TODO(jsternberg): Figure out what to do here?
		return &errors2.Error{
			Code: errors2.EInternal,
			Msg:  fmt.Sprintf("unexpected status code: %d %s", resp.StatusCode, resp.Status),
		}
	}

	perr := &errors2.Error{
		Code: StatusCodeToErrorCode(resp.StatusCode),
	}

	if resp.StatusCode == http.StatusUnsupportedMediaType {
		perr.Msg = fmt.Sprintf("invalid media type: %q", resp.Header.Get("Content-Type"))
		return perr
	}

	contentType := resp.Header.Get("Content-Type")
	if contentType == "" {
		// Assume JSON if there is no content-type.
		contentType = "application/json"
	}
	mediatype, _, _ := mime.ParseMediaType(contentType)

	var buf bytes.Buffer
	if _, err := io.Copy(&buf, resp.Body); err != nil {
		perr.Msg = "failed to read error response"
		perr.Err = err
		return perr
	}

	switch mediatype {
	case "application/json":
		if err := json.Unmarshal(buf.Bytes(), perr); err != nil {
			perr.Msg = fmt.Sprintf("attempted to unmarshal error as JSON but failed: %q", err)
			perr.Err = firstLineAsError(buf)
		}
	default:
		perr.Err = firstLineAsError(buf)
	}

	if perr.Code == "" {
		// given it was unset during attempt to unmarshal as JSON
		perr.Code = StatusCodeToErrorCode(resp.StatusCode)
	}

	return perr
}
func firstLineAsError(buf bytes.Buffer) error {
	line, _ := buf.ReadString('\n')
	return errors.New(strings.TrimSuffix(line, "\n"))
}
