package pairing

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"

	"go.uber.org/multierr"
	"go.uber.org/zap"

	"github.com/status-im/status-go/api"
	"github.com/status-im/status-go/multiaccounts"
	"github.com/status-im/status-go/multiaccounts/accounts"
	"github.com/status-im/status-go/params"
	"github.com/status-im/status-go/signal"
)

type PayloadReceiver interface {
	PayloadLocker

	// Receive accepts data from an inbound source into the PayloadReceiver's state
	Receive(data []byte) error

	// Received returns a decrypted and parsed payload from an inbound source
	Received() []byte
}

type PayloadStorer interface {
	Store() error
}

type BasePayloadReceiver struct {
	*PayloadLockPayload
	*PayloadReceived

	encryptor    *PayloadEncryptor
	unmarshaller ProtobufUnmarshaller
	storer       PayloadStorer

	receiveCallback func()
}

func NewBasePayloadReceiver(e *PayloadEncryptor, um ProtobufUnmarshaller, s PayloadStorer, callback func()) *BasePayloadReceiver {
	return &BasePayloadReceiver{
		PayloadLockPayload: &PayloadLockPayload{e},
		PayloadReceived:    &PayloadReceived{e},
		encryptor:          e,
		unmarshaller:       um,
		storer:             s,
		receiveCallback:    callback,
	}
}

// Receive takes a []byte representing raw data, parses and stores the data
func (bpr *BasePayloadReceiver) Receive(data []byte) error {
	err := bpr.encryptor.decrypt(data)
	if err != nil {
		return err
	}

	err = bpr.unmarshaller.UnmarshalProtobuf(bpr.Received())
	if err != nil {
		return err
	}

	err = bpr.storer.Store()
	if err != nil {
		return err
	}

	if bpr.receiveCallback != nil {
		bpr.receiveCallback()
	}

	return nil
}

/*
|--------------------------------------------------------------------------
| AccountPayload
|--------------------------------------------------------------------------
|
| AccountPayloadReceiver, AccountPayloadStorer and AccountPayloadMarshaller
|
*/

// NewAccountPayloadReceiver generates a new and initialised AccountPayload flavoured BasePayloadReceiver
// AccountPayloadReceiver is responsible for the whole receive and store cycle of an AccountPayload
func NewAccountPayloadReceiver(e *PayloadEncryptor, p *AccountPayload, config *ReceiverConfig, logger *zap.Logger) (*BasePayloadReceiver, error) {
	l := logger.Named("AccountPayloadManager")
	l.Debug("fired", zap.Any("config", config))

	e = e.Renew()

	aps, err := NewAccountPayloadStorer(p, config)
	if err != nil {
		return nil, err
	}

	return NewBasePayloadReceiver(e, NewPairingPayloadMarshaller(p, l), aps,
		func() {
			data := AccountData{Account: p.multiaccount, Password: p.password, ChatKey: p.chatKey}
			signal.SendLocalPairingEvent(Event{Type: EventReceivedAccount, Action: ActionPairingAccount, Data: data})
		},
	), nil
}

// AccountPayloadStorer is responsible for parsing, validating and storing AccountPayload data
type AccountPayloadStorer struct {
	*AccountPayload
	multiaccountsDB *multiaccounts.Database

	keystorePath   string
	kdfIterations  int
	loggedInKeyUID string
}

func NewAccountPayloadStorer(p *AccountPayload, config *ReceiverConfig) (*AccountPayloadStorer, error) {
	ppr := &AccountPayloadStorer{
		AccountPayload: p,
	}

	if config == nil {
		return ppr, nil
	}

	ppr.multiaccountsDB = config.DB
	ppr.kdfIterations = config.KDFIterations
	ppr.keystorePath = config.KeystorePath
	ppr.loggedInKeyUID = config.LoggedInKeyUID
	return ppr, nil
}

