package errs

import (
	"context"
	"errors"
	"fmt"
	"log"
	"reflect"
	"strconv"
	"strings"

	"github.com/tidbcloud/pkgv2/bizerr"
	"go.buf.build/protocolbuffers/go/linuxgit/tidbcloud/base"
	"google.golang.org/grpc"
	"google.golang.org/grpc/metadata"
)

// SetBaseResp Set BaseResp automatically in case developer forget to pass a &base.BaseResp{} in response
func SetBaseResp(resp interface{}) *base.BaseResp {
	if resp == nil {
		return nil
	}

	kind := reflect.TypeOf(resp).Kind()
	if !(kind == reflect.Ptr || kind == reflect.Interface) {
		return nil
	}

	v := reflect.ValueOf(resp)
	if v.IsNil() {
		return nil
	}

	baseResp := v.Elem().FieldByName("BaseResp")
	if !baseResp.IsValid() { // don't have BaseReq field
		return nil
	}
	if baseResp.IsNil() { // developer forget to set this field
		baseResp.Set(reflect.New(baseResp.Type().Elem()))
	}

	r, ok := baseResp.Interface().(*base.BaseResp)
	if ok {
		return r
	}

	return nil
}

func GetBaseResp(resp interface{}) *base.BaseResp {
	if resp == nil {
		return nil
	}

	return SetBaseResp(resp)
}

func isBizErr(err error) bool {
	_, ok := err.(bizerr.BizError)
	return ok
}

func bizErrSetter(resp interface{}, err bizerr.BizError) (resp_ interface{}, err_ error) {
	typed := GetBaseResp(resp)
	if typed == nil {
		return resp, err
	}
	typed.ErrCode = int64(err.ErrCode())
	typed.ErrMsg = err.ErrMsg()
	return resp, nil
}

func tiErrSetter(resp interface{}, err Error) (resp_ interface{}, err_ error) {
	typed := GetBaseResp(resp)
	if typed == nil {
		return resp, err
	}
	SetTiErrToBaseResp(err, typed)
	return resp, nil
}

func UnaryServerInterceptor() grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
		resp, err := handler(ctx, req)
		SetBaseResp(resp)
		if err == nil {
			return resp, err
		}

		if !(isBizErr(err) || errors.Is(err, TiErr)) {
			return resp, err
		}

		// val will be invalid if type and value are both nil, val will be nil if value is nil but type is not nil.
		if val := reflect.ValueOf(resp); !val.IsValid() || val.IsNil() {
			tp := getRespType(ctx, info)
			if tp == nil {
				return resp, err
			}
			resp = reflect.New(tp).Interface()
		}

		if isBizErr(err) {
			be, _ := err.(bizerr.BizError)
			be = be.Redact()
			return bizErrSetter(resp, be)
		} else {
			te, ok := AsTiError(err)
			if !ok {
				log.Printf("failed to convert err %s to tierr", err)
			}
			if e := grpc.SetHeader(ctx, metadata.Pairs("x-http-code", fmt.Sprintf("%d", te.HTTPCode()))); e != nil {
				log.Printf("failed to set x-http-code header: %s", e)
			}
			return tiErrSetter(resp, te)
		}
	}
}

// getRespType get response type by get return type of method which reflected from unaryServerInfo.Server
// TODO(chenfei): opt, it's hack. more reasonable way is better
func getRespType(ctx context.Context, info *grpc.UnaryServerInfo) reflect.Type {
	fullMethodName, ok := grpc.Method(ctx)
	if !ok {
		return nil
	}
	pos := strings.LastIndex(fullMethodName, "/")
	if pos < 0 {
		return nil
	}
	methodName := fullMethodName[pos+1:]
	method := reflect.ValueOf(info.Server).MethodByName(methodName)
	if !method.IsValid() {
		return nil
	}
	// two values have been returned, the first is the return message of method which we need, the second is the error.
	tp := method.Type().Out(0).Elem()
	return tp
}

func SetTiErrToBaseResp(err Error, resp *base.BaseResp) {
	if resp == nil {
		return
	}
	resp.ErrCode = err.BizCode()
	if IsServerError(err) {
		resp.ErrMsg = "Internal Error"
	} else {
		resp.ErrMsg = err.ErrMsg()
	}
	if resp.Tags == nil {
		resp.Tags = make(map[string]string)
	}
	resp.Tags[bizErrorKeyOrigErrMsg] = err.ErrMsg()
	resp.Tags[bizErrorKeyHttpCode] = strconv.Itoa(err.HTTPCode())
	return
}

func NewBaseRespFromTiErr(err Error) *base.BaseResp {
	resp := &base.BaseResp{}
	SetTiErrToBaseResp(err, resp)
	return resp
}

func GetOrigErrMsgFromBaseResp(resp *base.BaseResp) string {
	if resp.Tags != nil {
		if val, ok := resp.Tags[bizErrorKeyOrigErrMsg]; ok {
			return val
		}
	}
	return resp.ErrMsg
}

func GetTiErrFromBaseResp(resp *base.BaseResp) Error {
	bizCode := resp.ErrCode
	errMsg := resp.ErrMsg
	httpCode := defaultBizErrorHttpCode
	if resp.Tags != nil {
		if val, ok := resp.Tags[bizErrorKeyHttpCode]; ok {
			httpCodeFromResp, err := strconv.Atoi(val)
			if err == nil {
				httpCode = httpCodeFromResp
			}
		}
	}
	return newTierr(TiErr, httpCode, bizCode, errMsg, nil, nil)
}
