// Licensed to 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. Apache Software Foundation (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 node provides node selector for liaison.
package node

import (
	"context"
	"fmt"
	"sync"

	"github.com/pkg/errors"
	"golang.org/x/exp/slices"

	databasev1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/database/v1"
	"github.com/apache/skywalking-banyandb/banyand/metadata/schema"
	"github.com/apache/skywalking-banyandb/banyand/queue/pub"
	"github.com/apache/skywalking-banyandb/pkg/run"
)

var (
	_ Selector = (*pickFirstSelector)(nil)

	// ErrNoAvailableNode will be returned if no node is available.
	ErrNoAvailableNode = errors.New("selector: no available node")
)

// Selector keeps all data nodes in the memory and can provide different algorithm to pick an available node.
//
//go:generate mockgen -destination=mock/node_selector_mock.go -package=mock github.com/apache/skywalking-banyandb/pkg/node Selector
type Selector interface {
	OnInit(kinds []schema.Kind) (bool, []int64)
	AddNode(node *databasev1.Node)
	RemoveNode(node *databasev1.Node)
	SetNodeSelector(selector *pub.LabelSelector)
	Pick(group, name string, shardID, replicaID uint32) (string, error)
	run.PreRunner
	fmt.Stringer
}

// NewPickFirstSelector returns a simple selector that always returns the first node if exists.
func NewPickFirstSelector() (Selector, error) {
	return &pickFirstSelector{
		nodeIDMap: make(map[string]struct{}),
	}, nil
}

// pickFirstSelector always pick the first node in the sorted node ids list.
type pickFirstSelector struct {
	nodeIDMap map[string]struct{}
	nodeIDs   []string
	mu        sync.RWMutex
}

// OnInit implements Selector.
func (p *pickFirstSelector) OnInit(_ []schema.Kind) (bool, []int64) {
	return true, []int64{0}
}

// SetNodeSelector implements Selector.
func (p *pickFirstSelector) SetNodeSelector(_ *pub.LabelSelector) {}

// String implements Selector.
func (p *pickFirstSelector) String() string {
	n, err := p.Pick("", "", 0, 0)
	if err != nil {
		return fmt.Sprintf("%v", err)
	}
	p.mu.Lock()
	defer p.mu.Unlock()
	return fmt.Sprintf("pick [%s] from %s", n, p.nodeIDs)
}

func (p *pickFirstSelector) PreRun(context.Context) error {
	return nil
}

func (p *pickFirstSelector) Name() string {
	return "pick-first"
}

func (p *pickFirstSelector) AddNode(node *databasev1.Node) {
	nodeID := node.GetMetadata().GetName()
	p.mu.RLock()
	if _, ok := p.nodeIDMap[nodeID]; ok {
		p.mu.RUnlock()
		return
	}
	p.mu.RUnlock()
	p.mu.Lock()
	defer p.mu.Unlock()
	p.nodeIDMap[nodeID] = struct{}{}
	p.nodeIDs = append(p.nodeIDs, nodeID)
	slices.Sort(p.nodeIDs)
}

func (p *pickFirstSelector) RemoveNode(node *databasev1.Node) {
	nodeID := node.GetMetadata().GetName()
	p.mu.RLock()
	if _, ok := p.nodeIDMap[nodeID]; !ok {
		p.mu.RUnlock()
		return
	}
	p.mu.RUnlock()
	p.mu.Lock()
	defer p.mu.Unlock()
	delete(p.nodeIDMap, nodeID)
	idx, ok := slices.BinarySearch(p.nodeIDs, nodeID)
	if !ok {
		return
	}
	p.nodeIDs = slices.Delete(p.nodeIDs, idx, idx+1)
}

func (p *pickFirstSelector) Pick(_, _ string, _, _ uint32) (string, error) {
	p.mu.RLock()
	defer p.mu.RUnlock()
	if len(p.nodeIDs) == 0 {
		return "", ErrNoAvailableNode
	}
	return p.nodeIDs[0], nil
}
