package api_responses

import (
	"encoding/json"
	"fmt"
	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/date_utils"
	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors"
	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/map_utils"
	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/string_utils"
	"net/http"
	"regexp"
	"strconv"
	"strings"

	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/utils"

	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/logs"
	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/responses"

	"github.com/go-pg/pg/v10"

	"github.com/aws/aws-lambda-go/events"
)

var ContentTypeJSONHeader = map[string]string{"Content-Type": "application/json"}

type errorMsg struct {
	Message string `json:"message"`
	Error   string `json:"error,omitempty"`
}

// ServerError logs any error to os.Stderr and returns 500
// Internal Server Error response that the AWS API Gateway understands.
func ServerError(err error, msg string) (events.APIGatewayProxyResponse, error) {
	return Error(err, msg, http.StatusInternalServerError)
}

func Error(err error, msg string, statusCode int) (events.APIGatewayProxyResponse, error) {
	logs.ErrorWithFields(map[string]interface{}{
		"type":    "Server error",
		"message": msg,
		"code":    statusCode,
	}, err)

	serverError := errorMsg{
		Message: msg,
		Error:   err.Error(),
	}

	bodyBytes, err := json.Marshal(serverError)
	if err != nil {
		return events.APIGatewayProxyResponse{
			StatusCode: statusCode,
			Headers:    map_utils.MergeMaps(utils.CorsHeaders(), responses.ContentTypeJSONHeader),
			Body:       "{ \"error\": \"" + http.StatusText(http.StatusInternalServerError) + "\"}",
		}, nil
	}

	return events.APIGatewayProxyResponse{
		StatusCode: statusCode,
		Headers:    map_utils.MergeMaps(utils.CorsHeaders(), responses.ContentTypeJSONHeader),
		Body:       string(bodyBytes),
	}, errors.Error(msg)
}

func DatabaseServerErrorNew(err error, msg string) error {
	statusCode := StatusCodeFromSQLError(err)
	errorString := err.Error()

	if dbError := ErrorFromDBError(err); dbError != "" {
		errorString = dbError
		if strings.HasSuffix(msg, ".") {
			// Remove trailing full stop before adding dbError.
			msg = strings.TrimSuffix(msg, ".")
		}
		msg = msg + ": " + dbError
	}

	if statusCode == http.StatusNotFound {
		logs.Info("Database error: " + msg + ". Code: " + strconv.Itoa(statusCode))
	} else if statusCode == http.StatusConflict {
		logs.Info("Database conflict: " + msg + ". Code: " + strconv.Itoa(statusCode))
	} else {
		logs.ErrorWithFields(map[string]interface{}{
			"type":    "Database error",
			"message": msg,
			"code":    statusCode,
		}, err)
	}

	return ServerErrorStruct{
		error:   errors.Error(errorString),
		Message: msg,
	}
}

// implements error so that API handler can extract the msg
type ServerErrorStruct struct {
	error
	Message string
}

func NewServerError(err error, msg string) error {
	return ServerErrorStruct{
		error:   err,
		Message: msg,
	}
}

func DatabaseServerError(err error, msg string) (events.APIGatewayProxyResponse, error) {
	statusCode := StatusCodeFromSQLError(err)
	errorString := err.Error()

	if dbError := ErrorFromDBError(err); dbError != "" {
		errorString = dbError
		if strings.HasSuffix(msg, ".") {
			// Remove trailing full stop before adding dbError.
			msg = strings.TrimSuffix(msg, ".")
		}
		msg = msg + ": " + dbError
	}

	if statusCode == http.StatusNotFound {
		logs.Info("Database error: " + msg + ". Code: " + strconv.Itoa(statusCode))
	} else if statusCode == http.StatusConflict {
		logs.Info("Database conflict: " + msg + ". Code: " + strconv.Itoa(statusCode))
	} else {
		logs.ErrorWithFields(map[string]interface{}{
			"type":    "Database error",
			"message": msg,
			"code":    statusCode,
		}, err)
	}

	serverError := errorMsg{
		Message: msg,
		Error:   errorString,
	}

	bodyBytes, marshalError := json.Marshal(serverError)
	if marshalError != nil {
		return events.APIGatewayProxyResponse{
			StatusCode: statusCode,
			Headers:    map_utils.MergeMaps(utils.CorsHeaders(), responses.ContentTypeJSONHeader),
			Body:       "{ \"error\": \"" + http.StatusText(http.StatusInternalServerError) + "\"}",
		}, nil
	}

	// Don't send an error on DB conflict
	if statusCode == http.StatusConflict {
		err = nil
	}

	return events.APIGatewayProxyResponse{
		StatusCode: statusCode,
		Headers:    map_utils.MergeMaps(utils.CorsHeaders(), responses.ContentTypeJSONHeader),
		Body:       string(bodyBytes),
	}, err
}

