package legacy

import (
	"context"
	"fmt"
	"math/rand"
	"slices"
	"strconv"
	"strings"
	"time"

	apiserrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/apis/meta/internalversion"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apiserver/pkg/registry/rest"
	"k8s.io/utils/ptr"

	authlib "github.com/grafana/authlib/types"
	collections "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1"
	dashboardsV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1"
	"github.com/grafana/grafana/pkg/apimachinery/identity"
	gutils "github.com/grafana/grafana/pkg/apimachinery/utils"
	"github.com/grafana/grafana/pkg/registry/apis/preferences/utils"
	"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
	"github.com/grafana/grafana/pkg/services/star"
	"github.com/grafana/grafana/pkg/services/user"
)

var (
	_ rest.Scoper               = (*DashboardStarsStorage)(nil)
	_ rest.SingularNameProvider = (*DashboardStarsStorage)(nil)
	_ rest.Getter               = (*DashboardStarsStorage)(nil)
	_ rest.Lister               = (*DashboardStarsStorage)(nil)
	_ rest.Storage              = (*DashboardStarsStorage)(nil)
	_ rest.Creater              = (*DashboardStarsStorage)(nil)
	_ rest.Updater              = (*DashboardStarsStorage)(nil)
	_ rest.GracefulDeleter      = (*DashboardStarsStorage)(nil)
	_ rest.CollectionDeleter    = (*DashboardStarsStorage)(nil)
)

func NewDashboardStarsStorage(
	stars star.Service,
	users user.Service,
	namespacer request.NamespaceMapper,
	sql *LegacySQL,
) *DashboardStarsStorage {
	return &DashboardStarsStorage{
		stars:          stars,
		users:          users,
		namespacer:     namespacer,
		sql:            sql,
		tableConverter: collections.StarsResourceInfo.TableConverter(),
	}
}

type DashboardStarsStorage struct {
	namespacer     request.NamespaceMapper
	tableConverter rest.TableConvertor
	sql            *LegacySQL
	stars          star.Service
	users          user.Service
}

func (s *DashboardStarsStorage) New() runtime.Object {
	return collections.StarsKind().ZeroValue()
}

func (s *DashboardStarsStorage) Destroy() {}

func (s *DashboardStarsStorage) NamespaceScoped() bool {
	return true // namespace == org
}

func (s *DashboardStarsStorage) GetSingularName() string {
	return strings.ToLower(collections.StarsKind().Kind())
}

func (s *DashboardStarsStorage) NewList() runtime.Object {
	return collections.StarsKind().ZeroListValue()
}

func (s *DashboardStarsStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
	return s.tableConverter.ConvertToTable(ctx, object, tableOptions)
}

func (s *DashboardStarsStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
	ns, err := request.NamespaceInfoFrom(ctx, false)
	if err != nil {
		return nil, err
	}

	if ns.Value == "" {
		return nil, fmt.Errorf("cross cluster listing is not supported")
	}

	userInfo, err := identity.GetRequester(ctx)
	if err != nil {
		return nil, err
	}

	user := userInfo.GetIdentifier()
	if userInfo.GetIdentityType() == authlib.TypeAccessPolicy {
		user = "" // can see everything
	}

	list := &collections.StarsList{}
	found, rv, err := s.sql.getDashboardStars(ctx, ns.OrgID, user)
	if err != nil {
		return nil, err
	}
	for _, v := range found {
		list.Items = append(list.Items,
			asStarsResource(s.namespacer(v.OrgID), &v))
	}
	if rv > 0 {
		list.ResourceVersion = strconv.FormatInt(rv, 10)
	}
	return list, nil
}

func getNamespaceAndOwner(ctx context.Context, name string) (authlib.NamespaceInfo, utils.OwnerReference, error) {
	info, err := request.NamespaceInfoFrom(ctx, true)
	if err != nil {
		return info, utils.OwnerReference{}, err
	}
	owner, ok := utils.ParseOwnerFromName(name)
	if !ok {
		return info, owner, fmt.Errorf("invalid name %w", err)
	}
	if owner.Owner != utils.UserResourceOwner {
		return info, owner, fmt.Errorf("expecting name with prefix: %s-", utils.UserResourceOwner)
	}
	return info, owner, nil
}

func (s *DashboardStarsStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
	ns, owner, err := getNamespaceAndOwner(ctx, name)
	if err != nil {
		return nil, err
	}

	found, _, err := s.sql.getDashboardStars(ctx, ns.OrgID, owner.Identifier)
	if err != nil {
		return nil, err
	}

	if len(found) == 0 || len(found[0].Dashboards) == 0 {
		return nil, apiserrors.NewNotFound(collections.StarsResourceInfo.GroupResource(), name)
	}
	obj := asStarsResource(ns.Value, &found[0])
	return &obj, nil
}

func getStars(stars *collections.Stars, gk schema.GroupKind) []string {
	if stars == nil || len(stars.Spec.Resource) == 0 {
		return []string{}
	}
	for _, r := range stars.Spec.Resource {
		if r.Group == gk.Group && r.Kind == gk.Kind {
			return r.Names
		}
	}
	return []string{}
}

