package query

import (
	"context"
	"database/sql"
	_ "embed"
	"errors"
	"strings"
	"time"

	sq "github.com/Masterminds/squirrel"
	"golang.org/x/text/language"

	"github.com/zitadel/zitadel/internal/api/authz"
	"github.com/zitadel/zitadel/internal/api/call"
	"github.com/zitadel/zitadel/internal/crypto"
	"github.com/zitadel/zitadel/internal/database"
	"github.com/zitadel/zitadel/internal/domain"
	"github.com/zitadel/zitadel/internal/query/projection"
	"github.com/zitadel/zitadel/internal/telemetry/tracing"
	"github.com/zitadel/zitadel/internal/zerrors"
)

type Users struct {
	SearchResponse
	Users []*User
}

type User struct {
	ID                 string                     `json:"id,omitempty"`
	CreationDate       time.Time                  `json:"creation_date,omitempty"`
	ChangeDate         time.Time                  `json:"change_date,omitempty"`
	ResourceOwner      string                     `json:"resource_owner,omitempty"`
	Sequence           uint64                     `json:"sequence,omitempty"`
	State              domain.UserState           `json:"state,omitempty"`
	Type               domain.UserType            `json:"type,omitempty"`
	Username           string                     `json:"username,omitempty"`
	LoginNames         database.TextArray[string] `json:"login_names,omitempty"`
	PreferredLoginName string                     `json:"preferred_login_name,omitempty"`
	Human              *Human                     `json:"human,omitempty"`
	Machine            *Machine                   `json:"machine,omitempty"`
}

type Human struct {
	FirstName         string              `json:"first_name,omitempty"`
	LastName          string              `json:"last_name,omitempty"`
	NickName          string              `json:"nick_name,omitempty"`
	DisplayName       string              `json:"display_name,omitempty"`
	AvatarKey         string              `json:"avatar_key,omitempty"`
	PreferredLanguage language.Tag        `json:"preferred_language,omitempty"`
	Gender            domain.Gender       `json:"gender,omitempty"`
	Email             domain.EmailAddress `json:"email,omitempty"`
	IsEmailVerified   bool                `json:"is_email_verified,omitempty"`
	Phone             domain.PhoneNumber  `json:"phone,omitempty"`
	IsPhoneVerified   bool                `json:"is_phone_verified,omitempty"`
}

type Profile struct {
	ID                string
	CreationDate      time.Time
	ChangeDate        time.Time
	ResourceOwner     string
	Sequence          uint64
	FirstName         string
	LastName          string
	NickName          string
	DisplayName       string
	AvatarKey         string
	PreferredLanguage language.Tag
	Gender            domain.Gender
}

type Email struct {
	ID            string
	CreationDate  time.Time
	ChangeDate    time.Time
	ResourceOwner string
	Sequence      uint64
	Email         domain.EmailAddress
	IsVerified    bool
}

type Phone struct {
	ID            string
	CreationDate  time.Time
	ChangeDate    time.Time
	ResourceOwner string
	Sequence      uint64
	Phone         string
	IsVerified    bool
}

type Machine struct {
	Name            string               `json:"name,omitempty"`
	Description     string               `json:"description,omitempty"`
	Secret          *crypto.CryptoValue  `json:"secret,omitempty"`
	AccessTokenType domain.OIDCTokenType `json:"access_token_type,omitempty"`
}

type NotifyUser struct {
	ID                 string
	CreationDate       time.Time
	ChangeDate         time.Time
	ResourceOwner      string
	Sequence           uint64
	State              domain.UserState
	Type               domain.UserType
	Username           string
	LoginNames         database.TextArray[string]
	PreferredLoginName string
	FirstName          string
	LastName           string
	NickName           string
	DisplayName        string
	AvatarKey          string
	PreferredLanguage  language.Tag
	Gender             domain.Gender
	LastEmail          string
	VerifiedEmail      string
	LastPhone          string
	VerifiedPhone      string
	PasswordSet        bool
}

func (u *Users) RemoveNoPermission(ctx context.Context, permissionCheck domain.PermissionCheck) {
	removableIndexes := make([]int, 0)
	for i := range u.Users {
		ctxData := authz.GetCtxData(ctx)
		if ctxData.UserID != u.Users[i].ID {
			if err := permissionCheck(ctx, domain.PermissionUserRead, ctxData.OrgID, u.Users[i].ID); err != nil {
				removableIndexes = append(removableIndexes, i)
			}
		}
	}
	removed := 0
	for _, removeIndex := range removableIndexes {
		u.Users = removeUser(u.Users, removeIndex-removed)
		removed++
	}
	// reset count as some users could be removed
	u.SearchResponse.Count = uint64(len(u.Users))
}

func removeUser(slice []*User, s int) []*User {
	return append(slice[:s], slice[s+1:]...)
}

type UserSearchQueries struct {
	SearchRequest
	Queries []SearchQuery
}

var (
	userTable = table{
		name:          projection.UserTable,
		instanceIDCol: projection.UserInstanceIDCol,
	}
	UserIDCol = Column{
		name:  projection.UserIDCol,
		table: userTable,
	}
	UserCreationDateCol = Column{
		name:  projection.UserCreationDateCol,
		table: userTable,
	}
	UserChangeDateCol = Column{
		name:  projection.UserChangeDateCol,
		table: userTable,
	}
	UserResourceOwnerCol = Column{
		name:  projection.UserResourceOwnerCol,
		table: userTable,
	}
	UserInstanceIDCol = Column{
		name:  projection.UserInstanceIDCol,
		table: userTable,
	}
	UserStateCol = Column{
		name:  projection.UserStateCol,
		table: userTable,
	}
	UserSequenceCol = Column{
		name:  projection.UserSequenceCol,
		table: userTable,
	}
	UserUsernameCol = Column{
		name:           projection.UserUsernameCol,
		table:          userTable,
		isOrderByLower: true,
	}
	UserTypeCol = Column{
		name:  projection.UserTypeCol,
		table: userTable,
	}

	userLoginNamesTable         = loginNameTable.setAlias("login_names")
	userLoginNamesUserIDCol     = LoginNameUserIDCol.setTable(userLoginNamesTable)
	userLoginNamesNameCol       = LoginNameNameCol.setTable(userLoginNamesTable)
	userLoginNamesInstanceIDCol = LoginNameInstanceIDCol.setTable(userLoginNamesTable)
	userLoginNamesListCol       = Column{
		name:  "loginnames",
		table: userLoginNamesTable,
	}
	userLoginNamesLowerListCol = Column{
		name:  "loginnames_lower",
		table: userLoginNamesTable,
	}
	userPreferredLoginNameTable         = loginNameTable.setAlias("preferred_login_name")
	userPreferredLoginNameUserIDCol     = LoginNameUserIDCol.setTable(userPreferredLoginNameTable)
	userPreferredLoginNameCol           = LoginNameNameCol.setTable(userPreferredLoginNameTable)
	userPreferredLoginNameIsPrimaryCol  = LoginNameIsPrimaryCol.setTable(userPreferredLoginNameTable)
	userPreferredLoginNameInstanceIDCol = LoginNameInstanceIDCol.setTable(userPreferredLoginNameTable)
)

