package migrator

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"

	"github.com/grafana/grafana/pkg/infra/db"
	"github.com/grafana/grafana/pkg/services/encryption"
	"github.com/grafana/grafana/pkg/services/ngalert/notifier"
	"github.com/grafana/grafana/pkg/services/secrets"
	"github.com/grafana/grafana/pkg/services/secrets/manager"
	"github.com/grafana/grafana/pkg/services/sqlstore"
	"github.com/grafana/grafana/pkg/services/ssosettings/models"
	"github.com/grafana/grafana/pkg/services/ssosettings/ssosettingsimpl"
)

func (s simpleSecret) ReEncrypt(ctx context.Context, secretsSrv *manager.SecretsService, sqlStore db.DB) bool {
	var rows []struct {
		Id     int
		Secret []byte
	}

	if err := sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
		return sess.Table(s.tableName).Select(fmt.Sprintf("id, %s as secret", s.columnName)).Find(&rows)
	}); err != nil {
		logger.Warn("Could not find any secret to re-encrypt", "table", s.tableName)
		return false
	}

	var anyFailure bool

	for _, row := range rows {
		if len(row.Secret) == 0 {
			continue
		}

		err := sqlStore.InTransaction(ctx, func(ctx context.Context) error {
			decrypted, err := secretsSrv.Decrypt(ctx, row.Secret)
			if err != nil {
				logger.Warn("Could not decrypt secret while re-encrypting it", "table", s.tableName, "id", row.Id, "error", err)
				return err
			}

			encrypted, err := secretsSrv.Encrypt(ctx, decrypted, secrets.WithoutScope())
			if err != nil {
				logger.Warn("Could not encrypt secret while re-encrypting it", "table", s.tableName, "id", row.Id, "error", err)
				return err
			}

			updateSQL := fmt.Sprintf("UPDATE %s SET %s = ?, updated = ? WHERE id = ?", s.tableName, s.columnName)
			if err = sqlStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
				_, err := sess.Exec(updateSQL, encrypted, nowInUTC(), row.Id)
				return err
			}); err != nil {
				logger.Warn("Could not update secret while re-encrypting it", "table", s.tableName, "id", row.Id, "error", err)
				return err
			}

			return nil
		})

		if err != nil {
			anyFailure = true
		}
	}

	if anyFailure {
		logger.Warn(fmt.Sprintf("Column %s from %s has been re-encrypted with errors", s.columnName, s.tableName))
	} else {
		logger.Info(fmt.Sprintf("Column %s from %s has been re-encrypted successfully", s.columnName, s.tableName))
	}

	return !anyFailure
}

func (s b64Secret) ReEncrypt(ctx context.Context, secretsSrv *manager.SecretsService, sqlStore db.DB) bool {
	var rows []struct {
		Id     int
		Secret string
	}

	if err := sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
		return sess.Table(s.tableName).Select(fmt.Sprintf("id, %s as secret", s.columnName)).Find(&rows)
	}); err != nil {
		logger.Warn("Could not find any secret to re-encrypt", "table", s.tableName)
		return false
	}

	var anyFailure bool

	for _, row := range rows {
		if len(row.Secret) == 0 {
			continue
		}

		err := sqlStore.InTransaction(ctx, func(ctx context.Context) error {
			decoded, err := s.encoding.DecodeString(row.Secret)
			if err != nil {
				logger.Warn("Could not decode base64-encoded secret while re-encrypting it", "table", s.tableName, "id", row.Id, "error", err)
				return err
			}

			decrypted, err := secretsSrv.Decrypt(ctx, decoded)
			if err != nil {
				logger.Warn("Could not decrypt secret while re-encrypting it", "table", s.tableName, "id", row.Id, "error", err)
				return err
			}

			encrypted, err := secretsSrv.Encrypt(ctx, decrypted, secrets.WithoutScope())
			if err != nil {
				logger.Warn("Could not encrypt secret while re-encrypting it", "table", s.tableName, "id", row.Id, "error", err)
				return err
			}

			if err = sqlStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) (err error) {
				encoded := s.encoding.EncodeToString(encrypted)
				if s.hasUpdatedColumn {
					updateSQL := fmt.Sprintf("UPDATE %s SET %s = ?, updated = ? WHERE id = ?", s.tableName, s.columnName)
					_, err = sess.Exec(updateSQL, encoded, nowInUTC(), row.Id)
				} else {
					updateSQL := fmt.Sprintf("UPDATE %s SET %s = ? WHERE id = ?", s.tableName, s.columnName)
					_, err = sess.Exec(updateSQL, encoded, row.Id)
				}
				return
			}); err != nil {
				logger.Warn("Could not update secret while re-encrypting it", "table", s.tableName, "id", row.Id, "error", err)
				return err
			}

			return nil
		})

		if err != nil {
			anyFailure = true
		}
	}

	if anyFailure {
		logger.Warn(fmt.Sprintf("Column %s from %s has been re-encrypted with errors", s.columnName, s.tableName))
	} else {
		logger.Info(fmt.Sprintf("Column %s from %s has been re-encrypted successfully", s.columnName, s.tableName))
	}

	return !anyFailure
}

