package command

import (
	"context"
	"strings"

	"github.com/zitadel/zitadel/internal/domain"
	"github.com/zitadel/zitadel/internal/eventstore"
	"github.com/zitadel/zitadel/internal/repository/org"
	"github.com/zitadel/zitadel/internal/repository/policy"
	"github.com/zitadel/zitadel/internal/repository/user"
)

type PolicyDomainWriteModel struct {
	eventstore.WriteModel

	UserLoginMustBeDomain                  bool
	ValidateOrgDomains                     bool
	SMTPSenderAddressMatchesInstanceDomain bool
	State                                  domain.PolicyState
}

func (wm *PolicyDomainWriteModel) Reduce() error {
	for _, event := range wm.Events {
		switch e := event.(type) {
		case *policy.DomainPolicyAddedEvent:
			wm.UserLoginMustBeDomain = e.UserLoginMustBeDomain
			wm.ValidateOrgDomains = e.ValidateOrgDomains
			wm.SMTPSenderAddressMatchesInstanceDomain = e.SMTPSenderAddressMatchesInstanceDomain
			wm.State = domain.PolicyStateActive
		case *policy.DomainPolicyChangedEvent:
			if e.UserLoginMustBeDomain != nil {
				wm.UserLoginMustBeDomain = *e.UserLoginMustBeDomain
			}
			if e.ValidateOrgDomains != nil {
				wm.ValidateOrgDomains = *e.ValidateOrgDomains
			}
			if e.SMTPSenderAddressMatchesInstanceDomain != nil {
				wm.SMTPSenderAddressMatchesInstanceDomain = *e.SMTPSenderAddressMatchesInstanceDomain
			}
		case *policy.DomainPolicyRemovedEvent:
			wm.State = domain.PolicyStateRemoved
		}
	}
	return wm.WriteModel.Reduce()
}

type DomainPolicyUsernamesWriteModel struct {
	eventstore.WriteModel

	PrimaryDomain   string
	VerifiedDomains []string
	Users           []*domainPolicyUsers
}

type domainPolicyUsers struct {
	id       string
	username string
}

func NewDomainPolicyUsernamesWriteModel(orgID string) *DomainPolicyUsernamesWriteModel {
	return &DomainPolicyUsernamesWriteModel{
		WriteModel: eventstore.WriteModel{
			ResourceOwner: orgID,
		},
		Users: make([]*domainPolicyUsers, 0),
	}
}

func (wm *DomainPolicyUsernamesWriteModel) AppendEvents(events ...eventstore.Event) {
	wm.WriteModel.AppendEvents(events...)
}

func (wm *DomainPolicyUsernamesWriteModel) Reduce() error {
	for _, event := range wm.Events {
		switch e := event.(type) {
		case *org.DomainVerifiedEvent:
			wm.VerifiedDomains = append(wm.VerifiedDomains, e.Domain)
		case *org.DomainRemovedEvent:
			wm.removeDomain(e.Domain)
		case *org.DomainPrimarySetEvent:
			wm.PrimaryDomain = e.Domain
		case *user.HumanAddedEvent:
			wm.Users = append(wm.Users, &domainPolicyUsers{id: e.Aggregate().ID, username: e.UserName})
		case *user.HumanRegisteredEvent:
			wm.Users = append(wm.Users, &domainPolicyUsers{id: e.Aggregate().ID, username: e.UserName})
		case *user.MachineAddedEvent:
			wm.Users = append(wm.Users, &domainPolicyUsers{id: e.Aggregate().ID, username: e.UserName})
		case *user.UsernameChangedEvent:
			for _, user := range wm.Users {
				if user.id == e.Aggregate().ID {
					user.username = e.UserName
					break
				}
			}
		case *user.DomainClaimedEvent:
			for _, user := range wm.Users {
				if user.id == e.Aggregate().ID {
					user.username = e.UserName
					break
				}
			}
		case *user.UserRemovedEvent:
			wm.removeUser(e.Aggregate().ID)
		}
	}
	return wm.WriteModel.Reduce()
}

func (wm *DomainPolicyUsernamesWriteModel) removeDomain(domain string) {
	for i, verifiedDomain := range wm.VerifiedDomains {
		if verifiedDomain == domain {
			wm.VerifiedDomains[i] = wm.VerifiedDomains[len(wm.VerifiedDomains)-1]
			wm.VerifiedDomains[len(wm.VerifiedDomains)-1] = ""
			wm.VerifiedDomains = wm.VerifiedDomains[:len(wm.VerifiedDomains)-1]
			return
		}
	}
}

func (wm *DomainPolicyUsernamesWriteModel) removeUser(userID string) {
	for i, user := range wm.Users {
		if user.id == userID {
			wm.Users[i] = wm.Users[len(wm.Users)-1]
			wm.Users[len(wm.Users)-1] = nil
			wm.Users = wm.Users[:len(wm.Users)-1]
			return
		}
	}
}

func (wm *DomainPolicyUsernamesWriteModel) Query() *eventstore.SearchQueryBuilder {
	return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
		ResourceOwner(wm.ResourceOwner).
		AddQuery().
		AggregateTypes(org.AggregateType, user.AggregateType).
		EventTypes(
			org.OrgDomainVerifiedEventType,
			org.OrgDomainRemovedEventType,
			org.OrgDomainPrimarySetEventType,
			user.HumanAddedType,
			user.HumanRegisteredType,
			user.MachineAddedEventType,
			user.UserUserNameChangedType,
			user.UserDomainClaimedType,
			user.UserRemovedType,
		).
		Builder()
}

func (wm *DomainPolicyUsernamesWriteModel) NewUsernameChangedEvents(ctx context.Context, userLoginMustBeDomain bool) []eventstore.Command {
	events := make([]eventstore.Command, 0, len(wm.Users))
	for _, changeUser := range wm.Users {
		events = append(events, user.NewUsernameChangedEvent(ctx,
			&user.NewAggregate(changeUser.id, wm.ResourceOwner).Aggregate,
			changeUser.username,
			wm.newUsername(changeUser.username, userLoginMustBeDomain),
			userLoginMustBeDomain,
			user.UsernameChangedEventWithPolicyChange()),
		)
	}
	return events
}

func (wm *DomainPolicyUsernamesWriteModel) newUsername(username string, userLoginMustBeDomain bool) string {
	if !userLoginMustBeDomain {
		// if the UserLoginMustBeDomain will be false, then it's currently true
		// which means the usernames must be suffixed to ensure their uniqueness
		// and the preferred login name remains the same
		return username + "@" + wm.PrimaryDomain
	}
	// the UserLoginMustBeDomain is currently false
	// which means the usernames might already be suffixed by a verified domain
	// so let's remove a potential duplicate suffix
	for _, verifiedDomain := range wm.VerifiedDomains {
		if index := strings.LastIndex(username, "@"+verifiedDomain); index > 0 {
			return username[:index]
		}
	}
	return username
}