package central

import (
	"database/sql"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net/http"

	"github.com/pingcap/errors"
	"k8s.io/client-go/kubernetes"

	"github.com/tidbcloud/clusterproxy/pkg/k8s/tidb"
	"github.com/tidbcloud/clusterproxy/resolve"
	"github.com/tidbcloud/clusterproxy/route"
	"github.com/tidbcloud/clusterproxy/route/router"
	"github.com/tidbcloud/clusterproxy/util"
)

func init() {
	router.RegisterRoute(changePasswordRoute)
}

var changePasswordRoute = &route.Route{
	Name:    "change-password",
	Path:    "change_password",
	Version: route.V1,
	Schema: &route.Schema{
		PathRegex:       "^$",
		RequiredHeaders: []string{},
		JSONBodyModel:   &passwordModel{},
	},
	GetHandler: getHandler,
}

type passwordModelV1 struct {
	Password string `json:"password"`
}

type passwordModelV2 struct {
	NewPassword     string `json:"newPassword"`
	CurrentPassword string `json:"currentPassword"`
}

type passwordModel struct {
	Operator *tidb.OperatorModel `json:"operator"`
	Username string              `json:"username"`

	passwordModelV1
	passwordModelV2
}

func getHandler(c *route.Context, cl resolve.Cluster) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		body, err := io.ReadAll(r.Body)
		defer r.Body.Close() // nolint
		if err != nil {
			http.Error(w, "failed to read body contents", http.StatusInternalServerError)
			return
		}

		model := passwordModel{}
		err = json.Unmarshal(body, &model)
		if err != nil {
			// Surface non-internal errors to the response.
			http.Error(w, fmt.Sprintf("failed to parse the body contents: %v", err), http.StatusBadRequest)
			return
		}

		var errChange error
		if model.Operator != nil {
			newPassword, err := decodePasswordField(model.NewPassword, "new password")
			if err != nil {
				http.Error(w, fmt.Sprintf("%v", err), http.StatusBadRequest)
				return
			}
			errChange = ChangeClusterPasswordV3(c, cl, model.Operator, model.Username, newPassword)
		} else if model.Password != "" { // V1
			password, err := decodePasswordField(model.Password, "password")
			if err != nil {
				http.Error(w, fmt.Sprintf("%v", err), http.StatusBadRequest)
				return
			}
			errChange = ChangeClusterPasswordV1(c.Cli, cl, password)
		} else { // V2
			newPassword, err := decodePasswordField(model.NewPassword, "new password")
			if err != nil {
				http.Error(w, fmt.Sprintf("%v", err), http.StatusBadRequest)
				return
			}
			currentPassword, err := decodePasswordField(model.CurrentPassword, "current password")
			if err != nil {
				http.Error(w, fmt.Sprintf("%v", err), http.StatusBadRequest)
				return
			}
			errChange = ChangeClusterPasswordV2(c, cl, passwordModelV2{
				CurrentPassword: currentPassword,
				NewPassword:     newPassword,
			})
		}

		if errChange != nil {
			msg := fmt.Sprintf("failed to change the cluster password %v", errChange)
			http.Error(w, msg, http.StatusInternalServerError)
		}
	})
}

func decodePasswordField(password string, field string) (string, error) {
	if password == "" {
		return "", errors.Errorf("%s is empty", field)
	}
	decodedPassword, err := base64.URLEncoding.DecodeString(password)
	if err != nil {
		return "", errors.Errorf("failed to base64 decode %s", field)
	}
	return string(decodedPassword), nil
}

// TODO(chenfei): remove
// Deprecated
func ChangeClusterPasswordV1(cli kubernetes.Interface, cl resolve.Cluster, newPassword string) error {
	db, err := tidb.ConnectPrivileged(cli, cl)
	if err != nil {
		return errors.Annotatef(err, "connect to tidb as root for namespace %s", cl.Namespace)
	}
	defer db.Close() // nolint

	return alterPassword(db, newPassword)
}

// TODO(chenfei): remove
// Deprecated
func ChangeClusterPasswordV2(c *route.Context, cl resolve.Cluster, change passwordModelV2) error {
	db, err := tidb.Connect(c, cl, "cloud_admin", change.CurrentPassword)
	if err != nil {
		return errors.Annotatef(err, "connect to tidb as root for namespace %s", cl.Namespace)
	}
	defer db.Close() // nolint

	return alterPassword(db, change.NewPassword)
}

func ChangeClusterPasswordV3(c *route.Context, cl resolve.Cluster, operator *tidb.OperatorModel, username, newPassword string) error {
	db, err := tidb.ConnectCompat(c, cl, "", operator)
	if err != nil {
		return errors.Annotatef(err, "connect to tidb as %s for namespace %s", operator.Username, cl.Namespace)
	}
	defer db.Close() // nolint

	return alterPasswordV2(db, username, newPassword)
}

func alterPassword(db *sql.DB, newPassword string) error {
	return alterPasswordV2(db, "root", newPassword)
}

func alterPasswordV2(db *sql.DB, username, newPassword string) error {
	passwordEscaped := util.Escape(newPassword)
	usernameEscaped := util.Escape(username)
	sqlCommand := fmt.Sprintf("ALTER USER '%s'@'%%' IDENTIFIED BY '%s'", usernameEscaped, passwordEscaped)
	if _, err := db.Exec(sqlCommand); err != nil {
		return errors.Annotate(err, fmt.Sprintf("alter user %s", usernameEscaped))
	}
	sqlCommand = "FLUSH PRIVILEGES;"
	if _, err := db.Exec(sqlCommand); err != nil {
		return errors.Annotate(err, "flush")
	}
	return nil
}