func (aps *AccountPayloadStorer) Store() error {
	keyUID := aps.multiaccount.KeyUID
	if aps.loggedInKeyUID != "" && aps.loggedInKeyUID != keyUID {
		return ErrLoggedInKeyUIDConflict
	}
	if aps.loggedInKeyUID == keyUID {
		// skip storing keys if user is logged in with the same key
		return nil
	}

	err := validateKeys(aps.keys, aps.password)
	if err != nil {
		return err
	}

	if err = aps.storeKeys(aps.keystorePath); err != nil && err != ErrKeyFileAlreadyExists {
		return err
	}

	// skip storing multiaccount if key already exists
	if err == ErrKeyFileAlreadyExists {
		aps.exist = true
		aps.multiaccount, err = aps.multiaccountsDB.GetAccount(keyUID)
		if err != nil {
			return err
		}
		return nil
	}
	return aps.storeMultiAccount()
}

func (aps *AccountPayloadStorer) storeKeys(keyStorePath string) error {
	if keyStorePath == "" {
		return fmt.Errorf("keyStorePath can not be empty")
	}

	_, lastDir := filepath.Split(keyStorePath)

	// If lastDir == keystoreDir we presume we need to create the rest of the keystore path
	// else we presume the provided keystore is valid
	if lastDir == keystoreDir {
		if aps.multiaccount == nil || aps.multiaccount.KeyUID == "" {
			return fmt.Errorf("no known Key UID")
		}
		keyStorePath = filepath.Join(keyStorePath, aps.multiaccount.KeyUID)
		_, err := os.Stat(keyStorePath)
		if os.IsNotExist(err) {
			err := os.MkdirAll(keyStorePath, 0700)
			if err != nil {
				return err
			}
		} else if err != nil {
			return err
		} else {
			return ErrKeyFileAlreadyExists
		}
	}

	for name, data := range aps.keys {
		err := ioutil.WriteFile(filepath.Join(keyStorePath, name), data, 0600)
		if err != nil {
			writeErr := fmt.Errorf("failed to write key to path '%s' : %w", filepath.Join(keyStorePath, name), err)
			// If we get an error on any of the key files attempt to revert
			err := emptyDir(keyStorePath)
			if err != nil {
				// If we get an error when trying to empty the dir combine the write error and empty error
				emptyDirErr := fmt.Errorf("failed to revert and cleanup storeKeys : %w", err)
				return multierr.Combine(writeErr, emptyDirErr)
			}
			return writeErr
		}
	}
	return nil
}

func (aps *AccountPayloadStorer) storeMultiAccount() error {
	aps.multiaccount.KDFIterations = aps.kdfIterations
	return aps.multiaccountsDB.SaveAccount(*aps.multiaccount)
}

/*
|--------------------------------------------------------------------------
| RawMessagePayload
|--------------------------------------------------------------------------
|
| RawMessagePayloadReceiver and RawMessageStorer
|
*/

// NewRawMessagePayloadReceiver generates a new and initialised RawMessagesPayload flavoured BasePayloadReceiver
// RawMessagePayloadReceiver is responsible for the whole receive and store cycle of a RawMessagesPayload
func NewRawMessagePayloadReceiver(accountPayload *AccountPayload, e *PayloadEncryptor, backend *api.GethStatusBackend, config *ReceiverConfig) *BasePayloadReceiver {
	e = e.Renew()
	payload := NewRawMessagesPayload()

	return NewBasePayloadReceiver(e,
		NewRawMessagePayloadMarshaller(payload),
		NewRawMessageStorer(backend, payload, accountPayload, config), nil)
}

type RawMessageStorer struct {
	payload               *RawMessagesPayload
	syncRawMessageHandler *SyncRawMessageHandler
	accountPayload        *AccountPayload
	nodeConfig            *params.NodeConfig
	settingCurrentNetwork string
	deviceType            string
	deviceName            string
}

func NewRawMessageStorer(backend *api.GethStatusBackend, payload *RawMessagesPayload, accountPayload *AccountPayload, config *ReceiverConfig) *RawMessageStorer {
	return &RawMessageStorer{
		syncRawMessageHandler: NewSyncRawMessageHandler(backend),
		payload:               payload,
		accountPayload:        accountPayload,
		nodeConfig:            config.NodeConfig,
		settingCurrentNetwork: config.SettingCurrentNetwork,
		deviceType:            config.DeviceType,
		deviceName:            config.DeviceName,
	}
}

