package gitaly

import (
	"errors"
	"fmt"
)

type ErrorCode int

const (
	// UnknownError - what happened is unknown
	UnknownError ErrorCode = iota
	// NotFound - file/directory/ref was not found
	NotFound
	// FileTooBig - file is too big
	FileTooBig
	// RPCError - gRPC returned an error
	RPCError
	// ProtocolError - protocol violation, an unexpected situation occurred.
	ProtocolError
	// UnexpectedTreeEntryType - returned when TreeEntryResponse has an unexpected type.
	UnexpectedTreeEntryType
	// InvalidArgument - returned when Gitaly responds with InvalidArgument status code.
	InvalidArgument
)

func (e ErrorCode) String() string {
	switch e {
	case UnknownError:
		return "UnknownErr"
	case NotFound:
		return "NotFound"
	case FileTooBig:
		return "FileTooBig"
	case RPCError:
		return "RPCError"
	case ProtocolError:
		return "ProtocolError"
	case UnexpectedTreeEntryType:
		return "UnexpectedTreeEntryType"
	case InvalidArgument:
		return "InvalidArgument"
	default:
		return fmt.Sprintf("invalid ErrorCode: %d", e)
	}
}

type Error struct {
	Code    ErrorCode
	Cause   error
	Message string
	// RPCName is the name of gRPC method that failed.
	RPCName string
	// Path contains name of the file or directory the operation was being carried on.
	Path string
}

func NewNotFoundError(rpcName, path string) error {
	return &Error{
		Code:    NotFound,
		Message: "file/directory/ref not found",
		RPCName: rpcName,
		Path:    path,
	}
}

func NewFileTooBigError(err error, rpcName, path string) error {
	return &Error{
		Code:    FileTooBig,
		Cause:   err,
		Message: "file is too big",
		RPCName: rpcName,
		Path:    path,
	}
}

func NewUnexpectedTreeEntryTypeError(rpcName, path string) error {
	return &Error{
		Code:    UnexpectedTreeEntryType,
		Message: "file is not a usual file",
		RPCName: rpcName,
		Path:    path,
	}
}

func NewRPCError(err error, rpcName, path string) error {
	return &Error{
		Code:    RPCError,
		Cause:   err,
		Message: "RPC failed",
		RPCName: rpcName,
		Path:    path,
	}
}

func NewProtocolError(err error, message, rpcName, path string) error {
	return &Error{
		Code:    ProtocolError,
		Cause:   err,
		Message: message,
		RPCName: rpcName,
		Path:    path,
	}
}

func NewInvalidArgument(err error, rpcName, path string) error {
	return &Error{
		Code:    InvalidArgument,
		Cause:   err,
		Message: "invalid argument",
		RPCName: rpcName,
		Path:    path,
	}
}

func (e *Error) Error() string {
	format := "%s"
	args := []any{e.Code}
	if e.RPCName != "" {
		format += ": %s"
		args = append(args, e.RPCName)
	}
	format += ": %s"
	args = append(args, e.Message)
	if e.Path != "" {
		format += ": %s"
		args = append(args, e.Path)
	}
	if e.Cause != nil {
		format += ": %v"
		args = append(args, e.Cause)
	}
	return fmt.Sprintf(format, args...)
}

func (e *Error) Unwrap() error {
	return e.Cause
}

func ErrorCodeFromError(err error) ErrorCode {
	var e *Error
	if !errors.As(err, &e) {
		return UnknownError
	}
	return e.Code
}
