package store

import (
	"database/sql"
	"fmt"
	"io"

	"code.justin.tv/d8a/buddy/lib/alerts"
	"code.justin.tv/d8a/buddy/lib/config"
	"code.justin.tv/d8a/buddy/lib/terminal"

	"code.justin.tv/d8a/buddy/lib/sandstorm"
	"github.com/fatih/color"
	"github.com/pkg/errors"
)

type clusterUpdate struct {
	Cluster     *config.Cluster
	Differences []string
}

// PutClusters syncs the state of the buddy store to the clusters in `cfg` - users will be prompted &
// informed of what's going to happen, so this has to only be called from a responsive command
// line executed by a human (we currently call it from buddy-cli scrape)
func PutClusters(storeDb *sql.DB, cfg *config.ConfigFile, sandstormClient sandstorm.SandstormAPI) error {
	clusterList, err := GetClusters(storeDb)
	if err != nil {
		return err
	}

	actualClusters := make(map[string]*config.Cluster)
	for _, cluster := range clusterList {
		actualClusters[cluster.Name] = cluster
	}

	desiredClusters := make(map[string]*config.Cluster)
	for _, cluster := range cfg.Cluster {
		desiredClusters[cluster.Name] = cluster
	}

	var insertClusters []*config.Cluster
	var updateClusters []*clusterUpdate
	var deleteClusters []*config.Cluster
	for name, cluster := range desiredClusters {
		actualCluster, ok := actualClusters[name]
		if !ok {
			if cluster.Database == "" {
				color.Yellow("The cluster %s did not have a DBName in AWS.  What database on %s should be managed by buddy?", cluster.Name, cluster.Name)
				dbname, err := terminal.AskForInput("Database for buddy to connect to:")
				if err != nil {
					return errors.Wrap(err, fmt.Sprintf("couldn't retrieve the database name for %s from the command line", cluster.Name))
				}
				cluster.Database = dbname
			}

			insertClusters = append(insertClusters, cluster)
		} else {
			differences, equal := cluster.Equals(actualCluster)
			if !equal {
				cluster.Id = actualCluster.Id
				cluster.Database = actualCluster.Database

				updateClusters = append(updateClusters, &clusterUpdate{
					Cluster:     cluster,
					Differences: differences,
				})
			}
		}
	}

	for name, actualCluster := range actualClusters {
		_, ok := desiredClusters[name]
		if !ok {
			deleteClusters = append(deleteClusters, actualCluster)
		}
	}

	if len(insertClusters) > 0 || len(updateClusters) > 0 || len(deleteClusters) > 0 {
		fmt.Println("Cluster changes:")
		for _, cluster := range insertClusters {
			color.Green("+ %s", cluster.Name)
		}
		for _, cluster := range updateClusters {
			color.Yellow("~ %s", cluster.Cluster.Name)
			for _, difference := range cluster.Differences {
				color.Yellow("  - %s", difference)
			}
		}
		for _, cluster := range deleteClusters {
			color.Red("- %s", cluster.Name)
		}
		apply, err := terminal.AskForConfirmation("Apply these changes?")
		if err != nil {
			return err
		}

		if apply {
			for _, cluster := range insertClusters {

				err = insertCluster(storeDb, cluster)
				if err != nil {
					return err
				}
			}

			for _, cluster := range updateClusters {
				err = updateCluster(storeDb, cluster.Cluster)
				if err != nil {
					return err
				}
			}

			for _, cluster := range deleteClusters {
				err = deleteCluster(storeDb, cluster)
				if err != nil {
					return err
				}
			}

			idClusters, err := GetClusters(storeDb)
			if err != nil {
				return err
			}

			for _, idCluster := range idClusters {
				foundSuper := false
				for _, idUser := range idCluster.User {
					if idUser.Name == idCluster.SuperUser {
						foundSuper = true
						break
					}
				}

				if !foundSuper {
					err = InsertUser(storeDb, idCluster, &config.User{
						Name:   idCluster.SuperUser,
						Secret: sandstormClient.DefaultSecretPath(idCluster.Name, idCluster.SuperUser),
					})
					if err != nil {
						return err
					}
				}
			}

			fmt.Println("Changes applied")
		}
	}

	fmt.Println("")
	return nil
}

func insertCluster(db *sql.DB, cluster *config.Cluster) error {
	_, err := db.Exec("INSERT INTO clusters (name, driver, environment, migration_schema, migration_repository, alarm_priority, endpoint_url, endpoint_port, database, superuser, is_aurora, root_identifier) "+
		"VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12);",
		cluster.Name,
		cluster.Driver,
		cluster.Environment,
		cluster.Schema,
		cluster.Repository,
		int(cluster.AlarmPriority),
		cluster.Host,
		cluster.Port,
		cluster.Database,
		cluster.SuperUser,
		cluster.IsAurora,
		cluster.RootIdentifier)

	return err
}

func updateCluster(db *sql.DB, cluster *config.Cluster) error {
	_, err := db.Exec("UPDATE clusters SET name = $1, driver = $2, environment = $3, migration_schema = $4, endpoint_url = $5, "+
		"endpoint_port = $6, database = $7, superuser = $8, is_aurora = $9, root_identifier = $10 WHERE id = $11",
		cluster.Name,
		cluster.Driver,
		cluster.Environment,
		cluster.Schema,
		cluster.Host,
		cluster.Port,
		cluster.Database,
		cluster.SuperUser,
		cluster.IsAurora,
		cluster.RootIdentifier,
		cluster.Id)

	return err
}

