package retailcrm

import (
	"encoding/json"
	"fmt"
	"regexp"
	"strings"
)

var missingParameterMatcher = regexp.MustCompile(`^Parameter \'([\w\]\[\_\-]+)\' is missing$`)
var (
	// ErrMissingCredentials will be returned if no API key was provided to the API.
	ErrMissingCredentials = NewAPIError(`apiKey is missing`)
	// ErrInvalidCredentials will be returned if provided API key is invalid.
	ErrInvalidCredentials = NewAPIError(`wrong "apiKey" value`)
	// ErrAccessDenied will be returned in case of "Access denied" error.
	ErrAccessDenied = NewAPIError("access denied")
	// ErrAccountDoesNotExist will be returned if target system does not exist.
	ErrAccountDoesNotExist = NewAPIError("account does not exist")
	// ErrValidation will be returned in case of validation errors.
	ErrValidation = NewAPIError("validation error")
	// ErrMissingParameter will be returned if parameter is missing.
	// Underlying error messages list will contain parameter name in the "Name" key.
	ErrMissingParameter = NewAPIError("missing parameter")
	// ErrGeneric will be returned if error cannot be classified as one of the errors above.
	ErrGeneric = NewAPIError("API error")
)

// APIErrorsList struct.
type APIErrorsList map[string]string

// APIError returns when an API error was occurred.
type APIError interface {
	error
	fmt.Stringer
	withWrapped(error) APIError
	withErrors(APIErrorsList) APIError
	Unwrap() error
	Errors() APIErrorsList
}

type apiError struct {
	ErrorMsg   string        `json:"errorMsg,omitempty"`
	ErrorsList APIErrorsList `json:"errors,omitempty"`
	wrapped    error
}

// CreateAPIError from the provided response data. Different error types will be returned depending on the response,
// all of them can be matched using errors.Is. APi errors will always implement APIError interface.
func CreateAPIError(dataResponse []byte) error {
	a := &apiError{}

	if len(dataResponse) > 0 && dataResponse[0] == '<' {
		return ErrAccountDoesNotExist
	}

	if err := json.Unmarshal(dataResponse, &a); err != nil {
		return err
	}

	var found APIError
	switch a.ErrorMsg {
	case `"apiKey" is missing.`:
		found = ErrMissingCredentials
	case `Wrong "apiKey" value.`:
		found = ErrInvalidCredentials
	case "Access denied.":
		found = ErrAccessDenied
	case "Account does not exist.":
		found = ErrAccountDoesNotExist
	case "Errors in the entity format":
		fallthrough
	case "Validation error":
		found = ErrValidation
	default:
		if param, ok := asMissingParameterErr(a.ErrorMsg); ok {
			return a.withWrapped(ErrMissingParameter).withErrors(APIErrorsList{"Name": param})
		}
		found = ErrGeneric
	}

	result := NewAPIError(a.ErrorMsg).withWrapped(found)
	if len(a.ErrorsList) > 0 {
		return result.withErrors(a.ErrorsList)
	}

	return result
}

// CreateGenericAPIError for the situations when API response cannot be processed, but response was actually received.
func CreateGenericAPIError(message string) APIError {
	return NewAPIError(message).withWrapped(ErrGeneric)
}

// NewAPIError returns API error with the provided message.
func NewAPIError(message string) APIError {
	return &apiError{ErrorMsg: message}
}

// AsAPIError returns APIError and true if provided error is an APIError or contains wrapped APIError.
// Returns (nil, false) otherwise.
func AsAPIError(err error) (APIError, bool) {
	apiErr := unwrapAPIError(err)
	return apiErr, apiErr != nil
}

func unwrapAPIError(err error) APIError {
	if err == nil {
		return nil
	}

	if apiErr, ok := err.(APIError); ok { // nolint:errorlint
		return apiErr
	}

	wrapper, ok := err.(interface { // nolint:errorlint
		Unwrap() error
	})
	if ok {
		return unwrapAPIError(wrapper.Unwrap())
	}

	return nil
}

// asMissingParameterErr returns true if "Parameter 'name' is missing" error message is provided.
func asMissingParameterErr(message string) (string, bool) {
	matches := missingParameterMatcher.FindAllStringSubmatch(message, -1)
	if len(matches) == 1 && len(matches[0]) == 2 {
		return matches[0][1], true
	}
	return "", false
}

// Error returns errorMsg field from the response.
func (e *apiError) Error() string {
	return e.ErrorMsg
}

// Unwrap returns wrapped error. It is usually one of the predefined types like ErrGeneric or ErrValidation.
// It can be used directly, but it's main purpose is to make errors matchable via errors.Is call.
func (e *apiError) Unwrap() error {
	return e.wrapped
}

// Errors returns errors field from the response.
func (e *apiError) Errors() APIErrorsList {
	return e.ErrorsList
}

// String returns string representation of an APIError.
func (e *apiError) String() string {
	var sb strings.Builder
	sb.Grow(256) // nolint:gomnd
	sb.WriteString(fmt.Sprintf(`errorMsg: "%s"`, e.Error()))

	if len(e.Errors()) > 0 {
		i := 0
		useIndex := true
		errorList := make([]string, len(e.Errors()))

		for index, errText := range e.Errors() {
			if i == 0 && index == "0" {
				useIndex = false
			}

			if useIndex {
				errorList[i] = fmt.Sprintf(`%s: "%s"`, index, errText)
			} else {
				errorList[i] = errText
			}

			i++
		}

		sb.WriteString(", errors: [" + strings.Join(errorList, ", ") + "]")
	}

	return sb.String()
}

// withWrapped is a wrapped setter.
func (e *apiError) withWrapped(err error) APIError {
	e.wrapped = err
	return e
}

// withErrors is an ErrorsList setter.
func (e *apiError) withErrors(m APIErrorsList) APIError {
	e.ErrorsList = m
	return e
}