package main

import (
	"context"
	"errors"
	"fmt"
	"path"

	"github.com/algorand/go-algorand-sdk/crypto"
	"github.com/algorand/go-algorand-sdk/types"
	"github.com/hashicorp/vault/sdk/framework"
	"github.com/hashicorp/vault/sdk/logical"
)

// strategy for creating multi-sign algorand addresses.  The user
// addresses and operator addresses, and one vault-created address are
// combined to make the total signer list. The threshold must equal
// the number of user keys, plus one (the vault key).  The number of
// operator keys must meet or exceed the threshold.  These rules imply
// the user needs one signature provided by vault to complete a
// transaction, while operator can complete transactions without
// vault, should anything go seriously wrong with vault configuration.

type multisig struct {
	Address string

	// TODO(dnc): are the details from Algorand SDK struct needed here?
	Multisig crypto.MultisigAccount

	// User configured signing addresses
	UserSigner []Signer
	// signing key created by this plugin
	VaultSigner []Signer // today, always one key, but use slice here in case that changes
	// signing keys configured by operator.  Redundant with config data, but here we keep ordered list.
	OperatorSigner []Signer
}

// Read address configuration from Vault storage.  If not yet
// configured, nil is returned.
func (b *backend) address(ctx context.Context, store logical.Storage, name string) (*multisig, error) {
	entry, err := store.Get(ctx, "address/"+name)
	if err != nil {
		return nil, err
	}
	if entry == nil { // path has not been updated
		return nil, nil
	}

	addr := &multisig{}
	err = entry.DecodeJSON(&addr)
	return addr, err
}

func (b *backend) pathAddress() *framework.Path {
	return &framework.Path{
		Pattern: "address/" + framework.GenericNameRegex("name"),
		Fields: map[string]*framework.FieldSchema{
			"name": &framework.FieldSchema{
				Type:        framework.TypeString,
				Description: "Name of the signer",
			},
			"signer": &framework.FieldSchema{
				Type:        framework.TypeStringSlice,
				Description: "User public key(s), in Algorand address encoding",
			},
		},
		Callbacks: map[logical.Operation]framework.OperationFunc{
			logical.UpdateOperation: b.pathAddressUpdate,
			logical.ReadOperation:   b.pathAddressRead,
			// TODO(dnc): delete and list
		},
		HelpSynopsis: "Create an Algorang wallet, with specified user signing key(s)",

		HelpDescription: `

In multi-sign configuration, produces an Algorand address requiring
signatures from each of the specified user keys, and also a key known
only to Vault.

The address will also allow signing by operator keys, which serve as a
fallback in the unlikely event that the user or vault keys are lost.

 `}
}

func (b *backend) pathAddressRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
	name := d.Get("name").(string)
	addr, err := b.address(ctx, req.Storage, name)
	if err != nil {
		return nil, err
	}
	if addr == nil {
		return nil, fmt.Errorf("address (%q) not found", name)
	}

	res := &logical.Response{Data: map[string]interface{}{
		"name":    name,
		"signer":  addr.UserSigner,
		"address": addr.Address,
	}}

	return res, nil

}

func (b *backend) pathAddressUpdate(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
	cfg, err := b.config(ctx, req.Storage)
	if err != nil {
		return nil, err
	}

	if cfg.Threshold == 0 {
		// TODO(dnc): support single-sign, using key known only to vault
		return nil, fmt.Errorf("single-sign Algorand address is not yet supported; write %s to configure multi-sign", path.Join(req.MountPoint, "config"))
	}

	name := d.Get("name").(string)

	addr, err := b.address(ctx, req.Storage, name)
	if err != nil {
		return nil, err
	}
	if addr != nil {
		return nil, errors.New("update Algorand address is not supported; consider a new address instead")
	}

	multi := &multisig{} // create new multisig address

	signer, ok := d.GetOk("signer")
	if ok {
		multi.UserSigner = make([]Signer, 0, len(signer.([]string)))
		for _, a := range signer.([]string) {
			address, err := types.DecodeAddress(a)
			if err != nil {
				return nil, fmt.Errorf("bad Algorand address (%q): %w", a, err)
			}
			multi.UserSigner = append(multi.UserSigner, Signer(address))
		}
	}

	// Threshold should be exactly the number of user signers plus one Vault signer.
	if len(multi.UserSigner) != int(cfg.Threshold)-1 {
		return nil, fmt.Errorf("multi-sign threshold (%d) requires %d user signing addresses (got %d)", cfg.Threshold, cfg.Threshold-1, len(multi.UserSigner))
	}

	// create a key for this plugin to sign for this address
	keypair := crypto.GenerateAccount()
	entry, err := logical.StorageEntryJSON("keypair/"+name, keypair) // TODO(dnc): storing data structure from Algorand SDK, should we avoid struct that might change out from under us?
	if err != nil {
		return nil, fmt.Errorf("failed to encode keypair: %w", err)
	}
	err = req.Storage.Put(ctx, entry)
	if err != nil {
		return nil, err
	}
	multi.VaultSigner = []Signer{Signer(keypair.Address)}

	// copy from cfg.Signer in random order, place in
	// multi.OperatorSigner in a specific order.  Which address comes
	// first does not matter in terms of privilege, but it does affect
	// the address of the resulting multisign wallet (same set of
	// addresses, in different order, produces a different address), so
	// we remember the order we used in multi.OperatorSigner.
	multi.OperatorSigner = make([]Signer, 0, len(cfg.Signer))
	for _, a := range cfg.Signer {
		multi.OperatorSigner = append(multi.OperatorSigner, a)
	}

	// Gather all keys in multisign scheme
	order := make([]types.Address, 0, len(multi.OperatorSigner)+len(multi.UserSigner)+len(multi.VaultSigner))
	unique := make(map[Signer]struct{})
	var dummy struct{}
	for _, signer := range [][]Signer{multi.VaultSigner, multi.UserSigner, multi.OperatorSigner} {
		// loop to cast our Signer as types.Address for call to MultisigAccountWithParams
		for _, s := range signer {
			order = append(order, types.Address(s))
			_, ok := unique[s]
			if ok {
				return nil, fmt.Errorf("address (%s) appears twice in signer list", s)
			}
			unique[s] = dummy
		}
	}

	// Build an Algorand multisig address
	multi.Multisig, err = crypto.MultisigAccountWithParams(1 /* version */, cfg.Threshold, order)
	if err != nil {
		return nil, fmt.Errorf("failed to build Algorand multisig: %w", err)
	}

	multiAddress, err := multi.Multisig.Address()
	if err != nil {
		return nil, err
	}

	multi.Address = multiAddress.String()

	// Store address
	entry, err = logical.StorageEntryJSON("address/"+name, multi)
	if err != nil {
		return nil, fmt.Errorf("failed to encode multisig address: %w", err)
	}
	err = req.Storage.Put(ctx, entry)
	if err != nil {
		return nil, err
	}

	res := &logical.Response{Data: map[string]interface{}{
		"name":    name,
		"signer":  multi.UserSigner,
		"address": multi.Address,
	}}

	return res, nil
}