func (r *RawMessageStorer) Store() error {
	if r.accountPayload == nil || r.accountPayload.multiaccount == nil {
		return fmt.Errorf("no known multiaccount when storing raw messages")
	}
	return r.syncRawMessageHandler.HandleRawMessage(r.accountPayload, r.nodeConfig, r.settingCurrentNetwork, r.deviceType, r.deviceName, r.payload)
}

/*
|--------------------------------------------------------------------------
| InstallationPayload
|--------------------------------------------------------------------------
|
| InstallationPayloadReceiver and InstallationPayloadStorer
|
*/

// NewInstallationPayloadReceiver generates a new and initialised InstallationPayload flavoured BasePayloadReceiver
// InstallationPayloadReceiver is responsible for the whole receive and store cycle of a RawMessagesPayload specifically
// for sending / requesting installation data from the Receiver device.
func NewInstallationPayloadReceiver(e *PayloadEncryptor, backend *api.GethStatusBackend, deviceType string) *BasePayloadReceiver {
	e = e.Renew()
	payload := NewRawMessagesPayload()

	return NewBasePayloadReceiver(e,
		NewRawMessagePayloadMarshaller(payload),
		NewInstallationPayloadStorer(backend, payload, deviceType), nil)
}

type InstallationPayloadStorer struct {
	payload               *RawMessagesPayload
	syncRawMessageHandler *SyncRawMessageHandler
	deviceType            string
	backend               *api.GethStatusBackend
}

func NewInstallationPayloadStorer(backend *api.GethStatusBackend, payload *RawMessagesPayload, deviceType string) *InstallationPayloadStorer {
	return &InstallationPayloadStorer{
		payload:               payload,
		syncRawMessageHandler: NewSyncRawMessageHandler(backend),
		deviceType:            deviceType,
		backend:               backend,
	}
}

func (r *InstallationPayloadStorer) Store() error {
	messenger := r.backend.Messenger()
	if messenger == nil {
		return fmt.Errorf("messenger is nil when invoke InstallationPayloadRepository#Store()")
	}
	err := messenger.SetInstallationDeviceType(r.deviceType)
	if err != nil {
		return err
	}

	installations := GetMessengerInstallationsMap(messenger)

	err = messenger.HandleSyncRawMessages(r.payload.rawMessages)

	if err != nil {
		return err
	}

	if newInstallation := FindNewInstallations(messenger, installations); newInstallation != nil {
		signal.SendLocalPairingEvent(Event{
			Type:   EventReceivedInstallation,
			Action: ActionPairingInstallation,
			Data:   newInstallation})
	}

	return nil
}

/*
|--------------------------------------------------------------------------
| PayloadReceivers
|--------------------------------------------------------------------------
|
| Funcs for all PayloadReceivers AccountPayloadReceiver, RawMessagePayloadReceiver and InstallationPayloadMounter
|
*/

func NewPayloadReceivers(logger *zap.Logger, pe *PayloadEncryptor, backend *api.GethStatusBackend, config *ReceiverConfig) (PayloadReceiver, PayloadReceiver, PayloadMounterReceiver, error) {
	// A new SHARED AccountPayload
	p := new(AccountPayload)

	ar, err := NewAccountPayloadReceiver(pe, p, config, logger)
	if err != nil {
		return nil, nil, nil, err
	}
	rmr := NewRawMessagePayloadReceiver(p, pe, backend, config)
	imr := NewInstallationPayloadMounterReceiver(pe, backend, config.DeviceType)
	return ar, rmr, imr, nil
}

/*
|--------------------------------------------------------------------------
| KeystoreFilesPayload
|--------------------------------------------------------------------------
*/

func NewKeystoreFilesPayloadReceiver(backend *api.GethStatusBackend, e *PayloadEncryptor, config *KeystoreFilesReceiverConfig, logger *zap.Logger) (*BasePayloadReceiver, error) {
	l := logger.Named("KeystoreFilesPayloadManager")
	l.Debug("fired", zap.Any("config", config))

	e = e.Renew()

	// A new SHARED AccountPayload
	p := new(AccountPayload)

	kfps, err := NewKeystoreFilesPayloadStorer(backend, p, config)
	if err != nil {
		return nil, err
	}

	return NewBasePayloadReceiver(e, NewPairingPayloadMarshaller(p, l), kfps,
		func() {
			data := config.KeypairsToImport
			signal.SendLocalPairingEvent(Event{Type: EventReceivedKeystoreFiles, Action: ActionKeystoreFilesTransfer, Data: data})
		},
	), nil
}

