package service

import (
	"context"
	"sync"
	"time"

	"github.com/grafana/grafana/pkg/infra/db"
	"github.com/grafana/grafana/pkg/infra/log"
	"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
	"github.com/grafana/grafana/pkg/services/secrets"
)

func ProvideService(db db.DB, secretsService secrets.Service) *Service {
	s := &Service{
		db: db,
		decryptionCache: secureJSONDecryptionCache{
			cache: make(map[int64]cachedDecryptedJSON),
		},
		secretsService: secretsService,
		logger:         log.New("pluginsettings"),
	}

	return s
}

type Service struct {
	db              db.DB
	decryptionCache secureJSONDecryptionCache
	secretsService  secrets.Service

	logger log.Logger
}

type cachedDecryptedJSON struct {
	updated time.Time
	json    map[string]string
}

type secureJSONDecryptionCache struct {
	cache map[int64]cachedDecryptedJSON
	sync.Mutex
}

func (s *Service) GetPluginSettings(ctx context.Context, args *pluginsettings.GetArgs) ([]*pluginsettings.InfoDTO, error) {
	ps, err := s.getPluginSettingsInfo(ctx, args.OrgID)
	if err != nil {
		return nil, err
	}

	result := make([]*pluginsettings.InfoDTO, 0, len(ps))
	for _, p := range ps {
		result = append(result, &pluginsettings.InfoDTO{
			OrgID:         p.OrgID,
			PluginID:      p.PluginID,
			PluginVersion: p.PluginVersion,
			Enabled:       p.Enabled,
			Pinned:        p.Pinned,
		})
	}

	return result, nil
}

func (s *Service) GetPluginSettingByPluginID(ctx context.Context, args *pluginsettings.GetByPluginIDArgs) (*pluginsettings.DTO, error) {
	query := &pluginsettings.GetPluginSettingByIdQuery{
		OrgId:    args.OrgID,
		PluginId: args.PluginID,
	}

	err := s.getPluginSettingById(ctx, query)
	if err != nil {
		return nil, err
	}

	return &pluginsettings.DTO{
		ID:             query.Result.Id,
		OrgID:          query.Result.OrgId,
		PluginID:       query.Result.PluginId,
		PluginVersion:  query.Result.PluginVersion,
		JSONData:       query.Result.JsonData,
		SecureJSONData: query.Result.SecureJsonData,
		Enabled:        query.Result.Enabled,
		Pinned:         query.Result.Pinned,
		Updated:        query.Result.Updated,
	}, nil
}

func (s *Service) UpdatePluginSetting(ctx context.Context, args *pluginsettings.UpdateArgs) error {
	encryptedSecureJsonData, err := s.secretsService.EncryptJsonData(ctx, args.SecureJSONData, secrets.WithoutScope())
	if err != nil {
		return err
	}

	return s.updatePluginSetting(ctx, &pluginsettings.UpdatePluginSettingCmd{
		Enabled:                 args.Enabled,
		Pinned:                  args.Pinned,
		JsonData:                args.JSONData,
		SecureJsonData:          args.SecureJSONData,
		PluginVersion:           args.PluginVersion,
		PluginId:                args.PluginID,
		OrgId:                   args.OrgID,
		EncryptedSecureJsonData: encryptedSecureJsonData,
	})
}

func (s *Service) UpdatePluginSettingPluginVersion(ctx context.Context, args *pluginsettings.UpdatePluginVersionArgs) error {
	return s.updatePluginSettingVersion(ctx, &pluginsettings.UpdatePluginSettingVersionCmd{
		PluginVersion: args.PluginVersion,
		PluginId:      args.PluginID,
		OrgId:         args.OrgID,
	})
}

func (s *Service) DecryptedValues(ps *pluginsettings.DTO) map[string]string {
	s.decryptionCache.Lock()
	defer s.decryptionCache.Unlock()

	if item, present := s.decryptionCache.cache[ps.ID]; present && ps.Updated.Equal(item.updated) {
		return item.json
	}

	json, err := s.secretsService.DecryptJsonData(context.Background(), ps.SecureJSONData)
	if err != nil {
		s.logger.Error("Failed to decrypt secure json data", "error", err)
		return map[string]string{}
	}

	s.decryptionCache.cache[ps.ID] = cachedDecryptedJSON{
		updated: ps.Updated,
		json:    json,
	}

	return json
}

