// Copyright 2025 Cloudbase Solutions SRL
//
//	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 cache

import (
	"sync"
	"time"

	"github.com/cloudbase/garm/params"
)

var entityCache *EntityCache

func init() {
	ghEntityCache := &EntityCache{
		entities:  make(map[string]EntityItem),
		pools:     make(map[string]params.Pool),
		scalesets: make(map[uint]params.ScaleSet),
	}
	entityCache = ghEntityCache
}

type RunnerGroupEntry struct {
	RunnerGroupID int64
	time          time.Time
}

type EntityItem struct {
	Entity       params.ForgeEntity
	Pools        map[string]struct{}
	ScaleSets    map[uint]struct{}
	RunnerGroups map[string]RunnerGroupEntry
}

type EntityCache struct {
	mux sync.Mutex
	// entity IDs are UUID4s. It is highly unlikely they will collide (🤞).
	entities  map[string]EntityItem
	pools     map[string]params.Pool
	scalesets map[uint]params.ScaleSet
}

func (e *EntityCache) GetEntityForScaleSet(scaleSetID uint) (params.ForgeEntity, bool) {
	e.mux.Lock()
	defer e.mux.Unlock()

	scaleSet, ok := e.scalesets[scaleSetID]
	if !ok {
		return params.ForgeEntity{}, false
	}

	entity, err := scaleSet.GetEntity()
	if err != nil {
		return params.ForgeEntity{}, false
	}
	if cacheEntity, ok := e.entities[entity.ID]; ok {
		return cacheEntity.Entity, true
	}
	return params.ForgeEntity{}, false
}

func (e *EntityCache) GetEntityForPool(poolID string) (params.ForgeEntity, bool) {
	e.mux.Lock()
	defer e.mux.Unlock()

	pool, ok := e.pools[poolID]
	if !ok {
		return params.ForgeEntity{}, false
	}

	entity, err := pool.GetEntity()
	if err != nil {
		return params.ForgeEntity{}, false
	}
	if cacheEntity, ok := e.entities[entity.ID]; ok {
		return cacheEntity.Entity, true
	}
	return params.ForgeEntity{}, false
}

func (e *EntityCache) GetPoolByID(poolID string) (params.Pool, bool) {
	e.mux.Lock()
	defer e.mux.Unlock()

	if pool, ok := e.pools[poolID]; ok {
		return pool, ok
	}

	return params.Pool{}, false
}

func (e *EntityCache) GetScaleSetByID(scaleSetID uint) (params.ScaleSet, bool) {
	e.mux.Lock()
	defer e.mux.Unlock()

	if scaleSet, ok := e.scalesets[scaleSetID]; ok {
		return scaleSet, ok
	}

	return params.ScaleSet{}, false
}

func (e *EntityCache) UpdateCredentialsInAffectedEntities(creds params.ForgeCredentials) {
	e.mux.Lock()
	defer e.mux.Unlock()

	for entityID, cache := range e.entities {
		if cache.Entity.Credentials.GetID() == creds.GetID() {
			cache.Entity.Credentials = creds
			e.entities[entityID] = cache
		}
	}
}

func (e *EntityCache) GetEntity(entityID string) (params.ForgeEntity, bool) {
	e.mux.Lock()
	defer e.mux.Unlock()

	if cache, ok := e.entities[entityID]; ok {
		var creds params.ForgeCredentials
		var ok bool
		switch cache.Entity.Credentials.ForgeType {
		case params.GithubEndpointType:
			creds, ok = GetGithubCredentials(cache.Entity.Credentials.ID)
		case params.GiteaEndpointType:
			creds, ok = GetGiteaCredentials(cache.Entity.Credentials.ID)
		}
		if ok {
			cache.Entity.Credentials = creds
		}
		return cache.Entity, true
	}
	return params.ForgeEntity{}, false
}

func (e *EntityCache) SetEntity(entity params.ForgeEntity) {
	e.mux.Lock()
	defer e.mux.Unlock()

	cache, ok := e.entities[entity.ID]
	if !ok {
		e.entities[entity.ID] = EntityItem{
			Entity:       entity,
			Pools:        make(map[string]struct{}),
			ScaleSets:    make(map[uint]struct{}),
			RunnerGroups: make(map[string]RunnerGroupEntry),
		}
		return
	}
	cache.Entity = entity
	e.entities[entity.ID] = cache
}

