package clusters

import (
	"database/sql"
	"fmt"
	"time"

	"github.com/pkg/errors"

	"code.justin.tv/d8a/buddy/lib/clusters/clusterdb"
	"code.justin.tv/d8a/buddy/lib/config"
	"code.justin.tv/d8a/buddy/lib/sandstorm"
	"code.justin.tv/d8a/buddy/lib/store"
	"code.justin.tv/d8a/buddy/lib/terminal"
)

//EnsureChildUsers accepts a buddy store connection, sandstorm client, cluster, and role name, and ensures the following:
// - A role with the name given exists (will offer to create it if not)
// - The role is NOLOGIN
// - Users with the name <role>_01 and <role>_02 exist (will offer to create it if not)
// - Only one is set to LOGIN, the other is NOLOGIN
// - The users have no access grants
// - The users inherit from the role
// - The users are in the buddy store (will create them if missing)
// - The users have locatable secrets in sandstorm
// - The LOGIN user's sandstorm secret can be used to log in under that user
//
// If any of these conditions are not met, and programmatic correction cannot take place without endangering a potentially-live
// user's login status, then an error will be returned.
func EnsureChildUsers(storeDb *sql.DB, cluster *config.Cluster, clusterDb *clusterdb.ClusterDB, sandstormClient sandstorm.SandstormAPI, roleName string) error {
	if !clusterDb.CanUseRolePairs() {
		return fmt.Errorf("cluster %s can't use role pairs because the database doesn't support it", cluster.Name)
	}

	roles, err := clusterDb.ClusterUsers()
	if err != nil {
		return err
	}

	leftUser := fmt.Sprintf("%s_01", roleName)
	rightUser := fmt.Sprintf("%s_02", roleName)

	canlogin, ok := roles[roleName]
	if ok && canlogin {
		return fmt.Errorf("role %s is set up as LOGIN when it should be NOLOGIN", roleName)
	}

	if !ok {
		create, err := terminal.AskForConfirmation(fmt.Sprintf("role %s doesn't exist in the cluster %s- create it with no access grants?", roleName, cluster.Name))
		if err != nil {
			return err
		}
		if !create {
			return fmt.Errorf("create role %s in cluster %s and try again", roleName, cluster.Name)
		}

		err = clusterDb.CreateRole(roleName)
		if err != nil {
			return err
		}
		fmt.Printf("Role %s created- don't forget to grant it access to what it needs!\n", roleName)
	}

	leftCanLogin, leftOk := roles[leftUser]
	rightCanLogin, rightOk := roles[rightUser]
	err = setupUser(storeDb, clusterDb, sandstormClient, cluster, roleName, leftUser, leftOk, leftCanLogin, rightOk && rightCanLogin)
	if err != nil {
		return err
	}

	err = setupUser(storeDb, clusterDb, sandstormClient, cluster, roleName, rightUser, rightOk, rightCanLogin, !leftOk || leftCanLogin)
	if err != nil {
		return err
	}
	return nil
}

func EnsureUserPermissions(storeDb *sql.DB, clusterDb *clusterdb.ClusterDB, cluster *config.Cluster, sandstormClient sandstorm.SandstormAPI, roleName string, leftUser string, rightUser string) error {
	//Make sure both users inherit from the role
	roleInheritance, err := clusterDb.RoleParents()
	if err != nil {
		return err
	}

	err = checkRoleInheritance(roleInheritance, leftUser, roleName)
	if err != nil {
		return err
	}

	err = checkRoleInheritance(roleInheritance, rightUser, roleName)
	if err != nil {
		return err
	}

	//Make sure neither user has been granted anything on their own
	grantedRoles, err := clusterDb.GrantedRoles()
	if err != nil {
		return err
	}

	//Make sure there are no user-specific access grants
	for _, grantedRole := range grantedRoles {
		if grantedRole == leftUser {
			return fmt.Errorf("user %s has access grants- this will cause a lot of suffering in the long run.  move grants on user %s to role %s", leftUser, leftUser, roleName)
		} else if grantedRole == rightUser {
			return fmt.Errorf("user %s has access grants- this will cause a lot of suffering in the long run.  move grants on user %s to role %s", rightUser, rightUser, roleName)
		}
	}

	return nil
}

func setupUser(storeDb *sql.DB, clusterDb *clusterdb.ClusterDB, sandstormClient sandstorm.SandstormAPI, cluster *config.Cluster, roleName string, userName string, userExists bool, userEnabled bool, disableUser bool) error {
	if !userExists {
		err := attemptCreateUser(storeDb, clusterDb, sandstormClient, cluster, roleName, userName)
		if err != nil {
			return err
		}

		if disableUser {
			err = clusterDb.SetUserLogin(userName, false)
			if err != nil {
				return err
			}
		}
	} else {
		err := ensureSandstormPassword(storeDb, cluster, sandstormClient, userName, userEnabled)
		if err != nil {
			return err
		}
	}

	return nil
}

func checkRoleInheritance(roleInheritance map[string][]string, userName string, roleName string) error {
	userInherits, ok := roleInheritance[userName]

	if ok {
		ok = false
		for _, role := range userInherits {
			if role == roleName {
				ok = true
				break
			}
		}
	}

	if !ok {
		return fmt.Errorf("user %s does not inherit from role %s - GRANT %s to %s to make the role pair work properly", userName, roleName, roleName, userName)
	}
	return nil
}

