package model

import (
	"time"

	"golang.org/x/text/language"

	"github.com/zitadel/zitadel/internal/domain"
	"github.com/zitadel/zitadel/internal/errors"
	"github.com/zitadel/zitadel/internal/eventstore/v1/models"
	iam_model "github.com/zitadel/zitadel/internal/iam/model"
)

type UserView struct {
	ID                 string
	UserName           string
	CreationDate       time.Time
	ChangeDate         time.Time
	State              UserState
	Sequence           uint64
	ResourceOwner      string
	LastLogin          time.Time
	PreferredLoginName string
	LoginNames         []string
	*MachineView
	*HumanView
}

type HumanView struct {
	PasswordSet              bool
	PasswordInitRequired     bool
	PasswordChangeRequired   bool
	UsernameChangeRequired   bool
	PasswordChanged          time.Time
	FirstName                string
	LastName                 string
	NickName                 string
	DisplayName              string
	AvatarKey                string
	PreferredLanguage        string
	Gender                   Gender
	Email                    string
	IsEmailVerified          bool
	Phone                    string
	IsPhoneVerified          bool
	Country                  string
	Locality                 string
	PostalCode               string
	Region                   string
	StreetAddress            string
	OTPState                 MFAState
	OTPSMSAdded              bool
	OTPEmailAdded            bool
	U2FTokens                []*WebAuthNView
	PasswordlessTokens       []*WebAuthNView
	MFAMaxSetUp              domain.MFALevel
	MFAInitSkipped           time.Time
	InitRequired             bool
	PasswordlessInitRequired bool
}

type WebAuthNView struct {
	TokenID string
	Name    string
	State   MFAState
}

type MachineView struct {
	LastKeyAdded time.Time
	Name         string
	Description  string
}

type UserSearchRequest struct {
	Offset        uint64
	Limit         uint64
	SortingColumn UserSearchKey
	Asc           bool
	Queries       []*UserSearchQuery
}

type UserSearchKey int32

const (
	UserSearchKeyUnspecified UserSearchKey = iota
	UserSearchKeyUserID
	UserSearchKeyUserName
	UserSearchKeyFirstName
	UserSearchKeyLastName
	UserSearchKeyNickName
	UserSearchKeyDisplayName
	UserSearchKeyEmail
	UserSearchKeyState
	UserSearchKeyResourceOwner
	UserSearchKeyLoginNames
	UserSearchKeyType
	UserSearchKeyPreferredLoginName
	UserSearchKeyInstanceID
	UserSearchOwnerRemoved
)

type UserSearchQuery struct {
	Key    UserSearchKey
	Method domain.SearchMethod
	Value  interface{}
}

type UserSearchResponse struct {
	Offset      uint64
	Limit       uint64
	TotalResult uint64
	Result      []*UserView
	Sequence    uint64
	Timestamp   time.Time
}

type UserState int32

const (
	UserStateUnspecified UserState = iota
	UserStateActive
	UserStateInactive
	UserStateDeleted
	UserStateLocked
	UserStateSuspend
	UserStateInitial
)

type Gender int32

const (
	GenderUnspecified Gender = iota
	GenderFemale
	GenderMale
	GenderDiverse
)

func (r *UserSearchRequest) EnsureLimit(limit uint64) error {
	if r.Limit > limit {
		return errors.ThrowInvalidArgument(nil, "SEARCH-zz62F", "Errors.Limit.ExceedsDefault")
	}
	if r.Limit == 0 {
		r.Limit = limit
	}
	return nil
}

func (r *UserSearchRequest) AppendMyOrgQuery(orgID string) {
	r.Queries = append(r.Queries, &UserSearchQuery{Key: UserSearchKeyResourceOwner, Method: domain.SearchMethodEquals, Value: orgID})
}

func (u *UserView) MFATypesSetupPossible(level domain.MFALevel, policy *domain.LoginPolicy) []domain.MFAType {
	types := make([]domain.MFAType, 0)
	switch level {
	default:
		fallthrough
	case domain.MFALevelSecondFactor:
		if policy.HasSecondFactors() {
			for _, mfaType := range policy.SecondFactors {
				switch mfaType {
				case domain.SecondFactorTypeTOTP:
					if u.OTPState != MFAStateReady {
						types = append(types, domain.MFATypeTOTP)
					}
				case domain.SecondFactorTypeU2F:
					types = append(types, domain.MFATypeU2F)
				case domain.SecondFactorTypeOTPSMS:
					if !u.OTPSMSAdded {
						types = append(types, domain.MFATypeOTPSMS)
					}
				case domain.SecondFactorTypeOTPEmail:
					if !u.OTPEmailAdded {
						types = append(types, domain.MFATypeOTPEmail)
					}
				}
			}
		}
	}
	return types
}

