package cli

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"strconv"
	"strings"
	"time"

	"github.com/spf13/cobra"
	"github.com/tendermint/tendermint/crypto"

	"github.com/cosmos/cosmos-sdk/client"
	"github.com/cosmos/cosmos-sdk/client/flags"
	"github.com/cosmos/cosmos-sdk/client/tx"
	"github.com/cosmos/cosmos-sdk/codec"
	sdk "github.com/cosmos/cosmos-sdk/types"
	"github.com/cosmos/cosmos-sdk/version"
	govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
	paramsproposal "github.com/cosmos/cosmos-sdk/x/params/types/proposal"

	"github.com/kava-labs/kava/x/committee/types"
)

const PARAMS_CHANGE_PROPOSAL_EXAMPLE = `
{
  "title": "title",
  "description": "description",
  "changes": [{ "subspace": "subspace", "key": "key", "value": "value" }]
}
`

const COMMITTEE_CHANGE_PROPOSAL_EXAMPLE = `
{
  "title": "A Title",
  "description": "A proposal description.",
  "new_committee": {
    "@type": "/kava.committee.v1beta1.MemberCommittee",
    "base_committee": {
      "id": "34",
      "description": "member committee",
      "members": ["kava1ze7y9qwdddejmy7jlw4cymqqlt2wh05yhwmrv2"],
      "permissions": [],
      "vote_threshold": "1.000000000000000000",
      "proposal_duration": "86400s",
      "tally_option": "TALLY_OPTION_DEADLINE"
    }
  }
}
`

const COMMITTEE_DELETE_PROPOSAL_EXAMPLE = `
{
  "title": "A Title",
  "description": "A proposal description.",
  "committee_id": "1"
}
`

func GetTxCmd() *cobra.Command {
	txCmd := &cobra.Command{
		Use:                        types.ModuleName,
		Short:                      "committee governance transactions subcommands",
		DisableFlagParsing:         true,
		SuggestionsMinimumDistance: 2,
		RunE:                       client.ValidateCmd,
	}

	cmds := []*cobra.Command{
		getCmdVote(),
		getCmdSubmitProposal(),
	}

	for _, cmd := range cmds {
		flags.AddTxFlagsToCmd(cmd)
	}

	txCmd.AddCommand(cmds...)

	return txCmd
}

// getCmdSubmitProposal returns the command to submit a proposal to a committee
func getCmdSubmitProposal() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "submit-proposal [committee-id] [proposal-file]",
		Short: "Submit a governance proposal to a particular committee",
		Long: fmt.Sprintf(`Submit a proposal to a committee so they can vote on it.

The proposal file must be the json encoded forms of the proposal type you want to submit.
For example:
%s
`, PARAMS_CHANGE_PROPOSAL_EXAMPLE),
		Args:    cobra.ExactArgs(2),
		Example: fmt.Sprintf("%s tx %s submit-proposal 1 your-proposal.json", version.AppName, types.ModuleName),
		RunE: func(cmd *cobra.Command, args []string) error {
			clientCtx, err := client.GetClientTxContext(cmd)
			if err != nil {
				return err
			}

			// Get proposing address
			proposer := clientCtx.GetFromAddress()

			// Get committee ID
			committeeID, err := strconv.ParseUint(args[0], 10, 64)
			if err != nil {
				return fmt.Errorf("committee-id %s not a valid int", args[0])
			}

			// Get proposal content
			var pubProposal types.PubProposal
			contents, err := ioutil.ReadFile(args[1])
			if err != nil {
				return err
			}
			if err := clientCtx.Codec.UnmarshalInterfaceJSON(contents, &pubProposal); err != nil {
				return err
			}
			if err = pubProposal.ValidateBasic(); err != nil {
				return err
			}

			// Build message and run basic validation
			msg, err := types.NewMsgSubmitProposal(pubProposal, proposer, committeeID)
			if err != nil {
				return err
			}
			err = msg.ValidateBasic()
			if err != nil {
				return err
			}

			// Sign and broadcast message
			return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
		},
	}

	return cmd
}

// getCmdVote returns the command to vote on a proposal.
func getCmdVote() *cobra.Command {
	return &cobra.Command{
		Use:     "vote [proposal-id] [vote]",
		Args:    cobra.ExactArgs(2),
		Short:   "Vote for an active proposal",
		Long:    "Submit a [yes/no/abstain] vote for the proposal with id [proposal-id].",
		Example: fmt.Sprintf("%s tx %s vote 2 yes", version.AppName, types.ModuleName),
		RunE: func(cmd *cobra.Command, args []string) error {
			clientCtx, err := client.GetClientTxContext(cmd)
			if err != nil {
				return err
			}

			// Get voting address
			from := clientCtx.GetFromAddress()

			// validate that the proposal id is a uint
			proposalID, err := strconv.ParseUint(args[0], 10, 64)
			if err != nil {
				return fmt.Errorf("proposal-id %s not a valid int, please input a valid proposal-id", args[0])
			}

			rawVote := strings.ToLower(strings.TrimSpace(args[1]))
			if len(rawVote) == 0 {
				return fmt.Errorf("must specify a vote")
			}

			var vote types.VoteType
			switch rawVote {
			case "yes", "y":
				vote = types.VOTE_TYPE_YES
			case "no", "n":
				vote = types.VOTE_TYPE_NO
			case "abstain", "a":
				vote = types.VOTE_TYPE_ABSTAIN
			default:
				return fmt.Errorf("must specify a valid vote type: (yes/y, no/n, abstain/a)")
			}

			// Build vote message and run basic validation
			msg := types.NewMsgVote(from, proposalID, vote)
			err = msg.ValidateBasic()
			if err != nil {
				return err
			}

			return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
		},
	}
}

