// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

package user

import (
	"regexp"
	"strings"

	"code.vikunja.io/api/pkg/config"
	"code.vikunja.io/api/pkg/events"
	"code.vikunja.io/api/pkg/notifications"
	"golang.org/x/crypto/bcrypt"
	"xorm.io/xorm"
)

const (
	IssuerLocal = `local`
	IssuerLDAP  = `ldap`
)

// CreateUser creates a new user and inserts it into the database
func CreateUser(s *xorm.Session, user *User) (newUser *User, err error) {

	if user.Issuer == "" {
		user.Issuer = IssuerLocal
	}

	// Check if we have all required information
	err = checkIfUserIsValid(user)
	if err != nil {
		return nil, err
	}

	// Check if the user already exists with that username
	err = checkIfUserExists(s, user)
	if err != nil {
		return nil, err
	}

	if user.Issuer == IssuerLocal {
		// Hash the password
		user.Password, err = HashPassword(user.Password)
		if err != nil {
			return nil, err
		}
	}

	user.ID = 0
	user.Status = StatusActive
	user.AvatarProvider = config.DefaultSettingsAvatarProvider.GetString()
	user.AvatarFileID = config.DefaultSettingsAvatarFileID.GetInt64()
	user.EmailRemindersEnabled = config.DefaultSettingsEmailRemindersEnabled.GetBool()
	user.DiscoverableByName = config.DefaultSettingsDiscoverableByName.GetBool()
	user.DiscoverableByEmail = config.DefaultSettingsDiscoverableByEmail.GetBool()
	user.OverdueTasksRemindersEnabled = config.DefaultSettingsOverdueTaskRemindersEnabled.GetBool()
	user.OverdueTasksRemindersTime = config.DefaultSettingsOverdueTaskRemindersTime.GetString()
	user.DefaultProjectID = config.DefaultSettingsDefaultProjectID.GetInt64()
	user.WeekStart = config.DefaultSettingsWeekStart.GetInt()
	user.Timezone = config.DefaultSettingsTimezone.GetString()

	if user.Language == "" {
		user.Language = config.DefaultSettingsLanguage.GetString()
	}

	// Insert it
	_, err = s.Insert(user)
	if err != nil {
		return nil, err
	}

	// Get the  full new User
	newUserOut, err := GetUserByID(s, user.ID)
	if err != nil && !IsErrUserStatusError(err) {
		return nil, err
	}

	events.DispatchOnCommit(s, &CreatedEvent{
		User: newUserOut,
	})

	// Don't send a mail if no mailer is configured
	if !config.MailerEnabled.GetBool() || user.Issuer != IssuerLocal {
		return newUserOut, err
	}

	user.Status = StatusEmailConfirmationRequired
	token, err := generateToken(s, user, TokenEmailConfirm)
	if err != nil {
		return nil, err
	}

	_, err = s.
		Where("id = ?", user.ID).
		Cols("email", "status").
		Update(user)
	if err != nil {
		return
	}

	n := &EmailConfirmNotification{
		User:         user,
		IsNew:        true,
		ConfirmToken: token.Token,
	}

	err = notifications.Notify(user, n, s)
	return newUserOut, err
}

// CreateBotUser creates a bot user owned by the given owner.
// Bots have no email or password and cannot authenticate interactively.
// It intentionally bypasses checkIfUserIsValid / checkIfUserExists because
// those enforce email+password and would flag duplicate empty emails.
func CreateBotUser(s *xorm.Session, bot *User, owner *User) (*User, error) {
	if owner == nil || owner.ID == 0 {
		return nil, ErrNoUsernamePassword{}
	}
	if owner.IsBot() {
		return nil, &ErrBotNotOwned{UserID: owner.ID}
	}

	// Reuse the same username format rules as regular user creation
	if err := checkUsernameFormat(bot.Username); err != nil {
		return nil, err
	}
	if !strings.HasPrefix(bot.Username, "bot-") {
		return nil, &ErrBotUsernameMustHavePrefix{Username: bot.Username}
	}

	if _, err := GetUserByUsername(s, bot.Username); err == nil {
		return nil, ErrUsernameExists{Username: bot.Username}
	} else if !IsErrUserDoesNotExist(err) {
		return nil, err
	}

	bot.ID = 0
	bot.BotOwnerID = owner.ID
	bot.Status = StatusActive
	bot.Issuer = IssuerLocal
	bot.Password = ""
	bot.Email = ""

	if _, err := s.Insert(bot); err != nil {
		return nil, err
	}

	newBot, err := GetUserByID(s, bot.ID)
	if err != nil {
		return nil, err
	}

	events.DispatchOnCommit(s, &CreatedEvent{User: newBot})
	return newBot, nil
}

// HashPassword hashes a password
func HashPassword(password string) (string, error) {
	bytes, err := bcrypt.GenerateFromPassword([]byte(password), config.ServiceBcryptRounds.GetInt())
	return string(bytes), err
}

// checkUsernameFormat validates username format rules shared by regular and bot users.
func checkUsernameFormat(username string) error {
	if username == "" {
		return ErrNoUsernamePassword{}
	}

	if strings.Contains(username, " ") {
		return &ErrUsernameMustNotContainSpaces{
			Username: username,
		}
	}

	// Check if username matches the reserved link-share pattern
	linkSharePattern := regexp.MustCompile(`^link-share-\d+$`)
	if linkSharePattern.MatchString(username) {
		return ErrUsernameReserved{
			Username: username,
		}
	}

	return nil
}

func checkIfUserIsValid(user *User) error {
	if user.Email == "" ||
		(user.Issuer != IssuerLocal && user.Subject == "") ||
		(user.Issuer == IssuerLocal && (user.Password == "" ||
			user.Username == "")) {
		return ErrNoUsernamePassword{}
	}

	if err := checkUsernameFormat(user.Username); err != nil {
		return err
	}

	// Reserve the bot- prefix for bot users (created via CreateBotUser)
	if strings.HasPrefix(user.Username, "bot-") {
		return ErrUsernameReserved{
			Username: user.Username,
		}
	}

	return nil
}

func checkIfUserExists(s *xorm.Session, user *User) (err error) {
	exists := true
	_, err = GetUserByUsername(s, user.Username)
	if err != nil {
		if IsErrUserDoesNotExist(err) {
			exists = false
		} else {
			return err
		}
	}
	if exists {
		return ErrUsernameExists{user.ID, user.Username}
	}

	// Check if the user already existst with that email
	exists = true
	userToCheck := &User{
		Email:   user.Email,
		Issuer:  user.Issuer,
		Subject: user.Subject,
	}

	if user.Issuer != IssuerLocal {
		userToCheck.Email = ""
	}

	_, err = getUser(s, userToCheck, false)
	if err != nil {
		if IsErrUserDoesNotExist(err) {
			exists = false
		} else {
			return err
		}
	}
	if exists && user.Issuer == IssuerLocal {
		return ErrUserEmailExists{user.ID, user.Email}
	}

	return nil
}