var (
	humanTable = table{
		name:          projection.UserHumanTable,
		instanceIDCol: projection.HumanUserInstanceIDCol,
	}
	// profile
	HumanUserIDCol = Column{
		name:  projection.HumanUserIDCol,
		table: humanTable,
	}
	HumanFirstNameCol = Column{
		name:           projection.HumanFirstNameCol,
		table:          humanTable,
		isOrderByLower: true,
	}
	HumanLastNameCol = Column{
		name:           projection.HumanLastNameCol,
		table:          humanTable,
		isOrderByLower: true,
	}
	HumanNickNameCol = Column{
		name:           projection.HumanNickNameCol,
		table:          humanTable,
		isOrderByLower: true,
	}
	HumanDisplayNameCol = Column{
		name:           projection.HumanDisplayNameCol,
		table:          humanTable,
		isOrderByLower: true,
	}
	HumanPreferredLanguageCol = Column{
		name:  projection.HumanPreferredLanguageCol,
		table: humanTable,
	}
	HumanGenderCol = Column{
		name:  projection.HumanGenderCol,
		table: humanTable,
	}
	HumanAvatarURLCol = Column{
		name:  projection.HumanAvatarURLCol,
		table: humanTable,
	}

	// email
	HumanEmailCol = Column{
		name:           projection.HumanEmailCol,
		table:          humanTable,
		isOrderByLower: true,
	}
	HumanIsEmailVerifiedCol = Column{
		name:  projection.HumanIsEmailVerifiedCol,
		table: humanTable,
	}

	// phone
	HumanPhoneCol = Column{
		name:  projection.HumanPhoneCol,
		table: humanTable,
	}
	HumanIsPhoneVerifiedCol = Column{
		name:  projection.HumanIsPhoneVerifiedCol,
		table: humanTable,
	}
)

var (
	machineTable = table{
		name:          projection.UserMachineTable,
		instanceIDCol: projection.MachineUserInstanceIDCol,
	}
	MachineUserIDCol = Column{
		name:  projection.MachineUserIDCol,
		table: machineTable,
	}
	MachineNameCol = Column{
		name:           projection.MachineNameCol,
		table:          machineTable,
		isOrderByLower: true,
	}
	MachineDescriptionCol = Column{
		name:  projection.MachineDescriptionCol,
		table: machineTable,
	}
	MachineSecretCol = Column{
		name:  projection.MachineSecretCol,
		table: machineTable,
	}
	MachineAccessTokenTypeCol = Column{
		name:  projection.MachineAccessTokenTypeCol,
		table: machineTable,
	}
)

var (
	notifyTable = table{
		name:          projection.UserNotifyTable,
		instanceIDCol: projection.NotifyInstanceIDCol,
	}
	NotifyUserIDCol = Column{
		name:  projection.NotifyUserIDCol,
		table: notifyTable,
	}
	NotifyEmailCol = Column{
		name:           projection.NotifyLastEmailCol,
		table:          notifyTable,
		isOrderByLower: true,
	}
	NotifyVerifiedEmailCol = Column{
		name:           projection.NotifyVerifiedEmailCol,
		table:          notifyTable,
		isOrderByLower: true,
	}
	NotifyPhoneCol = Column{
		name:  projection.NotifyLastPhoneCol,
		table: notifyTable,
	}
	NotifyVerifiedPhoneCol = Column{
		name:  projection.NotifyVerifiedPhoneCol,
		table: notifyTable,
	}
	NotifyPasswordSetCol = Column{
		name:  projection.NotifyPasswordSetCol,
		table: notifyTable,
	}
)

//go:embed user_by_id.sql
var userByIDQuery string

func (q *Queries) GetUserByID(ctx context.Context, shouldTriggerBulk bool, userID string) (user *User, err error) {
	ctx, span := tracing.NewSpan(ctx)
	defer func() { span.EndWithError(err) }()

	if shouldTriggerBulk {
		triggerUserProjections(ctx)
	}

	err = q.client.QueryRowContext(ctx,
		func(row *sql.Row) error {
			user, err = scanUser(row)
			return err
		},
		userByIDQuery,
		userID,
		authz.GetInstance(ctx).InstanceID(),
	)
	return user, err
}

//go:embed user_by_login_name.sql
var userByLoginNameQuery string

func (q *Queries) GetUserByLoginName(ctx context.Context, shouldTriggered bool, loginName string) (user *User, err error) {
	ctx, span := tracing.NewSpan(ctx)
	defer func() { span.EndWithError(err) }()

	if shouldTriggered {
		triggerUserProjections(ctx)
	}

	loginName = strings.ToLower(loginName)

	username := loginName
	domainIndex := strings.LastIndex(loginName, "@")
	var domainSuffix string
	// split between the last @ (so ignore it if the login name ends with it)
	if domainIndex > 0 && domainIndex != len(loginName)-1 {
		domainSuffix = loginName[domainIndex+1:]
		username = loginName[:domainIndex]
	}

	err = q.client.QueryRowContext(ctx,
		func(row *sql.Row) error {
			user, err = scanUser(row)
			return err
		},
		userByLoginNameQuery,
		username,
		domainSuffix,
		loginName,
		authz.GetInstance(ctx).InstanceID(),
	)
	return user, err
}

