/*
 * 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 remote

import (
	"errors"
	"fmt"

	"github.com/cloudwego/kitex/pkg/kerrors"
)

// corresponding with thrift TApplicationException, cannot change it
const (
	UnknownApplicationException = 0
	UnknownMethod               = 1
	InvalidMessageTypeException = 2
	WrongMethodName             = 3
	BadSequenceID               = 4
	MissingResult               = 5
	InternalError               = 6
	ProtocolError               = 7
	InvalidTransform            = 8
	InvalidProtocol             = 9
	UnsupportedClientType       = 10
	// kitex's own type id from number 20
	UnknownService = 20
	NoServiceName  = 21
)

var defaultTransErrorMessage = map[int32]string{
	UnknownApplicationException: "unknown application exception",
	UnknownMethod:               "unknown method",
	InvalidMessageTypeException: "invalid message type",
	WrongMethodName:             "wrong method name",
	BadSequenceID:               "bad sequence ID",
	MissingResult:               "missing result",
	InternalError:               "unknown internal error",
	ProtocolError:               "unknown protocol error",
	InvalidTransform:            "Invalid transform",
	InvalidProtocol:             "Invalid protocol",
	UnsupportedClientType:       "Unsupported client type",
	UnknownService:              "unknown service",
}

// TransError is the error that can be transmitted, it corresponds to TApplicationException in Thrift
type TransError struct {
	message string
	typeID  int32
	rawErr  error
}

// Error implements the error interface.
func (e TransError) Error() string {
	if e.message != "" {
		return e.message
	}
	return defaultTransErrorMessage[e.typeID]
}

// TypeID return err type id
func (e TransError) TypeID() int32 {
	return e.typeID
}

// Unwrap the transError to expose raw error
func (e TransError) Unwrap() error {
	return e.rawErr
}

// Is to check if inner error that transError wrap is target error
func (e TransError) Is(target error) bool {
	return e == target || errors.Is(e.rawErr, target)
}

// AppendMessage append extra msg for TransError
func (e TransError) AppendMessage(extraMsg string) *TransError {
	if extraMsg == "" {
		return &e
	}
	msg := fmt.Sprintf("%s %s", e.message, extraMsg)
	// should not modify origin error
	return &TransError{message: msg, typeID: e.typeID, rawErr: e.rawErr}
}

// NewTransErrorWithMsg to build TransError with typeID and errMsg
func NewTransErrorWithMsg(typeID int32, message string) *TransError {
	return &TransError{message: message, typeID: typeID}
}

// NewTransError to build TransError with typeID and rawErr.
// rawErr can be used by errors.Is(target) to check err type, like read timeout.
func NewTransError(typeID int32, err error) *TransError {
	if e, ok := err.(*TransError); ok {
		return e
	}
	// biz error should add biz info which is convenient to be recognized by client side
	var dErr *kerrors.DetailedError
	if errors.As(err, &dErr) && dErr.Is(kerrors.ErrBiz) {
		if e, ok := dErr.Unwrap().(*TransError); ok {
			e = e.AppendMessage(fmt.Sprintf("[%s]", kerrors.ErrBiz.Error()))
			return e
		}
	}
	if typeID == InternalError {
		// try to get more specific err type if typeID is InternalError
		if tID, ok := err.(TypeId); ok {
			typeID = tID.TypeId()
		} else if tID, ok := err.(TypeID); ok {
			typeID = tID.TypeID()
		}
	}
	return &TransError{message: err.Error(), typeID: typeID, rawErr: err}
}

// TypeId is used to assert Error with has 'TypeID() int32'
type TypeID interface {
	TypeID() int32
}

// TypeId is used to assert Error with has 'TypeId() int32' like TApplicationException
type TypeId interface {
	TypeId() int32
}