func (e *EntityCache) ReplaceEntityPools(entityID string, pools []params.Pool) {
	e.mux.Lock()
	defer e.mux.Unlock()

	cache, ok := e.entities[entityID]
	if !ok {
		return
	}

	poolsByID := map[string]struct{}{}
	for _, pool := range pools {
		poolEntity, err := pool.GetEntity()
		if err != nil || poolEntity.ID != entityID {
			continue
		}
		e.pools[pool.ID] = pool
		// map the pool ID to the entity. We have to do an extra lookup
		// in the pools map, but it makes it easier to lookup just pools later
		// when we want to find the pool for the instance.
		poolsByID[pool.ID] = struct{}{}
	}
	cache.Pools = poolsByID
	e.entities[entityID] = cache
}

func (e *EntityCache) ReplaceEntityScaleSets(entityID string, scaleSets []params.ScaleSet) {
	e.mux.Lock()
	defer e.mux.Unlock()

	cache, ok := e.entities[entityID]
	if !ok {
		return
	}

	scaleSetsByID := map[uint]struct{}{}
	for _, scaleSet := range scaleSets {
		scaleSetEntity, err := scaleSet.GetEntity()
		if err != nil || scaleSetEntity.ID != entityID {
			continue
		}
		e.scalesets[scaleSet.ID] = scaleSet
		scaleSetsByID[scaleSet.ID] = struct{}{}
	}
	cache.ScaleSets = scaleSetsByID
	e.entities[entityID] = cache
}

func (e *EntityCache) DeleteEntity(entityID string) {
	e.mux.Lock()
	defer e.mux.Unlock()
	delete(e.entities, entityID)
}

func (e *EntityCache) SetEntityPool(entityID string, pool params.Pool) {
	e.mux.Lock()
	defer e.mux.Unlock()

	poolEntity, err := pool.GetEntity()
	if err != nil || poolEntity.ID != entityID {
		return
	}

	if cache, ok := e.entities[entityID]; ok {
		e.pools[pool.ID] = pool
		cache.Pools[pool.ID] = struct{}{}
		e.entities[entityID] = cache
	}
}

func (e *EntityCache) SetEntityScaleSet(entityID string, scaleSet params.ScaleSet) {
	e.mux.Lock()
	defer e.mux.Unlock()

	scaleSetEntity, err := scaleSet.GetEntity()
	if err != nil || scaleSetEntity.ID != entityID {
		return
	}

	if cache, ok := e.entities[entityID]; ok {
		e.scalesets[scaleSet.ID] = scaleSet
		cache.ScaleSets[scaleSet.ID] = struct{}{}
		e.entities[entityID] = cache
	}
}

func (e *EntityCache) DeleteEntityPool(entityID string, poolID string) {
	e.mux.Lock()
	defer e.mux.Unlock()

	if cache, ok := e.entities[entityID]; ok {
		delete(cache.Pools, poolID)
		e.entities[entityID] = cache
	}
}

func (e *EntityCache) DeleteEntityScaleSet(entityID string, scaleSetID uint) {
	e.mux.Lock()
	defer e.mux.Unlock()

	if cache, ok := e.entities[entityID]; ok {
		delete(cache.ScaleSets, scaleSetID)
		e.entities[entityID] = cache
	}
}

func (e *EntityCache) GetEntityPool(entityID string, poolID string) (params.Pool, bool) {
	e.mux.Lock()
	defer e.mux.Unlock()

	if cache, ok := e.entities[entityID]; ok {
		if _, ok := cache.Pools[poolID]; ok {
			if cachePool, ok := e.pools[poolID]; ok {
				return cachePool, true
			}
		}
	}
	return params.Pool{}, false
}

func (e *EntityCache) GetEntityScaleSet(entityID string, scaleSetID uint) (params.ScaleSet, bool) {
	e.mux.Lock()
	defer e.mux.Unlock()

	if cache, ok := e.entities[entityID]; ok {
		if _, ok := cache.ScaleSets[scaleSetID]; ok {
			if scaleSet, ok := e.scalesets[scaleSetID]; ok {
				return scaleSet, true
			}
		}
	}
	return params.ScaleSet{}, false
}