func (s jsonSecret) ReEncrypt(ctx context.Context, secretsSrv *manager.SecretsService, sqlStore db.DB) bool {
	var rows []struct {
		Id             int
		SecureJsonData map[string][]byte
	}

	if err := sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
		return sess.Table(s.tableName).Cols("id", "secure_json_data").Find(&rows)
	}); err != nil {
		logger.Warn("Could not find any secret to re-encrypt", "table", s.tableName)
		return false
	}

	var anyFailure bool

	for _, row := range rows {
		if len(row.SecureJsonData) == 0 {
			continue
		}

		err := sqlStore.InTransaction(ctx, func(ctx context.Context) error {
			decrypted, err := secretsSrv.DecryptJsonData(ctx, row.SecureJsonData)
			if err != nil {
				logger.Warn("Could not decrypt secrets while re-encrypting them", "table", s.tableName, "id", row.Id, "error", err)
				return err
			}

			toUpdate := struct {
				SecureJsonData map[string][]byte
				Updated        string
			}{Updated: nowInUTC()}

			toUpdate.SecureJsonData, err = secretsSrv.EncryptJsonData(ctx, decrypted, secrets.WithoutScope())
			if err != nil {
				logger.Warn("Could not re-encrypt secrets", "table", s.tableName, "id", row.Id, "error", err)
				return err
			}

			if err := sqlStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
				_, err := sess.Table(s.tableName).Where("id = ?", row.Id).Update(toUpdate)
				return err
			}); err != nil {
				logger.Warn("Could not update secrets while re-encrypting them", "table", s.tableName, "id", row.Id, "error", err)
				return err
			}

			return nil
		})

		if err != nil {
			anyFailure = true
		}
	}

	if anyFailure {
		logger.Warn(fmt.Sprintf("Secure json data secrets from %s have been re-encrypted with errors", s.tableName))
	} else {
		logger.Info(fmt.Sprintf("Secure json data secrets from %s have been re-encrypted successfully", s.tableName))
	}

	return !anyFailure
}

func (s alertingSecret) ReEncrypt(ctx context.Context, secretsSrv *manager.SecretsService, sqlStore db.DB) bool {
	var results []struct {
		Id                        int
		AlertmanagerConfiguration string
	}

	selectSQL := "SELECT id, alertmanager_configuration FROM alert_configuration"
	if err := sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
		return sess.SQL(selectSQL).Find(&results)
	}); err != nil {
		logger.Warn("Could not find any alert_configuration secret to re-encrypt")
		return false
	}

	var anyFailure bool

	for _, result := range results {
		result := result

		err := sqlStore.InTransaction(ctx, func(ctx context.Context) error {
			postableUserConfig, err := notifier.Load([]byte(result.AlertmanagerConfiguration))
			if err != nil {
				logger.Warn("Could not load alert_configuration while re-encrypting it", "id", result.Id, "error", err)
				return err
			}

			for _, receiver := range postableUserConfig.AlertmanagerConfig.Receivers {
				for _, gmr := range receiver.GrafanaManagedReceivers {
					for k, v := range gmr.SecureSettings {
						decoded, err := base64.StdEncoding.DecodeString(v)
						if err != nil {
							logger.Warn("Could not decode base64-encoded alert_configuration secret", "id", result.Id, "key", k, "error", err)
							return err
						}

						decrypted, err := secretsSrv.Decrypt(ctx, decoded)
						if err != nil {
							logger.Warn("Could not decrypt alert_configuration secret", "id", result.Id, "key", k, "error", err)
							return err
						}

						reencrypted, err := secretsSrv.Encrypt(ctx, decrypted, secrets.WithoutScope())
						if err != nil {
							logger.Warn("Could not re-encrypt alert_configuration secret", "id", result.Id, "key", k, "error", err)
							return err
						}

						gmr.SecureSettings[k] = base64.StdEncoding.EncodeToString(reencrypted)
					}
				}
			}

			marshalled, err := json.Marshal(postableUserConfig)
			if err != nil {
				logger.Warn("Could not marshal alert_configuration while re-encrypting it", "id", result.Id, "error", err)
				return err
			}

			result.AlertmanagerConfiguration = string(marshalled)
			if err := sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
				_, err := sess.Table("alert_configuration").Where("id = ?", result.Id).Update(&result)
				return err
			}); err != nil {
				logger.Warn("Could not update alert_configuration secret while re-encrypting it", "id", result.Id, "error", err)
				return err
			}

			return nil
		})

		if err != nil {
			anyFailure = true
		}
	}

	if anyFailure {
		logger.Warn("Alerting configuration secrets have been re-encrypted with errors")
	} else {
		logger.Info("Alerting configuration secrets have been re-encrypted successfully")
	}

	return !anyFailure
}