// Deprecated: use either GetUserByID or GetUserByLoginName
func (q *Queries) GetUser(ctx context.Context, shouldTriggerBulk bool, queries ...SearchQuery) (user *User, err error) {
	ctx, span := tracing.NewSpan(ctx)
	defer func() { span.EndWithError(err) }()

	if shouldTriggerBulk {
		triggerUserProjections(ctx)
	}

	query, scan := prepareUserQuery(ctx, q.client)
	for _, q := range queries {
		query = q.toQuery(query)
	}
	eq := sq.Eq{
		UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),
	}
	stmt, args, err := query.Where(eq).ToSql()
	if err != nil {
		return nil, zerrors.ThrowInternal(err, "QUERY-Dnhr2", "Errors.Query.SQLStatment")
	}

	err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
		user, err = scan(row)
		return err
	}, stmt, args...)
	return user, err
}

func (q *Queries) GetHumanProfile(ctx context.Context, userID string, queries ...SearchQuery) (profile *Profile, err error) {
	ctx, span := tracing.NewSpan(ctx)
	defer func() { span.EndWithError(err) }()

	query, scan := prepareProfileQuery(ctx, q.client)
	for _, q := range queries {
		query = q.toQuery(query)
	}
	eq := sq.Eq{
		UserIDCol.identifier():         userID,
		UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),
	}
	stmt, args, err := query.Where(eq).ToSql()
	if err != nil {
		return nil, zerrors.ThrowInternal(err, "QUERY-Dgbg2", "Errors.Query.SQLStatment")
	}

	err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
		profile, err = scan(row)
		return err
	}, stmt, args...)
	return profile, err
}

func (q *Queries) GetHumanEmail(ctx context.Context, userID string, queries ...SearchQuery) (email *Email, err error) {
	ctx, span := tracing.NewSpan(ctx)
	defer func() { span.EndWithError(err) }()

	query, scan := prepareEmailQuery(ctx, q.client)
	for _, q := range queries {
		query = q.toQuery(query)
	}
	eq := sq.Eq{
		UserIDCol.identifier():         userID,
		UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),
	}
	stmt, args, err := query.Where(eq).ToSql()
	if err != nil {
		return nil, zerrors.ThrowInternal(err, "QUERY-BHhj3", "Errors.Query.SQLStatment")
	}

	err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
		email, err = scan(row)
		return err
	}, stmt, args...)
	return email, err
}

func (q *Queries) GetHumanPhone(ctx context.Context, userID string, queries ...SearchQuery) (phone *Phone, err error) {
	ctx, span := tracing.NewSpan(ctx)
	defer func() { span.EndWithError(err) }()

	query, scan := preparePhoneQuery(ctx, q.client)
	for _, q := range queries {
		query = q.toQuery(query)
	}
	eq := sq.Eq{
		UserIDCol.identifier():         userID,
		UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),
	}
	stmt, args, err := query.Where(eq).ToSql()
	if err != nil {
		return nil, zerrors.ThrowInternal(err, "QUERY-Dg43g", "Errors.Query.SQLStatment")
	}

	err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
		phone, err = scan(row)
		return err
	}, stmt, args...)
	return phone, err
}

//go:embed user_notify_by_id.sql
var notifyUserByIDQuery string

func (q *Queries) GetNotifyUserByID(ctx context.Context, shouldTriggered bool, userID string) (user *NotifyUser, err error) {
	ctx, span := tracing.NewSpan(ctx)
	defer func() { span.EndWithError(err) }()

	if shouldTriggered {
		triggerUserProjections(ctx)
	}

	err = q.client.QueryRowContext(ctx,
		func(row *sql.Row) error {
			user, err = scanNotifyUser(row)
			return err
		},
		notifyUserByIDQuery,
		userID,
		authz.GetInstance(ctx).InstanceID(),
	)
	return user, err
}

//go:embed user_notify_by_login_name.sql
var notifyUserByLoginNameQuery string

func (q *Queries) GetNotifyUserByLoginName(ctx context.Context, shouldTriggered bool, loginName string) (user *NotifyUser, err error) {
	ctx, span := tracing.NewSpan(ctx)
	defer func() { span.EndWithError(err) }()

	if shouldTriggered {
		triggerUserProjections(ctx)
	}

	loginName = strings.ToLower(loginName)

	username := loginName
	domainIndex := strings.LastIndex(loginName, "@")
	var domainSuffix string
	// split between the last @ (so ignore it if the login name ends with it)
	if domainIndex > 0 && domainIndex != len(loginName)-1 {
		domainSuffix = loginName[domainIndex+1:]
		username = loginName[:domainIndex]
	}

	err = q.client.QueryRowContext(ctx,
		func(row *sql.Row) error {
			user, err = scanNotifyUser(row)
			return err
		},
		notifyUserByLoginNameQuery,
		username,
		domainSuffix,
		loginName,
		authz.GetInstance(ctx).InstanceID(),
	)
	return user, err
}

func (q *Queries) GetNotifyUser(ctx context.Context, shouldTriggered bool, queries ...SearchQuery) (user *NotifyUser, err error) {
	ctx, span := tracing.NewSpan(ctx)
	defer func() { span.EndWithError(err) }()

	if shouldTriggered {
		triggerUserProjections(ctx)
	}

	query, scan := prepareNotifyUserQuery(ctx, q.client)
	for _, q := range queries {
		query = q.toQuery(query)
	}
	eq := sq.Eq{
		UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),
	}
	stmt, args, err := query.Where(eq).ToSql()
	if err != nil {
		return nil, zerrors.ThrowInternal(err, "QUERY-Err3g", "Errors.Query.SQLStatment")
	}

	err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
		user, err = scan(row)
		return err
	}, stmt, args...)
	return user, err
}