func (e *EntityCache) FindPoolsMatchingAllTags(entityID string, tags []string) []params.Pool {
	e.mux.Lock()
	defer e.mux.Unlock()

	if cache, ok := e.entities[entityID]; ok {
		var pools []params.Pool
		for poolID := range cache.Pools {
			if pool, ok := e.pools[poolID]; ok {
				if pool.HasRequiredLabels(tags) {
					pools = append(pools, pool)
				}
			}
		}
		// Sort the pools by creation date.
		sortByCreationDate(pools)
		return pools
	}
	return nil
}

func (e *EntityCache) GetEntityPools(entityID string) []params.Pool {
	e.mux.Lock()
	defer e.mux.Unlock()

	if cache, ok := e.entities[entityID]; ok {
		var pools []params.Pool
		for poolID := range cache.Pools {
			if pool, ok := e.pools[poolID]; ok {
				pools = append(pools, pool)
			}
		}
		// Sort the pools by creation date.
		sortByCreationDate(pools)
		return pools
	}
	return nil
}

func (e *EntityCache) GetEntityScaleSets(entityID string) []params.ScaleSet {
	e.mux.Lock()
	defer e.mux.Unlock()

	if cache, ok := e.entities[entityID]; ok {
		var scaleSets []params.ScaleSet
		for scaleSetID := range cache.ScaleSets {
			if scaleSet, ok := e.scalesets[scaleSetID]; ok {
				scaleSets = append(scaleSets, scaleSet)
			}
		}
		// Sort the scale sets by creation date.
		sortByID(scaleSets)
		return scaleSets
	}
	return nil
}

func (e *EntityCache) GetEntitiesUsingEndpoint(endpoint params.ForgeEndpoint) []params.ForgeEntity {
	e.mux.Lock()
	defer e.mux.Unlock()

	var entities []params.ForgeEntity
	for _, cache := range e.entities {
		if cache.Entity.Credentials.Endpoint.Name != endpoint.Name {
			continue
		}
		entities = append(entities, cache.Entity)
	}
	sortByCreationDate(entities)
	return entities
}

func (e *EntityCache) GetEntitiesUsingCredentials(creds params.ForgeCredentials) []params.ForgeEntity {
	e.mux.Lock()
	defer e.mux.Unlock()

	var entities []params.ForgeEntity
	for _, cache := range e.entities {
		if cache.Entity.Credentials.ForgeType != creds.ForgeType {
			continue
		}

		if cache.Entity.Credentials.GetID() == creds.GetID() {
			entities = append(entities, cache.Entity)
		}
	}
	sortByCreationDate(entities)
	return entities
}

func (e *EntityCache) GetAllEntities() []params.ForgeEntity {
	e.mux.Lock()
	defer e.mux.Unlock()

	var entities []params.ForgeEntity
	for _, cache := range e.entities {
		// Get the credentials from the credentials cache.
		var creds params.ForgeCredentials
		var ok bool
		switch cache.Entity.Credentials.ForgeType {
		case params.GithubEndpointType:
			creds, ok = GetGithubCredentials(cache.Entity.Credentials.ID)
		case params.GiteaEndpointType:
			creds, ok = GetGiteaCredentials(cache.Entity.Credentials.ID)
		}
		if ok {
			cache.Entity.Credentials = creds
		}
		entities = append(entities, cache.Entity)
	}
	sortByCreationDate(entities)
	return entities
}

func (e *EntityCache) GetAllPools() []params.Pool {
	e.mux.Lock()
	defer e.mux.Unlock()

	var pools []params.Pool
	for _, pool := range e.pools {
		pools = append(pools, pool)
	}
	sortByCreationDate(pools)
	return pools
}

func (e *EntityCache) GetAllScaleSets() []params.ScaleSet {
	e.mux.Lock()
	defer e.mux.Unlock()

	var scaleSets []params.ScaleSet
	for _, scaleSet := range e.scalesets {
		scaleSets = append(scaleSets, scaleSet)
	}
	sortByID(scaleSets)
	return scaleSets
}

func (e *EntityCache) SetEntityRunnerGroup(entityID, runnerGroupName string, runnerGroupID int64) {
	e.mux.Lock()
	defer e.mux.Unlock()

	if _, ok := e.entities[entityID]; ok {
		e.entities[entityID].RunnerGroups[runnerGroupName] = RunnerGroupEntry{
			RunnerGroupID: runnerGroupID,
			time:          time.Now().UTC(),
		}
	}
}

