package keeper

import (
	"bytes"
	"errors"
	"fmt"
	"time"

	sdkerrors "cosmossdk.io/errors"
	storetypes "cosmossdk.io/store/types"

	"github.com/cosmos/cosmos-sdk/client"
	sdk "github.com/cosmos/cosmos-sdk/types"

	"github.com/atomone-hub/atomone/x/gov/types"
	v1 "github.com/atomone-hub/atomone/x/gov/types/v1"
)

// SubmitProposal creates a new proposal given an array of messages
func (keeper Keeper) SubmitProposal(ctx sdk.Context, messages []sdk.Msg, metadata, title, summary string, proposer sdk.AccAddress) (v1.Proposal, error) {
	err := keeper.assertMetadataLength(metadata)
	if err != nil {
		return v1.Proposal{}, err
	}

	// assert summary is no longer than predefined max length of metadata
	err = keeper.assertMetadataLength(summary)
	if err != nil {
		return v1.Proposal{}, err
	}

	// assert title is no longer than predefined max length of metadata
	err = keeper.assertMetadataLength(title)
	if err != nil {
		return v1.Proposal{}, err
	}

	// Will hold a comma-separated string of all Msg type URLs.
	msgsStr := ""

	// Loop through all messages and confirm that each has a handler and the gov module account
	// as the only signer
	for _, msg := range messages {
		msgsStr += fmt.Sprintf(",%s", sdk.MsgTypeURL(msg))

		// perform a basic validation of the message
		if m, ok := msg.(sdk.HasValidateBasic); ok {
			if err := m.ValidateBasic(); err != nil {
				return v1.Proposal{}, sdkerrors.Wrap(types.ErrInvalidProposalMsg, err.Error())
			}
		}

		signers, _, err := keeper.cdc.GetMsgV1Signers(msg)
		if err != nil {
			return v1.Proposal{}, err
		}
		if len(signers) != 1 {
			return v1.Proposal{}, types.ErrInvalidSigner
		}

		// assert that the governance module account is the only signer of the messages
		if !bytes.Equal(signers[0], keeper.GetGovernanceAccount(ctx).GetAddress()) {
			return v1.Proposal{}, sdkerrors.Wrap(types.ErrInvalidSigner, sdk.AccAddress(signers[0]).String())
		}

		// use the msg service router to see that there is a valid route for that message.
		handler := keeper.router.Handler(msg)
		if handler == nil {
			return v1.Proposal{}, sdkerrors.Wrap(types.ErrUnroutableProposalMsg, sdk.MsgTypeURL(msg))
		}

		// Only if it's a MsgExecLegacyContent do we try to execute the
		// proposal in a cached context.
		// For other Msgs, we do not verify the proposal messages any further.
		// They may fail upon execution.
		// ref: https://github.com/cosmos/cosmos-sdk/pull/10868#discussion_r784872842
		if msg, ok := msg.(*v1.MsgExecLegacyContent); ok {
			cacheCtx, _ := ctx.CacheContext()
			if _, err := handler(cacheCtx, msg); err != nil {
				if errors.Is(err, types.ErrNoProposalHandlerExists) {
					return v1.Proposal{}, err
				}
				return v1.Proposal{}, sdkerrors.Wrap(types.ErrInvalidProposalContent, err.Error())
			}
		}

	}

	proposalID, err := keeper.GetProposalID(ctx)
	if err != nil {
		return v1.Proposal{}, err
	}

	submitTime := ctx.BlockHeader().Time
	depositPeriod := keeper.GetParams(ctx).MaxDepositPeriod

	proposal, err := v1.NewProposal(messages, proposalID, submitTime, submitTime.Add(*depositPeriod), metadata, title, summary, proposer)
	if err != nil {
		return v1.Proposal{}, err
	}

	keeper.SetProposal(ctx, proposal)
	keeper.InsertInactiveProposalQueue(ctx, proposalID, *proposal.DepositEndTime)
	keeper.SetProposalID(ctx, proposalID+1)

	keeper.IncrementInactiveProposalsNumber(ctx)

	// called right after a proposal is submitted
	keeper.Hooks().AfterProposalSubmission(ctx, proposalID)

	ctx.EventManager().EmitEvent(
		sdk.NewEvent(
			types.EventTypeSubmitProposal,
			sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposalID)),
			sdk.NewAttribute(types.AttributeKeyProposalMessages, msgsStr),
		),
	)

	return proposal, nil
}

