// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package database

import (
	"context"
	"errors"

	"github.com/hashicorp/vault/sdk/database/dbplugin"
	v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5"
	"github.com/hashicorp/vault/sdk/logical"
	"github.com/mitchellh/mapstructure"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

// WAL storage key used for the rollback of root database credentials
const rotateRootWALKey = "rotateRootWALKey"

// WAL entry used for the rollback of root database credentials
type rotateRootCredentialsWAL struct {
	ConnectionName string
	UserName       string
	NewPassword    string
	OldPassword    string
}

// walRollback handles WAL entries that result from partial failures
// to rotate the root credentials of a database. It is responsible
// for rolling back root database credentials when doing so would
// reconcile the credentials with Vault storage.
func (b *databaseBackend) walRollback(ctx context.Context, req *logical.Request, kind string, data interface{}) error {
	if kind != rotateRootWALKey {
		return errors.New("unknown type to rollback")
	}

	// Decode the WAL data
	var entry rotateRootCredentialsWAL
	if err := mapstructure.Decode(data, &entry); err != nil {
		return err
	}

	// Get the current database configuration from storage
	config, err := b.DatabaseConfig(ctx, req.Storage, entry.ConnectionName)
	if err != nil {
		return err
	}

	// The password in storage doesn't match the new password
	// in the WAL entry. This means there was a partial failure
	// to update either the database or storage.
	if config.ConnectionDetails["password"] != entry.NewPassword {
		// Clear any cached connection to inform the rollback decision
		err := b.ClearConnection(entry.ConnectionName)
		if err != nil {
			return err
		}

		// Attempt to get a connection with the current configuration.
		// If successful, the WAL entry can be deleted. This means
		// the root credentials are the same according to the database
		// and storage.
		_, err = b.GetConnection(ctx, req.Storage, entry.ConnectionName)
		if err == nil {
			return nil
		}

		return b.rollbackDatabaseCredentials(ctx, config, entry)
	}

	// The password in storage matches the new password in
	// the WAL entry, so there is nothing to roll back. This
	// means the new password was successfully updated in the
	// database and storage, but the WAL wasn't deleted.
	return nil
}

// rollbackDatabaseCredentials rolls back root database credentials for
// the connection associated with the passed WAL entry. It will create
// a connection to the database using the WAL entry new password in
// order to alter the password to be the WAL entry old password.
func (b *databaseBackend) rollbackDatabaseCredentials(ctx context.Context, config *DatabaseConfig, entry rotateRootCredentialsWAL) error {
	// Attempt to get a connection with the WAL entry new password.
	config.ConnectionDetails["password"] = entry.NewPassword
	dbi, err := b.GetConnectionWithConfig(ctx, entry.ConnectionName, config)
	if err != nil {
		return err
	}

	// Ensure the connection used to roll back the database password is not cached
	defer func() {
		if err := b.ClearConnection(entry.ConnectionName); err != nil {
			b.Logger().Error("error closing database plugin connection", "err", err)
		}
	}()

	updateReq := v5.UpdateUserRequest{
		Username:       entry.UserName,
		CredentialType: v5.CredentialTypePassword,
		Password: &v5.ChangePassword{
			NewPassword: entry.OldPassword,
			Statements: v5.Statements{
				Commands: config.RootCredentialsRotateStatements,
			},
		},
	}

	// It actually is the root user here, but we only want to use SetCredentials since
	// RotateRootCredentials doesn't give any control over what password is used
	_, err = dbi.database.UpdateUser(ctx, updateReq, false)
	if status.Code(err) == codes.Unimplemented || err == dbplugin.ErrPluginStaticUnsupported {
		return nil
	}
	return err
}