func (s ssoSettingsSecret) ReEncrypt(ctx context.Context, secretsSrv *manager.SecretsService, sqlStore db.DB) bool {
	results := make([]*models.SSOSettings, 0)

	err := sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
		return sess.Find(&results)
	})

	if err != nil {
		logger.Warn("Failed to fetch SSO settings to re-encrypt", "err", err)
		return false
	}

	var anyFailure bool

	for _, result := range results {
		err := sqlStore.InTransaction(ctx, func(ctx context.Context) error {
			result.Settings, err = s.reEncryptSecretsInMap(ctx, result.Settings, secretsSrv, nil, "")
			if err != nil {
				logger.Warn("failed re-encrypting SSO settings secret", "id", result.ID, "error", err)
				return err
			}

			err = sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
				_, err := sess.Where("id = ?", result.ID).Update(result)
				return err
			})
			if err != nil {
				logger.Warn("Could not update SSO settings secrets while re-encrypting it", "id", result.ID, "error", err)
				return err
			}

			return nil
		})

		if err != nil {
			anyFailure = true
		}
	}

	if anyFailure {
		logger.Warn("SSO settings secrets have been re-encrypted with errors")
	} else {
		logger.Info("SSO settings secrets have been re-encrypted successfully")
	}

	return !anyFailure
}

func (s ssoSettingsSecret) decryptValue(ctx context.Context, value any, secretsSrv *manager.SecretsService) ([]byte, error) {
	strValue, ok := value.(string)
	if !ok {
		return nil, fmt.Errorf("SSO secret value is not a string")
	}

	if strValue == "" {
		return nil, nil
	}

	decoded, err := base64.RawStdEncoding.DecodeString(strValue)
	if err != nil {
		return nil, fmt.Errorf("could not decode base64-encoded SSO settings secret: %w", err)
	}

	decrypted, err := secretsSrv.Decrypt(ctx, decoded)
	if err != nil {
		return nil, fmt.Errorf("could not decrypt SSO settings secret: %w", err)
	}

	return decrypted, nil
}

func (s ssoSettingsSecret) reEncryptSecretsInMap(ctx context.Context, m map[string]any, secretsSrv *manager.SecretsService, encryptionSrv encryption.Internal, secretKey string) (map[string]any, error) {
	var err error

	result := make(map[string]any)
	for k, v := range m {
		switch v := v.(type) {
		case string:
			result[k] = v
			if ssosettingsimpl.IsSecretField(k) {
				decrypted, err := s.decryptValue(ctx, v, secretsSrv)
				if err != nil {
					logger.Warn("Could not decrypt SSO settings secret", "field", k, "error", err)
					return nil, err
				}

				if decrypted == nil {
					continue
				}

				var reencrypted []byte
				if encryptionSrv == nil {
					reencrypted, err = secretsSrv.Encrypt(ctx, decrypted, secrets.WithoutScope())
				} else {
					reencrypted, err = encryptionSrv.Encrypt(ctx, decrypted, secretKey)
				}
				if err != nil {
					logger.Warn("Could not re-encrypt SSO settings secret", "id", "field", k, "error", err)
					return nil, err
				}

				result[k] = base64.RawStdEncoding.EncodeToString(reencrypted)
			}
		case []any:
			result[k], err = s.reEncryptSecretsInSlice(ctx, v, secretsSrv, encryptionSrv, secretKey)
			if err != nil {
				return nil, err
			}
		case map[string]any:
			result[k], err = s.reEncryptSecretsInMap(ctx, v, secretsSrv, encryptionSrv, secretKey)
			if err != nil {
				return nil, err
			}
		default:
			result[k] = v
		}
	}

	return result, nil
}

func (s ssoSettingsSecret) reEncryptSecretsInSlice(ctx context.Context, a []any, secretsSrv *manager.SecretsService, encryptionSrv encryption.Internal, secretKey string) ([]any, error) {
	result := make([]any, 0)
	for _, v := range a {
		switch v := v.(type) {
		case []any:
			inner, err := s.reEncryptSecretsInSlice(ctx, v, secretsSrv, encryptionSrv, secretKey)
			if err != nil {
				return nil, err
			}
			result = append(result, inner)
		case map[string]any:
			inner, err := s.reEncryptSecretsInMap(ctx, v, secretsSrv, encryptionSrv, secretKey)
			if err != nil {
				return nil, err
			}
			result = append(result, inner)
		default:
			result = append(result, v)
		}
	}

	return result, nil
}
