package user

import (
	"context"

	"github.com/muhlemmer/gu"
	"google.golang.org/protobuf/types/known/timestamppb"

	"github.com/zitadel/zitadel/internal/api/authz"
	object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta"
	"github.com/zitadel/zitadel/internal/domain"
	"github.com/zitadel/zitadel/internal/query"
	"github.com/zitadel/zitadel/internal/zerrors"
	user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)

func (s *Server) GetUserByID(ctx context.Context, req *user.GetUserByIDRequest) (_ *user.GetUserByIDResponse, err error) {
	resp, err := s.query.GetUserByID(ctx, true, req.GetUserId())
	if err != nil {
		return nil, err
	}
	if authz.GetCtxData(ctx).UserID != req.GetUserId() {
		if err := s.checkPermission(ctx, domain.PermissionUserRead, resp.ResourceOwner, req.GetUserId()); err != nil {
			return nil, err
		}
	}
	return &user.GetUserByIDResponse{
		Details: object.DomainToDetailsPb(&domain.ObjectDetails{
			Sequence:      resp.Sequence,
			EventDate:     resp.ChangeDate,
			ResourceOwner: resp.ResourceOwner,
		}),
		User: userToPb(resp, s.assetAPIPrefix(ctx)),
	}, nil
}

func (s *Server) ListUsers(ctx context.Context, req *user.ListUsersRequest) (*user.ListUsersResponse, error) {
	queries, err := listUsersRequestToModel(req)
	if err != nil {
		return nil, err
	}
	res, err := s.query.SearchUsers(ctx, queries)
	if err != nil {
		return nil, err
	}
	res.RemoveNoPermission(ctx, s.checkPermission)
	return &user.ListUsersResponse{
		Result:  UsersToPb(res.Users, s.assetAPIPrefix(ctx)),
		Details: object.ToListDetails(res.SearchResponse),
	}, nil
}

func UsersToPb(users []*query.User, assetPrefix string) []*user.User {
	u := make([]*user.User, len(users))
	for i, user := range users {
		u[i] = userToPb(user, assetPrefix)
	}
	return u
}

func userToPb(userQ *query.User, assetPrefix string) *user.User {
	return &user.User{
		UserId: userQ.ID,
		Details: object.DomainToDetailsPb(&domain.ObjectDetails{
			Sequence:      userQ.Sequence,
			EventDate:     userQ.ChangeDate,
			ResourceOwner: userQ.ResourceOwner,
		}),
		State:              userStateToPb(userQ.State),
		Username:           userQ.Username,
		LoginNames:         userQ.LoginNames,
		PreferredLoginName: userQ.PreferredLoginName,
		Type:               userTypeToPb(userQ, assetPrefix),
	}
}

func userTypeToPb(userQ *query.User, assetPrefix string) user.UserType {
	if userQ.Human != nil {
		return &user.User_Human{
			Human: humanToPb(userQ.Human, assetPrefix, userQ.ResourceOwner),
		}
	}
	if userQ.Machine != nil {
		return &user.User_Machine{
			Machine: machineToPb(userQ.Machine),
		}
	}
	return nil
}

func humanToPb(userQ *query.Human, assetPrefix, owner string) *user.HumanUser {
	var passwordChanged *timestamppb.Timestamp
	if !userQ.PasswordChanged.IsZero() {
		passwordChanged = timestamppb.New(userQ.PasswordChanged)
	}
	return &user.HumanUser{
		Profile: &user.HumanProfile{
			GivenName:         userQ.FirstName,
			FamilyName:        userQ.LastName,
			NickName:          gu.Ptr(userQ.NickName),
			DisplayName:       gu.Ptr(userQ.DisplayName),
			PreferredLanguage: gu.Ptr(userQ.PreferredLanguage.String()),
			Gender:            gu.Ptr(genderToPb(userQ.Gender)),
			AvatarUrl:         domain.AvatarURL(assetPrefix, owner, userQ.AvatarKey),
		},
		Email: &user.HumanEmail{
			Email:      string(userQ.Email),
			IsVerified: userQ.IsEmailVerified,
		},
		Phone: &user.HumanPhone{
			Phone:      string(userQ.Phone),
			IsVerified: userQ.IsPhoneVerified,
		},
		PasswordChangeRequired: userQ.PasswordChangeRequired,
		PasswordChanged:        passwordChanged,
	}
}

func machineToPb(userQ *query.Machine) *user.MachineUser {
	return &user.MachineUser{
		Name:            userQ.Name,
		Description:     userQ.Description,
		HasSecret:       userQ.EncodedSecret != "",
		AccessTokenType: accessTokenTypeToPb(userQ.AccessTokenType),
	}
}

