package dashboards

import (
	"context"
	"time"

	"github.com/grafana/grafana/pkg/infra/log"
	"github.com/grafana/grafana/pkg/infra/metrics"
)

type duplicate struct {
	Sum             uint8
	InvolvedReaders map[string]struct{}
}

func newDuplicate() *duplicate {
	return &duplicate{InvolvedReaders: make(map[string]struct{})}
}

type duplicateEntries struct {
	Titles map[dashboardIdentity]*duplicate
	UIDs   map[string]*duplicate
}

func (d *duplicateEntries) InvolvedReaders() map[string]struct{} {
	involvedReaders := make(map[string]struct{})

	for _, duplicate := range d.UIDs {
		if duplicate.Sum <= 1 {
			continue
		}

		for readerName := range duplicate.InvolvedReaders {
			involvedReaders[readerName] = struct{}{}
		}
	}

	for _, duplicate := range d.Titles {
		if duplicate.Sum <= 1 {
			continue
		}

		for readerName := range duplicate.InvolvedReaders {
			involvedReaders[readerName] = struct{}{}
		}
	}

	return involvedReaders
}

type duplicateValidator struct {
	logger  log.Logger
	readers []*FileReader
}

func newDuplicateValidator(logger log.Logger, readers []*FileReader) duplicateValidator {
	return duplicateValidator{logger: logger, readers: readers}
}

func (c *duplicateValidator) getDuplicates() map[int64]duplicateEntries {
	duplicatesByOrg := map[int64]duplicateEntries{}

	for _, reader := range c.readers {
		readerName := reader.Cfg.Name
		orgID := reader.Cfg.OrgID
		tracker := reader.getUsageTracker()

		if _, exists := duplicatesByOrg[orgID]; !exists {
			duplicatesByOrg[orgID] = duplicateEntries{
				Titles: make(map[dashboardIdentity]*duplicate),
				UIDs:   make(map[string]*duplicate),
			}
		}

		for uid, times := range tracker.uidUsage {
			if _, ok := duplicatesByOrg[orgID].UIDs[uid]; !ok {
				duplicatesByOrg[orgID].UIDs[uid] = newDuplicate()
			}
			duplicatesByOrg[orgID].UIDs[uid].Sum += times
			duplicatesByOrg[orgID].UIDs[uid].InvolvedReaders[readerName] = struct{}{}
		}

		for id, times := range tracker.titleUsage {
			if _, ok := duplicatesByOrg[orgID].Titles[id]; !ok {
				duplicatesByOrg[orgID].Titles[id] = newDuplicate()
			}
			duplicatesByOrg[orgID].Titles[id].Sum += times
			duplicatesByOrg[orgID].Titles[id].InvolvedReaders[readerName] = struct{}{}
		}
	}

	return duplicatesByOrg
}

func (c *duplicateValidator) logWarnings(duplicatesByOrg map[int64]duplicateEntries) {
	for orgID, duplicates := range duplicatesByOrg {
		for uid, usage := range duplicates.UIDs {
			if usage.Sum > 1 {
				c.logger.Warn("the same UID is used more than once", "orgId", orgID, "uid", uid, "times", usage.Sum, "providers",
					keysToSlice(usage.InvolvedReaders))
			}
		}

		for id, usage := range duplicates.Titles {
			if usage.Sum > 1 {
				metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Provisioning).Inc()
				// nolint:staticcheck
				c.logger.Warn("dashboard title is not unique in folder", "orgId", orgID, "title", id.title, "folderID", id.folderID, "times",
					usage.Sum, "providers", keysToSlice(usage.InvolvedReaders))
			}
		}
	}
}

func (c *duplicateValidator) takeAwayWritePermissions(duplicatesByOrg map[int64]duplicateEntries) {
	// reset write permissions for all readers
	for _, reader := range c.readers {
		reader.changeWritePermissions(false)
	}

	for orgID, duplicates := range duplicatesByOrg {
		involvedReaders := duplicates.InvolvedReaders()
		for _, reader := range c.readers {
			_, exists := involvedReaders[reader.Cfg.Name]

			if exists {
				// We restrict reader permissions to write to the database here to prevent overloading
				reader.changeWritePermissions(true)
				c.logger.Warn("dashboards provisioning provider has no database write permissions because of duplicates", "provider", reader.Cfg.Name, "orgId", orgID)
			}
		}
	}
}

func (c *duplicateValidator) Run(ctx context.Context) {
	ticker := time.NewTicker(30 * time.Second)
	for {
		select {
		case <-ticker.C:
			c.validate()
		case <-ctx.Done():
			return
		}
	}
}

func (c *duplicateValidator) validate() {
	duplicates := c.getDuplicates()

	c.logWarnings(duplicates)
	c.takeAwayWritePermissions(duplicates)
}

func keysToSlice(data map[string]struct{}) []string {
	entries := make([]string, 0, len(data))

	for entry := range data {
		entries = append(entries, entry)
	}

	return entries
}