func (s *Service) getPluginSettingsInfo(ctx context.Context, orgID int64) ([]*pluginsettings.PluginSettingInfo, error) {
	sql := `SELECT org_id, plugin_id, enabled, pinned, plugin_version FROM plugin_setting `
	params := make([]any, 0)

	if orgID != 0 {
		sql += "WHERE org_id=?"
		params = append(params, orgID)
	}

	var rslt []*pluginsettings.PluginSettingInfo
	err := s.db.WithDbSession(ctx, func(sess *db.Session) error {
		return sess.SQL(sql, params...).Find(&rslt)
	})
	if err != nil {
		return nil, err
	}

	return rslt, nil
}

func (s *Service) getPluginSettingById(ctx context.Context, query *pluginsettings.GetPluginSettingByIdQuery) error {
	return s.db.WithDbSession(ctx, func(sess *db.Session) error {
		pluginSetting := pluginsettings.PluginSetting{OrgId: query.OrgId, PluginId: query.PluginId}
		has, err := sess.Get(&pluginSetting)
		if err != nil {
			return err
		} else if !has {
			return pluginsettings.ErrPluginSettingNotFound
		}
		query.Result = &pluginSetting
		return nil
	})
}

func (s *Service) updatePluginSetting(ctx context.Context, cmd *pluginsettings.UpdatePluginSettingCmd) error {
	return s.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
		var pluginSetting pluginsettings.PluginSetting

		exists, err := sess.Where("org_id=? and plugin_id=?", cmd.OrgId, cmd.PluginId).Get(&pluginSetting)
		if err != nil {
			return err
		}
		sess.UseBool("enabled")
		sess.UseBool("pinned")
		if !exists {
			pluginSetting = pluginsettings.PluginSetting{
				PluginId:       cmd.PluginId,
				OrgId:          cmd.OrgId,
				Enabled:        cmd.Enabled,
				Pinned:         cmd.Pinned,
				JsonData:       cmd.JsonData,
				PluginVersion:  cmd.PluginVersion,
				SecureJsonData: cmd.EncryptedSecureJsonData,
				Created:        time.Now(),
				Updated:        time.Now(),
			}

			// add state change event on commit success
			sess.PublishAfterCommit(&pluginsettings.PluginStateChangedEvent{
				PluginId: cmd.PluginId,
				OrgId:    cmd.OrgId,
				Enabled:  cmd.Enabled,
			})

			_, err = sess.Insert(&pluginSetting)
			return err
		}

		for key, encryptedData := range cmd.EncryptedSecureJsonData {
			pluginSetting.SecureJsonData[key] = encryptedData
		}

		// add state change event on commit success
		if pluginSetting.Enabled != cmd.Enabled {
			sess.PublishAfterCommit(&pluginsettings.PluginStateChangedEvent{
				PluginId: cmd.PluginId,
				OrgId:    cmd.OrgId,
				Enabled:  cmd.Enabled,
			})
		}

		pluginSetting.Updated = time.Now()
		pluginSetting.Enabled = cmd.Enabled
		pluginSetting.JsonData = cmd.JsonData
		pluginSetting.Pinned = cmd.Pinned
		pluginSetting.PluginVersion = cmd.PluginVersion

		_, err = sess.ID(pluginSetting.Id).Update(&pluginSetting)
		return err
	})
}

func (s *Service) updatePluginSettingVersion(ctx context.Context, cmd *pluginsettings.UpdatePluginSettingVersionCmd) error {
	return s.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
		_, err := sess.Exec("UPDATE plugin_setting SET plugin_version=? WHERE org_id=? AND plugin_id=?", cmd.PluginVersion, cmd.OrgId, cmd.PluginId)
		return err
	})
}