// GetGovCmdSubmitProposal returns a command to submit a proposal to the gov module. It is passed to the gov module for use on its command subtree.
func GetGovCmdSubmitProposal() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "committee [proposal-file] [deposit]",
		Short: "Submit a governance proposal to change a committee.",
		Long: fmt.Sprintf(`Submit a governance proposal to create, alter, or delete a committee.

The proposal file must be the json encoded form of the proposal type you want to submit.
For example, to create or update a committee:
%s

and to delete a committee:
%s
`, COMMITTEE_CHANGE_PROPOSAL_EXAMPLE, COMMITTEE_DELETE_PROPOSAL_EXAMPLE),
		Args: cobra.ExactArgs(2),
		RunE: func(cmd *cobra.Command, args []string) error {
			clientCtx, err := client.GetClientTxContext(cmd)
			if err != nil {
				return err
			}

			// Get proposing address
			proposer := clientCtx.GetFromAddress()

			// Get the deposit
			deposit, err := sdk.ParseCoinsNormalized(args[1])
			if err != nil {
				return err
			}

			// Get the proposal
			bz, err := ioutil.ReadFile(args[0])
			if err != nil {
				return err
			}
			var content govtypes.Content
			if err := clientCtx.Codec.UnmarshalInterfaceJSON(bz, &content); err != nil {
				return err
			}
			if err = content.ValidateBasic(); err != nil {
				return err
			}

			// Build message and run basic validation
			msg, err := govtypes.NewMsgSubmitProposal(content, deposit, proposer)
			if err != nil {
				return err
			}
			err = msg.ValidateBasic()
			if err != nil {
				return err
			}

			// Sign and broadcast message
			return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
		},
	}
	return cmd
}

// MustGetExampleCommitteeChangeProposal is a helper function to return an example json proposal
func MustGetExampleCommitteeChangeProposal(cdc codec.Codec) string {
	exampleChangeProposal, err := types.NewCommitteeChangeProposal(
		"A Title",
		"A description of this proposal.",
		types.MustNewMemberCommittee(
			1,
			"The description of this committee.",
			[]sdk.AccAddress{sdk.AccAddress(crypto.AddressHash([]byte("exampleAddress")))},
			[]types.Permission{
				&types.GodPermission{},
			},
			sdk.MustNewDecFromStr("0.8"),
			time.Hour*24*7,
			types.TALLY_OPTION_FIRST_PAST_THE_POST,
		),
	)
	if err != nil {
		panic(err)
	}
	exampleChangeProposalBz, err := cdc.MarshalJSON(&exampleChangeProposal)
	if err != nil {
		panic(err)
	}
	var out bytes.Buffer
	if err = json.Indent(&out, exampleChangeProposalBz, "", "  "); err != nil {
		panic(err)
	}
	return out.String()
}

// MustGetExampleCommitteeDeleteProposal is a helper function to return an example json proposal
func MustGetExampleCommitteeDeleteProposal(cdc codec.Codec) string {
	exampleDeleteProposal := types.NewCommitteeDeleteProposal(
		"A Title",
		"A description of this proposal.",
		1,
	)
	bz, err := cdc.MarshalJSON(&exampleDeleteProposal)
	if err != nil {
		panic(err)
	}
	var out bytes.Buffer
	if err = json.Indent(&out, bz, "", "  "); err != nil {
		panic(err)
	}
	return out.String()
}

// MustGetExampleParameterChangeProposal is a helper function to return an example json proposal
func MustGetExampleParameterChangeProposal(cdc codec.Codec) string {
	value := fmt.Sprintf("\"%d\"", 1000000000)
	exampleParameterChangeProposal := paramsproposal.NewParameterChangeProposal(
		"A Title",
		"A description of this proposal.",
		[]paramsproposal.ParamChange{paramsproposal.NewParamChange("cdp", "SurplusAuctionThreshold", value)},
	)
	bz, err := cdc.MarshalJSON(exampleParameterChangeProposal)
	if err != nil {
		panic(err)
	}
	var out bytes.Buffer
	if err = json.Indent(&out, bz, "", "  "); err != nil {
		panic(err)
	}
	return out.String()
}
