// Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
// All rights reserved. This file is part of cc-backend.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package graph

import (
	"context"
	"fmt"
	"slices"
	"sync"

	"github.com/ClusterCockpit/cc-backend/internal/graph/model"
)

// statsGroupCache is a per-request cache for grouped JobsStatistics results.
// It deduplicates identical (filter+groupBy) SQL queries that arise when the
// frontend requests multiple sort/page slices of the same underlying data
// (e.g. topUserJobs, topUserNodes, topUserAccs all group by USER).
type statsGroupCache struct {
	mu      sync.Mutex
	entries map[string]*cacheEntry
}

type cacheEntry struct {
	once   sync.Once
	result []*model.JobsStatistics
	err    error
}

type ctxKey int

const statsGroupCacheKey ctxKey = iota

// newStatsGroupCache creates a new empty cache.
func newStatsGroupCache() *statsGroupCache {
	return &statsGroupCache{
		entries: make(map[string]*cacheEntry),
	}
}

// WithStatsGroupCache injects a new cache into the context.
func WithStatsGroupCache(ctx context.Context) context.Context {
	return context.WithValue(ctx, statsGroupCacheKey, newStatsGroupCache())
}

// getStatsGroupCache retrieves the cache from context, or nil if absent.
func getStatsGroupCache(ctx context.Context) *statsGroupCache {
	if c, ok := ctx.Value(statsGroupCacheKey).(*statsGroupCache); ok {
		return c
	}
	return nil
}

// cacheKey builds a deterministic string key from filter + groupBy + reqFields.
func statsCacheKey(filter []*model.JobFilter, groupBy *model.Aggregate, reqFields map[string]bool) string {
	return fmt.Sprintf("%v|%v|%v", filter, *groupBy, reqFields)
}

// getOrCompute returns cached results for the given key, computing them on
// first access via the provided function.
func (c *statsGroupCache) getOrCompute(
	key string,
	compute func() ([]*model.JobsStatistics, error),
) ([]*model.JobsStatistics, error) {
	c.mu.Lock()
	entry, ok := c.entries[key]
	if !ok {
		entry = &cacheEntry{}
		c.entries[key] = entry
	}
	c.mu.Unlock()

	entry.once.Do(func() {
		entry.result, entry.err = compute()
	})
	return entry.result, entry.err
}

// sortAndPageStats sorts a copy of allStats by the given sortBy field (descending)
// and returns the requested page slice.
func sortAndPageStats(allStats []*model.JobsStatistics, sortBy *model.SortByAggregate, page *model.PageRequest) []*model.JobsStatistics {
	// Work on a shallow copy so the cached slice order is not mutated.
	sorted := make([]*model.JobsStatistics, len(allStats))
	copy(sorted, allStats)

	if sortBy != nil {
		getter := statsFieldGetter(*sortBy)
		slices.SortFunc(sorted, func(a, b *model.JobsStatistics) int {
			return getter(b) - getter(a) // descending
		})
	}

	if page != nil && page.ItemsPerPage != -1 {
		start := (page.Page - 1) * page.ItemsPerPage
		if start >= len(sorted) {
			return nil
		}
		end := start + page.ItemsPerPage
		if end > len(sorted) {
			end = len(sorted)
		}
		sorted = sorted[start:end]
	}

	return sorted
}

// sortByFieldName maps a SortByAggregate enum to the corresponding reqFields key.
// This ensures the DB computes the column that sortAndPageStats will sort by.
func sortByFieldName(sortBy model.SortByAggregate) string {
	switch sortBy {
	case model.SortByAggregateTotaljobs:
		return "totalJobs"
	case model.SortByAggregateTotalusers:
		return "totalUsers"
	case model.SortByAggregateTotalwalltime:
		return "totalWalltime"
	case model.SortByAggregateTotalnodes:
		return "totalNodes"
	case model.SortByAggregateTotalnodehours:
		return "totalNodeHours"
	case model.SortByAggregateTotalcores:
		return "totalCores"
	case model.SortByAggregateTotalcorehours:
		return "totalCoreHours"
	case model.SortByAggregateTotalaccs:
		return "totalAccs"
	case model.SortByAggregateTotalacchours:
		return "totalAccHours"
	default:
		return "totalJobs"
	}
}

// statsFieldGetter returns a function that extracts the sortable int field
// from a JobsStatistics struct for the given sort key.
func statsFieldGetter(sortBy model.SortByAggregate) func(*model.JobsStatistics) int {
	switch sortBy {
	case model.SortByAggregateTotaljobs:
		return func(s *model.JobsStatistics) int { return s.TotalJobs }
	case model.SortByAggregateTotalusers:
		return func(s *model.JobsStatistics) int { return s.TotalUsers }
	case model.SortByAggregateTotalwalltime:
		return func(s *model.JobsStatistics) int { return s.TotalWalltime }
	case model.SortByAggregateTotalnodes:
		return func(s *model.JobsStatistics) int { return s.TotalNodes }
	case model.SortByAggregateTotalnodehours:
		return func(s *model.JobsStatistics) int { return s.TotalNodeHours }
	case model.SortByAggregateTotalcores:
		return func(s *model.JobsStatistics) int { return s.TotalCores }
	case model.SortByAggregateTotalcorehours:
		return func(s *model.JobsStatistics) int { return s.TotalCoreHours }
	case model.SortByAggregateTotalaccs:
		return func(s *model.JobsStatistics) int { return s.TotalAccs }
	case model.SortByAggregateTotalacchours:
		return func(s *model.JobsStatistics) int { return s.TotalAccHours }
	default:
		return func(s *model.JobsStatistics) int { return s.TotalJobs }
	}
}