func userStateToPb(state domain.UserState) user.UserState {
	switch state {
	case domain.UserStateActive:
		return user.UserState_USER_STATE_ACTIVE
	case domain.UserStateInactive:
		return user.UserState_USER_STATE_INACTIVE
	case domain.UserStateDeleted:
		return user.UserState_USER_STATE_DELETED
	case domain.UserStateInitial:
		return user.UserState_USER_STATE_INITIAL
	case domain.UserStateLocked:
		return user.UserState_USER_STATE_LOCKED
	case domain.UserStateUnspecified:
		return user.UserState_USER_STATE_UNSPECIFIED
	case domain.UserStateSuspend:
		return user.UserState_USER_STATE_UNSPECIFIED
	default:
		return user.UserState_USER_STATE_UNSPECIFIED
	}
}

func genderToPb(gender domain.Gender) user.Gender {
	switch gender {
	case domain.GenderDiverse:
		return user.Gender_GENDER_DIVERSE
	case domain.GenderFemale:
		return user.Gender_GENDER_FEMALE
	case domain.GenderMale:
		return user.Gender_GENDER_MALE
	case domain.GenderUnspecified:
		return user.Gender_GENDER_UNSPECIFIED
	default:
		return user.Gender_GENDER_UNSPECIFIED
	}
}

func accessTokenTypeToPb(accessTokenType domain.OIDCTokenType) user.AccessTokenType {
	switch accessTokenType {
	case domain.OIDCTokenTypeBearer:
		return user.AccessTokenType_ACCESS_TOKEN_TYPE_BEARER
	case domain.OIDCTokenTypeJWT:
		return user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT
	default:
		return user.AccessTokenType_ACCESS_TOKEN_TYPE_BEARER
	}
}

func listUsersRequestToModel(req *user.ListUsersRequest) (*query.UserSearchQueries, error) {
	offset, limit, asc := object.ListQueryToQuery(req.Query)
	queries, err := userQueriesToQuery(req.Queries, 0 /*start from level 0*/)
	if err != nil {
		return nil, err
	}
	return &query.UserSearchQueries{
		SearchRequest: query.SearchRequest{
			Offset:        offset,
			Limit:         limit,
			Asc:           asc,
			SortingColumn: userFieldNameToSortingColumn(req.SortingColumn),
		},
		Queries: queries,
	}, nil
}

func userFieldNameToSortingColumn(field user.UserFieldName) query.Column {
	switch field {
	case user.UserFieldName_USER_FIELD_NAME_EMAIL:
		return query.HumanEmailCol
	case user.UserFieldName_USER_FIELD_NAME_FIRST_NAME:
		return query.HumanFirstNameCol
	case user.UserFieldName_USER_FIELD_NAME_LAST_NAME:
		return query.HumanLastNameCol
	case user.UserFieldName_USER_FIELD_NAME_DISPLAY_NAME:
		return query.HumanDisplayNameCol
	case user.UserFieldName_USER_FIELD_NAME_USER_NAME:
		return query.UserUsernameCol
	case user.UserFieldName_USER_FIELD_NAME_STATE:
		return query.UserStateCol
	case user.UserFieldName_USER_FIELD_NAME_TYPE:
		return query.UserTypeCol
	case user.UserFieldName_USER_FIELD_NAME_NICK_NAME:
		return query.HumanNickNameCol
	case user.UserFieldName_USER_FIELD_NAME_CREATION_DATE:
		return query.UserCreationDateCol
	case user.UserFieldName_USER_FIELD_NAME_UNSPECIFIED:
		return query.UserIDCol
	default:
		return query.UserIDCol
	}
}

func userQueriesToQuery(queries []*user.SearchQuery, level uint8) (_ []query.SearchQuery, err error) {
	q := make([]query.SearchQuery, len(queries))
	for i, query := range queries {
		q[i], err = userQueryToQuery(query, level)
		if err != nil {
			return nil, err
		}
	}
	return q, nil
}

