// Package errorcode allows to deal with graph error codes
package errorcode

import (
	"context"
	"errors"
	"net/http"
	"time"

	rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
	"github.com/go-chi/chi/v5/middleware"
	"github.com/go-chi/render"
	libregraph "github.com/owncloud/libre-graph-api-go"
)

// Error defines a custom error struct, containing and MS Graph error code and a textual error message
type Error struct {
	errorCode ErrorCode
	msg       string
	origin    ErrorOrigin
}

// ErrorOrigin gives information about where the error originated
type ErrorOrigin int

const (
	// ErrorOriginUnknown is the default error source
	// and indicates that the error does not have any information about its origin
	ErrorOriginUnknown ErrorOrigin = iota

	// ErrorOriginCS3 indicates that the error originated from a CS3 service
	ErrorOriginCS3
)

// ErrorCode defines code as used in MS Graph - see https://docs.microsoft.com/en-us/graph/errors?context=graph%2Fapi%2F1.0&view=graph-rest-1.0
type ErrorCode int

// List taken from https://github.com/microsoft/microsoft-graph-docs-1/blob/main/concepts/errors.md#code-property
const (
	// AccessDenied defines the error if the caller doesn't have permission to perform the action.
	AccessDenied ErrorCode = iota
	// ActivityLimitReached defines the error if the app or user has been throttled.
	ActivityLimitReached
	// GeneralException defines the error if an unspecified error has occurred.
	GeneralException
	// InvalidAuthenticationToken defines the error if the access token is missing
	InvalidAuthenticationToken
	// InvalidRange defines the error if the specified byte range is invalid or unavailable.
	InvalidRange
	// InvalidRequest defines the error if the request is malformed or incorrect.
	InvalidRequest
	// ItemNotFound defines the error if the resource could not be found.
	ItemNotFound
	// MalwareDetected defines the error if malware was detected in the requested resource.
	MalwareDetected
	// NameAlreadyExists defines the error if the specified item name already exists.
	NameAlreadyExists
	// NotAllowed defines the error if the action is not allowed by the system.
	NotAllowed
	// NotSupported defines the error if the request is not supported by the system.
	NotSupported
	// ResourceModified defines the error if the resource being updated has changed since the caller last read it, usually an eTag mismatch.
	ResourceModified
	// ResyncRequired defines the error if the delta token is no longer valid, and the app must reset the sync state.
	ResyncRequired
	// ServiceNotAvailable defines the error if the service is not available. Try the request again after a delay. There may be a Retry-After header.
	ServiceNotAvailable
	// SyncStateNotFound defines the error when the sync state generation is not found. The delta token is expired and data must be synchronized again.
	SyncStateNotFound
	// QuotaLimitReached the user has reached their quota limit.
	QuotaLimitReached
	// Unauthenticated the caller is not authenticated.
	Unauthenticated
	// PreconditionFailed the request cannot be made and this error response is sent back
	PreconditionFailed
	// ItemIsLocked The item is locked by another process. Try again later.
	ItemIsLocked
)

var errorCodes = [...]string{
	"accessDenied",
	"activityLimitReached",
	"generalException",
	"InvalidAuthenticationToken",
	"invalidRange",
	"invalidRequest",
	"itemNotFound",
	"malwareDetected",
	"nameAlreadyExists",
	"notAllowed",
	"notSupported",
	"resourceModified",
	"resyncRequired",
	"serviceNotAvailable",
	"syncStateNotFound",
	"quotaLimitReached",
	"unauthenticated",
	"preconditionFailed",
	"itemIsLocked",
}

// New constructs a new errorcode.Error
func New(e ErrorCode, msg string) Error {
	return Error{
		errorCode: e,
		msg:       msg,
	}
}

// New errorcode.Error from cs3rpc Status Code
func NewFromStatusCode(code rpc.Code, msg string) Error {
	return New(cs3StatusToErrCode(code), msg)
}

// Render writes a Graph ErrorCode object to the response writer
func (e ErrorCode) Render(w http.ResponseWriter, r *http.Request, status int, msg string) {
	render.Status(r, status)
	render.JSON(w, r, e.CreateOdataError(r.Context(), msg))
}

// CreateOdataError creates and populates a Graph ErrorCode object
func (e ErrorCode) CreateOdataError(ctx context.Context, msg string) *libregraph.OdataError {
	innererror := map[string]interface{}{
		"date": time.Now().UTC().Format(time.RFC3339),
	}

	innererror["request-id"] = middleware.GetReqID(ctx)
	return &libregraph.OdataError{
		Error: libregraph.OdataErrorMain{
			Code:       e.String(),
			Message:    msg,
			Innererror: innererror,
		},
	}
}

// Render writes a Graph Error object to the response writer
func (e Error) Render(w http.ResponseWriter, r *http.Request) {
	var status int
	switch e.errorCode {
	case AccessDenied:
		status = http.StatusForbidden
	case NotSupported:
		status = http.StatusNotImplemented
	case InvalidRange:
		status = http.StatusRequestedRangeNotSatisfiable
	case InvalidRequest:
		status = http.StatusBadRequest
	case ItemNotFound:
		status = http.StatusNotFound
	case NameAlreadyExists:
		status = http.StatusConflict
	case NotAllowed:
		status = http.StatusMethodNotAllowed
	case ItemIsLocked:
		status = http.StatusLocked
	case PreconditionFailed:
		status = http.StatusPreconditionFailed
	default:
		status = http.StatusInternalServerError
	}
	e.errorCode.Render(w, r, status, e.msg)
}

// String returns the string corresponding to the ErrorCode
func (e ErrorCode) String() string {
	return errorCodes[e]
}

// Error returns the concatenation of the error string and optional message
func (e Error) Error() string {
	errString := errorCodes[e.errorCode]
	if e.msg != "" {
		errString += ": " + e.msg
	}
	return errString
}

func (e Error) GetCode() ErrorCode {
	return e.errorCode
}

// GetOrigin returns the source of the error
func (e Error) GetOrigin() ErrorOrigin {
	return e.origin
}

// WithOrigin returns a new Error with the provided origin
func (e Error) WithOrigin(o ErrorOrigin) Error {
	e.origin = o
	return e
}

// RenderError render the Graph Error based on a code or default one
func RenderError(w http.ResponseWriter, r *http.Request, err error) {
	e, ok := ToError(err)
	if !ok {
		GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
		return
	}

	e.Render(w, r)
}

// ToError checks if the error is of type Error and returns it,
// the second parameter indicates if the error conversion was successful
func ToError(err error) (Error, bool) {
	var e Error
	if errors.As(err, &e) {
		return e, true
	}

	return Error{}, false
}

func cs3StatusToErrCode(code rpc.Code) (errcode ErrorCode) {
	switch code {
	case rpc.Code_CODE_UNAUTHENTICATED:
		errcode = Unauthenticated
	case rpc.Code_CODE_PERMISSION_DENIED:
		errcode = AccessDenied
	case rpc.Code_CODE_NOT_FOUND:
		errcode = ItemNotFound
	case rpc.Code_CODE_LOCKED:
		errcode = ItemIsLocked
	case rpc.Code_CODE_INVALID_ARGUMENT:
		errcode = InvalidRequest
	case rpc.Code_CODE_FAILED_PRECONDITION:
		errcode = InvalidRequest
	default:
		errcode = GeneralException
	}
	return errcode
}
