package mfilesclient

import (
	"errors"
	"fmt"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/proto"

	"github.com/M-Files/mfiles-grpc-api/go/mfiles"
)

// Trailers of a failed call.
const (
	headerErrorStack = "mfiles-error-serialized-bin" // a serialized MFiles.RPCErrorInfoArray
	headerErrorText  = "mfiles-error-text-bin"       // the long error message, UTF-8
)

// M-Files error codes that programs often handle. Compare them with Error.HResult or Error.Has.
const (
	CodeNotFound     uint32 = 0x8004000B // "Not found."
	CodeAccessDenied uint32 = 0x800407E4 // "Access denied."
)

// [START error]

// Error is a failed M-Files call. Test for it with errors.As; status.Code(err) still gives the
// gRPC status code.
type Error struct {
	Status  *status.Status         // the gRPC status: Unknown for M-Files errors
	HResult uint32                 // the M-Files error code, e.g. 0x8004000B; 0 when unknown
	Codes   []uint32               // the codes of the error stack, the original error first
	Message string                 // the error message (the gRPC status message)
	Details string                 // the long error message with the server's error stack
	Stack   []*mfiles.RPCErrorInfo // the server's error stack, the original error first
}

func (e *Error) Error() string {
	if len(e.Codes) == 0 {
		return fmt.Sprintf("gRPC error %s: %s", e.Status.Code(), e.Message)
	}
	return fmt.Sprintf("M-Files error 0x%08X (%s): %s", e.HResult, e.Status.Code(), e.Message)
}

// SignedHResult is the error code as a signed 32-bit integer, e.g. -2147221493.
func (e *Error) SignedHResult() int32 { return int32(e.HResult) }

// GRPCStatus lets status.Code and status.FromError see through the *Error.
func (e *Error) GRPCStatus() *status.Status { return e.Status }

// Has reports whether code is anywhere in the error stack.
func (e *Error) Has(code uint32) bool {
	for _, c := range e.Codes {
		if c == code {
			return true
		}
	}
	return false
}

// AsError returns the *Error in err's chain, if there is one.
func AsError(err error) (*Error, bool) {
	var mfilesErr *Error
	ok := errors.As(err, &mfilesErr)
	return mfilesErr, ok
}

// decodeError turns a failed call into *Error, using the error trailers when present.
func decodeError(err error, trailer metadata.MD) error {
	if err == nil {
		return nil
	}
	st, ok := status.FromError(err)
	if !ok || st.Code() == codes.OK {
		return err
	}
	result := &Error{Status: st, Message: st.Message()}
	if values := trailer.Get(headerErrorText); len(values) > 0 {
		result.Details = values[0]
	}
	if values := trailer.Get(headerErrorStack); len(values) > 0 {
		stack := &mfiles.RPCErrorInfoArray{}
		if proto.Unmarshal([]byte(values[0]), stack) == nil {
			result.Stack = stack.GetValue()
		}
	}
	for _, info := range result.Stack {
		result.Codes = append(result.Codes, uint32(info.GetErrorCode()))
	}
	result.HResult = errorCode(result.Codes)
	return result
}

// errorPassThrough is the generic code of an error that is only passed up the stack; it does
// not say what went wrong.
const errorPassThrough = 0x80040001

// errorCode is the code of the top-level error, the last entry of the stack: the error the
// status message describes, such as 0x800407E4 for "Access denied." Entries with the generic
// code 0x80040001 are skipped.
func errorCode(stack []uint32) uint32 {
	for i := len(stack) - 1; i >= 0; i-- {
		if stack[i] != errorPassThrough {
			return stack[i]
		}
	}
	if len(stack) > 0 {
		return stack[len(stack)-1]
	}
	return 0
}

// [END error]
