package main

import (
	"bytes"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"

	"github.com/fatih/color"

	"github.com/ipfs/go-cid"
	"github.com/urfave/cli/v2"
	"golang.org/x/xerrors"

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

	"github.com/filecoin-project/lotus/chain/stmgr"
	"github.com/filecoin-project/lotus/chain/types"
	lcli "github.com/filecoin-project/lotus/cli"
	"github.com/filecoin-project/specs-actors/v2/actors/builtin/multisig"
)

var msgCmd = &cli.Command{
	Name:      "msg",
	Usage:     "Translate message between various formats",
	ArgsUsage: "Message in any form",
	Action: func(cctx *cli.Context) error {
		if cctx.Args().Len() != 1 {
			return xerrors.Errorf("expected 1 argument")
		}

		msg, err := messageFromString(cctx, cctx.Args().First())
		if err != nil {
			return err
		}

		switch msg := msg.(type) {
		case *types.SignedMessage:
			return printSignedMessage(cctx, msg)
		case *types.Message:
			return printMessage(cctx, msg)
		default:
			return xerrors.Errorf("this error message can't be printed")
		}
	},
}

func printSignedMessage(cctx *cli.Context, smsg *types.SignedMessage) error {
	color.Green("Signed:")
	color.Blue("CID: %s\n", smsg.Cid())

	b, err := smsg.Serialize()
	if err != nil {
		return err
	}
	color.Magenta("HEX: %x\n", b)
	color.Blue("B64: %s\n", base64.StdEncoding.EncodeToString(b))
	jm, err := json.MarshalIndent(smsg, "", "  ")
	if err != nil {
		return xerrors.Errorf("marshaling as json: %w", err)
	}

	color.Magenta("JSON: %s\n", string(jm))
	fmt.Println()
	fmt.Println("---")
	color.Green("Signed Message Details:")
	fmt.Printf("Signature(hex): %x\n", smsg.Signature.Data)
	fmt.Printf("Signature(b64): %s\n", base64.StdEncoding.EncodeToString(smsg.Signature.Data))

	sigtype, err := smsg.Signature.Type.Name()
	if err != nil {
		sigtype = err.Error()
	}
	fmt.Printf("Signature type: %d (%s)\n", smsg.Signature.Type, sigtype)

	fmt.Println("-------")
	return printMessage(cctx, &smsg.Message)
}

func printMessage(cctx *cli.Context, msg *types.Message) error {
	if msg.Version != 0x6d736967 {
		color.Green("Unsigned:")
		color.Yellow("CID: %s\n", msg.Cid())

		b, err := msg.Serialize()
		if err != nil {
			return err
		}
		color.Cyan("HEX: %x\n", b)
		color.Yellow("B64: %s\n", base64.StdEncoding.EncodeToString(b))

		jm, err := json.MarshalIndent(msg, "", "  ")
		if err != nil {
			return xerrors.Errorf("marshaling as json: %w", err)
		}

		color.Cyan("JSON: %s\n", string(jm))
		fmt.Println()
	} else {
		color.Green("Msig Propose:")
		pp := &multisig.ProposeParams{
			To:     msg.To,
			Value:  msg.Value,
			Method: msg.Method,
			Params: msg.Params,
		}
		var b bytes.Buffer
		if err := pp.MarshalCBOR(&b); err != nil {
			return err
		}

		color.Cyan("HEX: %x\n", b.Bytes())
		color.Yellow("B64: %s\n", base64.StdEncoding.EncodeToString(b.Bytes()))
		jm, err := json.MarshalIndent(pp, "", "  ")
		if err != nil {
			return xerrors.Errorf("marshaling as json: %w", err)
		}

		color.Cyan("JSON: %s\n", string(jm))
		fmt.Println()
	}

	fmt.Println("---")
	color.Green("Message Details:")
	fmt.Println("Value:", types.FIL(msg.Value))
	fmt.Println("Max Fees:", types.FIL(msg.RequiredFunds()))
	fmt.Println("Max Total Cost:", types.FIL(big.Add(msg.RequiredFunds(), msg.Value)))

	api, closer, err := lcli.GetFullNodeAPI(cctx)
	if err != nil {
		return err
	}

	defer closer()
	ctx := lcli.ReqContext(cctx)

	toact, err := api.StateGetActor(ctx, msg.To, types.EmptyTSK)
	if err != nil {
		return nil
	}

	fmt.Println("Method:", stmgr.MethodsMap[toact.Code][msg.Method].Name)
	p, err := lcli.JsonParams(toact.Code, msg.Method, msg.Params)
	if err != nil {
		return err
	}

	fmt.Println("Params:", p)

	return nil
}

