package walletunlocker

import (
	"fmt"

	"github.com/lightningnetwork/lnd/lnrpc"
	"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
	"github.com/lightningnetwork/lnd/macaroons"
	"github.com/roasbeef/btcd/chaincfg"
	"github.com/roasbeef/btcwallet/wallet"
	"golang.org/x/net/context"
)

// UnlockerService implements the WalletUnlocker service used to provide lnd
// with a password for wallet encryption at startup.
type UnlockerService struct {
	// CreatePasswords is a channel where passwords provided by the rpc
	// client to be used to initially create and encrypt a wallet will be
	// sent.
	CreatePasswords chan []byte

	// UnlockPasswords is a channel where passwords provided by the rpc
	// client to be used to unlock and decrypt an existing wallet will be
	// sent.
	UnlockPasswords chan []byte

	chainDir  string
	netParams *chaincfg.Params
	authSvc   *macaroons.Service
}

// New creates and returns a new UnlockerService.
func New(authSvc *macaroons.Service, chainDir string,
	params *chaincfg.Params) *UnlockerService {
	return &UnlockerService{
		CreatePasswords: make(chan []byte, 1),
		UnlockPasswords: make(chan []byte, 1),
		chainDir:        chainDir,
		netParams:       params,
	}
}

// CreateWallet will read the password provided in the CreateWalletRequest and
// send it over the CreatePasswords channel in case no wallet already exist in
// the chain's wallet database directory.
func (u *UnlockerService) CreateWallet(ctx context.Context,
	in *lnrpc.CreateWalletRequest) (*lnrpc.CreateWalletResponse, error) {

	// Require the provided password to have a length of at
	// least 8 characters.
	password := in.Password
	if len(password) < 8 {
		return nil, fmt.Errorf("password must have " +
			"at least 8 characters")
	}

	netDir := btcwallet.NetworkDir(u.chainDir, u.netParams)
	loader := wallet.NewLoader(u.netParams, netDir)

	// Check if wallet already exists.
	walletExists, err := loader.WalletExists()
	if err != nil {
		return nil, err
	}

	if walletExists {
		// Cannot create wallet if it already exists!
		return nil, fmt.Errorf("wallet already exists")
	}

	// Attempt to create a password for the macaroon service.
	if u.authSvc != nil {
		err = u.authSvc.CreateUnlock(&password)
		if err != nil {
			return nil, fmt.Errorf("unable to create/unlock "+
				"macaroon store: %v", err)
		}
	}

	// We send the password over the CreatePasswords channel, such that it
	// can be used by lnd to open or create the wallet.
	u.CreatePasswords <- password

	return &lnrpc.CreateWalletResponse{}, nil
}

// UnlockWallet sends the password provided by the incoming UnlockWalletRequest
// over the UnlockPasswords channel in case it successfully decrypts an
// existing wallet found in the chain's wallet database directory.
func (u *UnlockerService) UnlockWallet(ctx context.Context,
	in *lnrpc.UnlockWalletRequest) (*lnrpc.UnlockWalletResponse, error) {

	netDir := btcwallet.NetworkDir(u.chainDir, u.netParams)
	loader := wallet.NewLoader(u.netParams, netDir)

	// Check if wallet already exists.
	walletExists, err := loader.WalletExists()
	if err != nil {
		return nil, err
	}

	if !walletExists {
		// Cannot unlock a wallet that does not exist!
		return nil, fmt.Errorf("wallet not found")
	}

	// Try opening the existing wallet with the provided password.
	_, err = loader.OpenExistingWallet(in.Password, false)
	if err != nil {
		// Could not open wallet, most likely this means that
		// provided password was incorrect.
		return nil, err
	}

	// We successfully opened the wallet, but we'll need to unload
	// it to make sure lnd can open it later.
	if err := loader.UnloadWallet(); err != nil {
		// TODO: not return error here?
		return nil, err
	}

	// Attempt to create a password for the macaroon service.
	if u.authSvc != nil {
		err = u.authSvc.CreateUnlock(&in.Password)
		if err != nil {
			return nil, fmt.Errorf("unable to create/unlock "+
				"macaroon store: %v", err)
		}
	}

	// At this point we was able to open the existing wallet with the
	// provided password. We send the password over the UnlockPasswords
	// channel, such that it can be used by lnd to open the wallet.
	u.UnlockPasswords <- in.Password

	return &lnrpc.UnlockWalletResponse{}, nil
}
