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

import (
	"context"
	"encoding/json"
	"fmt"
	"slices"
	"sort"
	"strings"
	"sync"
	"time"

	"github.com/pkg/errors"

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

type roundRobinSelector struct {
	schemaRegistry metadata.Repo
	nodeSelector   *pub.LabelSelector
	name           string
	lookupTable    []key
	nodes          []string
	mu             sync.RWMutex
}

func (r *roundRobinSelector) String() string {
	r.mu.RLock()
	defer r.mu.RUnlock()
	result := make(map[string]string)
	for _, entry := range r.lookupTable {
		copies := entry.replicas + 1
		for i := range copies {
			n, err := r.Pick(entry.group, "", entry.shardID, i)
			key := fmt.Sprintf("%s-%d-%d", entry.group, entry.shardID, i)
			if err != nil {
				result[key] = fmt.Sprintf("%v", err)
				continue
			}
			result[key] = n
		}
	}
	if len(result) < 1 {
		return ""
	}
	jsonBytes, err := json.Marshal(result)
	if err != nil {
		return fmt.Sprintf("%v", err)
	}
	return convert.BytesToString(jsonBytes)
}

// NewRoundRobinSelector creates a new round-robin selector.
func NewRoundRobinSelector(name string, schemaRegistry metadata.Repo) Selector {
	rrs := &roundRobinSelector{
		name:           name,
		nodes:          make([]string, 0),
		schemaRegistry: schemaRegistry,
		lookupTable:    make([]key, 0),
	}
	return rrs
}

func (r *roundRobinSelector) Name() string {
	return r.name
}

func (r *roundRobinSelector) SetNodeSelector(selector *pub.LabelSelector) {
	r.nodeSelector = selector
}

func (r *roundRobinSelector) PreRun(context.Context) error {
	r.schemaRegistry.RegisterHandler(r.name, schema.KindGroup, r)
	return nil
}

func (r *roundRobinSelector) OnAddOrUpdate(schemaMetadata schema.Metadata) {
	if schemaMetadata.Kind != schema.KindGroup {
		return
	}
	group, ok := schemaMetadata.Spec.(*commonv1.Group)
	if !ok || !validateGroup(group) {
		return
	}
	r.mu.Lock()
	defer r.mu.Unlock()
	r.removeGroup(group.Metadata.Name)
	for i := uint32(0); i < group.ResourceOpts.ShardNum; i++ {
		r.lookupTable = append(r.lookupTable, newKey(group.Metadata.Name, i, group.ResourceOpts.Replicas))
	}
	r.sortEntries()
}

func (r *roundRobinSelector) removeGroup(group string) {
	for i := 0; i < len(r.lookupTable); {
		if r.lookupTable[i].group == group {
			copy(r.lookupTable[i:], r.lookupTable[i+1:])
			r.lookupTable = r.lookupTable[:len(r.lookupTable)-1]
		} else {
			i++
		}
	}
}

func (r *roundRobinSelector) OnDelete(schemaMetadata schema.Metadata) {
	if schemaMetadata.Kind != schema.KindGroup {
		return
	}
	r.mu.Lock()
	defer r.mu.Unlock()
	group := schemaMetadata.Spec.(*commonv1.Group)
	r.removeGroup(group.Metadata.Name)
}

func (r *roundRobinSelector) OnInit(kinds []schema.Kind) (bool, []int64) {
	if len(kinds) != 1 {
		return false, nil
	}
	if kinds[0] != schema.KindGroup {
		return false, nil
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
	defer cancel()
	gg, err := r.schemaRegistry.GroupRegistry().ListGroup(ctx)
	if err != nil {
		panic(fmt.Sprintf("failed to list group: %v", err))
	}
	r.mu.Lock()
	defer r.mu.Unlock()
	var revision int64
	r.lookupTable = r.lookupTable[:0]
	for _, g := range gg {
		if !validateGroup(g) {
			continue
		}
		if g.Metadata.ModRevision > revision {
			revision = g.Metadata.ModRevision
		}
		for i := uint32(0); i < g.ResourceOpts.ShardNum; i++ {
			r.lookupTable = append(r.lookupTable, newKey(g.Metadata.Name, i, g.ResourceOpts.Replicas))
		}
	}
	r.sortEntries()
	return true, []int64{revision}
}

func (r *roundRobinSelector) AddNode(node *databasev1.Node) {
	if r.nodeSelector != nil && !r.nodeSelector.Matches(node.Labels) {
		return
	}
	r.mu.Lock()
	defer r.mu.Unlock()
	r.nodes = append(r.nodes, node.Metadata.Name)
	sort.StringSlice(r.nodes).Sort()
}

func (r *roundRobinSelector) RemoveNode(node *databasev1.Node) {
	r.mu.Lock()
	defer r.mu.Unlock()
	for i, n := range r.nodes {
		if n == node.Metadata.Name {
			r.nodes = append(r.nodes[:i], r.nodes[i+1:]...)
			break
		}
	}
}

func (r *roundRobinSelector) Pick(group, _ string, shardID, replicaID uint32) (string, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	k := key{group: group, shardID: shardID}
	if len(r.nodes) == 0 {
		return "", errors.New("no nodes available")
	}
	i := sort.Search(len(r.lookupTable), func(i int) bool {
		if r.lookupTable[i].group == group {
			return r.lookupTable[i].shardID >= shardID
		}
		return r.lookupTable[i].group > group
	})
	if i < len(r.lookupTable) && r.lookupTable[i].equal(k) {
		return r.selectNode(i, replicaID), nil
	}
	return "", fmt.Errorf("%s-%d is a unknown shard", group, shardID)
}

func (r *roundRobinSelector) sortEntries() {
	slices.SortFunc(r.lookupTable, func(a, b key) int {
		n := strings.Compare(a.group, b.group)
		if n != 0 {
			return n
		}
		return int(a.shardID) - int(b.shardID)
	})
}

func (r *roundRobinSelector) selectNode(index int, replicasID uint32) string {
	adjustedIndex := index + int(replicasID)
	return r.nodes[adjustedIndex%len(r.nodes)]
}

func validateGroup(group *commonv1.Group) bool {
	if group.Catalog == commonv1.Catalog_CATALOG_UNSPECIFIED {
		return false
	}
	if group.ResourceOpts == nil {
		return false
	}
	return true
}

type key struct {
	group    string
	shardID  uint32
	replicas uint32
}

func (k key) equal(other key) bool {
	return k.group == other.group && k.shardID == other.shardID
}

func newKey(group string, shardID, replicas uint32) key {
	return key{
		group:    group,
		shardID:  shardID,
		replicas: replicas,
	}
}
