/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 base implements invoker for the manipulation of cluster strategy.
package base

import (
	"github.com/dubbogo/gost/log/logger"

	perrors "github.com/pkg/errors"

	"go.uber.org/atomic"
)

import (
	"dubbo.apache.org/dubbo-go/v3/cluster/directory"
	"dubbo.apache.org/dubbo-go/v3/cluster/loadbalance"
	"dubbo.apache.org/dubbo-go/v3/common"
	"dubbo.apache.org/dubbo-go/v3/common/constant"
	"dubbo.apache.org/dubbo-go/v3/common/extension"
	"dubbo.apache.org/dubbo-go/v3/protocol/base"
)

type BaseClusterInvoker struct {
	Directory      directory.Directory
	AvailableCheck bool
	Destroyed      *atomic.Bool
	StickyInvoker  base.Invoker
}

func NewBaseClusterInvoker(directory directory.Directory) BaseClusterInvoker {
	return BaseClusterInvoker{
		Directory:      directory,
		AvailableCheck: true,
		Destroyed:      atomic.NewBool(false),
	}
}

func (invoker *BaseClusterInvoker) GetURL() *common.URL {
	return invoker.Directory.GetURL()
}

func (invoker *BaseClusterInvoker) Destroy() {
	// this is must atom operation
	if invoker.Destroyed.CompareAndSwap(false, true) {
		invoker.Directory.Destroy()
	}
}

func (invoker *BaseClusterInvoker) IsAvailable() bool {
	if invoker.StickyInvoker != nil {
		return invoker.StickyInvoker.IsAvailable()
	}
	return invoker.Directory.IsAvailable()
}

// CheckInvokers checks invokers' status if is available or not
func (invoker *BaseClusterInvoker) CheckInvokers(invokers []base.Invoker, invocation base.Invocation) error {
	if len(invokers) == 0 {
		ip := common.GetLocalIp()
		return perrors.Errorf("Failed to invoke the method %v. No provider available for the service %v from "+
			"registry %v on the consumer %v using the dubbo version %v .Please check if the providers have been started and registered.",
			invocation.MethodName(), invoker.Directory.GetURL().SubURL.Key(), invoker.Directory.GetURL().String(), ip, constant.Version)
	}
	return nil
}

// CheckWhetherDestroyed checks if cluster invoker was destroyed or not
func (invoker *BaseClusterInvoker) CheckWhetherDestroyed() error {
	if invoker.Destroyed.Load() {
		ip := common.GetLocalIp()
		return perrors.Errorf("Rpc cluster invoker for %v on consumer %v use dubbo version %v is now destroyed! can not invoke any more. ",
			invoker.Directory.GetURL().Service(), ip, constant.Version)
	}
	return nil
}

func (invoker *BaseClusterInvoker) DoSelect(lb loadbalance.LoadBalance, invocation base.Invocation, invokers []base.Invoker, invoked []base.Invoker) base.Invoker {
	var selectedInvoker base.Invoker
	if len(invokers) <= 0 {
		return selectedInvoker
	}

	url := invokers[0].GetURL()
	sticky := url.GetParamBool(constant.StickyKey, false)
	// Get the service method sticky config if have
	sticky = url.GetMethodParamBool(invocation.MethodName(), constant.StickyKey, sticky)

	if invoker.StickyInvoker != nil && !isInvoked(invoker.StickyInvoker, invokers) {
		invoker.StickyInvoker = nil
	}

	if sticky && invoker.AvailableCheck &&
		invoker.StickyInvoker != nil && invoker.StickyInvoker.IsAvailable() &&
		(invoked == nil || !isInvoked(invoker.StickyInvoker, invoked)) {
		return invoker.StickyInvoker
	}

	selectedInvoker = invoker.doSelectInvoker(lb, invocation, invokers, invoked)
	if sticky {
		invoker.StickyInvoker = selectedInvoker
	}
	return selectedInvoker
}

func (invoker *BaseClusterInvoker) doSelectInvoker(lb loadbalance.LoadBalance, invocation base.Invocation, invokers []base.Invoker, invoked []base.Invoker) base.Invoker {
	if len(invokers) == 0 {
		return nil
	}
	go base.TryRefreshBlackList()
	if len(invokers) == 1 {
		if invokers[0].IsAvailable() {
			return invokers[0]
		}
		base.SetInvokerUnhealthyStatus(invokers[0])
		logger.Errorf("the invokers of %s is nil. ", invokers[0].GetURL().ServiceKey())
		return nil
	}

	selectedInvoker := lb.Select(invokers, invocation)

	// judge if the selected Invoker is invoked and available
	if (!selectedInvoker.IsAvailable() && invoker.AvailableCheck) || isInvoked(selectedInvoker, invoked) {
		base.SetInvokerUnhealthyStatus(selectedInvoker)
		otherInvokers := getOtherInvokers(invokers, selectedInvoker)
		// do reselect
		for i := 0; i < 3; i++ {
			if len(otherInvokers) == 0 {
				// no other ivk to reselect, return to fallback
				break
			}
			reselectedInvoker := lb.Select(otherInvokers, invocation)
			if isInvoked(reselectedInvoker, invoked) {
				otherInvokers = getOtherInvokers(otherInvokers, reselectedInvoker)
				continue
			}
			if !reselectedInvoker.IsAvailable() {
				logger.Infof("the invoker of %s is not available, maybe some network error happened or the server is shutdown.",
					invoker.GetURL().Ip)
				base.SetInvokerUnhealthyStatus(reselectedInvoker)
				otherInvokers = getOtherInvokers(otherInvokers, reselectedInvoker)
				continue
			}
			return reselectedInvoker
		}
	}

	return selectedInvoker
}

func isInvoked(selectedInvoker base.Invoker, invoked []base.Invoker) bool {
	for _, i := range invoked {
		if i == selectedInvoker {
			return true
		}
	}
	return false
}

func GetLoadBalance(invoker base.Invoker, methodName string) loadbalance.LoadBalance {
	url := invoker.GetURL()

	// Get the service loadbalance config
	lb := url.GetParam(constant.LoadbalanceKey, constant.DefaultLoadBalance)

	// Get the service method loadbalance config if have
	if v := url.GetMethodParam(methodName, constant.LoadbalanceKey, ""); len(v) > 0 {
		lb = v
	}
	return extension.GetLoadbalance(lb)
}

func getOtherInvokers(invokers []base.Invoker, invoker base.Invoker) []base.Invoker {
	otherInvokers := make([]base.Invoker, 0)
	for _, i := range invokers {
		if i != invoker {
			otherInvokers = append(otherInvokers, i)
		}
	}
	return otherInvokers
}