func ErrorFromDBError(err error) string {
	pgErr, ok := err.(pg.Error)
	if !ok {
		return err.Error()
	}

	message := humanReadableDatabaseError(pgErr)
	return message
}

func humanReadableDatabaseError(pgErr pg.Error) string {
	postgresErrorCode := pgErr.Field('C')
	if postgresErrorCode == "23505" { // Conflict
		detail := pgErr.Field('D')
		if detail == "" {
			return pgErr.Error()
		}

		r, err := regexp.Compile("\\(.*?.*?\\)") // Match all between ( and )
		if err != nil {
			return pgErr.Error()
		}

		matches := r.FindAllString(detail, -1)
		if len(matches) != 2 {
			return pgErr.Error()
		}

		keysString := matches[0]
		keysString = trimBrackets(keysString)
		keys := strings.Split(keysString, ",")

		conflictString := ""
		for _, key := range keys {
			cleanKey := strings.TrimSpace(key)
			if cleanKey == "provider_id" {
				continue // Don't check for provider ID uniqueness
			}

			cleanKey = strings.ReplaceAll(cleanKey, "_", " ")
			if conflictString == "" {
				conflictString = cleanKey
			} else {
				conflictString = conflictString + ", " + cleanKey
			}
		}

		message := fmt.Sprintf("The specified %s already exists", conflictString)
		return message
	}

	return pgErr.Error()
}

func trimBrackets(value string) string {
	value = strings.TrimPrefix(value, "(")
	value = strings.TrimSuffix(value, ")")
	return value
}

// ClientError creates responses due to request client error
func ClientError(status int, message string) (events.APIGatewayProxyResponse, error) {
	logs.WarnWithFields(map[string]interface{}{
		"type": "Client error",
		"code": status,
	}, errors.Error(message))

	e := errorMsg{
		Message: message,
	}
	b, err := json.Marshal(e)
	if err != nil {
		logs.Info("Could not create error messsage for ", message)
	}

	return events.APIGatewayProxyResponse{
		StatusCode: status,
		Headers:    map_utils.MergeMaps(utils.CorsHeaders(), responses.ContentTypeJSONHeader),
		Body:       string(b),
	}, errors.Error(message)
}

func StatusCodeFromSQLError(err error) int {
	if err == pg.ErrNoRows {
		return http.StatusNotFound
	}

	pgErr, ok := err.(pg.Error)
	if !ok || pgErr == nil || !pgErr.IntegrityViolation() {
		return http.StatusInternalServerError
	}

	// See Postgres docs for error codes: https://www.postgresql.org/docs/10/errcodes-appendix.html
	postgresErrorCode := pgErr.Field('C')
	switch postgresErrorCode {
	case "23505":
		return http.StatusConflict
	default:
		return http.StatusInternalServerError
	}
}

func GenericJSONResponseWithContentAndHeaders(code int, content string, headers map[string]string) events.APIGatewayProxyResponse {
	response := events.APIGatewayProxyResponse{
		StatusCode: code,
		Body:       content,
		Headers:    map_utils.MergeMaps(utils.CorsHeaders(), ContentTypeJSONHeader, headers),
	}
	return response
}

func GenericJSONResponseWithMessage(code int, err error) events.APIGatewayProxyResponse {
	var message string
	var body map[string]string

	if err != nil {
		customErr := err.(*errors.CustomError)
		message = customErr.Formatted(errors.FormattingOptions{NewLines: false, Causes: true})
		body = map[string]string{
			"message": string_utils.Capitalize(message),
		}
	}

	responseBody := message
	if bodyBytes, err := json.Marshal(body); err == nil {
		responseBody = string(bodyBytes)
	}

	return events.APIGatewayProxyResponse{
		StatusCode: code,
		Body:       responseBody,
		Headers:    map_utils.MergeMaps(utils.CorsHeaders(), ContentTypeJSONHeader),
	}
}

func TimeResponse() events.APIGatewayProxyResponse {
	currentTime, _ := json.Marshal(date_utils.CurrentDate())
	return events.APIGatewayProxyResponse{
		StatusCode: http.StatusOK,
		Body:       string(currentTime),
		Headers:    map_utils.MergeMaps(utils.CorsHeaders(), ContentTypeJSONHeader),
	}
}