package queries

import (
	"sync"

	"github.com/fiskerinc/cloud-services/pkg/common"
	"github.com/fiskerinc/cloud-services/pkg/security"
	"github.com/fiskerinc/cloud-services/pkg/validator"

	"github.com/go-pg/pg/v10"
	"github.com/go-pg/pg/v10/orm"
	"github.com/pkg/errors"
	berrors "errors"
)

type FileKeysInterface interface {
	Delete(fileID string) (orm.Result, error)
	Insert(filekey common.FileKey) (orm.Result, error)
	Get(fileID string) (*common.FileKey, error)
	GetMulti(fileIDs []string) ([]common.FileKey, error)
}

type FileKeys struct {
	QueryBase
	encryptor security.IEncryptor
	once      sync.Once
}

func (fk *FileKeys) getEncryptor() (security.IEncryptor, error) {
	var err error
	fk.once.Do(func() {
		encrypt := security.Encrypt{}
		fk.encryptor, err = encrypt.GetEncryptor()
	})

	return fk.encryptor, err
}

// Delete deletes fileID of FileKey from database
func (fk *FileKeys) Delete(fileID string) (orm.Result, error) {
	if fileID == "" {
		return nil, errors.WithStack(&validator.FieldError{
			ErrorMsg: "FileID required",
		})
	}

	conn := fk.GetDBConn()
	return fk.resultWithStack(conn.Model(&common.FileKey{
		FileID: fileID,
	}).WherePK().Delete())
}

// Insert makes a copy of FileKey, encrypts encryption parameters, and inserts into database
func (fk *FileKeys) Insert(filekey common.FileKey) (orm.Result, error) {
	err := validator.ValidateStruct(filekey)
	if err != nil {
		return nil, errors.WithStack(err)
	}

	err = fk.encrypt(&filekey)
	if err != nil {
		return nil, err
	}

	return fk.resultWithStack(fk.GetDBConn().Model(&filekey).Insert())
}

func (fk *FileKeys) Get(fileID string) (*common.FileKey, error) {
	if fileID == "" {
		return nil, errors.WithStack(&validator.FieldError{
			ErrorMsg: "FileID required",
		})
	}

	filekey := []common.FileKey{}
	err := fk.GetDBConn().Model(&filekey).Where("file_id = ?", fileID).Select()
	if err != nil {
		return nil, errors.WithStack(err)
	}

	if len(filekey) == 0 {
		return &common.FileKey{}, nil
	}

	err = fk.decrypt(&filekey[0])
	if err != nil {
		return nil, err
	}

	return &filekey[0], nil
}

func (fk *FileKeys) GetMulti(fileIDs []string) ([]common.FileKey, error) {
	filekeys := []common.FileKey{}
	if len(fileIDs) == 0 {
		return filekeys, nil
	}
	err := fk.GetDBConn().Model(&filekeys).Where("file_id in (?)", pg.In(fileIDs)).Select()
	if err != nil {
		return nil, errors.WithStack(err)
	}
	var masterError error
	for i := range filekeys {
		err = fk.decrypt(&filekeys[i])
		if err != nil {
			masterError = berrors.Join(masterError, err)
			filekeys[i].Error = err.Error()
		}
	}

	return filekeys, masterError
}

func (fk *FileKeys) encrypt(filekey *common.FileKey) error {
	encryptor, err := fk.getEncryptor()
	if err != nil {
		return err
	}

	filekey.Key = encryptor.EncryptChunk([]byte(filekey.Key))
	filekey.Auth = encryptor.EncryptChunk([]byte(filekey.Auth))
	filekey.Nonce = encryptor.EncryptChunk([]byte(filekey.Nonce))

	return nil
}

func (fk *FileKeys) decrypt(filekey *common.FileKey) error {

	encryptor, err := fk.getEncryptor()
	if err != nil {
		return err
	}

	value, err := encryptor.DecryptChunk([]byte(filekey.Key))
	if err != nil {
		return err
	}

	filekey.Key = value
	value, err = encryptor.DecryptChunk([]byte(filekey.Auth))
	if err != nil {
		return err
	}

	filekey.Auth = value
	value, err = encryptor.DecryptChunk([]byte(filekey.Nonce))
	if err != nil {
		return err
	}

	filekey.Nonce = value
	return nil
}
