////////////////////////////////////////////////////////////////////////////////
// Copyright © 2020 xx network SEZC                                           //
//                                                                            //
// Use of this source code is governed by a license that can be found in the  //
// LICENSE file                                                               //
////////////////////////////////////////////////////////////////////////////////

package backup

import (
	"sync"
	"time"

	"gitlab.com/elixxir/client/api/messenger"
	"gitlab.com/elixxir/client/storage/versioned"
	"gitlab.com/elixxir/crypto/cyclic"
	"gitlab.com/elixxir/primitives/fact"
	"gitlab.com/xx_network/primitives/id"

	"github.com/pkg/errors"
	jww "github.com/spf13/jwalterweatherman"
	"gitlab.com/elixxir/crypto/backup"
	"gitlab.com/elixxir/crypto/fastRNG"
	"gitlab.com/xx_network/crypto/signature/rsa"
)

// Error messages.
const (
	// InitializeBackup
	errSavePassword      = "failed to save password: %+v"
	errSaveKeySaltParams = "failed to save key, salt, and params: %+v"

	// Backup.StopBackup
	errDeletePassword = "failed to delete password: %+v"
	errDeleteCrypto   = "failed to delete key, salt, and parameters: %+v"
)

// Backup stores the user's key and backup callback used to encrypt and transmit
// the backup data.
type Backup struct {
	// Callback that is called with the encrypted backup when triggered
	updateBackupCb UpdateBackupFn

	container *messenger.Container

	jsonParams string

	// Client structures
	e2e     E2e
	session Session
	ud      UserDiscovery
	kv      *versioned.KV
	rng     *fastRNG.StreamGenerator

	mux sync.RWMutex
}

// E2e is a subset of functions from the interface e2e.Handler.
type E2e interface {
	GetAllPartnerIDs() []*id.ID
	GetHistoricalDHPubkey() *cyclic.Int
	GetHistoricalDHPrivkey() *cyclic.Int
}

// Session is a subset of functions from the interface storage.Session.
type Session interface {
	GetRegCode() (string, error)
	GetTransmissionID() *id.ID
	GetTransmissionSalt() []byte
	GetReceptionID() *id.ID
	GetReceptionSalt() []byte
	GetReceptionRSA() *rsa.PrivateKey
	GetTransmissionRSA() *rsa.PrivateKey
	GetTransmissionRegistrationValidationSignature() []byte
	GetReceptionRegistrationValidationSignature() []byte
	GetRegistrationTimestamp() time.Time
}

type UserDiscovery interface {
	GetFacts() fact.FactList
}

// UpdateBackupFn is the callback that encrypted backup data is returned on
type UpdateBackupFn func(encryptedBackup []byte)

// InitializeBackup creates a new Backup object with the callback to return
// backups when triggered. On initialization, 32-bit key is derived from the
// user's password via Argon2 and a 16-bit salt is generated. Both are saved to
// storage along with the parameters used in Argon2 to be used when encrypting
// new backups.
// Call this to turn on backups for the first time or to replace the user's
// password.
func InitializeBackup(password string, updateBackupCb UpdateBackupFn,
	container *messenger.Container, e2e E2e, session Session, ud UserDiscovery,
	kv *versioned.KV, rng *fastRNG.StreamGenerator) (*Backup, error) {
	b := &Backup{
		updateBackupCb: updateBackupCb,
		container:      container,
		e2e:            e2e,
		session:        session,
		ud:             ud,
		kv:             kv,
		rng:            rng,
	}

	// Derive key and get generated salt and parameters
	rand := b.rng.GetStream()
	salt, err := backup.MakeSalt(rand)
	if err != nil {
		return nil, err
	}
	rand.Close()

	params := backup.DefaultParams()
	params.Memory = 256 * 1024 // 256 MiB
	params.Threads = 4
	params.Time = 100
	key := backup.DeriveKey(password, salt, params)

	// Save key, salt, and parameters to storage
	err = saveBackup(key, salt, params, b.kv)
	if err != nil {
		return nil, errors.Errorf(errSaveKeySaltParams, err)
	}

	// Setting backup trigger in client
	b.container.SetBackup(b.TriggerBackup)

	b.TriggerBackup("InitializeBackup")
	jww.INFO.Print("Initialized backup with new user key.")

	return b, nil
}

// ResumeBackup resumes a backup by restoring the Backup object and registering
// a new callback. Call this to resume backups that have already been
// initialized. Returns an error if backups have not already been initialized.
func ResumeBackup(updateBackupCb UpdateBackupFn, container *messenger.Container,
	e2e E2e, session Session, ud UserDiscovery, kv *versioned.KV,
	rng *fastRNG.StreamGenerator) (*Backup, error) {
	_, _, _, err := loadBackup(store.GetKV())
	if err != nil {
		return nil, err
	}

	b := &Backup{
		updateBackupCb: updateBackupCb,
		container:      container,
		jsonParams:     loadJson(kv),
		e2e:            e2e,
		session:        session,
		ud:             ud,
		kv:             kv,
		rng:            rng,
	}

	// Setting backup trigger in client
	b.container.SetBackup(b.TriggerBackup)

	jww.INFO.Print("Resumed backup with password loaded from storage.")

	return b, nil
}

