package auth

import (
	"time"

	"git.mstar.dev/mstar/goutils/other"
	"git.mstar.dev/mstar/goutils/sliceutils"
	"github.com/google/uuid"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"

	"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
	"git.mstar.dev/mstar/linstrom/storage-new/models"
)

// Start a login process with a username (NOT account ID) and password
// Returns the next state, a token corresponding to that state and error
// Token will be empty on failure, error describes the reason for the
// failure
func (a *Authenticator) PerformPasswordLogin(
	username string,
	password string,
) (nextState LoginNextState, token string, err error) {
	if ok, err := a.canUsernameLogin(username); !ok {
		return LoginNextFailure, "", other.Error("auth", "user may not login", err)
	}
	acc, err := dbgen.User.GetByUsername(username)
	switch err {
	case nil:
		break
	case gorm.ErrRecordNotFound:
		return LoginNextFailure, "", ErrInvalidCombination
	}

	var method *models.UserAuthMethod
	for _, authMethod := range acc.AuthMethods {
		if oneStorageAuthToLoginState(authMethod.AuthMethod) == LoginStartPassword {
			method = &authMethod
			break
		}
	}
	if method == nil {
		return LoginNextFailure, "", ErrUnsupportedAuthMethod
	}
	if !comparePassword(password, method.Token) {
		return LoginNextFailure, "", ErrInvalidCombination
	}
	nextStates := ConvertNewStorageAuthMethodsToLoginState(
		sliceutils.Map(
			acc.AuthMethods,
			func(t models.UserAuthMethod) models.AuthenticationMethodType {
				return t.AuthMethod
			},
		),
		false,
	)
	// Catch unknown login methods
	if nextStates&LoginUnknown == LoginUnknown {
		return LoginNextFailure, "", ErrUnknownAuthMethod
	}

	if nextStates == LoginNextFailure {
		// Login ok, no 2fa needed
		// Create a new token. Don't generate the token itself, let postgres handle that
		token := models.AccessToken{
			User:      *acc,
			UserId:    acc.ID,
			ExpiresAt: time.Now().Add(time.Hour * 24 * 365),
		}
		err = dbgen.AccessToken.
			// technically, the chance of a conflict is so incredibly low that it should never ever occur
			// since both the username and a randomly generated uuid would need to be created the same. Twice
			// But, just in case, do this
			Clauses(clause.OnConflict{DoNothing: true}).
			Omit(dbgen.AccessToken.Token).
			Create(&token)
		if err != nil {
			return LoginNextFailure, "", other.Error(
				"auth",
				"failed to create new access token",
				err,
			)
		}
		return LoginNextSucess, token.Token, nil
	}
	// TODO: Generate login process token
	loginToken := models.LoginProcessToken{
		User:      *acc,
		UserId:    acc.ID,
		ExpiresAt: calcAccessExpirationTimestamp(),
		Token:     uuid.NewString(),
	}
	err = dbgen.LoginProcessToken.Clauses(clause.OnConflict{UpdateAll: true}).
		Create(&loginToken)

	if err != nil {
		return LoginNextFailure, "", other.Error(
			"auth",
			"failed to create login process token",
			err,
		)
	}

	return nextStates, loginToken.Token, nil
}

// Register a password to an account
// There is no check for whether that is allowed from the active roles.
// If the given username already has a password set, it gets updated to the new one.
// If there is no password set yet (i.e. during account registration or passkey only so far)
// it creates the password link
func (a *Authenticator) PerformPasswordRegister(username, password string) error {
	acc, err := dbgen.User.GetByUsername(username)
	if err != nil {
		return other.Error("auth", "failed to get user to add a password to", err)
	}
	passwordHash, err := hashPassword(password)
	if err != nil {
		return other.Error("auth", "failed to hash password", err)
	}
	passwordMethods := sliceutils.Filter(
		acc.AuthMethods,
		func(t models.UserAuthMethod) bool { return t.AuthMethod == models.AuthMethodPassword },
	)
	if len(passwordMethods) > 0 {
		// TODO: Decide whether PerformPasswordRegister can also update existing passwords
		// Imo yes, since an account can have at most one password set and having a separate method for updating
		// would increase complexity
		// For now, do perform an update
		dbPass := passwordMethods[0]
		_, err = dbgen.UserAuthMethod.Where(dbgen.UserAuthMethod.ID.Eq(dbPass.ID)).
			Update(dbgen.UserAuthMethod.Token, passwordHash)
		if err != nil {
			return other.Error("auth", "failed to update password", err)
		}
	} else {
		dbPass := models.UserAuthMethod{
			Token:      passwordHash,
			AuthMethod: models.AuthMethodPassword,
			User:       *acc,
			UserId:     acc.ID,
		}
		err = dbgen.UserAuthMethod.Omit(dbgen.UserAuthMethod.ID).Create(&dbPass)
		if err != nil {
			return other.Error("auth", "failed to insert password", err)
		}
	}
	return nil
}
