/*
 * Copyright 2021 CloudWeGo Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package kerrors

import (
	"errors"
	"fmt"
	"io"
	"os"
	"strings"
)

// Basic error types
var (
	ErrInternalException  = &basicError{"internal exception"}
	ErrServiceDiscovery   = &basicError{"service discovery error"}
	ErrGetConnection      = &basicError{"get connection error"}
	ErrLoadbalance        = &basicError{"loadbalance error"}
	ErrNoMoreInstance     = &basicError{"no more instances to retry"}
	ErrRPCTimeout         = &basicError{"rpc timeout"}
	ErrCanceledByBusiness = &basicError{"canceled by business"}
	ErrTimeoutByBusiness  = &basicError{"timeout by business"}
	ErrACL                = &basicError{"request forbidden"}
	ErrCircuitBreak       = &basicError{"forbidden by circuitbreaker"}
	ErrRemoteOrNetwork    = &basicError{"remote or network error"}
	ErrOverlimit          = &basicError{"request over limit"}
	ErrPanic              = &basicError{"panic"}
	ErrBiz                = &basicError{"biz error"}

	ErrRetry = &basicError{"retry error"}
	// ErrRoute happens when router fail to route this call
	ErrRoute = &basicError{"rpc route failed"}
	// ErrPayloadValidation happens when payload validation failed
	ErrPayloadValidation = &basicError{"payload validation error"}
)

// More detailed error types
var (
	ErrNotSupported         = ErrInternalException.WithCause(errors.New("operation not supported"))
	ErrNoResolver           = ErrInternalException.WithCause(errors.New("no resolver available"))
	ErrNoDestService        = ErrInternalException.WithCause(errors.New("no dest service"))
	ErrNoDestAddress        = ErrInternalException.WithCause(errors.New("no dest address"))
	ErrNoConnection         = ErrInternalException.WithCause(errors.New("no connection available"))
	ErrConnOverLimit        = ErrOverlimit.WithCause(errors.New("to many connections"))
	ErrQPSOverLimit         = ErrOverlimit.WithCause(errors.New("request too frequent"))
	ErrNoIvkRequest         = ErrInternalException.WithCause(errors.New("invoker request not set"))
	ErrServiceCircuitBreak  = ErrCircuitBreak.WithCause(errors.New("service circuitbreak"))
	ErrInstanceCircuitBreak = ErrCircuitBreak.WithCause(errors.New("instance circuitbreak"))
	ErrNoInstance           = ErrServiceDiscovery.WithCause(errors.New("no instance available"))
)

// ErrNonExistentMethod is used when the method is not found in a service info.
func ErrNonExistentMethod(svcName, method string) error {
	return ErrInternalException.WithCause(fmt.Errorf("non-existent method, service: %s, method: %s", svcName, method))
}

// ErrNotStreamingMethod is used when the method is not a streaming method in a service info.
func ErrNotStreamingMethod(svcName, method string) error {
	return ErrInternalException.WithCause(fmt.Errorf("not a streaming method, service: %s, method: %s", svcName, method))
}

// ErrNotUnaryMethod is used when the method is not a unary method in a service info.
func ErrNotUnaryMethod(svcName, method string) error {
	return ErrInternalException.WithCause(fmt.Errorf("not a unary method, service: %s, method: %s", svcName, method))
}

type basicError struct {
	message string
}

// Error implements the error interface.
func (be *basicError) Error() string {
	return be.message
}

// WithCause creates a detailed error which attach the given cause to current error.
func (be *basicError) WithCause(cause error) error {
	return &DetailedError{basic: be, cause: cause}
}

// WithCauseAndStack creates a detailed error which attach the given cause to current error and wrap stack.
func (be *basicError) WithCauseAndStack(cause error, stack string) error {
	return &DetailedError{basic: be, cause: cause, stack: stack}
}

// WithCauseAndExtraMsg creates a detailed error which attach the given cause to current error and wrap extra msg to supply error msg.
func (be *basicError) WithCauseAndExtraMsg(cause error, extraMsg string) error {
	return &DetailedError{basic: be, cause: cause, extraMsg: extraMsg}
}

// Timeout supports the os.IsTimeout checking.
func (be *basicError) Timeout() bool {
	return be == ErrRPCTimeout
}

// DetailedError contains more information.
type DetailedError struct {
	basic    *basicError
	cause    error
	stack    string
	extraMsg string
}

// Error implements the error interface.
func (de *DetailedError) Error() string {
	msg := appendErrMsg(de.basic.Error(), de.extraMsg)
	if de.cause != nil {
		return msg + ": " + de.cause.Error()
	}
	return msg
}

// Format the error.
func (de *DetailedError) Format(s fmt.State, verb rune) {
	switch verb {
	case 'v':
		if s.Flag('+') {
			msg := appendErrMsg(de.basic.Error(), de.extraMsg)
			_, _ = io.WriteString(s, msg)
			if de.cause != nil {
				_, _ = fmt.Fprintf(s, ": %+v", de.cause)
			}
			return
		}
		fallthrough
	case 's', 'q':
		_, _ = io.WriteString(s, de.Error())
	}
}

// ErrorType returns the basic error type.
func (de *DetailedError) ErrorType() error {
	return de.basic
}

// Unwrap returns the cause of detailed error.
func (de *DetailedError) Unwrap() error {
	return de.cause
}

// Is returns if the given error matches the current error.
func (de *DetailedError) Is(target error) bool {
	return de == target || de.basic == target || errors.Is(de.cause, target)
}

// As returns if the given target matches the current error, if so sets
// target to the error value and returns true
func (de *DetailedError) As(target interface{}) bool {
	if errors.As(de.basic, target) {
		return true
	}
	return errors.As(de.cause, target)
}

// Timeout supports the os.IsTimeout checking.
func (de *DetailedError) Timeout() bool {
	return de.basic == ErrRPCTimeout || os.IsTimeout(de.cause)
}

// Stack record stack info
func (de *DetailedError) Stack() string {
	return de.stack
}

// WithExtraMsg to add extra msg to supply error msg
func (de *DetailedError) WithExtraMsg(extraMsg string) {
	de.extraMsg = extraMsg
}

func appendErrMsg(errMsg, extra string) string {
	if extra == "" {
		return errMsg
	}
	var strBuilder strings.Builder
	strBuilder.Grow(len(errMsg) + len(extra) + 2)
	strBuilder.WriteString(errMsg)
	strBuilder.WriteByte('[')
	strBuilder.WriteString(extra)
	strBuilder.WriteByte(']')
	return strBuilder.String()
}

// IsKitexError reports whether the given err is an error generated by kitex.
func IsKitexError(err error) bool {
	if _, ok := err.(*basicError); ok {
		return true
	}

	if _, ok := err.(*DetailedError); ok {
		return true
	}
	return false
}

// TimeoutCheckFunc is used to check whether the given err is a timeout error.
var TimeoutCheckFunc func(err error) bool

// IsTimeoutError check if the error is timeout
func IsTimeoutError(err error) bool {
	if TimeoutCheckFunc != nil {
		return TimeoutCheckFunc(err)
	}
	return errors.Is(err, ErrRPCTimeout)
}