// getKeySaltParams derives a key from the user's password, a generated salt,
// and the default parameters and return all three.
func (b *Backup) getKeySaltParams(password string) (
	key, salt []byte, params backup.Params, err error) {
	rand := b.rng.GetStream()
	salt, err = backup.MakeSalt(rand)
	if err != nil {
		return
	}
	rand.Close()

	params = backup.DefaultParams()
	key = backup.DeriveKey(password, salt, params)

	return
}

// TriggerBackup assembles the backup and calls it on the registered backup
// callback. Does nothing if no encryption key or backup callback is registered.
// The passed in reason will be printed to the log when the backup is sent. It
// should be in the past tense. For example, if a contact is deleted, the
// reason can be "contact deleted" and the log will show:
//	Triggering backup: contact deleted
func (b *Backup) TriggerBackup(reason string) {
	b.mux.RLock()
	defer b.mux.RUnlock()

	if b == nil || b.kv == nil {
		jww.ERROR.Printf("TriggerBackup called on unitialized object")
		return
	}

	key, salt, params, err := loadBackup(b.kv)
	if err != nil {
		jww.ERROR.Printf("Backup Failed: could not load key, salt, and "+
			"parameters for encrypting backup from storage: %+v", err)
		return
	}

	// Grab backup data
	collatedBackup := b.assembleBackup()

	// Encrypt backup data with user key
	rand := b.rng.GetStream()
	encryptedBackup, err := collatedBackup.Encrypt(rand, key, salt, params)
	if err != nil {
		jww.FATAL.Panicf("Failed to encrypt backup: %+v", err)
	}
	rand.Close()

	jww.INFO.Printf("Backup triggered: %s", reason)

	// Send backup on callback
	b.mux.RLock()
	defer b.mux.RUnlock()
	if b.updateBackupCb != nil {
		go b.updateBackupCb(encryptedBackup)
	} else {
		jww.WARN.Printf("could not call backup callback, stopped...")
	}
}

func (b *Backup) AddJson(newJson string) {
	b.mux.Lock()
	defer b.mux.Unlock()

	if newJson != b.jsonParams {
		b.jsonParams = newJson
		if err := storeJson(newJson, b.kv); err != nil {
			jww.FATAL.Panicf("Failed to store json: %+v", err)
		}
		go b.TriggerBackup("New Json")
	}
}

// StopBackup stops the backup processes and deletes the user's password, key,
// salt, and parameters from storage.
func (b *Backup) StopBackup() error {
	b.mux.Lock()
	defer b.mux.Unlock()
	b.updateBackupCb = nil

	err := deleteBackup(b.store.GetKV())
	if err != nil {
		return errors.Errorf(errDeleteCrypto, err)
	}

	jww.INFO.Print("Stopped backups.")

	return nil
}

// IsBackupRunning returns true if the backup has been initialized and is
// running. Returns false if it has been stopped.
func (b *Backup) IsBackupRunning() bool {
	b.mux.RLock()
	defer b.mux.RUnlock()
	return b.updateBackupCb != nil
}

// assembleBackup gathers all the contents of the backup and stores them in a
// backup.Backup. This backup contains:
//  1. Cryptographic information for the transmission identity
//  2. Cryptographic information for the reception identity
//  3. User's UD facts (username, email, phone number)
//  4. Contact list
func (b *Backup) assembleBackup() backup.Backup {
	bu := backup.Backup{
		TransmissionIdentity:      backup.TransmissionIdentity{},
		ReceptionIdentity:         backup.ReceptionIdentity{},
		UserDiscoveryRegistration: backup.UserDiscoveryRegistration{},
		Contacts:                  backup.Contacts{},
	}

	// Get registration timestamp
	bu.RegistrationTimestamp = b.session.GetRegistrationTimestamp().UnixNano()

	// Get registration code; ignore the error because if there is no
	// registration, then an empty string is returned
	bu.RegistrationCode, _ = b.session.GetRegCode()

	// Get transmission identity
	bu.TransmissionIdentity = backup.TransmissionIdentity{
		RSASigningPrivateKey: b.session.GetTransmissionRSA(),
		RegistrarSignature:   b.session.GetTransmissionRegistrationValidationSignature(),
		Salt:                 b.session.GetTransmissionSalt(),
		ComputedID:           b.session.GetTransmissionID(),
	}

	// Get reception identity
	bu.ReceptionIdentity = backup.ReceptionIdentity{
		RSASigningPrivateKey: b.session.GetReceptionRSA(),
		RegistrarSignature:   b.session.GetReceptionRegistrationValidationSignature(),
		Salt:                 b.session.GetReceptionSalt(),
		ComputedID:           b.session.GetReceptionID(),
		DHPrivateKey:         b.e2e.GetHistoricalDHPrivkey(),
		DHPublicKey:          b.e2e.GetHistoricalDHPubkey(),
	}

	// Get facts
	if b.ud != nil {
		bu.UserDiscoveryRegistration.FactList = b.ud.GetFacts()
	} else {
		bu.UserDiscoveryRegistration.FactList = fact.FactList{}
	}

	// Get contacts
	bu.Contacts.Identities = b.e2e.GetAllPartnerIDs()

	// Add the memoized json params
	bu.JSONParams = b.jsonParams

	return bu
}