// Create implements rest.Creater.
func (s *DashboardStarsStorage) write(ctx context.Context, obj *collections.Stars) (runtime.Object, error) {
	ns, owner, err := getNamespaceAndOwner(ctx, obj.Name)
	if err != nil {
		return nil, err
	}

	// Send an error if we try to save a non-dashboard star
	for _, res := range obj.Spec.Resource {
		if res.Group != "dashboard.grafana.app" || res.Kind != "Dashboard" {
			return nil, fmt.Errorf("only dashboard stars are supported until the migration to unified storage is complete")
		}
	}

	user, err := s.users.GetByUID(ctx, &user.GetUserByUIDQuery{
		UID: owner.Identifier,
	})
	if err != nil {
		return nil, err
	}
	if user.OrgID != ns.OrgID {
		return nil, fmt.Errorf("namespace mismatch")
	}

	stars := getStars(obj, schema.GroupKind{Group: "dashboard.grafana.app", Kind: "Dashboard"})
	if len(stars) == 0 {
		err = s.stars.DeleteByUser(ctx, user.ID)
		return &collections.Stars{ObjectMeta: metav1.ObjectMeta{
			Name:              obj.Name,
			Namespace:         obj.Namespace,
			DeletionTimestamp: ptr.To(metav1.Now()),
		}}, err
	}

	current, _, err := s.sql.getDashboardStars(ctx, ns.OrgID, owner.Identifier)
	if err != nil {
		return nil, err
	}

	changed := false
	now := time.Now()
	randID := now.UnixNano() + rand.Int63n(5000)
	previous := make(map[string]bool)
	if len(current) > 0 {
		for _, v := range current[0].Dashboards {
			previous[v] = true
		}
	}
	for idx, dashboard := range stars {
		if previous[dashboard] {
			delete(previous, dashboard)
			continue // nothing needed
		}
		if idx > 0 {
			time.Sleep(75 * time.Millisecond) // values are ordered by update time; this keeps the order predictable
		}
		err = s.stars.Add(ctx, &star.StarDashboardCommand{
			UserID:       user.ID,
			OrgID:        user.OrgID,
			DashboardUID: dashboard,
			DashboardID:  randID,
			Updated:      now,
		})
		if err != nil {
			return nil, err
		}
		changed = true
		randID++
	}

	for k := range previous {
		err = s.stars.Delete(ctx, &star.UnstarDashboardCommand{
			UserID:       user.ID,
			OrgID:        user.OrgID,
			DashboardUID: k,
		})
		if err != nil {
			return nil, err
		}
		changed = true
	}

	if changed {
		return s.Get(ctx, obj.Name, &metav1.GetOptions{})
	}
	return obj, nil // nothing required
}

// Create implements rest.Creater.
func (s *DashboardStarsStorage) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
	stars, ok := obj.(*collections.Stars)
	if !ok {
		return nil, fmt.Errorf("expected stars object")
	}

	return s.write(ctx, stars)
}

// Update implements rest.Updater.
func (s *DashboardStarsStorage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
	old, err := s.Get(ctx, name, &metav1.GetOptions{})
	if err != nil {
		return nil, false, err
	}

	obj, err := objInfo.UpdatedObject(ctx, old)
	if err != nil {
		return nil, false, err
	}

	stars, ok := obj.(*collections.Stars)
	if !ok {
		return nil, false, fmt.Errorf("expected stars object")
	}

	obj, err = s.write(ctx, stars)
	return obj, false, err
}

// Delete implements rest.GracefulDeleter.
func (s *DashboardStarsStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
	obj, err := s.write(ctx, &collections.Stars{ObjectMeta: metav1.ObjectMeta{Name: name}})
	if err != nil {
		return nil, false, err
	}
	return obj, true, err
}

// DeleteCollection implements rest.CollectionDeleter.
func (s *DashboardStarsStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) {
	return nil, fmt.Errorf("not implemented")
}

func asStarsResource(ns string, v *dashboardStars) collections.Stars {
	slices.Sort(v.Dashboards) // ensure names are in sorted order
	stars := collections.Stars{
		TypeMeta: metav1.TypeMeta{
			APIVersion: collections.APIGroup + "/" + collections.APIVersion,
			Kind:       collections.StarsKind().Kind(),
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:              fmt.Sprintf("user-%s", v.UserUID),
			Namespace:         ns,
			ResourceVersion:   strconv.FormatInt(v.Last, 10),
			CreationTimestamp: metav1.NewTime(time.UnixMilli(v.First)),
			Annotations: map[string]string{
				gutils.AnnoKeyCreatedBy: fmt.Sprintf("user:%s", v.UserUID),
			},
		},
		Spec: collections.StarsSpec{
			Resource: []collections.StarsResource{{
				Group: dashboardsV1.APIGroup,
				Kind:  "Dashboard",
				Names: v.Dashboards,
			}},
		},
	}
	stars.Spec.Normalize()
	if v.Last > v.First {
		m, _ := gutils.MetaAccessor(&stars)
		m.SetUpdatedTimestampMillis(v.Last)
	}
	return stars
}
