package annotationsimpl

import (
	"context"
	"errors"
	"fmt"
	"sort"

	"github.com/grafana/dskit/concurrency"

	"github.com/grafana/grafana/pkg/infra/log"
	"github.com/grafana/grafana/pkg/services/annotations"
	"github.com/grafana/grafana/pkg/services/annotations/accesscontrol"
)

// CompositeStore is a read store that combines two or more read stores, and queries all stores in parallel.
type CompositeStore struct {
	logger  log.Logger
	readers []readStore
}

func NewCompositeStore(logger log.Logger, readers ...readStore) *CompositeStore {
	return &CompositeStore{
		logger:  logger,
		readers: readers,
	}
}

// Satisfy the commonStore interface, in practice this is not used.
func (c *CompositeStore) Type() string {
	return "composite"
}

// Get returns annotations from all stores, and combines the results.
func (c *CompositeStore) Get(ctx context.Context, query annotations.ItemQuery, accessResources *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error) {
	itemCh := make(chan []*annotations.ItemDTO, len(c.readers))

	err := concurrency.ForEachJob(ctx, len(c.readers), len(c.readers), func(ctx context.Context, i int) (err error) {
		defer handleJobPanic(c.logger, c.readers[i].Type(), &err)

		items, err := c.readers[i].Get(ctx, query, accessResources)
		itemCh <- items
		return err
	})
	if err != nil {
		return make([]*annotations.ItemDTO, 0), err
	}

	close(itemCh)
	res := make([]*annotations.ItemDTO, 0)
	for items := range itemCh {
		res = append(res, items...)
	}
	sort.Sort(annotations.SortedItems(res))

	return res, nil
}

// GetTags returns tags from all stores, and combines the results.
func (c *CompositeStore) GetTags(ctx context.Context, query annotations.TagsQuery) (annotations.FindTagsResult, error) {
	resCh := make(chan annotations.FindTagsResult, len(c.readers))

	err := concurrency.ForEachJob(ctx, len(c.readers), len(c.readers), func(ctx context.Context, i int) (err error) {
		defer handleJobPanic(c.logger, c.readers[i].Type(), &err)

		res, err := c.readers[i].GetTags(ctx, query)
		resCh <- res
		return err
	})
	if err != nil {
		return annotations.FindTagsResult{}, err
	}

	close(resCh)
	res := make([]*annotations.TagsDTO, 0)
	for r := range resCh {
		res = append(res, r.Tags...)
	}
	sort.Sort(annotations.SortedTags(res))

	return annotations.FindTagsResult{Tags: res}, nil
}

// handleJobPanic is a helper function that recovers from a panic in a concurrent job.,
// It will log the error and set the job error if it is not nil.
func handleJobPanic(logger log.Logger, storeType string, jobErr *error) {
	if r := recover(); r != nil {
		logger.Error("Annotation store panic", "error", r, "store", storeType, "stack", log.Stack(1))
		errMsg := "concurrent job panic"

		if jobErr != nil {
			err := errors.New(errMsg)
			if panicErr, ok := r.(error); ok {
				err = fmt.Errorf("%s: %w", errMsg, panicErr)
			}
			*jobErr = err
		}
	}
}
