/******************************************************************************
 * Copyright (c)  2023 PingCAP, Inc.                                          *
 * Licensed 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 regionalservice

import (
	"container/heap"
	"sync"
	"time"

	"github.com/tidbcloud/observability/pkg/common/structs"
)

type overLayRuleCache struct {
	items               map[string]*item
	mux                 sync.RWMutex
	priorityQueue       *priorityQueue
	expireTime          time.Duration
	sleepTime           time.Duration
	maxItems            int
	maxDeletionPerCycle int
}

type item struct {
	rules      []structs.AlertRuleGroup
	key        string
	expireAt   time.Time
	queueIndex int
}

func (item *item) expired() bool {
	return item.expireAt.Before(time.Now())
}

var ruleQueryCache overLayRuleCache

func InitAlertRuleCache(cacheConfig structs.OverlayAlertRuleCacheConfig) {
	queue := &priorityQueue{}
	heap.Init(queue)
	ruleQueryCache = overLayRuleCache{
		items:               make(map[string]*item),
		expireTime:          cacheConfig.ExpireTime,
		maxItems:            cacheConfig.MaxItems,
		priorityQueue:       queue,
		sleepTime:           cacheConfig.SleepTime,
		maxDeletionPerCycle: cacheConfig.MaxDeletionPerCycle,
	}
	go ruleQueryCache.startExpirationProcessing()
}

func (c *overLayRuleCache) startExpirationProcessing() {
	timer := time.NewTimer(c.expireTime)
	for {
		var sleepTime time.Duration
		sleepTime = c.sleepTime
		c.mux.Lock()
		if c.priorityQueue.Len() > 0 {
			sleepTime = time.Until(c.priorityQueue.items[0].expireAt)
			if sleepTime < 1 {
				sleepTime = c.sleepTime
			}
		}

		c.mux.Unlock()
		timer.Reset(sleepTime)

		<-timer.C
		timer.Stop()
		c.mux.Lock()
		if c.priorityQueue.Len() == 0 {
			c.mux.Unlock()
			continue
		}

		// index will only be advanced if the current entry will not be evicted
		i := 0
		maxDeletedEveryTime := c.maxDeletionPerCycle
		for item := c.priorityQueue.items[i]; item.expired(); item = c.priorityQueue.items[i] {
			maxDeletedEveryTime--
			c.priorityQueue.remove(item)
			delete(c.items, item.key)

			if c.priorityQueue.Len() == 0 || maxDeletedEveryTime <= 0 {
				break
			}
		}
		c.mux.Unlock()
	}
}

func (c *overLayRuleCache) set(key string, data []structs.AlertRuleGroup) {
	c.mux.Lock()
	defer c.mux.Unlock()
	if len(c.items) > c.maxItems {
		return
	}
	if c.items == nil {
		return
	}
	value := item{
		rules:    data,
		key:      key,
		expireAt: time.Now().Add(c.expireTime),
	}

	_, exists := c.items[key]

	c.items[key] = &value
	if exists {
		c.priorityQueue.update(c.items[key])
	} else {
		c.priorityQueue.push(c.items[key])
	}

}

func (c *overLayRuleCache) get(key string) ([]structs.AlertRuleGroup, bool) {
	c.mux.RLock()
	defer c.mux.RUnlock()
	d, ok := c.items[key]
	if !ok {
		return nil, ok
	}
	return d.rules, ok
}

func (c *overLayRuleCache) delete(key string) {
	c.mux.Lock()
	defer c.mux.Unlock()
	object, exists := c.items[key]
	if !exists {
		return
	}
	delete(c.items, object.key)
	c.priorityQueue.remove(object)
}

func (pq *priorityQueue) update(item *item) {
	heap.Fix(pq, item.queueIndex)
}

type priorityQueue struct {
	items []*item
}

func (pq *priorityQueue) push(item *item) {
	heap.Push(pq, item)
}

//nolint:unused
func (pq *priorityQueue) pop() *item {
	if pq.Len() == 0 {
		return nil
	}
	return heap.Pop(pq).(*item)
}

func (pq *priorityQueue) remove(item *item) {
	heap.Remove(pq, item.queueIndex)
}

func (pq priorityQueue) Len() int {
	length := len(pq.items)
	return length
}

func (pq priorityQueue) Less(i, j int) bool {
	if pq.items[i].expireAt.IsZero() {
		return false
	}
	if pq.items[j].expireAt.IsZero() {
		return true
	}
	return pq.items[i].expireAt.Before(pq.items[j].expireAt)
}

func (pq priorityQueue) Swap(i, j int) {
	pq.items[i], pq.items[j] = pq.items[j], pq.items[i]
	pq.items[i].queueIndex = i
	pq.items[j].queueIndex = j
}

func (pq *priorityQueue) Push(x interface{}) {
	item := x.(*item)
	item.queueIndex = len(pq.items)
	pq.items = append(pq.items, item)
}

func (pq *priorityQueue) Pop() interface{} {
	old := pq.items
	n := len(old)
	item := old[n-1]
	item.queueIndex = -1
	pq.items = old[0 : n-1]
	return item
}
