package migrator

import (
	"context"
	"encoding/base64"
	"time"

	"github.com/grafana/grafana/pkg/infra/db"
	"github.com/grafana/grafana/pkg/infra/log"
	"github.com/grafana/grafana/pkg/services/encryption"
	"github.com/grafana/grafana/pkg/services/featuremgmt"
	"github.com/grafana/grafana/pkg/services/secrets/manager"
	"github.com/grafana/grafana/pkg/setting"
)

type SecretsRotator interface {
	// ReEncrypt returns true on success, false on any failure.
	ReEncrypt(context.Context, *manager.SecretsService, db.DB) bool
	// Rollback returns true on success, false on any failure.
	Rollback(context.Context, *manager.SecretsService, encryption.Internal, db.DB, string) bool
}

type SecretsMigrator struct {
	encryptionSrv encryption.Internal
	secretsSrv    *manager.SecretsService
	sqlStore      db.DB
	settings      setting.Provider
	features      featuremgmt.FeatureToggles
	rotators      []SecretsRotator
}

func ProvideSecretsMigrator(
	encryptionSrv encryption.Internal,
	service *manager.SecretsService,
	sqlStore db.DB,
	settings setting.Provider,
	features featuremgmt.FeatureToggles,
) *SecretsMigrator {
	rotators := []SecretsRotator{
		simpleSecret{tableName: "dashboard_snapshot", columnName: "dashboard_encrypted"},
		b64Secret{simpleSecret: simpleSecret{tableName: "user_auth", columnName: "o_auth_access_token"}, encoding: base64.StdEncoding},
		b64Secret{simpleSecret: simpleSecret{tableName: "user_auth", columnName: "o_auth_refresh_token"}, encoding: base64.StdEncoding},
		b64Secret{simpleSecret: simpleSecret{tableName: "user_auth", columnName: "o_auth_token_type"}, encoding: base64.StdEncoding},
		b64Secret{simpleSecret: simpleSecret{tableName: "user_auth", columnName: "o_auth_id_token"}, encoding: base64.StdEncoding},
		b64Secret{simpleSecret: simpleSecret{tableName: "secrets", columnName: "value"}, hasUpdatedColumn: true, encoding: base64.RawStdEncoding},
		jsonSecret{tableName: "data_source"},
		jsonSecret{tableName: "plugin_setting"},
		b64Secret{simpleSecret: simpleSecret{tableName: "signing_key", columnName: "private_key"}, encoding: base64.RawStdEncoding},
		alertingSecret{},
		ssoSettingsSecret{},
		b64Secret{simpleSecret: simpleSecret{tableName: "user_external_session", columnName: "access_token"}, encoding: base64.StdEncoding},
		b64Secret{simpleSecret: simpleSecret{tableName: "user_external_session", columnName: "id_token"}, encoding: base64.StdEncoding},
		b64Secret{simpleSecret: simpleSecret{tableName: "user_external_session", columnName: "refresh_token"}, encoding: base64.StdEncoding},
		b64Secret{simpleSecret: simpleSecret{tableName: "user_external_session", columnName: "session_id"}, encoding: base64.StdEncoding},
		b64Secret{simpleSecret: simpleSecret{tableName: "user_external_session", columnName: "name_id"}, encoding: base64.StdEncoding},
		provisioningSecrets{},
	}

	return &SecretsMigrator{
		encryptionSrv: encryptionSrv,
		secretsSrv:    service,
		sqlStore:      sqlStore,
		settings:      settings,
		features:      features,
		rotators:      rotators,
	}
}

func (m *SecretsMigrator) RegisterRotators(rotators ...SecretsRotator) {
	m.rotators = append(m.rotators, rotators...)
}

func (m *SecretsMigrator) ReEncryptSecrets(ctx context.Context) (bool, error) {
	err := m.initProvidersIfNeeded()
	if err != nil {
		return false, err
	}

	var anyFailure bool

	for _, r := range m.rotators {
		if success := r.ReEncrypt(ctx, m.secretsSrv, m.sqlStore); !success {
			anyFailure = true
		}
	}

	return !anyFailure, nil
}

func (m *SecretsMigrator) RollBackSecrets(ctx context.Context) (bool, error) {
	err := m.initProvidersIfNeeded()
	if err != nil {
		return false, err
	}

	var anyFailure bool

	for _, r := range m.rotators {
		if success := r.Rollback(ctx,
			m.secretsSrv,
			m.encryptionSrv,
			m.sqlStore,
			m.settings.KeyValue("security", "secret_key").Value(),
		); !success {
			anyFailure = true
		}
	}

	if anyFailure {
		logger.Warn("Some errors happened, not cleaning up data keys table...")
		return false, nil
	}

	if sqlErr := m.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
		_, err := sess.Exec("DELETE FROM data_keys")
		return err
	}); sqlErr != nil {
		logger.Warn("Error while cleaning up data keys table...", "error", sqlErr)
		return false, nil
	}

	return true, nil
}

func (m *SecretsMigrator) initProvidersIfNeeded() error {
	//nolint:staticcheck // not yet migrated to OpenFeature
	if m.features.IsEnabledGlobally(featuremgmt.FlagDisableEnvelopeEncryption) {
		logger.Info("Envelope encryption is not enabled but trying to init providers anyway...")

		if err := m.secretsSrv.InitProviders(); err != nil {
			logger.Error("Envelope encryption providers initialization failed", "error", err)
			return err
		}
	}

	return nil
}

type simpleSecret struct {
	tableName  string
	columnName string
}

func NewSimpleSecret(tableName, columnName string) simpleSecret {
	return simpleSecret{
		tableName:  tableName,
		columnName: columnName,
	}
}

type b64Secret struct {
	simpleSecret
	hasUpdatedColumn bool
	encoding         *base64.Encoding
}

func NewBase64Secret(simple simpleSecret, encoding *base64.Encoding) b64Secret {
	return b64Secret{
		simpleSecret: simple,
		encoding:     encoding,
	}
}

type jsonSecret struct {
	tableName string
}

type alertingSecret struct{}

type ssoSettingsSecret struct{}

func nowInUTC() string {
	return time.Now().UTC().Format("2006-01-02 15:04:05")
}

var logger = log.New("secrets.migrations")
