package errutil

import (
	"errors"
	"fmt"
)

// Base represents the static information about a specific error.
// Always use NewBase to create new instances of Base.
type Base struct {
	// Because Base is typically instantiated as a package or global
	// variable, having private members reduces the probability of a
	// bug messing with the error base.
	reason        StatusReason
	messageID     string
	publicMessage string
	logLevel      LogLevel
}

// NewBase initializes a Base that is used to construct Error:s.
// The reason is used to determine the status code that should be
// returned for the error, and the msgID is passed to the caller
// to serve as the base for user facing error messages.
//
// msgID should be structured as component.error-brief, for example
//   login.failed-authentication
//   dashboards.validation-error
//   dashboards.uid-already-exists
func NewBase(reason StatusReason, msgID string, opts ...BaseOpt) Base {
	b := Base{
		reason:    reason,
		messageID: msgID,
		logLevel:  reason.Status().LogLevel(),
	}

	for _, opt := range opts {
		b = opt(b)
	}

	return b
}

type BaseOpt func(Base) Base

// WithLogLevel sets a custom log level for all errors instantiated from
// this Base.
//
// Used as a functional option to NewBase.
func WithLogLevel(lvl LogLevel) BaseOpt {
	return func(b Base) Base {
		b.logLevel = lvl
		return b
	}
}

// WithPublicMessage sets the default public message that will be used
// for errors based on this Base.
//
// Used as a functional option to NewBase.
func WithPublicMessage(message string) BaseOpt {
	return func(b Base) Base {
		b.publicMessage = message
		return b
	}
}

// Errorf creates a new Error with the Reason and MessageID from
// Base, and Message and Underlying will be populated using
// the rules of fmt.Errorf.
func (b Base) Errorf(format string, args ...interface{}) Error {
	err := fmt.Errorf(format, args...)

	return Error{
		Reason:        b.reason,
		LogMessage:    err.Error(),
		PublicMessage: b.publicMessage,
		MessageID:     b.messageID,
		Underlying:    errors.Unwrap(err),
		LogLevel:      b.logLevel,
	}
}

// Error makes Base implement the error type. Relying on this is
// discouraged, as the Error type can carry additional information
// that's valuable when debugging.
func (b Base) Error() string {
	return b.Errorf("").Error()
}

func (b Base) Status() StatusReason {
	if b.reason == nil {
		return StatusUnknown
	}
	return b.reason.Status()
}

// Is validates that an Error has the same reason and messageID as the
// Base.
func (b Base) Is(err error) bool {
	// The linter complains that it wants to use errors.As because it
	// handles unwrapping, we don't want to do that here since we want
	// to validate the equality between the two objects.
	// errors.Is handles the unwrapping, should you want it.
	//nolint:errorlint
	base, isBase := err.(Base)
	//nolint:errorlint
	gfErr, isGrafanaError := err.(Error)

	switch {
	case isGrafanaError:
		return b.reason == gfErr.Reason && b.messageID == gfErr.MessageID
	case isBase:
		return b.reason == base.reason && b.messageID == base.messageID
	default:
		return false
	}
}

// Error is the error type for errors within Grafana, extending
// the Go error type with Grafana specific metadata to reduce
// boilerplate error handling for status codes and internationalization
// support.
//
// Error implements Unwrap and Is to natively support Go 1.13 style
// errors as described in https://go.dev/blog/go1.13-errors .
type Error struct {
	Reason        StatusReason
	MessageID     string
	LogMessage    string
	Underlying    error
	PublicMessage string
	PublicPayload map[string]interface{}
	LogLevel      LogLevel
}

// MarshalJSON returns an error, we do not want raw Error:s being
// marshaled into JSON.
//
// Use Public to convert the Error into a PublicError which can be
// marshaled. This is not done automatically, as that conversion is
// lossy.
func (e Error) MarshalJSON() ([]byte, error) {
	return nil, fmt.Errorf("errutil.Error cannot be directly marshaled into JSON")
}

// Error implements the error interface.
func (e Error) Error() string {
	return fmt.Sprintf("[%s] %s", e.MessageID, e.LogMessage)
}

// Unwrap is used by errors.As to iterate over the sequence of
// underlying errors until a matching type is found.
func (e Error) Unwrap() error {
	return e.Underlying
}

// Is is used by errors.Is to allow for custom definitions of equality
// between two errors.
func (e Error) Is(other error) bool {
	// The linter complains that it wants to use errors.As because it
	// handles unwrapping, we don't want to do that here since we want
	// to validate the equality between the two objects.
	// errors.Is handles the unwrapping, should you want it.
	//nolint:errorlint
	o, isGrafanaError := other.(Error)
	//nolint:errorlint
	base, isBase := other.(Base)

	switch {
	case isGrafanaError:
		return o.Reason == e.Reason && o.MessageID == e.MessageID && o.Error() == e.Error()
	case isBase:
		return base.Is(e)
	default:
		return false
	}
}

// PublicError is derived from Error and only contains information
// available to the end user.
type PublicError struct {
	StatusCode int                    `json:"statusCode"`
	MessageID  string                 `json:"messageId"`
	Message    string                 `json:"message,omitempty"`
	Extra      map[string]interface{} `json:"extra,omitempty"`
}

// Public returns a subset of the error with non-sensitive information
// that may be relayed to the caller.
func (e Error) Public() PublicError {
	message := e.PublicMessage
	if message == "" {
		if e.Reason == StatusUnknown {
			// The unknown status is equal to the empty string.
			message = string(StatusInternal)
		} else {
			message = string(e.Reason.Status())
		}
	}

	return PublicError{
		StatusCode: e.Reason.Status().HTTPStatus(),
		MessageID:  e.MessageID,
		Message:    message,
		Extra:      e.PublicPayload,
	}
}