func (q *Queries) SearchUsers(ctx context.Context, queries *UserSearchQueries) (users *Users, err error) {
	ctx, span := tracing.NewSpan(ctx)
	defer func() { span.EndWithError(err) }()

	query, scan := prepareUsersQuery(ctx, q.client)
	eq := sq.Eq{UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID()}
	stmt, args, err := queries.toQuery(query).Where(eq).
		ToSql()
	if err != nil {
		return nil, zerrors.ThrowInternal(err, "QUERY-Dgbg2", "Errors.Query.SQLStatment")
	}

	err = q.client.QueryContext(ctx, func(rows *sql.Rows) error {
		users, err = scan(rows)
		return err
	}, stmt, args...)
	if err != nil {
		return nil, zerrors.ThrowInternal(err, "QUERY-AG4gs", "Errors.Internal")
	}
	users.State, err = q.latestState(ctx, userTable)
	return users, err
}

func (q *Queries) IsUserUnique(ctx context.Context, username, email, resourceOwner string) (isUnique bool, err error) {
	ctx, span := tracing.NewSpan(ctx)
	defer func() { span.EndWithError(err) }()

	query, scan := prepareUserUniqueQuery(ctx, q.client)
	queries := make([]SearchQuery, 0, 3)
	if username != "" {
		usernameQuery, err := NewUserUsernameSearchQuery(username, TextEquals)
		if err != nil {
			return false, err
		}
		queries = append(queries, usernameQuery)
	}
	if email != "" {
		emailQuery, err := NewUserEmailSearchQuery(email, TextEquals)
		if err != nil {
			return false, err
		}
		queries = append(queries, emailQuery)
	}
	if resourceOwner != "" {
		resourceOwnerQuery, err := NewUserResourceOwnerSearchQuery(resourceOwner, TextEquals)
		if err != nil {
			return false, err
		}
		queries = append(queries, resourceOwnerQuery)
	}
	for _, q := range queries {
		query = q.toQuery(query)
	}
	eq := sq.Eq{UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID()}
	stmt, args, err := query.Where(eq).ToSql()
	if err != nil {
		return false, zerrors.ThrowInternal(err, "QUERY-Dg43g", "Errors.Query.SQLStatment")
	}

	err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
		isUnique, err = scan(row)
		return err
	}, stmt, args...)
	return isUnique, err
}

func (q *UserSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
	query = q.SearchRequest.toQuery(query)
	for _, q := range q.Queries {
		query = q.toQuery(query)
	}
	return query
}

func (r *UserSearchQueries) AppendMyResourceOwnerQuery(orgID string) error {
	query, err := NewUserResourceOwnerSearchQuery(orgID, TextEquals)
	if err != nil {
		return err
	}
	r.Queries = append(r.Queries, query)
	return nil
}

func NewUserOrSearchQuery(values []SearchQuery) (SearchQuery, error) {
	return NewOrQuery(values...)
}
func NewUserAndSearchQuery(values []SearchQuery) (SearchQuery, error) {
	return NewAndQuery(values...)
}
func NewUserNotSearchQuery(value SearchQuery) (SearchQuery, error) {
	return NewNotQuery(value)
}
func NewUserInUserIdsSearchQuery(values []string) (SearchQuery, error) {
	return NewInTextQuery(UserIDCol, values)
}
func NewUserInUserEmailsSearchQuery(values []string) (SearchQuery, error) {
	return NewInTextQuery(HumanEmailCol, values)
}

func NewUserResourceOwnerSearchQuery(value string, comparison TextComparison) (SearchQuery, error) {
	return NewTextQuery(UserResourceOwnerCol, value, comparison)
}

func NewUserUsernameSearchQuery(value string, comparison TextComparison) (SearchQuery, error) {
	return NewTextQuery(UserUsernameCol, value, comparison)
}

func NewUserFirstNameSearchQuery(value string, comparison TextComparison) (SearchQuery, error) {
	return NewTextQuery(HumanFirstNameCol, value, comparison)
}

func NewUserLastNameSearchQuery(value string, comparison TextComparison) (SearchQuery, error) {
	return NewTextQuery(HumanLastNameCol, value, comparison)
}

func NewUserNickNameSearchQuery(value string, comparison TextComparison) (SearchQuery, error) {
	return NewTextQuery(HumanNickNameCol, value, comparison)
}

func NewUserDisplayNameSearchQuery(value string, comparison TextComparison) (SearchQuery, error) {
	return NewTextQuery(HumanDisplayNameCol, value, comparison)
}

func NewUserEmailSearchQuery(value string, comparison TextComparison) (SearchQuery, error) {
	return NewTextQuery(HumanEmailCol, value, comparison)
}

func NewUserPhoneSearchQuery(value string, comparison TextComparison) (SearchQuery, error) {
	return NewTextQuery(HumanPhoneCol, value, comparison)
}

func NewUserVerifiedEmailSearchQuery(value string, comparison TextComparison) (SearchQuery, error) {
	return NewTextQuery(NotifyVerifiedEmailCol, value, comparison)
}

func NewUserVerifiedPhoneSearchQuery(value string, comparison TextComparison) (SearchQuery, error) {
	return NewTextQuery(NotifyVerifiedPhoneCol, value, comparison)
}

func NewUserStateSearchQuery(value int32) (SearchQuery, error) {
	return NewNumberQuery(UserStateCol, value, NumberEquals)
}

func NewUserTypeSearchQuery(value int32) (SearchQuery, error) {
	return NewNumberQuery(UserTypeCol, value, NumberEquals)
}

func NewUserPreferredLoginNameSearchQuery(value string, comparison TextComparison) (SearchQuery, error) {
	return NewTextQuery(userPreferredLoginNameCol, value, comparison)
}

func NewUserLoginNamesSearchQuery(value string) (SearchQuery, error) {
	return NewTextQuery(userLoginNamesLowerListCol, strings.ToLower(value), TextListContains)
}