type KeystoreFilesPayloadStorer struct {
	*AccountPayload

	keystorePath                   string
	loggedInKeyUID                 string
	expectedKeypairsToImport       []string
	expectedKeystoreFilesToReceive []string
	backend                        *api.GethStatusBackend
}

func NewKeystoreFilesPayloadStorer(backend *api.GethStatusBackend, p *AccountPayload, config *KeystoreFilesReceiverConfig) (*KeystoreFilesPayloadStorer, error) {
	if config == nil {
		return nil, fmt.Errorf("empty keystore files receiver config")
	}

	kfps := &KeystoreFilesPayloadStorer{
		AccountPayload:           p,
		keystorePath:             config.KeystorePath,
		loggedInKeyUID:           config.LoggedInKeyUID,
		expectedKeypairsToImport: config.KeypairsToImport,
		backend:                  backend,
	}

	accountService := backend.StatusNode().AccountService()

	for _, keyUID := range kfps.expectedKeypairsToImport {
		kp, err := accountService.GetKeypairByKeyUID(keyUID)
		if err != nil {
			return nil, err
		}

		if kp.Type == accounts.KeypairTypeSeed {
			kfps.expectedKeystoreFilesToReceive = append(kfps.expectedKeystoreFilesToReceive, kp.DerivedFrom[2:])
		}

		for _, acc := range kp.Accounts {
			kfps.expectedKeystoreFilesToReceive = append(kfps.expectedKeystoreFilesToReceive, acc.Address.Hex()[2:])
		}
	}

	return kfps, nil
}

func (kfps *KeystoreFilesPayloadStorer) Store() error {
	err := validateReceivedKeystoreFiles(kfps.expectedKeystoreFilesToReceive, kfps.keys, kfps.password)
	if err != nil {
		return err
	}

	return kfps.storeKeys(kfps.keystorePath)
}

func (kfps *KeystoreFilesPayloadStorer) storeKeys(keyStorePath string) error {
	messenger := kfps.backend.Messenger()
	if messenger == nil {
		return fmt.Errorf("messenger is nil")
	}

	if keyStorePath == "" {
		return fmt.Errorf("keyStorePath can not be empty")
	}

	_, lastDir := filepath.Split(keyStorePath)

	// If lastDir == keystoreDir we presume we need to create the rest of the keystore path
	// else we presume the provided keystore is valid
	if lastDir == keystoreDir {
		keyStorePath = filepath.Join(keyStorePath, kfps.loggedInKeyUID)
		_, err := os.Stat(keyStorePath)
		if os.IsNotExist(err) {
			err := os.MkdirAll(keyStorePath, 0700)
			if err != nil {
				return err
			}
		} else if err != nil {
			return err
		}
	}

	for name, data := range kfps.keys {
		found := false
		for _, key := range kfps.expectedKeystoreFilesToReceive {
			if strings.Contains(name, strings.ToLower(key)) {
				found = true
			}
		}
		if !found {
			continue
		}

		err := ioutil.WriteFile(filepath.Join(keyStorePath, name), data, 0600)
		if err != nil {
			writeErr := fmt.Errorf("failed to write key to path '%s' : %w", filepath.Join(keyStorePath, name), err)
			// If we get an error on any of the key files attempt to revert
			err := emptyDir(keyStorePath)
			if err != nil {
				// If we get an error when trying to empty the dir combine the write error and empty error
				emptyDirErr := fmt.Errorf("failed to revert and cleanup storeKeys : %w", err)
				return multierr.Combine(writeErr, emptyDirErr)
			}
			return writeErr
		}
	}

	for _, keyUID := range kfps.expectedKeypairsToImport {
		err := messenger.MarkKeypairFullyOperable(keyUID)
		if err != nil {
			return err
		}
	}

	return nil
}