func (e *EntityCache) GetEntityRunnerGroup(entityID, runnerGroupName string) (int64, bool) {
	e.mux.Lock()
	defer e.mux.Unlock()

	if _, ok := e.entities[entityID]; ok {
		if runnerGroup, ok := e.entities[entityID].RunnerGroups[runnerGroupName]; ok {
			if time.Now().UTC().After(runnerGroup.time.Add(1 * time.Hour)) {
				delete(e.entities[entityID].RunnerGroups, runnerGroupName)
				return 0, false
			}
			return runnerGroup.RunnerGroupID, true
		}
	}
	return 0, false
}

func SetEntityRunnerGroup(entityID, runnerGroupName string, runnerGroupID int64) {
	entityCache.SetEntityRunnerGroup(entityID, runnerGroupName, runnerGroupID)
}

func GetEntityRunnerGroup(entityID, runnerGroupName string) (int64, bool) {
	return entityCache.GetEntityRunnerGroup(entityID, runnerGroupName)
}

func GetEntity(entityID string) (params.ForgeEntity, bool) {
	return entityCache.GetEntity(entityID)
}

func SetEntity(entity params.ForgeEntity) {
	entityCache.SetEntity(entity)
}

func ReplaceEntityPools(entityID string, pools []params.Pool) {
	entityCache.ReplaceEntityPools(entityID, pools)
}

func ReplaceEntityScaleSets(entityID string, scaleSets []params.ScaleSet) {
	entityCache.ReplaceEntityScaleSets(entityID, scaleSets)
}

func DeleteEntity(entityID string) {
	entityCache.DeleteEntity(entityID)
}

func SetEntityPool(entityID string, pool params.Pool) {
	entityCache.SetEntityPool(entityID, pool)
}

func SetEntityScaleSet(entityID string, scaleSet params.ScaleSet) {
	entityCache.SetEntityScaleSet(entityID, scaleSet)
}

func DeleteEntityPool(entityID string, poolID string) {
	entityCache.DeleteEntityPool(entityID, poolID)
}

func DeleteEntityScaleSet(entityID string, scaleSetID uint) {
	entityCache.DeleteEntityScaleSet(entityID, scaleSetID)
}

func GetEntityPool(entityID string, poolID string) (params.Pool, bool) {
	return entityCache.GetEntityPool(entityID, poolID)
}

func GetEntityScaleSet(entityID string, scaleSetID uint) (params.ScaleSet, bool) {
	return entityCache.GetEntityScaleSet(entityID, scaleSetID)
}

func FindPoolsMatchingAllTags(entityID string, tags []string) []params.Pool {
	return entityCache.FindPoolsMatchingAllTags(entityID, tags)
}

func GetEntityPools(entityID string) []params.Pool {
	return entityCache.GetEntityPools(entityID)
}

func GetEntityScaleSets(entityID string) []params.ScaleSet {
	return entityCache.GetEntityScaleSets(entityID)
}

func UpdateCredentialsInAffectedEntities(creds params.ForgeCredentials) {
	entityCache.UpdateCredentialsInAffectedEntities(creds)
}

func GetEntitiesUsingCredentials(creds params.ForgeCredentials) []params.ForgeEntity {
	return entityCache.GetEntitiesUsingCredentials(creds)
}

func GetAllEntities() []params.ForgeEntity {
	return entityCache.GetAllEntities()
}

func GetAllPools() []params.Pool {
	return entityCache.GetAllPools()
}

func GetAllScaleSets() []params.ScaleSet {
	return entityCache.GetAllScaleSets()
}

func GetEntityForScaleSet(scaleSetID uint) (params.ForgeEntity, bool) {
	return entityCache.GetEntityForScaleSet(scaleSetID)
}

func GetEntityForPool(poolID string) (params.ForgeEntity, bool) {
	return entityCache.GetEntityForPool(poolID)
}

func GetPoolByID(poolID string) (params.Pool, bool) {
	return entityCache.GetPoolByID(poolID)
}

func GetScaleSetByID(scaleSetID uint) (params.ScaleSet, bool) {
	return entityCache.GetScaleSetByID(scaleSetID)
}

func GetEntitiesUsingEndpoint(endpoint params.ForgeEndpoint) []params.ForgeEntity {
	return entityCache.GetEntitiesUsingEndpoint(endpoint)
}