func NewUserLoginNameExistsQuery(value string, comparison TextComparison) (SearchQuery, error) {
	//linking queries for the subselect
	instanceQuery, err := NewColumnComparisonQuery(LoginNameInstanceIDCol, UserInstanceIDCol, ColumnEquals)
	if err != nil {
		return nil, err
	}
	userIDQuery, err := NewColumnComparisonQuery(LoginNameUserIDCol, UserIDCol, ColumnEquals)
	if err != nil {
		return nil, err
	}
	//text query to select data from the linked sub select
	loginNameQuery, err := NewTextQuery(LoginNameNameCol, value, comparison)
	if err != nil {
		return nil, err
	}
	//full definition of the sub select
	subSelect, err := NewSubSelect(LoginNameUserIDCol, []SearchQuery{instanceQuery, userIDQuery, loginNameQuery})
	if err != nil {
		return nil, err
	}
	// "WHERE * IN (*)" query with subquery as list-data provider
	return NewListQuery(
		UserIDCol,
		subSelect,
		ListIn,
	)
}

func triggerUserProjections(ctx context.Context) {
	triggerBatch(ctx, projection.UserProjection, projection.LoginNameProjection)
}

func prepareLoginNamesQuery() (string, []interface{}, error) {
	return sq.Select(
		userLoginNamesUserIDCol.identifier(),
		"ARRAY_AGG("+userLoginNamesNameCol.identifier()+")::TEXT[] AS "+userLoginNamesListCol.name,
		"ARRAY_AGG(LOWER("+userLoginNamesNameCol.identifier()+"))::TEXT[] AS "+userLoginNamesLowerListCol.name,
		userLoginNamesInstanceIDCol.identifier(),
	).From(userLoginNamesTable.identifier()).
		GroupBy(
			userLoginNamesUserIDCol.identifier(),
			userLoginNamesInstanceIDCol.identifier(),
		).ToSql()
}

func preparePreferredLoginNamesQuery() (string, []interface{}, error) {
	return sq.Select(
		userPreferredLoginNameUserIDCol.identifier(),
		userPreferredLoginNameCol.identifier(),
		userPreferredLoginNameInstanceIDCol.identifier(),
	).From(userPreferredLoginNameTable.identifier()).
		Where(sq.Eq{
			userPreferredLoginNameIsPrimaryCol.identifier(): true,
		},
		).ToSql()
}

func scanUser(row *sql.Row) (*User, error) {
	u := new(User)
	var count int
	preferredLoginName := sql.NullString{}

	humanID := sql.NullString{}
	firstName := sql.NullString{}
	lastName := sql.NullString{}
	nickName := sql.NullString{}
	displayName := sql.NullString{}
	preferredLanguage := sql.NullString{}
	gender := sql.NullInt32{}
	avatarKey := sql.NullString{}
	email := sql.NullString{}
	isEmailVerified := sql.NullBool{}
	phone := sql.NullString{}
	isPhoneVerified := sql.NullBool{}

	machineID := sql.NullString{}
	name := sql.NullString{}
	description := sql.NullString{}
	var secret *crypto.CryptoValue
	accessTokenType := sql.NullInt32{}

	err := row.Scan(
		&u.ID,
		&u.CreationDate,
		&u.ChangeDate,
		&u.ResourceOwner,
		&u.Sequence,
		&u.State,
		&u.Type,
		&u.Username,
		&u.LoginNames,
		&preferredLoginName,
		&humanID,
		&firstName,
		&lastName,
		&nickName,
		&displayName,
		&preferredLanguage,
		&gender,
		&avatarKey,
		&email,
		&isEmailVerified,
		&phone,
		&isPhoneVerified,
		&machineID,
		&name,
		&description,
		&secret,
		&accessTokenType,
		&count,
	)

	if err != nil || count != 1 {
		if errors.Is(err, sql.ErrNoRows) || count != 1 {
			return nil, zerrors.ThrowNotFound(err, "QUERY-Dfbg2", "Errors.User.NotFound")
		}
		return nil, zerrors.ThrowInternal(err, "QUERY-Bgah2", "Errors.Internal")
	}

	u.PreferredLoginName = preferredLoginName.String

	if humanID.Valid {
		u.Human = &Human{
			FirstName:         firstName.String,
			LastName:          lastName.String,
			NickName:          nickName.String,
			DisplayName:       displayName.String,
			AvatarKey:         avatarKey.String,
			PreferredLanguage: language.Make(preferredLanguage.String),
			Gender:            domain.Gender(gender.Int32),
			Email:             domain.EmailAddress(email.String),
			IsEmailVerified:   isEmailVerified.Bool,
			Phone:             domain.PhoneNumber(phone.String),
			IsPhoneVerified:   isPhoneVerified.Bool,
		}
	} else if machineID.Valid {
		u.Machine = &Machine{
			Name:            name.String,
			Description:     description.String,
			Secret:          secret,
			AccessTokenType: domain.OIDCTokenType(accessTokenType.Int32),
		}
	}
	return u, nil
}

func prepareUserQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*User, error)) {
	loginNamesQuery, loginNamesArgs, err := prepareLoginNamesQuery()
	if err != nil {
		return sq.SelectBuilder{}, nil
	}
	preferredLoginNameQuery, preferredLoginNameArgs, err := preparePreferredLoginNamesQuery()
	if err != nil {
		return sq.SelectBuilder{}, nil
	}
	return sq.Select(
			UserIDCol.identifier(),
			UserCreationDateCol.identifier(),
			UserChangeDateCol.identifier(),
			UserResourceOwnerCol.identifier(),
			UserSequenceCol.identifier(),
			UserStateCol.identifier(),
			UserTypeCol.identifier(),
			UserUsernameCol.identifier(),
			userLoginNamesListCol.identifier(),
			userPreferredLoginNameCol.identifier(),
			HumanUserIDCol.identifier(),
			HumanFirstNameCol.identifier(),
			HumanLastNameCol.identifier(),
			HumanNickNameCol.identifier(),
			HumanDisplayNameCol.identifier(),
			HumanPreferredLanguageCol.identifier(),
			HumanGenderCol.identifier(),
			HumanAvatarURLCol.identifier(),
			HumanEmailCol.identifier(),
			HumanIsEmailVerifiedCol.identifier(),
			HumanPhoneCol.identifier(),
			HumanIsPhoneVerifiedCol.identifier(),
			MachineUserIDCol.identifier(),
			MachineNameCol.identifier(),
			MachineDescriptionCol.identifier(),
			MachineSecretCol.identifier(),
			MachineAccessTokenTypeCol.identifier(),
			countColumn.identifier(),
		).
			From(userTable.identifier()).
			LeftJoin(join(HumanUserIDCol, UserIDCol)).
			LeftJoin(join(MachineUserIDCol, UserIDCol)).
			LeftJoin("("+loginNamesQuery+") AS "+userLoginNamesTable.alias+" ON "+
				userLoginNamesUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+
				userLoginNamesInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(),
				loginNamesArgs...).
			LeftJoin("("+preferredLoginNameQuery+") AS "+userPreferredLoginNameTable.alias+" ON "+
				userPreferredLoginNameUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+
				userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier()+db.Timetravel(call.Took(ctx)),
				preferredLoginNameArgs...).
			PlaceholderFormat(sq.Dollar),

		scanUser
}

func prepareProfileQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Profile, error)) {
	return sq.Select(
			UserIDCol.identifier(),
			UserCreationDateCol.identifier(),
			UserChangeDateCol.identifier(),
			UserResourceOwnerCol.identifier(),
			UserSequenceCol.identifier(),
			HumanUserIDCol.identifier(),
			HumanFirstNameCol.identifier(),
			HumanLastNameCol.identifier(),
			HumanNickNameCol.identifier(),
			HumanDisplayNameCol.identifier(),
			HumanPreferredLanguageCol.identifier(),
			HumanGenderCol.identifier(),
			HumanAvatarURLCol.identifier()).
			From(userTable.identifier()).
			LeftJoin(join(HumanUserIDCol, UserIDCol) + db.Timetravel(call.Took(ctx))).
			PlaceholderFormat(sq.Dollar),
		func(row *sql.Row) (*Profile, error) {
			p := new(Profile)

			humanID := sql.NullString{}
			firstName := sql.NullString{}
			lastName := sql.NullString{}
			nickName := sql.NullString{}
			displayName := sql.NullString{}
			preferredLanguage := sql.NullString{}
			gender := sql.NullInt32{}
			avatarKey := sql.NullString{}
			err := row.Scan(
				&p.ID,
				&p.CreationDate,
				&p.ChangeDate,
				&p.ResourceOwner,
				&p.Sequence,
				&humanID,
				&firstName,
				&lastName,
				&nickName,
				&displayName,
				&preferredLanguage,
				&gender,
				&avatarKey,
			)
			if err != nil {
				if errors.Is(err, sql.ErrNoRows) {
					return nil, zerrors.ThrowNotFound(err, "QUERY-HNhb3", "Errors.User.NotFound")
				}
				return nil, zerrors.ThrowInternal(err, "QUERY-Rfheq", "Errors.Internal")
			}
			if !humanID.Valid {
				return nil, zerrors.ThrowPreconditionFailed(nil, "QUERY-WLTce", "Errors.User.NotHuman")
			}

			p.FirstName = firstName.String
			p.LastName = lastName.String
			p.NickName = nickName.String
			p.DisplayName = displayName.String
			p.AvatarKey = avatarKey.String
			p.PreferredLanguage = language.Make(preferredLanguage.String)
			p.Gender = domain.Gender(gender.Int32)

			return p, nil
		}
}

func prepareEmailQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Email, error)) {
	return sq.Select(
			UserIDCol.identifier(),
			UserCreationDateCol.identifier(),
			UserChangeDateCol.identifier(),
			UserResourceOwnerCol.identifier(),
			UserSequenceCol.identifier(),
			HumanUserIDCol.identifier(),
			HumanEmailCol.identifier(),
			HumanIsEmailVerifiedCol.identifier()).
			From(userTable.identifier()).
			LeftJoin(join(HumanUserIDCol, UserIDCol) + db.Timetravel(call.Took(ctx))).
			PlaceholderFormat(sq.Dollar),
		func(row *sql.Row) (*Email, error) {
			e := new(Email)

			humanID := sql.NullString{}
			email := sql.NullString{}
			isEmailVerified := sql.NullBool{}

			err := row.Scan(
				&e.ID,
				&e.CreationDate,
				&e.ChangeDate,
				&e.ResourceOwner,
				&e.Sequence,
				&humanID,
				&email,
				&isEmailVerified,
			)
			if err != nil {
				if errors.Is(err, sql.ErrNoRows) {
					return nil, zerrors.ThrowNotFound(err, "QUERY-Hms2s", "Errors.User.NotFound")
				}
				return nil, zerrors.ThrowInternal(err, "QUERY-Nu42d", "Errors.Internal")
			}
			if !humanID.Valid {
				return nil, zerrors.ThrowPreconditionFailed(nil, "QUERY-pt7HY", "Errors.User.NotHuman")
			}

			e.Email = domain.EmailAddress(email.String)
			e.IsVerified = isEmailVerified.Bool

			return e, nil
		}
}

func preparePhoneQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Phone, error)) {
	return sq.Select(
			UserIDCol.identifier(),
			UserCreationDateCol.identifier(),
			UserChangeDateCol.identifier(),
			UserResourceOwnerCol.identifier(),
			UserSequenceCol.identifier(),
			HumanUserIDCol.identifier(),
			HumanPhoneCol.identifier(),
			HumanIsPhoneVerifiedCol.identifier()).
			From(userTable.identifier()).
			LeftJoin(join(HumanUserIDCol, UserIDCol) + db.Timetravel(call.Took(ctx))).
			PlaceholderFormat(sq.Dollar),
		func(row *sql.Row) (*Phone, error) {
			e := new(Phone)

			humanID := sql.NullString{}
			phone := sql.NullString{}
			isPhoneVerified := sql.NullBool{}

			err := row.Scan(
				&e.ID,
				&e.CreationDate,
				&e.ChangeDate,
				&e.ResourceOwner,
				&e.Sequence,
				&humanID,
				&phone,
				&isPhoneVerified,
			)
			if err != nil {
				if errors.Is(err, sql.ErrNoRows) {
					return nil, zerrors.ThrowNotFound(err, "QUERY-DAvb3", "Errors.User.NotFound")
				}
				return nil, zerrors.ThrowInternal(err, "QUERY-Bmf2h", "Errors.Internal")
			}
			if !humanID.Valid {
				return nil, zerrors.ThrowPreconditionFailed(nil, "QUERY-hliQl", "Errors.User.NotHuman")
			}

			e.Phone = phone.String
			e.IsVerified = isPhoneVerified.Bool

			return e, nil
		}
}

func prepareNotifyUserQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*NotifyUser, error)) {
	loginNamesQuery, loginNamesArgs, err := prepareLoginNamesQuery()
	if err != nil {
		return sq.SelectBuilder{}, nil
	}
	preferredLoginNameQuery, preferredLoginNameArgs, err := preparePreferredLoginNamesQuery()
	if err != nil {
		return sq.SelectBuilder{}, nil
	}
	return sq.Select(
			UserIDCol.identifier(),
			UserCreationDateCol.identifier(),
			UserChangeDateCol.identifier(),
			UserResourceOwnerCol.identifier(),
			UserSequenceCol.identifier(),
			UserStateCol.identifier(),
			UserTypeCol.identifier(),
			UserUsernameCol.identifier(),
			userLoginNamesListCol.identifier(),
			userPreferredLoginNameCol.identifier(),
			HumanUserIDCol.identifier(),
			HumanFirstNameCol.identifier(),
			HumanLastNameCol.identifier(),
			HumanNickNameCol.identifier(),
			HumanDisplayNameCol.identifier(),
			HumanPreferredLanguageCol.identifier(),
			HumanGenderCol.identifier(),
			HumanAvatarURLCol.identifier(),
			NotifyUserIDCol.identifier(),
			NotifyEmailCol.identifier(),
			NotifyVerifiedEmailCol.identifier(),
			NotifyPhoneCol.identifier(),
			NotifyVerifiedPhoneCol.identifier(),
			NotifyPasswordSetCol.identifier(),
			countColumn.identifier(),
		).
			From(userTable.identifier()).
			LeftJoin(join(HumanUserIDCol, UserIDCol)).
			LeftJoin(join(NotifyUserIDCol, UserIDCol)).
			LeftJoin("("+loginNamesQuery+") AS "+userLoginNamesTable.alias+" ON "+
				userLoginNamesUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+
				userLoginNamesInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(),
				loginNamesArgs...).
			LeftJoin("("+preferredLoginNameQuery+") AS "+userPreferredLoginNameTable.alias+" ON "+
				userPreferredLoginNameUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+
				userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier()+db.Timetravel(call.Took(ctx)),
				preferredLoginNameArgs...).
			PlaceholderFormat(sq.Dollar),
		scanNotifyUser
}

func scanNotifyUser(row *sql.Row) (*NotifyUser, error) {
	u := new(NotifyUser)
	var count int
	loginNames := database.TextArray[string]{}
	preferredLoginName := sql.NullString{}

	humanID := sql.NullString{}
	firstName := sql.NullString{}
	lastName := sql.NullString{}
	nickName := sql.NullString{}
	displayName := sql.NullString{}
	preferredLanguage := sql.NullString{}
	gender := sql.NullInt32{}
	avatarKey := sql.NullString{}

	notifyUserID := sql.NullString{}
	notifyEmail := sql.NullString{}
	notifyVerifiedEmail := sql.NullString{}
	notifyPhone := sql.NullString{}
	notifyVerifiedPhone := sql.NullString{}
	notifyPasswordSet := sql.NullBool{}

	err := row.Scan(
		&u.ID,
		&u.CreationDate,
		&u.ChangeDate,
		&u.ResourceOwner,
		&u.Sequence,
		&u.State,
		&u.Type,
		&u.Username,
		&loginNames,
		&preferredLoginName,
		&humanID,
		&firstName,
		&lastName,
		&nickName,
		&displayName,
		&preferredLanguage,
		&gender,
		&avatarKey,
		&notifyUserID,
		&notifyEmail,
		&notifyVerifiedEmail,
		&notifyPhone,
		&notifyVerifiedPhone,
		&notifyPasswordSet,
		&count,
	)

	if err != nil || count != 1 {
		if errors.Is(err, sql.ErrNoRows) || count != 1 {
			return nil, zerrors.ThrowNotFound(err, "QUERY-Dgqd2", "Errors.User.NotFound")
		}
		return nil, zerrors.ThrowInternal(err, "QUERY-Dbwsg", "Errors.Internal")
	}

	if !notifyUserID.Valid {
		return nil, zerrors.ThrowPreconditionFailed(nil, "QUERY-Sfw3f", "Errors.User.NotFound")
	}

	u.LoginNames = loginNames
	if preferredLoginName.Valid {
		u.PreferredLoginName = preferredLoginName.String
	}
	if humanID.Valid {
		u.FirstName = firstName.String
		u.LastName = lastName.String
		u.NickName = nickName.String
		u.DisplayName = displayName.String
		u.AvatarKey = avatarKey.String
		u.PreferredLanguage = language.Make(preferredLanguage.String)
		u.Gender = domain.Gender(gender.Int32)
	}
	u.LastEmail = notifyEmail.String
	u.VerifiedEmail = notifyVerifiedEmail.String
	u.LastPhone = notifyPhone.String
	u.VerifiedPhone = notifyVerifiedPhone.String
	u.PasswordSet = notifyPasswordSet.Bool

	return u, nil
}

func prepareUserUniqueQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (bool, error)) {
	return sq.Select(
			UserIDCol.identifier(),
			UserStateCol.identifier(),
			UserUsernameCol.identifier(),
			HumanUserIDCol.identifier(),
			HumanEmailCol.identifier(),
			HumanIsEmailVerifiedCol.identifier()).
			From(userTable.identifier()).
			LeftJoin(join(HumanUserIDCol, UserIDCol) + db.Timetravel(call.Took(ctx))).
			PlaceholderFormat(sq.Dollar),
		func(row *sql.Row) (bool, error) {
			userID := sql.NullString{}
			state := sql.NullInt32{}
			username := sql.NullString{}
			humanID := sql.NullString{}
			email := sql.NullString{}
			isEmailVerified := sql.NullBool{}

			err := row.Scan(
				&userID,
				&state,
				&username,
				&humanID,
				&email,
				&isEmailVerified,
			)
			if err != nil {
				if errors.Is(err, sql.ErrNoRows) {
					return true, nil
				}
				return false, zerrors.ThrowInternal(err, "QUERY-Cxces", "Errors.Internal")
			}
			return !userID.Valid, nil
		}
}

func prepareUsersQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) {
	loginNamesQuery, loginNamesArgs, err := prepareLoginNamesQuery()
	if err != nil {
		return sq.SelectBuilder{}, nil
	}
	preferredLoginNameQuery, preferredLoginNameArgs, err := preparePreferredLoginNamesQuery()
	if err != nil {
		return sq.SelectBuilder{}, nil
	}
	return sq.Select(
			UserIDCol.identifier(),
			UserCreationDateCol.identifier(),
			UserChangeDateCol.identifier(),
			UserResourceOwnerCol.identifier(),
			UserSequenceCol.identifier(),
			UserStateCol.identifier(),
			UserTypeCol.identifier(),
			UserUsernameCol.identifier(),
			userLoginNamesListCol.identifier(),
			userPreferredLoginNameCol.identifier(),
			HumanUserIDCol.identifier(),
			HumanFirstNameCol.identifier(),
			HumanLastNameCol.identifier(),
			HumanNickNameCol.identifier(),
			HumanDisplayNameCol.identifier(),
			HumanPreferredLanguageCol.identifier(),
			HumanGenderCol.identifier(),
			HumanAvatarURLCol.identifier(),
			HumanEmailCol.identifier(),
			HumanIsEmailVerifiedCol.identifier(),
			HumanPhoneCol.identifier(),
			HumanIsPhoneVerifiedCol.identifier(),
			MachineUserIDCol.identifier(),
			MachineNameCol.identifier(),
			MachineDescriptionCol.identifier(),
			MachineSecretCol.identifier(),
			MachineAccessTokenTypeCol.identifier(),
			countColumn.identifier()).
			From(userTable.identifier()).
			LeftJoin(join(HumanUserIDCol, UserIDCol)).
			LeftJoin(join(MachineUserIDCol, UserIDCol)).
			LeftJoin("("+loginNamesQuery+") AS "+userLoginNamesTable.alias+" ON "+
				userLoginNamesUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+
				userLoginNamesInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(),
				loginNamesArgs...).
			LeftJoin("("+preferredLoginNameQuery+") AS "+userPreferredLoginNameTable.alias+" ON "+
				userPreferredLoginNameUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+
				userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier()+db.Timetravel(call.Took(ctx)),
				preferredLoginNameArgs...).
			PlaceholderFormat(sq.Dollar),
		func(rows *sql.Rows) (*Users, error) {
			users := make([]*User, 0)
			var count uint64
			for rows.Next() {
				u := new(User)
				loginNames := database.TextArray[string]{}
				preferredLoginName := sql.NullString{}

				humanID := sql.NullString{}
				firstName := sql.NullString{}
				lastName := sql.NullString{}
				nickName := sql.NullString{}
				displayName := sql.NullString{}
				preferredLanguage := sql.NullString{}
				gender := sql.NullInt32{}
				avatarKey := sql.NullString{}
				email := sql.NullString{}
				isEmailVerified := sql.NullBool{}
				phone := sql.NullString{}
				isPhoneVerified := sql.NullBool{}

				machineID := sql.NullString{}
				name := sql.NullString{}
				description := sql.NullString{}
				secret := new(crypto.CryptoValue)
				accessTokenType := sql.NullInt32{}

				err := rows.Scan(
					&u.ID,
					&u.CreationDate,
					&u.ChangeDate,
					&u.ResourceOwner,
					&u.Sequence,
					&u.State,
					&u.Type,
					&u.Username,
					&loginNames,
					&preferredLoginName,
					&humanID,
					&firstName,
					&lastName,
					&nickName,
					&displayName,
					&preferredLanguage,
					&gender,
					&avatarKey,
					&email,
					&isEmailVerified,
					&phone,
					&isPhoneVerified,
					&machineID,
					&name,
					&description,
					secret,
					&accessTokenType,
					&count,
				)
				if err != nil {
					return nil, err
				}

				u.LoginNames = loginNames
				if preferredLoginName.Valid {
					u.PreferredLoginName = preferredLoginName.String
				}

				if humanID.Valid {
					u.Human = &Human{
						FirstName:         firstName.String,
						LastName:          lastName.String,
						NickName:          nickName.String,
						DisplayName:       displayName.String,
						AvatarKey:         avatarKey.String,
						PreferredLanguage: language.Make(preferredLanguage.String),
						Gender:            domain.Gender(gender.Int32),
						Email:             domain.EmailAddress(email.String),
						IsEmailVerified:   isEmailVerified.Bool,
						Phone:             domain.PhoneNumber(phone.String),
						IsPhoneVerified:   isPhoneVerified.Bool,
					}
				} else if machineID.Valid {
					u.Machine = &Machine{
						Name:            name.String,
						Description:     description.String,
						Secret:          secret,
						AccessTokenType: domain.OIDCTokenType(accessTokenType.Int32),
					}
				}

				users = append(users, u)
			}

			if err := rows.Close(); err != nil {
				return nil, zerrors.ThrowInternal(err, "QUERY-frhbd", "Errors.Query.CloseRows")
			}

			return &Users{
				Users: users,
				SearchResponse: SearchResponse{
					Count: count,
				},
			}, nil
		}
}