// FixRoleKeys will check the database to see if only one user from the role pair is in use.
// if so, it will lock the not-in-use pair.  If both keys are in use, an error will be returned.
// If neither, the second user will be hsut down. This is an automated way of dealing with
// situations where two users are live, which is generally due to either abortive rotation
// attempts that have since been corrected, or somebody spinning up two users without paying
// much attention.
func FixRoleKeys(clusterDb *clusterdb.ClusterDB, rolePair *config.RolePair) (string, error) {
	seenLeftUser := false
	seenRightUser := false
	lastSeenNewUser := time.Now()

	if len(rolePair.Users) != 2 {
		return "", fmt.Errorf("the role pair %s contains %d users instead of the appropriate 2", rolePair.Name, len(rolePair.Users))
	}

	leftUserName := rolePair.Users[0].Name
	rightUserName := rolePair.Users[1].Name

	fmt.Printf("Checking user activity on %s and %s\n", leftUserName, rightUserName)

	err := terminal.WaitForCondition("user activity to become clear", 15*time.Minute, 10*time.Second, func() (bool, error) {
		//Grab users from pg_stat_activity & see which are visible in that list
		users, err := clusterDb.ActiveUsers()
		if err != nil {
			return false, err
		}

		for _, user := range users {
			if !seenLeftUser && user == leftUserName {
				seenLeftUser = true
				lastSeenNewUser = time.Now()
			}
			if !seenRightUser && user == rightUserName {
				seenRightUser = true
				lastSeenNewUser = time.Now()
			}
		}

		//If both users are active during this period, that's a problem
		if seenLeftUser && seenRightUser {
			return false, fmt.Errorf("both %s and %s are receiving active database traffic", leftUserName, rightUserName)
		}

		if lastSeenNewUser.Add(5 * time.Minute).Before(time.Now()) {
			return true, nil
		}

		return false, nil
	})

	if err != nil {
		return "", err
	}

	userToClose := leftUserName
	userToOpen := rightUserName
	if seenLeftUser && !seenRightUser {
		userToClose = rightUserName
		userToClose = leftUserName
	} else if seenLeftUser && seenRightUser {
		fmt.Println("Neither user had database traffic in 5 minutes.")
	}

	fmt.Printf("Closing user %s\n", userToClose)
	return userToOpen, clusterDb.SetUserLogin(userToClose, false)
}

func attemptCreateUser(storeDb *sql.DB, clusterDb *clusterdb.ClusterDB, sandstormClient sandstorm.SandstormAPI, cluster *config.Cluster, roleName string, userName string) error {
	create, err := terminal.AskForConfirmation(fmt.Sprintf("User %s doesn't exist in the cluster %s- create it inherited from role %s?", userName, cluster.Name, roleName))
	if err != nil {
		return err
	}

	if !create {
		return fmt.Errorf("create user %s in cluster %s and try again", userName, cluster.Name)
	}

	password := sandstorm.GeneratePassword()

	storeUser := cluster.GetUser(userName)
	if storeUser == nil {
		storeUser = &config.User{
			Name:   userName,
			Secret: fmt.Sprintf(sandstormClient.DefaultSecretPath(cluster.Name, userName)),
		}
		cluster.User = append(cluster.User, storeUser)
		err = store.InsertUser(storeDb, cluster, storeUser)
		if err != nil {
			return err
		}
	}
	err = sandstormClient.WriteUserPassword(cluster, userName, password)
	if err != nil {
		return err
	}

	err = clusterDb.CreateUser(userName, roleName, password)
	if err != nil {
		return err
	}

	return nil
}

func ensureSandstormPassword(storeDb *sql.DB, cluster *config.Cluster, sandstormClient sandstorm.SandstormAPI, username string, testLogin bool) error {
	//User is already in database- if we can't find working sandstorm credentials for the user, we have a problem
	user := cluster.GetUser(username)
	var bestSecretGuess string
	if user == nil {
		bestSecretGuess = sandstormClient.DefaultSecretPath(cluster.Name, username)
	} else {
		bestSecretGuess = user.Secret
	}

	secret, err := sandstormClient.Manager().Get(bestSecretGuess)
	if err != nil || secret == nil {
		return fmt.Errorf("user %s in cluster %s already exists in the database, but a functioning secret could not be found in sandstorm.  You may need to create the secret, or you may need to use `buddy-cli configure %s add-user %s <secret path>` to show buddy where it is", username, cluster.Name, cluster.Name, username)
	}

	if user == nil {
		user = &config.User{
			Name:   username,
			Secret: bestSecretGuess,
		}
		cluster.User = append(cluster.User, user)
		err = store.InsertUser(storeDb, cluster, user)
		if err != nil {
			return err
		}
	}

	if testLogin {
		_, err := clusterdb.OpenDbConn(cluster, sandstormClient, username)
		if err != nil {
			return errors.Wrap(err, fmt.Sprintf("user %s in cluster %s already exists in the database and has a sandstorm secret %s - however, an error occurred while attempting to connect as %s and it may be due to sandstorm containing the wrong password", username, cluster.Name, bestSecretGuess, username))
		}
	}

	return nil
}
