// Licensed to the LF AI & Data foundation 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 compaction

import (
	"context"
	"sync"

	"github.com/samber/lo"
	"go.uber.org/zap"
	"golang.org/x/sync/semaphore"

	"github.com/milvus-io/milvus/pkg/log"
	"github.com/milvus-io/milvus/pkg/proto/datapb"
	"github.com/milvus-io/milvus/pkg/util/merr"
	"github.com/milvus-io/milvus/pkg/util/paramtable"
	"github.com/milvus-io/milvus/pkg/util/typeutil"
)

const (
	maxTaskQueueNum    = 1024
	maxParallelTaskNum = 10
)

type Executor interface {
	Start(ctx context.Context)
	Execute(task Compactor) (bool, error)
	Slots() int64
	RemoveTask(planID int64)
	GetResults(planID int64) []*datapb.CompactionPlanResult
	DiscardByDroppedChannel(channel string)
	DiscardPlan(channel string)
}

type executor struct {
	executing          *typeutil.ConcurrentMap[int64, Compactor]                    // planID to compactor
	completedCompactor *typeutil.ConcurrentMap[int64, Compactor]                    // planID to compactor
	completed          *typeutil.ConcurrentMap[int64, *datapb.CompactionPlanResult] // planID to CompactionPlanResult
	taskCh             chan Compactor
	taskSem            *semaphore.Weighted             // todo remove this, unify with slot logic
	dropped            *typeutil.ConcurrentSet[string] // vchannel dropped
	usingSlots         int64
	slotMu             sync.RWMutex

	// To prevent concurrency of release channel and compaction get results
	// all released channel's compaction tasks will be discarded
	resultGuard sync.RWMutex
}

func NewExecutor() *executor {
	return &executor{
		executing:          typeutil.NewConcurrentMap[int64, Compactor](),
		completedCompactor: typeutil.NewConcurrentMap[int64, Compactor](),
		completed:          typeutil.NewConcurrentMap[int64, *datapb.CompactionPlanResult](),
		taskCh:             make(chan Compactor, maxTaskQueueNum),
		taskSem:            semaphore.NewWeighted(maxParallelTaskNum),
		dropped:            typeutil.NewConcurrentSet[string](),
		usingSlots:         0,
	}
}

func (e *executor) Execute(task Compactor) (bool, error) {
	e.slotMu.Lock()
	defer e.slotMu.Unlock()
	if paramtable.Get().DataNodeCfg.SlotCap.GetAsInt64()-e.usingSlots >= task.GetSlotUsage() {
		newSlotUsage := task.GetSlotUsage()
		// compatible for old datacoord or unexpected request
		if task.GetSlotUsage() <= 0 {
			switch task.GetCompactionType() {
			case datapb.CompactionType_ClusteringCompaction:
				newSlotUsage = paramtable.Get().DataCoordCfg.ClusteringCompactionSlotUsage.GetAsInt64()
			case datapb.CompactionType_MixCompaction:
				newSlotUsage = paramtable.Get().DataCoordCfg.MixCompactionSlotUsage.GetAsInt64()
			case datapb.CompactionType_Level0DeleteCompaction:
				newSlotUsage = paramtable.Get().DataCoordCfg.L0DeleteCompactionSlotUsage.GetAsInt64()
			}
			log.Warn("illegal task slot usage, change it to a default value", zap.Int64("illegalSlotUsage", task.GetSlotUsage()), zap.Int64("newSlotUsage", newSlotUsage))
		}
		e.usingSlots = e.usingSlots + newSlotUsage
	} else {
		return false, merr.WrapErrDataNodeSlotExhausted()
	}
	_, ok := e.executing.GetOrInsert(task.GetPlanID(), task)
	if ok {
		log.Warn("duplicated compaction task",
			zap.Int64("planID", task.GetPlanID()),
			zap.String("channel", task.GetChannelName()))
		return false, merr.WrapErrDuplicatedCompactionTask()
	}
	e.taskCh <- task
	return true, nil
}

func (e *executor) Slots() int64 {
	return paramtable.Get().DataNodeCfg.SlotCap.GetAsInt64() - e.getUsingSlots()
}

func (e *executor) getUsingSlots() int64 {
	e.slotMu.RLock()
	defer e.slotMu.RUnlock()
	return e.usingSlots
}

func (e *executor) toCompleteState(task Compactor) {
	task.Complete()
	e.getAndRemoveExecuting(task.GetPlanID())
}

func (e *executor) getAndRemoveExecuting(planID typeutil.UniqueID) (Compactor, bool) {
	task, ok := e.executing.GetAndRemove(planID)
	if ok {
		e.slotMu.Lock()
		e.usingSlots = e.usingSlots - task.GetSlotUsage()
		e.slotMu.Unlock()
	}
	return task, ok
}

