package validate

import (
	"context"
	"slices"

	"github.com/go-playground/validator/v10"
	libregraph "github.com/opencloud-eu/libre-graph-api-go"

	"github.com/qsfera/server/services/graph/pkg/unifiedrole"
)

type contextKey int

const (
	ContextKeyRoleIDsValueKey contextKey = iota
)

// initLibregraph initializes libregraph validation
func initLibregraph(v *validator.Validate) {
	for _, f := range []func(*validator.Validate){
		libregraphDriveItemInvite,
		libregraphDriveRecipient,
		libregraphPermission,
	} {
		f(v)
	}
}

// libregraphDriveItemInvite validates libregraph.DriveItemInvite
func libregraphDriveItemInvite(v *validator.Validate) {
	s := libregraph.DriveItemInvite{}

	v.RegisterStructValidationMapRules(map[string]string{
		"Recipients":         "len=1,dive",
		"Roles":              "max=1",
		"ExpirationDateTime": "omitnil,gt",
	}, s)

	v.RegisterStructValidationCtx(func(ctx context.Context, sl validator.StructLevel) {
		driveItemInvite := sl.Current().Interface().(libregraph.DriveItemInvite)
		rolesAndActions(ctx, sl, driveItemInvite.Roles, driveItemInvite.LibreGraphPermissionsActions, false)
	}, s)
}

// libregraphDriveRecipient validates libregraph.DriveRecipient
func libregraphDriveRecipient(v *validator.Validate) {
	v.RegisterStructValidationMapRules(map[string]string{
		"ObjectId":                "ne=",
		"LibreGraphRecipientType": "oneof=user group",
	}, libregraph.DriveRecipient{})
}

// libregraphPermission validates libregraph.Permission
func libregraphPermission(v *validator.Validate) {
	s := libregraph.Permission{}

	v.RegisterStructValidationMapRules(map[string]string{
		"Roles": "max=1",
	}, s)
	v.RegisterStructValidationCtx(func(ctx context.Context, sl validator.StructLevel) {
		permission := sl.Current().Interface().(libregraph.Permission)

		if _, ok := permission.GetIdOk(); ok {
			sl.ReportError(permission.Id, "Id", "Id", "readonly", "")
		}

		if _, ok := permission.GetHasPasswordOk(); ok {
			sl.ReportError(permission.HasPassword, "hasPassword", "HasPassword", "readonly", "")
		}

		rolesAndActions(ctx, sl, permission.Roles, permission.LibreGraphPermissionsActions, true)
	}, s)
}

func rolesAndActions(ctx context.Context, sl validator.StructLevel, roles, actions []string, allowEmpty bool) {
	totalRoles := len(roles)
	totalActions := len(actions)
	switch {
	case allowEmpty && totalRoles == 0 && totalActions == 0:
		break
	case totalRoles != 0 && totalActions != 0:
		fallthrough
	case totalRoles == totalActions:
		sl.ReportError(roles, "Roles", "Roles", "one_or_another", "")
		sl.ReportError(actions, "LibreGraphPermissionsActions", "LibreGraphPermissionsActions", "one_or_another", "")
	}

	var availableRoles []string
	var availableActions []string
	var definitions []*libregraph.UnifiedRoleDefinition

	switch roles, ok := ctx.Value(ContextKeyRoleIDsValueKey).([]string); {
	case ok:
		definitions = unifiedrole.GetRoles(unifiedrole.RoleFilterIDs(roles...))
	default:
		// it the ctx does not contain the allowed role IDs, we need to fall back to all roles
		definitions = unifiedrole.GetRoles(unifiedrole.RoleFilterAll())
	}

	for _, definition := range definitions {
		if slices.Contains(availableRoles, definition.GetId()) {
			continue
		}

		availableRoles = append(availableRoles, definition.GetId())

		for _, permission := range definition.GetRolePermissions() {
			for _, action := range permission.GetAllowedResourceActions() {
				if slices.Contains(availableActions, action) {
					continue
				}

				availableActions = append(availableActions, action)
			}
		}
	}

	for _, role := range roles {
		if slices.Contains(availableRoles, role) {
			continue
		}

		sl.ReportError(roles, "Roles", "Roles", "available_role", "")
	}

	for _, action := range actions {
		if slices.Contains(availableActions, action) {
			continue
		}

		sl.ReportError(actions, "LibreGraphPermissionsActions", "LibreGraphPermissionsActions", "available_action", "")
	}
}

// ContextWithAllowedRoleIDs returns a new context which includes the allowed role IDs.
func ContextWithAllowedRoleIDs(ctx context.Context, rolesIds []string) context.Context {
	return context.WithValue(ctx, ContextKeyRoleIDsValueKey, rolesIds)
}