func messageFromString(cctx *cli.Context, smsg string) (types.ChainMsg, error) {
	// a CID is least likely to just decode
	if c, err := cid.Parse(smsg); err == nil {
		return messageFromCID(cctx, c)
	}

	// try baseX serializations next
	{
		// hex first, some hay strings may be decodable as b64
		if b, err := hex.DecodeString(smsg); err == nil {
			return messageFromBytes(cctx, b)
		}

		// b64 next
		if b, err := base64.StdEncoding.DecodeString(smsg); err == nil {
			return messageFromBytes(cctx, b)
		}

		// b64u??
		if b, err := base64.URLEncoding.DecodeString(smsg); err == nil {
			return messageFromBytes(cctx, b)
		}
	}

	// maybe it's json?
	if _, err := messageFromJson(cctx, []byte(smsg)); err == nil {
		return nil, err
	}

	// declare defeat
	return nil, xerrors.Errorf("couldn't decode the message")
}

func messageFromJson(cctx *cli.Context, msgb []byte) (types.ChainMsg, error) {
	// Unsigned
	{
		var msg types.Message
		if err := json.Unmarshal(msgb, &msg); err == nil {
			if msg.To != address.Undef {
				return &msg, nil
			}
		}
	}

	// Signed
	{
		var msg types.SignedMessage
		if err := json.Unmarshal(msgb, &msg); err == nil {
			if msg.Message.To != address.Undef {
				return &msg, nil
			}
		}
	}

	return nil, xerrors.New("probably not a json-serialized message")
}

func messageFromBytes(cctx *cli.Context, msgb []byte) (types.ChainMsg, error) {
	// Signed
	{
		var msg types.SignedMessage
		if err := msg.UnmarshalCBOR(bytes.NewReader(msgb)); err == nil {
			return &msg, nil
		}
	}

	// Unsigned
	{
		var msg types.Message
		if err := msg.UnmarshalCBOR(bytes.NewReader(msgb)); err == nil {
			return &msg, nil
		}
	}

	// Multisig propose?
	{
		var pp multisig.ProposeParams
		if err := pp.UnmarshalCBOR(bytes.NewReader(msgb)); err == nil {
			i, err := address.NewIDAddress(0)
			if err != nil {
				return nil, err
			}

			return &types.Message{
				// Hack(-ish)
				Version: 0x6d736967,
				From:    i,

				To:    pp.To,
				Value: pp.Value,

				Method: pp.Method,
				Params: pp.Params,

				GasFeeCap:  big.Zero(),
				GasPremium: big.Zero(),
			}, nil
		}
	}

	// Encoded json???
	{
		if msg, err := messageFromJson(cctx, msgb); err == nil {
			return msg, nil
		}
	}

	return nil, xerrors.New("probably not a cbor-serialized message")
}

func messageFromCID(cctx *cli.Context, c cid.Cid) (types.ChainMsg, error) {
	api, closer, err := lcli.GetFullNodeAPI(cctx)
	if err != nil {
		return nil, err
	}

	defer closer()
	ctx := lcli.ReqContext(cctx)

	msgb, err := api.ChainReadObj(ctx, c)
	if err != nil {
		return nil, err
	}

	return messageFromBytes(cctx, msgb)
}