func (e *executor) RemoveTask(planID int64) {
	e.completed.GetAndRemove(planID)
	task, loaded := e.completedCompactor.GetAndRemove(planID)
	if loaded {
		log.Info("Compaction task removed", zap.Int64("planID", planID), zap.String("channel", task.GetChannelName()))
	}
}

func (e *executor) Start(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			return
		case task := <-e.taskCh:
			err := e.taskSem.Acquire(ctx, 1)
			if err != nil {
				return
			}
			go func() {
				defer e.taskSem.Release(1)
				e.executeTask(task)
			}()
		}
	}
}

func (e *executor) executeTask(task Compactor) {
	log := log.With(
		zap.Int64("planID", task.GetPlanID()),
		zap.Int64("Collection", task.GetCollection()),
		zap.String("channel", task.GetChannelName()),
	)

	defer func() {
		e.toCompleteState(task)
	}()

	log.Info("start to execute compaction")

	result, err := task.Compact()
	if err != nil {
		log.Warn("compaction task failed", zap.Error(err))
		return
	}
	e.completed.Insert(result.GetPlanID(), result)
	e.completedCompactor.Insert(result.GetPlanID(), task)

	log.Info("end to execute compaction")
}

func (e *executor) stopTask(planID int64) {
	task, loaded := e.getAndRemoveExecuting(planID)
	if loaded {
		log.Warn("compaction executor stop task", zap.Int64("planID", planID), zap.String("vChannelName", task.GetChannelName()))
		task.Stop()
	}
}

func (e *executor) isValidChannel(channel string) bool {
	// if vchannel marked dropped, compaction should not proceed
	return !e.dropped.Contain(channel)
}

func (e *executor) DiscardByDroppedChannel(channel string) {
	e.dropped.Insert(channel)
	e.DiscardPlan(channel)
}

func (e *executor) DiscardPlan(channel string) {
	e.resultGuard.Lock()
	defer e.resultGuard.Unlock()

	e.executing.Range(func(planID int64, task Compactor) bool {
		if task.GetChannelName() == channel {
			e.stopTask(planID)
		}
		return true
	})

	// remove all completed plans of channel
	e.completed.Range(func(planID int64, result *datapb.CompactionPlanResult) bool {
		if result.GetChannel() == channel {
			e.RemoveTask(planID)
			log.Info("remove compaction plan and results",
				zap.String("channel", channel),
				zap.Int64("planID", planID))
		}
		return true
	})
}

func (e *executor) GetResults(planID int64) []*datapb.CompactionPlanResult {
	if planID != 0 {
		result := e.getCompactionResult(planID)
		return []*datapb.CompactionPlanResult{result}
	}
	return e.getAllCompactionResults()
}

func (e *executor) getCompactionResult(planID int64) *datapb.CompactionPlanResult {
	e.resultGuard.RLock()
	defer e.resultGuard.RUnlock()
	_, ok := e.executing.Get(planID)
	if ok {
		result := &datapb.CompactionPlanResult{
			State:  datapb.CompactionTaskState_executing,
			PlanID: planID,
		}
		return result
	}
	result, ok2 := e.completed.Get(planID)
	if !ok2 {
		return &datapb.CompactionPlanResult{
			PlanID: planID,
			State:  datapb.CompactionTaskState_failed,
		}
	}
	return result
}

func (e *executor) getAllCompactionResults() []*datapb.CompactionPlanResult {
	e.resultGuard.RLock()
	defer e.resultGuard.RUnlock()
	var (
		executing          []int64
		completed          []int64
		completedLevelZero []int64
	)
	results := make([]*datapb.CompactionPlanResult, 0)
	// get executing results
	e.executing.Range(func(planID int64, task Compactor) bool {
		executing = append(executing, planID)
		results = append(results, &datapb.CompactionPlanResult{
			State:  datapb.CompactionTaskState_executing,
			PlanID: planID,
		})
		return true
	})

	// get completed results
	e.completed.Range(func(planID int64, result *datapb.CompactionPlanResult) bool {
		completed = append(completed, planID)
		results = append(results, result)

		if result.GetType() == datapb.CompactionType_Level0DeleteCompaction {
			completedLevelZero = append(completedLevelZero, planID)
		}
		return true
	})

	// remove level zero results
	lo.ForEach(completedLevelZero, func(planID int64, _ int) {
		e.completed.Remove(planID)
		e.completedCompactor.Remove(planID)
	})

	if len(results) > 0 {
		log.Info("DataNode Compaction results",
			zap.Int64s("executing", executing),
			zap.Int64s("completed", completed),
			zap.Int64s("completed levelzero", completedLevelZero),
		)
	}

	return results
}