// GetProposal gets a proposal from store by ProposalID.
// Panics if can't unmarshal the proposal.
func (keeper Keeper) GetProposal(ctx sdk.Context, proposalID uint64) (v1.Proposal, bool) {
	store := ctx.KVStore(keeper.storeKey)

	bz := store.Get(types.ProposalKey(proposalID))
	if bz == nil {
		return v1.Proposal{}, false
	}

	var proposal v1.Proposal
	if err := keeper.UnmarshalProposal(bz, &proposal); err != nil {
		panic(err)
	}

	return proposal, true
}

// SetProposal sets a proposal to store.
// Panics if can't marshal the proposal.
func (keeper Keeper) SetProposal(ctx sdk.Context, proposal v1.Proposal) {
	bz, err := keeper.MarshalProposal(proposal)
	if err != nil {
		panic(err)
	}

	store := ctx.KVStore(keeper.storeKey)

	if proposal.Status == v1.StatusVotingPeriod {
		store.Set(types.VotingPeriodProposalKey(proposal.Id), []byte{1})
	} else {
		store.Delete(types.VotingPeriodProposalKey(proposal.Id))
	}

	store.Set(types.ProposalKey(proposal.Id), bz)
}

// DeleteProposal deletes a proposal from store.
// Panics if the proposal doesn't exist.
func (keeper Keeper) DeleteProposal(ctx sdk.Context, proposalID uint64) {
	store := ctx.KVStore(keeper.storeKey)
	proposal, ok := keeper.GetProposal(ctx, proposalID)
	if !ok {
		panic(fmt.Sprintf("couldn't find proposal with id#%d", proposalID))
	}

	if proposal.DepositEndTime != nil {
		keeper.RemoveFromInactiveProposalQueue(ctx, proposalID, *proposal.DepositEndTime)
	}
	if proposal.VotingEndTime != nil {
		keeper.RemoveFromActiveProposalQueue(ctx, proposalID, *proposal.VotingEndTime)
		store.Delete(types.VotingPeriodProposalKey(proposalID))
		// Delete from QuorumCheckQueue: as we do not know with certainty the value
		// of the first part of the key (the time part), we need to iterate over it,
		// starting by proposal.VotingStartTime, because we know for sure that the
		// time part is greater than that.
		keeper.IterateQuorumCheckQueue(ctx, *proposal.VotingStartTime,
			func(p v1.Proposal, t time.Time, _ v1.QuorumCheckQueueEntry) bool {
				if p.Id == proposalID {
					// found the proposal, delete from queue and stop
					keeper.RemoveFromQuorumCheckQueue(ctx, p.Id, t)
					return true
				}
				return false
			})
	}

	store.Delete(types.ProposalKey(proposalID))
}

// IterateProposals iterates over all the proposals and performs a callback function.
// Panics when the iterator encounters a proposal which can't be unmarshaled.
func (keeper Keeper) IterateProposals(ctx sdk.Context, cb func(proposal v1.Proposal) (stop bool)) {
	store := ctx.KVStore(keeper.storeKey)

	iterator := storetypes.KVStorePrefixIterator(store, types.ProposalsKeyPrefix)
	defer iterator.Close()

	for ; iterator.Valid(); iterator.Next() {
		var proposal v1.Proposal
		err := keeper.UnmarshalProposal(iterator.Value(), &proposal)
		if err != nil {
			panic(err)
		}

		if cb(proposal) {
			break
		}
	}
}

// GetProposals returns all the proposals from store
func (keeper Keeper) GetProposals(ctx sdk.Context) (proposals v1.Proposals) {
	keeper.IterateProposals(ctx, func(proposal v1.Proposal) bool {
		proposals = append(proposals, &proposal)
		return false
	})
	return
}

// GetProposalsFiltered retrieves proposals filtered by a given set of params which
// include pagination parameters along with voter and depositor addresses and a
// proposal status. The voter address will filter proposals by whether or not
// that address has voted on proposals. The depositor address will filter proposals
// by whether or not that address has deposited to them. Finally, status will filter
// proposals by status.
//
// NOTE: If no filters are provided, all proposals will be returned in paginated
// form.
func (keeper Keeper) GetProposalsFiltered(ctx sdk.Context, params v1.QueryProposalsParams) v1.Proposals {
	proposals := keeper.GetProposals(ctx)
	filteredProposals := make([]*v1.Proposal, 0, len(proposals))

	for _, p := range proposals {
		matchVoter, matchDepositor, matchStatus := true, true, true

		// match status (if supplied/valid)
		if v1.ValidProposalStatus(params.ProposalStatus) {
			matchStatus = p.Status == params.ProposalStatus
		}

		// match voter address (if supplied)
		if len(params.Voter) > 0 {
			_, matchVoter = keeper.GetVote(ctx, p.Id, params.Voter)
		}

		// match depositor (if supplied)
		if len(params.Depositor) > 0 {
			_, matchDepositor = keeper.GetDeposit(ctx, p.Id, params.Depositor)
		}

		if matchVoter && matchDepositor && matchStatus {
			filteredProposals = append(filteredProposals, p)
		}
	}

	start, end := client.Paginate(len(filteredProposals), params.Page, params.Limit, 100)
	if start < 0 || end < 0 {
		filteredProposals = []*v1.Proposal{}
	} else {
		filteredProposals = filteredProposals[start:end]
	}

	return filteredProposals
}

