// Copyright (C) 2019 Marius Schellenberger

package db

import (
	"errors"
	"git.giftfish.de/ston1th/gowiki/pkg/core"
	"git.giftfish.de/ston1th/gowiki/pkg/otp"
	"git.giftfish.de/ston1th/jwt/v3"
	"golang.org/x/crypto/bcrypt"
	"regexp"
	"sort"
	"strconv"
)

const userPrefix = "user/"

var (
	errUserLocked   = errors.New("db: user locked")
	errUserLogin    = errors.New("db: wrong login")
	errUserPin      = errors.New("db: wrong pin")
	errUserNotFound = errors.New("db: user not found")
	errUserExists   = errors.New("db: user already exist")
	errNameReserved = errors.New("db: name is reserved")

	userRe = regexp.MustCompile("[^a-zA-Z0-9]+")
)

func (db *DB) GetUsers() (users core.Users, err error) {
	err = db.store.ForEachPrefix(userPrefix, func(k string, _ []byte) error {
		u, err := db.GetUser(k)
		u.Password = ""
		if err == nil {
			users = append(users, u)
		}
		return nil
	})
	sort.Sort(users)
	return
}

func (db *DB) GetUserWithoutPassword(username string) (u core.User, err error) {
	u, err = db.GetUser(username)
	u.Password = ""
	return
}

func (db *DB) GetUserSecret(username string) (secret string, err error) {
	var user core.User
	user, err = db.GetUser(username)
	secret = user.Secret
	return
}

func (db *DB) GetUser(username string) (u core.User, err error) {
	err = db.store.Get(userPrefix+username, &u)
	return
}

func (db *DB) UserExists(username string) error {
	return db.store.Get(userPrefix+username, nil)
}

func (db *DB) CreateUser(username, password string, admin bool) error {
	username = userRe.ReplaceAllString(username, "")
	if _, ok := core.Contains(username, reservedNames); ok {
		return errNameReserved
	}
	if err := db.UserExists(username); err == nil {
		return errUserExists
	}
	hash, err := bcrypt.GenerateFromPassword(validatePassword(password), bcryptCost)
	if err != nil {
		return err
	}
	err = db.CreateSection(username, nil, true)
	if err != nil {
		return err
	}
	return db.store.Set(userPrefix+username, &core.User{
		Username: username,
		Password: string(hash),
		Created:  strconv.Itoa(int(jwt.Now())),
		Admin:    admin,
	})
}

func (db *DB) Login(username, password string) (u core.User, err error) {
	if username == defUser && !db.admin {
		err = errors.New("db: user disabled")
		return
	}
	u, err = db.GetUser(username)
	if err != nil {
		return
	}
	if u.Locked == 3 {
		err = errUserLocked
		return
	}
	if err = bcrypt.CompareHashAndPassword([]byte(u.Password), validatePassword(password)); err != nil {
		u.Locked++
		err = db.store.Set(userPrefix+username, u)
		if err != nil {
			return
		}
		err = errUserLogin
		return
	}
	if u.Locked > 0 {
		u.Locked = 0
		err = db.store.Set(userPrefix+username, u)
		if err != nil {
			return
		}
	}
	return
}

func (db *DB) Totp(username, pin string) (u core.User, err error) {
	u, err = db.GetUser(username)
	if err != nil {
		return
	}
	if u.Locked == 3 {
		err = errUserLocked
		return
	}
	if !otp.Validate(pin, u.Secret) {
		u.Locked++
		err = db.store.Set(userPrefix+username, u)
		if err != nil {
			return
		}
		err = errUserPin
		return
	}
	if u.Locked > 0 {
		u.Locked = 0
		err = db.store.Set(userPrefix+username, u)
		if err != nil {
			return
		}
	}
	return
}

func (db *DB) UnlockUser(username string) (err error) {
	var u core.User
	u, err = db.GetUser(username)
	if err != nil {
		return
	}
	if u.Locked > 0 {
		u.Locked = 0
		err = db.store.Set(userPrefix+username, u)
		if err != nil {
			return
		}
	}
	return
}

func (db *DB) UpdateUserSecret(username, secret string) error {
	u, err := db.GetUser(username)
	if err != nil {
		return err
	}
	u.Secret = secret
	return db.store.Set(userPrefix+username, u)
}

func (db *DB) UpdateUserPassword(username, password string) error {
	if password == "" {
		return errors.New("empty password")
	}
	u, err := db.GetUser(username)
	if err != nil {
		return err
	}
	hash, err := bcrypt.GenerateFromPassword(validatePassword(password), bcryptCost)
	if err != nil {
		return err
	}
	u.Password = string(hash)
	return db.store.Set(userPrefix+username, u)
}

func (db *DB) ResetAdmin() error {
	return db.UpdateUserPassword(defUser, defPassword)
}

func (db *DB) AdminUpdateUser(username, password string, admin bool) error {
	if username == defUser && !admin {
		return errors.New("user '" + defUser + "' cannot lose admin privileges")
	}
	u, err := db.GetUser(username)
	if err != nil {
		return err
	}
	u.Admin = admin
	if password != "" {
		hash, err := bcrypt.GenerateFromPassword(validatePassword(password), bcryptCost)
		if err != nil {
			return err
		}
		u.Password = string(hash)
	}
	return db.store.Set(userPrefix+username, u)
}

func (db *DB) DeleteUser(username string) error {
	if username == defUser {
		return errors.New("user '" + defUser + "' cannot be deleted")
	}
	u, err := db.GetUser(username)
	if err != nil {
		return err
	}
	err = db.store.Delete(userPrefix + username)
	if err != nil {
		return err
	}
	err = db.lockout(username, u.Created)
	if err != nil {
		return err
	}
	return db.DeleteSection(username)
}