func deleteCluster(db *sql.DB, cluster *config.Cluster) error {
	_, err := db.Exec("DELETE FROM clusters WHERE id = $1", cluster.Id)
	return err
}

// UpdateAlarmPriority writes `cluster`'s alarm priority to the buddy store
func UpdateAlarmPriority(storeDb *sql.DB, cluster *config.Cluster) error {
	_, err := storeDb.Exec("UPDATE clusters SET alarm_priority=$1 WHERE id = $2", int(cluster.AlarmPriority), cluster.Id)
	return err
}

// UpdateRepository write's `cluster`'s iceman migration repo name to the buddy store
func UpdateRepository(storeDb *sql.DB, cluster *config.Cluster) error {
	_, err := storeDb.Exec("UPDATE clusters SET migration_repository=$1 WHERE id = $2", cluster.Repository, cluster.Id)
	return err
}

// UpdateDBName writes `cluster`'s database name to the buddy store
func UpdateDBName(storeDb *sql.DB, cluster *config.Cluster) error {
	_, err := storeDb.Exec("UPDATE clusters SET database=$1 WHERE id = $2", cluster.Database, cluster.Id)
	return err
}

// GetClusters reads all clusters, populated with users and role pairs, from the store DB
func GetClusters(storeDb *sql.DB) (config.ClusterList, error) {
	var output config.ClusterList

	rootClusters, err := readRootClusters(storeDb)
	if err != nil {
		return output, errors.Wrap(err, "could not read root clusters")
	}

	pairs, err := readUserPairs(storeDb)
	if err != nil {
		return output, errors.Wrap(err, "could not read role pairs for clusters")
	}

	pairsById := make(map[int]*config.RolePair)
	for _, pair := range pairs {
		cluster, ok := rootClusters[pair.clusterId]
		if ok {
			rolePair := &config.RolePair{
				Id:   pair.id,
				Name: pair.name,
			}
			cluster.RolePair = append(cluster.RolePair, rolePair)
			pairsById[rolePair.Id] = rolePair
		}
	}

	services, err := readRoleServices(storeDb)
	if err != nil {
		return output, errors.Wrap(err, "could not read role services for clusters")
	}

	for _, service := range services {
		pair, ok := pairsById[service.rolePairId]
		if ok {
			roleService := &config.RoleService{
				Id:             service.id,
				Name:           service.name,
				UsernameSecret: service.usernameSecret,
				PasswordSecret: service.passwordSecret,
				RolePair:       pair,
			}
			pair.RoleServices = append(pair.RoleServices, roleService)
		}
	}

	users, err := readUsers(storeDb)
	if err != nil {
		return output, errors.Wrap(err, "could not read users for clusters")
	}

	for _, user := range users {
		cluster, ok := rootClusters[user.clusterId]
		if ok {
			clusterUser := &config.User{
				Id:     user.id,
				Name:   user.name,
				Secret: user.secret,
			}

			if user.rolePairId.Valid {
				pair, ok := pairsById[int(user.rolePairId.Int64)]
				if ok {
					clusterUser.RolePair = pair
					pair.Users = append(pair.Users, clusterUser)
				}
			}

			cluster.User = append(cluster.User, clusterUser)
		}
	}

	for _, cluster := range rootClusters {
		output = append(output, cluster)
	}

	return output, nil
}

func readRootClusters(db *sql.DB) (map[int]*config.Cluster, error) {
	clusterResults := make(map[int]*config.Cluster)
	rows, err := db.Query("SELECT id, name, driver, environment, migration_schema, migration_repository, alarm_priority, endpoint_url, endpoint_port, database, superuser, is_aurora, root_identifier FROM clusters")
	if err != nil {
		return clusterResults, err
	}

	defer TryClose(rows)

	for rows.Next() {
		cluster, err := readCluster(rows)
		if err != nil {
			return clusterResults, err
		}
		clusterResults[cluster.Id] = cluster
	}

	return clusterResults, nil
}

func readCluster(rows *sql.Rows) (*config.Cluster, error) {
	var id, alarm_priority, endpoint_port int
	var is_aurora bool
	var name, driver, environment, migration_schema, migration_repository, endpoint_url, database, superuser, root_identifier string
	err := rows.Scan(&id, &name, &driver, &environment, &migration_schema, &migration_repository, &alarm_priority, &endpoint_url, &endpoint_port, &database, &superuser, &is_aurora, &root_identifier)
	if err != nil {
		return nil, err
	}

	return &config.Cluster{
		Id:             id,
		Name:           name,
		Driver:         driver,
		Environment:    environment,
		Schema:         migration_schema,
		Repository:     migration_repository,
		AlarmPriority:  alerts.AlarmPriority(alarm_priority),
		Host:           endpoint_url,
		Port:           endpoint_port,
		Database:       database,
		SuperUser:      superuser,
		IsAurora:       is_aurora,
		RootIdentifier: root_identifier,
	}, nil
}

// TryClose will close a DB/Rows/whatever io.Closer object and print the error to log, if any - good for calling with a defer
func TryClose(closeable io.Closer) {
	if closeable != nil {
		err := closeable.Close()
		if err != nil {
			fmt.Println(err)
		}
	}
}