// GetProposalID gets the highest proposal ID
func (keeper Keeper) GetProposalID(ctx sdk.Context) (proposalID uint64, err error) {
	store := ctx.KVStore(keeper.storeKey)
	bz := store.Get(types.ProposalIDKey)
	if bz == nil {
		return 0, sdkerrors.Wrap(types.ErrInvalidGenesis, "initial proposal ID hasn't been set")
	}

	proposalID = types.GetProposalIDFromBytes(bz)
	return proposalID, nil
}

// SetProposalID sets the new proposal ID to the store
func (keeper Keeper) SetProposalID(ctx sdk.Context, proposalID uint64) {
	store := ctx.KVStore(keeper.storeKey)
	store.Set(types.ProposalIDKey, types.GetProposalIDBytes(proposalID))
}

// ActivateVotingPeriod activates the voting period of a proposal
func (keeper Keeper) ActivateVotingPeriod(ctx sdk.Context, proposal v1.Proposal) {
	startTime := ctx.BlockHeader().Time
	proposal.VotingStartTime = &startTime
	params := keeper.GetParams(ctx)
	endTime := proposal.VotingStartTime.Add(*params.VotingPeriod)
	proposal.VotingEndTime = &endTime
	proposal.Status = v1.StatusVotingPeriod
	keeper.SetProposal(ctx, proposal)

	keeper.RemoveFromInactiveProposalQueue(ctx, proposal.Id, *proposal.DepositEndTime)
	keeper.DecrementInactiveProposalsNumber(ctx)
	keeper.InsertActiveProposalQueue(ctx, proposal.Id, *proposal.VotingEndTime)
	if params.QuorumCheckCount > 0 {
		// add proposal to quorum check queue
		quorumTimeoutTime := proposal.VotingStartTime.Add(*params.QuorumTimeout)
		keeper.InsertQuorumCheckQueue(ctx, proposal.Id, quorumTimeoutTime,
			v1.NewQuorumCheckQueueEntry(quorumTimeoutTime, params.QuorumCheckCount),
		)
	}
	keeper.IncrementActiveProposalsNumber(ctx)
}

// MarshalProposal marshals the proposal and returns binary encoded bytes.
func (keeper Keeper) MarshalProposal(proposal v1.Proposal) ([]byte, error) {
	bz, err := keeper.cdc.Marshal(&proposal)
	if err != nil {
		return nil, err
	}
	return bz, nil
}

// UnmarshalProposal unmarshals the proposal.
func (keeper Keeper) UnmarshalProposal(bz []byte, proposal *v1.Proposal) error {
	err := keeper.cdc.Unmarshal(bz, proposal)
	if err != nil {
		return err
	}
	return nil
}

// ProposalKinds returns a v1.ProposalKinds useful to determine which kind of
// messages are included in a proposal.
func (k Keeper) ProposalKinds(p v1.Proposal) v1.ProposalKinds {
	if len(p.Messages) == 0 {
		return v1.ProposalKindAny
	}
	var kinds v1.ProposalKinds
	for _, msg := range p.Messages {
		var sdkMsg sdk.Msg
		if err := k.cdc.UnpackAny(msg, &sdkMsg); err == nil {
			switch sdkMsg.(type) {
			case *v1.MsgProposeConstitutionAmendment:
				kinds |= v1.ProposalKindConstitutionAmendment
			case *v1.MsgProposeLaw:
				kinds |= v1.ProposalKindLaw
			default:
				kinds |= v1.ProposalKindAny
			}
		} else {
			// If we can't unpack the message, it's likely a broken proposal.
			// although almost impossible, we still want to handle it gracefully.
			// We assume that the proposal is of any kind.
			kinds |= v1.ProposalKindAny
		}
	}
	return kinds
}
