package accounts

import (
	"notabug.org/apiote/amuse/db"

	"encoding/base64"
	"errors"
	"fmt"
	"math/rand"
	"strings"

	"golang.org/x/crypto/argon2"
	"notabug.org/apiote/gott"
)

func findNoUser(args ...interface{}) (interface{}, error) {
	authData := args[0].(*AuthData)
	authResult := args[1].(*AuthResult)
	user, err := db.GetUser(authData.username)
	authResult.user = user
	if _, ok := err.(db.EmptyError); ok {
		err = nil
	} else if err == nil {
		err = AuthError{
			Err: errors.New("user_exists"),
		}
	}
	return gott.Tuple(args), err
}

func prepareSalt(args ...interface{}) (interface{}, error) {
	argon := args[2].(*Argon)
	salt := make([]byte, 16)
	_, err := rand.Read(salt)
	argon.salt = salt
	return gott.Tuple(args), err
}

func hashPassword(args ...interface{}) interface{} {
	authData := args[0].(*AuthData)
	argon := args[2].(*Argon)
	password := authData.password

	hash := argon2.IDKey([]byte(password), argon.salt, 1, 64*1024, 4, 32)
	b64Salt := base64.RawStdEncoding.EncodeToString(argon.salt)
	b64Hash := base64.RawStdEncoding.EncodeToString(hash)
	format := "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s"
	fullHash := fmt.Sprintf(format, argon2.Version, 64*1024, 1, 4, b64Salt, b64Hash)
	authData.password = fullHash
	return gott.Tuple(args)
}

func createRecoveryCodes(args ...interface{}) interface{} {
	authData := args[0].(*AuthData)
	sfaSecret := authData.sfa
	if sfaSecret != "" {
		result := args[1].(*AuthResult)
		codes := []string{}
		for i := 0; i < 12; i++ {
			code := rand.Int63n(999999999999)
			codeStr := fmt.Sprintf("%012d", code)
			codes = append(codes, codeStr)
		}
		result.recoveryCodesRaw = strings.Join(codes, ",")
	}
	return gott.Tuple(args)
}

func insertUser(args ...interface{}) (interface{}, error) {
	authData := args[0].(*AuthData)
	result := args[1].(*AuthResult)
	sfaSecret := authData.sfa
	err := db.InsertUser(authData.username, authData.password, sfaSecret, result.recoveryCodesRaw)
	return gott.Tuple(args), err
}

func Signup(username, password, sfaSecret string) (string, error) {
	r, err := gott.
		NewResult(gott.Tuple{&AuthData{username: username, password: password,
			sfa: sfaSecret}, &AuthResult{}, &Argon{}}).
		Bind(findNoUser).
		Bind(prepareSalt).
		Map(hashPassword).
		Map(createRecoveryCodes).
		Bind(insertUser).
		Finish()

	if err != nil {
		return "", err
	}
	return r.(gott.Tuple)[1].(*AuthResult).recoveryCodesRaw, err
}