func (u *UserView) MFATypesAllowed(level domain.MFALevel, policy *domain.LoginPolicy, isInternalAuthentication bool) ([]domain.MFAType, bool) {
	types := make([]domain.MFAType, 0)
	required := true
	switch level {
	default:
		required = domain.RequiresMFA(policy.ForceMFA, policy.ForceMFALocalOnly, isInternalAuthentication)
		fallthrough
	case domain.MFALevelSecondFactor:
		if policy.HasSecondFactors() {
			for _, mfaType := range policy.SecondFactors {
				switch mfaType {
				case domain.SecondFactorTypeTOTP:
					if u.OTPState == MFAStateReady {
						types = append(types, domain.MFATypeTOTP)
					}
				case domain.SecondFactorTypeU2F:
					if u.IsU2FReady() {
						types = append(types, domain.MFATypeU2F)
					}
				case domain.SecondFactorTypeOTPSMS:
					if u.OTPSMSAdded {
						types = append(types, domain.MFATypeOTPSMS)
					}
				case domain.SecondFactorTypeOTPEmail:
					if u.OTPEmailAdded {
						types = append(types, domain.MFATypeOTPEmail)
					}
				}
			}
		}
	}
	return types, required
}

func (u *UserView) IsU2FReady() bool {
	for _, token := range u.U2FTokens {
		if token.State == MFAStateReady {
			return true
		}
	}
	return false
}

func (u *UserView) IsPasswordlessReady() bool {
	for _, token := range u.PasswordlessTokens {
		if token.State == MFAStateReady {
			return true
		}
	}
	return false
}

func (u *UserView) HasRequiredOrgMFALevel(policy *iam_model.LoginPolicyView) bool {
	if !policy.ForceMFA {
		return true
	}
	switch u.MFAMaxSetUp {
	case domain.MFALevelSecondFactor:
		return policy.HasSecondFactors()
	case domain.MFALevelMultiFactor:
		return policy.HasMultiFactors()
	default:
		return false
	}
}

func (u *UserView) GetProfile() (*Profile, error) {
	if u.HumanView == nil {
		return nil, errors.ThrowPreconditionFailed(nil, "MODEL-WLTce", "Errors.User.NotHuman")
	}
	return &Profile{
		ObjectRoot: models.ObjectRoot{
			AggregateID:   u.ID,
			Sequence:      u.Sequence,
			ResourceOwner: u.ResourceOwner,
			CreationDate:  u.CreationDate,
			ChangeDate:    u.ChangeDate,
		},
		FirstName:          u.FirstName,
		LastName:           u.LastName,
		NickName:           u.NickName,
		DisplayName:        u.DisplayName,
		PreferredLanguage:  language.Make(u.PreferredLanguage),
		Gender:             u.Gender,
		PreferredLoginName: u.PreferredLoginName,
		LoginNames:         u.LoginNames,
		AvatarKey:          u.AvatarKey,
	}, nil
}

func (u *UserView) GetPhone() (*Phone, error) {
	if u.HumanView == nil {
		return nil, errors.ThrowPreconditionFailed(nil, "MODEL-him4a", "Errors.User.NotHuman")
	}
	return &Phone{
		ObjectRoot: models.ObjectRoot{
			AggregateID:   u.ID,
			Sequence:      u.Sequence,
			ResourceOwner: u.ResourceOwner,
			CreationDate:  u.CreationDate,
			ChangeDate:    u.ChangeDate,
		},
		PhoneNumber:     u.Phone,
		IsPhoneVerified: u.IsPhoneVerified,
	}, nil
}

func (u *UserView) GetEmail() (*Email, error) {
	if u.HumanView == nil {
		return nil, errors.ThrowPreconditionFailed(nil, "MODEL-PWd6K", "Errors.User.NotHuman")
	}
	return &Email{
		ObjectRoot: models.ObjectRoot{
			AggregateID:   u.ID,
			Sequence:      u.Sequence,
			ResourceOwner: u.ResourceOwner,
			CreationDate:  u.CreationDate,
			ChangeDate:    u.ChangeDate,
		},
		EmailAddress:    u.Email,
		IsEmailVerified: u.IsEmailVerified,
	}, nil
}

func (u *UserView) GetAddress() (*Address, error) {
	if u.HumanView == nil {
		return nil, errors.ThrowPreconditionFailed(nil, "MODEL-DN61m", "Errors.User.NotHuman")
	}
	return &Address{
		ObjectRoot: models.ObjectRoot{
			AggregateID:   u.ID,
			Sequence:      u.Sequence,
			ResourceOwner: u.ResourceOwner,
			CreationDate:  u.CreationDate,
			ChangeDate:    u.ChangeDate,
		},
		Country:       u.Country,
		Locality:      u.Locality,
		PostalCode:    u.PostalCode,
		Region:        u.Region,
		StreetAddress: u.StreetAddress,
	}, nil
}