func userQueryToQuery(query *user.SearchQuery, level uint8) (query.SearchQuery, error) {
	if level > 20 {
		// can't go deeper than 20 levels of nesting.
		return nil, zerrors.ThrowInvalidArgument(nil, "USER-zsQ97", "Errors.Query.TooManyNestingLevels")
	}
	switch q := query.Query.(type) {
	case *user.SearchQuery_UserNameQuery:
		return userNameQueryToQuery(q.UserNameQuery)
	case *user.SearchQuery_FirstNameQuery:
		return firstNameQueryToQuery(q.FirstNameQuery)
	case *user.SearchQuery_LastNameQuery:
		return lastNameQueryToQuery(q.LastNameQuery)
	case *user.SearchQuery_NickNameQuery:
		return nickNameQueryToQuery(q.NickNameQuery)
	case *user.SearchQuery_DisplayNameQuery:
		return displayNameQueryToQuery(q.DisplayNameQuery)
	case *user.SearchQuery_EmailQuery:
		return emailQueryToQuery(q.EmailQuery)
	case *user.SearchQuery_StateQuery:
		return stateQueryToQuery(q.StateQuery)
	case *user.SearchQuery_TypeQuery:
		return typeQueryToQuery(q.TypeQuery)
	case *user.SearchQuery_LoginNameQuery:
		return loginNameQueryToQuery(q.LoginNameQuery)
	case *user.SearchQuery_OrganizationIdQuery:
		return resourceOwnerQueryToQuery(q.OrganizationIdQuery)
	case *user.SearchQuery_InUserIdsQuery:
		return inUserIdsQueryToQuery(q.InUserIdsQuery)
	case *user.SearchQuery_OrQuery:
		return orQueryToQuery(q.OrQuery, level)
	case *user.SearchQuery_AndQuery:
		return andQueryToQuery(q.AndQuery, level)
	case *user.SearchQuery_NotQuery:
		return notQueryToQuery(q.NotQuery, level)
	case *user.SearchQuery_InUserEmailsQuery:
		return inUserEmailsQueryToQuery(q.InUserEmailsQuery)
	default:
		return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid")
	}
}

func userNameQueryToQuery(q *user.UserNameQuery) (query.SearchQuery, error) {
	return query.NewUserUsernameSearchQuery(q.UserName, object.TextMethodToQuery(q.Method))
}

func firstNameQueryToQuery(q *user.FirstNameQuery) (query.SearchQuery, error) {
	return query.NewUserFirstNameSearchQuery(q.FirstName, object.TextMethodToQuery(q.Method))
}

func lastNameQueryToQuery(q *user.LastNameQuery) (query.SearchQuery, error) {
	return query.NewUserLastNameSearchQuery(q.LastName, object.TextMethodToQuery(q.Method))
}

func nickNameQueryToQuery(q *user.NickNameQuery) (query.SearchQuery, error) {
	return query.NewUserNickNameSearchQuery(q.NickName, object.TextMethodToQuery(q.Method))
}

func displayNameQueryToQuery(q *user.DisplayNameQuery) (query.SearchQuery, error) {
	return query.NewUserDisplayNameSearchQuery(q.DisplayName, object.TextMethodToQuery(q.Method))
}

func emailQueryToQuery(q *user.EmailQuery) (query.SearchQuery, error) {
	return query.NewUserEmailSearchQuery(q.EmailAddress, object.TextMethodToQuery(q.Method))
}

func stateQueryToQuery(q *user.StateQuery) (query.SearchQuery, error) {
	return query.NewUserStateSearchQuery(int32(q.State))
}

func typeQueryToQuery(q *user.TypeQuery) (query.SearchQuery, error) {
	return query.NewUserTypeSearchQuery(int32(q.Type))
}

func loginNameQueryToQuery(q *user.LoginNameQuery) (query.SearchQuery, error) {
	return query.NewUserLoginNameExistsQuery(q.LoginName, object.TextMethodToQuery(q.Method))
}

func resourceOwnerQueryToQuery(q *user.OrganizationIdQuery) (query.SearchQuery, error) {
	return query.NewUserResourceOwnerSearchQuery(q.OrganizationId, query.TextEquals)
}

func inUserIdsQueryToQuery(q *user.InUserIDQuery) (query.SearchQuery, error) {
	return query.NewUserInUserIdsSearchQuery(q.UserIds)
}
func orQueryToQuery(q *user.OrQuery, level uint8) (query.SearchQuery, error) {
	mappedQueries, err := userQueriesToQuery(q.Queries, level+1)
	if err != nil {
		return nil, err
	}
	return query.NewUserOrSearchQuery(mappedQueries)
}
func andQueryToQuery(q *user.AndQuery, level uint8) (query.SearchQuery, error) {
	mappedQueries, err := userQueriesToQuery(q.Queries, level+1)
	if err != nil {
		return nil, err
	}
	return query.NewUserAndSearchQuery(mappedQueries)
}
func notQueryToQuery(q *user.NotQuery, level uint8) (query.SearchQuery, error) {
	mappedQuery, err := userQueryToQuery(q.Query, level+1)
	if err != nil {
		return nil, err
	}
	return query.NewUserNotSearchQuery(mappedQuery)
}

func inUserEmailsQueryToQuery(q *user.InUserEmailsQuery) (query.SearchQuery, error) {
	return query.NewUserInUserEmailsSearchQuery(q.UserEmails)
}