package resources

import (
	"context"
	"strings"

	"github.com/zitadel/zitadel/internal/api/scim/metadata"
	"github.com/zitadel/zitadel/internal/api/scim/resources/filter"
	"github.com/zitadel/zitadel/internal/api/scim/resources/patch"
	"github.com/zitadel/zitadel/internal/command"
	"github.com/zitadel/zitadel/internal/domain"
)

type userPatcher struct {
	ctx                  context.Context
	user                 *ScimUser
	metadataChanges      map[metadata.Key]*domain.Metadata
	metadataKeysToRemove map[metadata.Key]bool
	handler              *UsersHandler
}

func (h *UsersHandler) applyPatchesToChangeHuman(ctx context.Context, user *ScimUser, operations patch.OperationCollection) (*command.ChangeHuman, error) {
	patcher := &userPatcher{
		ctx:                  ctx,
		user:                 user,
		metadataChanges:      make(map[metadata.Key]*domain.Metadata),
		metadataKeysToRemove: make(map[metadata.Key]bool),
		handler:              h,
	}

	if err := operations.Apply(patcher, user); err != nil {
		return nil, err
	}

	// we rely on the change detection of the write model to only execute commands that really change data
	changeCommand, err := h.mapToChangeHuman(ctx, user)
	if err != nil {
		return nil, err
	}

	patcher.applyMetadataChangesToCommand(changeCommand)
	return changeCommand, nil
}

func (p *userPatcher) FilterEvaluator() *filter.Evaluator {
	return p.handler.filterEvaluator
}

func (p *userPatcher) Added(attributePath []string) error {
	return p.updateMetadata(attributePath)
}

func (p *userPatcher) Replaced(attributePath []string) error {
	return p.updateMetadata(attributePath)
}

func (p *userPatcher) Removed(attributePath []string) error {
	return p.updateMetadata(attributePath)
}

func (p *userPatcher) applyMetadataChangesToCommand(command *command.ChangeHuman) {
	command.MetadataKeysToRemove = make([]string, 0, len(p.metadataKeysToRemove))
	for key := range p.metadataKeysToRemove {
		command.MetadataKeysToRemove = append(command.MetadataKeysToRemove, string(key))
	}

	command.Metadata = make([]*domain.Metadata, 0, len(p.metadataChanges))
	for _, update := range p.metadataChanges {
		command.Metadata = append(command.Metadata, update)
	}
}

func (p *userPatcher) updateMetadata(attributePath []string) error {
	if len(attributePath) == 0 {
		return nil
	}

	// try full path first (e.g. name.middleName)
	// try root only if full path did not match (e.g. for entitlements.value only entitlements is mapped)
	var ok bool
	var keys []metadata.Key
	if len(attributePath) > 1 {
		keys, ok = metadata.AttributePathToMetadataKeys[strings.Join(attributePath, ".")]
	}

	if !ok {
		keys, ok = metadata.AttributePathToMetadataKeys[attributePath[0]]
		if !ok {
			return nil
		}
	}

	for _, key := range keys {
		value, err := getValueForMetadataKey(p.user, key)
		if err != nil {
			return err
		}

		if len(value) > 0 {
			delete(p.metadataKeysToRemove, key)
			p.metadataChanges[key] = &domain.Metadata{
				Key:   string(metadata.ScopeKey(p.ctx, key)),
				Value: value,
			}
		} else {
			p.metadataKeysToRemove[key] = true
			delete(p.metadataChanges, key)
		}
	}
	return nil
}