package ledgerwallet

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"

	"github.com/ipfs/go-cid"
	"github.com/ipfs/go-datastore"
	"github.com/ipfs/go-datastore/query"
	logging "github.com/ipfs/go-log/v2"
	ledgerfil "github.com/whyrusleeping/ledger-filecoin-go"
	"golang.org/x/xerrors"

	"github.com/filecoin-project/go-address"
	"github.com/filecoin-project/go-state-types/crypto"

	"github.com/filecoin-project/lotus/api"
	"github.com/filecoin-project/lotus/chain/types"
	"github.com/filecoin-project/lotus/node/modules/dtypes"
)

var log = logging.Logger("wallet-ledger")

type LedgerWallet struct {
	ds datastore.Datastore
}

func NewWallet(ds dtypes.MetadataDS) *LedgerWallet {
	return &LedgerWallet{ds}
}

type LedgerKeyInfo struct {
	Address address.Address
	Path    []uint32
}

var _ api.Wallet = (*LedgerWallet)(nil)

func (lw LedgerWallet) WalletSign(ctx context.Context, signer address.Address, toSign []byte, meta api.MsgMeta) (*crypto.Signature, error) {
	ki, err := lw.getKeyInfo(signer)
	if err != nil {
		return nil, err
	}

	fl, err := ledgerfil.FindLedgerFilecoinApp()
	if err != nil {
		return nil, err
	}
	defer fl.Close() // nolint:errcheck
	if meta.Type != api.MTChainMsg {
		return nil, fmt.Errorf("ledger can only sign chain messages")
	}

	{
		var cmsg types.Message
		if err := cmsg.UnmarshalCBOR(bytes.NewReader(meta.Extra)); err != nil {
			return nil, xerrors.Errorf("unmarshalling message: %w", err)
		}

		_, bc, err := cid.CidFromBytes(toSign)
		if err != nil {
			return nil, xerrors.Errorf("getting cid from signing bytes: %w", err)
		}

		if !cmsg.Cid().Equals(bc) {
			return nil, xerrors.Errorf("cid(meta.Extra).bytes() != toSign")
		}
	}

	sig, err := fl.SignSECP256K1(ki.Path, meta.Extra)
	if err != nil {
		return nil, err
	}

	return &crypto.Signature{
		Type: crypto.SigTypeSecp256k1,
		Data: sig.SignatureBytes(),
	}, nil
}

func (lw LedgerWallet) getKeyInfo(addr address.Address) (*LedgerKeyInfo, error) {
	kib, err := lw.ds.Get(keyForAddr(addr))
	if err != nil {
		return nil, err
	}

	var out LedgerKeyInfo
	if err := json.Unmarshal(kib, &out); err != nil {
		return nil, xerrors.Errorf("unmarshalling ledger key info: %w", err)
	}

	return &out, nil
}

func (lw LedgerWallet) WalletDelete(ctx context.Context, k address.Address) error {
	return lw.ds.Delete(keyForAddr(k))
}

func (lw LedgerWallet) WalletExport(ctx context.Context, k address.Address) (*types.KeyInfo, error) {
	return nil, fmt.Errorf("cannot export keys from ledger wallets")
}

func (lw LedgerWallet) WalletHas(ctx context.Context, k address.Address) (bool, error) {
	_, err := lw.ds.Get(keyForAddr(k))
	if err == nil {
		return true, nil
	}
	if err == datastore.ErrNotFound {
		return false, nil
	}
	return false, err
}

func (lw LedgerWallet) WalletImport(ctx context.Context, kinfo *types.KeyInfo) (address.Address, error) {
	var ki LedgerKeyInfo
	if err := json.Unmarshal(kinfo.PrivateKey, &ki); err != nil {
		return address.Undef, err
	}
	return lw.importKey(ki)
}

func (lw LedgerWallet) importKey(ki LedgerKeyInfo) (address.Address, error) {
	if ki.Address == address.Undef {
		return address.Undef, fmt.Errorf("no address given in imported key info")
	}
	if len(ki.Path) != filHdPathLen {
		return address.Undef, fmt.Errorf("bad hd path len: %d, expected: %d", len(ki.Path), filHdPathLen)
	}
	bb, err := json.Marshal(ki)
	if err != nil {
		return address.Undef, xerrors.Errorf("marshaling key info: %w", err)
	}

	if err := lw.ds.Put(keyForAddr(ki.Address), bb); err != nil {
		return address.Undef, err
	}

	return ki.Address, nil
}

func (lw LedgerWallet) WalletList(ctx context.Context) ([]address.Address, error) {
	res, err := lw.ds.Query(query.Query{Prefix: dsLedgerPrefix})
	if err != nil {
		return nil, err
	}
	defer res.Close() // nolint:errcheck

	var out []address.Address
	for {
		res, ok := res.NextSync()
		if !ok {
			break
		}

		var ki LedgerKeyInfo
		if err := json.Unmarshal(res.Value, &ki); err != nil {
			return nil, err
		}

		out = append(out, ki.Address)
	}
	return out, nil
}

const hdHard = 0x80000000

var filHDBasePath = []uint32{hdHard | 44, hdHard | 461, hdHard, 0}
var filHdPathLen = 5

func (lw LedgerWallet) WalletNew(ctx context.Context, t types.KeyType) (address.Address, error) {
	if t != types.KTSecp256k1Ledger {
		return address.Undef, fmt.Errorf("unsupported key type: '%s', only '%s' supported",
			t, types.KTSecp256k1Ledger)
	}

	res, err := lw.ds.Query(query.Query{Prefix: dsLedgerPrefix})
	if err != nil {
		return address.Undef, err
	}
	defer res.Close() // nolint:errcheck

	var maxi int64 = -1
	for {
		res, ok := res.NextSync()
		if !ok {
			break
		}

		var ki LedgerKeyInfo
		if err := json.Unmarshal(res.Value, &ki); err != nil {
			return address.Undef, err
		}
		if i := ki.Path[filHdPathLen-1]; maxi == -1 || maxi < int64(i) {
			maxi = int64(i)
		}
	}

	fl, err := ledgerfil.FindLedgerFilecoinApp()
	if err != nil {
		return address.Undef, xerrors.Errorf("finding ledger: %w", err)
	}
	defer fl.Close() // nolint:errcheck

	path := append(append([]uint32(nil), filHDBasePath...), uint32(maxi+1))
	_, _, addr, err := fl.GetAddressPubKeySECP256K1(path)
	if err != nil {
		return address.Undef, xerrors.Errorf("getting public key from ledger: %w", err)
	}

	log.Warnf("creating key: %s, accept the key in ledger device", addr)
	_, _, addr, err = fl.ShowAddressPubKeySECP256K1(path)
	if err != nil {
		return address.Undef, xerrors.Errorf("verifying public key with ledger: %w", err)
	}

	a, err := address.NewFromString(addr)
	if err != nil {
		return address.Undef, fmt.Errorf("parsing address: %w", err)
	}

	var lki LedgerKeyInfo
	lki.Address = a
	lki.Path = path

	return lw.importKey(lki)
}

func (lw *LedgerWallet) Get() api.Wallet {
	if lw == nil {
		return nil
	}

	return lw
}

var dsLedgerPrefix = "/ledgerkey/"

func keyForAddr(addr address.Address) datastore.Key {
	return datastore.NewKey(dsLedgerPrefix + addr.String())
}
