package keeper

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

	errorsmod "cosmossdk.io/errors"

	sdk "github.com/cosmos/cosmos-sdk/types"
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
	"github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1"

	"wasmd.mleku.dev/x/wasm/types"
)

// NewLegacyWasmProposalHandler creates a new governance Handler for wasm proposals
//
// Deprecated: Do not use v1beta1 handlers anymore. Gov v1 is the way to go
func NewLegacyWasmProposalHandler(k decoratedKeeper, enabledProposalTypes []types.ProposalType) v1beta1.Handler {
	return NewLegacyWasmProposalHandlerX(NewGovPermissionKeeper(k), enabledProposalTypes)
}

// NewLegacyWasmProposalHandlerX creates a new governance Handler for wasm proposals
//
// Deprecated: Do not use v1beta1 handlers anymore. Gov v1 is the way to go
func NewLegacyWasmProposalHandlerX(k types.ContractOpsKeeper, enabledProposalTypes []types.ProposalType) v1beta1.Handler {
	enabledTypes := make(map[string]struct{}, len(enabledProposalTypes))
	for i := range enabledProposalTypes {
		enabledTypes[string(enabledProposalTypes[i])] = struct{}{}
	}
	return func(ctx sdk.Context, content v1beta1.Content) error {
		if content == nil {
			return errorsmod.Wrap(sdkerrors.ErrUnknownRequest, "content must not be empty")
		}
		if _, ok := enabledTypes[content.ProposalType()]; !ok {
			return errorsmod.Wrapf(sdkerrors.ErrUnknownRequest, "unsupported wasm proposal content type: %q",
				content.ProposalType())
		}
		switch c := content.(type) {
		case *types.StoreCodeProposal:
			return handleStoreCodeProposal(ctx, k, *c)
		case *types.InstantiateContractProposal:
			return handleInstantiateProposal(ctx, k, *c)
		case *types.InstantiateContract2Proposal:
			return handleInstantiate2Proposal(ctx, k, *c)
		case *types.MigrateContractProposal:
			return handleMigrateProposal(ctx, k, *c)
		case *types.SudoContractProposal:
			return handleSudoProposal(ctx, k, *c)
		case *types.ExecuteContractProposal:
			return handleExecuteProposal(ctx, k, *c)
		case *types.UpdateAdminProposal:
			return handleUpdateAdminProposal(ctx, k, *c)
		case *types.ClearAdminProposal:
			return handleClearAdminProposal(ctx, k, *c)
		case *types.PinCodesProposal:
			return handlePinCodesProposal(ctx, k, *c)
		case *types.UnpinCodesProposal:
			return handleUnpinCodesProposal(ctx, k, *c)
		case *types.UpdateInstantiateConfigProposal:
			return handleUpdateInstantiateConfigProposal(ctx, k, *c)
		case *types.StoreAndInstantiateContractProposal:
			return handleStoreAndInstantiateContractProposal(ctx, k, *c)
		default:
			return errorsmod.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized wasm proposal content type: %T", c)
		}
	}
}

//nolint:staticcheck
func handleStoreCodeProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.StoreCodeProposal) error {
	if err := p.ValidateBasic(); err != nil {
		return err
	}

	runAsAddr, err := sdk.AccAddressFromBech32(p.RunAs)
	if err != nil {
		return errorsmod.Wrap(err, "run as address")
	}
	codeID, checksum, err := k.Create(ctx, runAsAddr, p.WASMByteCode, p.InstantiatePermission)
	if err != nil {
		return err
	}

	if len(p.CodeHash) != 0 && !bytes.Equal(checksum, p.CodeHash) {
		return fmt.Errorf("code-hash mismatch: %X, checksum: %X", p.CodeHash, checksum)
	}

	// if code should not be pinned return earlier
	if p.UnpinCode {
		return nil
	}
	return k.PinCode(ctx, codeID)
}

//nolint:staticcheck
func handleInstantiateProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.InstantiateContractProposal) error {
	if err := p.ValidateBasic(); err != nil {
		return err
	}
	runAsAddr, err := sdk.AccAddressFromBech32(p.RunAs)
	if err != nil {
		return errorsmod.Wrap(err, "run as address")
	}
	var adminAddr sdk.AccAddress
	if p.Admin != "" {
		if adminAddr, err = sdk.AccAddressFromBech32(p.Admin); err != nil {
			return errorsmod.Wrap(err, "admin")
		}
	}

	_, data, err := k.Instantiate(ctx, p.CodeID, runAsAddr, adminAddr, p.Msg, p.Label, p.Funds)
	if err != nil {
		return err
	}

	ctx.EventManager().EmitEvent(sdk.NewEvent(
		types.EventTypeGovContractResult,
		sdk.NewAttribute(types.AttributeKeyResultDataHex, hex.EncodeToString(data)),
	))
	return nil
}

//nolint:staticcheck
func handleInstantiate2Proposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.InstantiateContract2Proposal) error {
	// Validatebasic with proposal
	if err := p.ValidateBasic(); err != nil {
		return err
	}

	// Get runAsAddr as AccAddress
	runAsAddr, err := sdk.AccAddressFromBech32(p.RunAs)
	if err != nil {
		return errorsmod.Wrap(err, "run as address")
	}

	// Get admin address
	var adminAddr sdk.AccAddress
	if p.Admin != "" {
		if adminAddr, err = sdk.AccAddressFromBech32(p.Admin); err != nil {
			return errorsmod.Wrap(err, "admin")
		}
	}

	_, data, err := k.Instantiate2(ctx, p.CodeID, runAsAddr, adminAddr, p.Msg, p.Label, p.Funds, p.Salt, p.FixMsg)
	if err != nil {
		return err
	}

	ctx.EventManager().EmitEvent(sdk.NewEvent(
		types.EventTypeGovContractResult,
		sdk.NewAttribute(types.AttributeKeyResultDataHex, hex.EncodeToString(data)),
	))
	return nil
}

//nolint:staticcheck
func handleStoreAndInstantiateContractProposal(ctx sdk.Context, k types.ContractOpsKeeper,
	p types.StoreAndInstantiateContractProposal) error {
	if err := p.ValidateBasic(); err != nil {
		return err
	}
	runAsAddr, err := sdk.AccAddressFromBech32(p.RunAs)
	if err != nil {
		return errorsmod.Wrap(err, "run as address")
	}
	var adminAddr sdk.AccAddress
	if p.Admin != "" {
		if adminAddr, err = sdk.AccAddressFromBech32(p.Admin); err != nil {
			return errorsmod.Wrap(err, "admin")
		}
	}

	codeID, checksum, err := k.Create(ctx, runAsAddr, p.WASMByteCode, p.InstantiatePermission)
	if err != nil {
		return err
	}

	if p.CodeHash != nil && !bytes.Equal(checksum, p.CodeHash) {
		return errorsmod.Wrap(fmt.Errorf("code-hash mismatch: %X, checksum: %X", p.CodeHash, checksum), "code-hash mismatch")
	}

	if !p.UnpinCode {
		if err := k.PinCode(ctx, codeID); err != nil {
			return err
		}
	}

	_, data, err := k.Instantiate(ctx, codeID, runAsAddr, adminAddr, p.Msg, p.Label, p.Funds)
	if err != nil {
		return err
	}

	ctx.EventManager().EmitEvent(sdk.NewEvent(
		types.EventTypeGovContractResult,
		sdk.NewAttribute(types.AttributeKeyResultDataHex, hex.EncodeToString(data)),
	))
	return nil
}

//nolint:staticcheck
func handleMigrateProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.MigrateContractProposal) error {
	if err := p.ValidateBasic(); err != nil {
		return err
	}

	contractAddr, err := sdk.AccAddressFromBech32(p.Contract)
	if err != nil {
		return errorsmod.Wrap(err, "contract")
	}

	// runAs is not used if this is permissioned, so just put any valid address there (second contractAddr)
	data, err := k.Migrate(ctx, contractAddr, contractAddr, p.CodeID, p.Msg)
	if err != nil {
		return err
	}

	ctx.EventManager().EmitEvent(sdk.NewEvent(
		types.EventTypeGovContractResult,
		sdk.NewAttribute(types.AttributeKeyResultDataHex, hex.EncodeToString(data)),
	))
	return nil
}

//nolint:staticcheck
func handleSudoProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.SudoContractProposal) error {
	if err := p.ValidateBasic(); err != nil {
		return err
	}

	contractAddr, err := sdk.AccAddressFromBech32(p.Contract)
	if err != nil {
		return errorsmod.Wrap(err, "contract")
	}
	data, err := k.Sudo(ctx, contractAddr, p.Msg)
	if err != nil {
		return err
	}

	ctx.EventManager().EmitEvent(sdk.NewEvent(
		types.EventTypeGovContractResult,
		sdk.NewAttribute(types.AttributeKeyResultDataHex, hex.EncodeToString(data)),
	))
	return nil
}

//nolint:staticcheck
func handleExecuteProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.ExecuteContractProposal) error {
	if err := p.ValidateBasic(); err != nil {
		return err
	}

	contractAddr, err := sdk.AccAddressFromBech32(p.Contract)
	if err != nil {
		return errorsmod.Wrap(err, "contract")
	}
	runAsAddr, err := sdk.AccAddressFromBech32(p.RunAs)
	if err != nil {
		return errorsmod.Wrap(err, "run as address")
	}
	data, err := k.Execute(ctx, contractAddr, runAsAddr, p.Msg, p.Funds)
	if err != nil {
		return err
	}

	ctx.EventManager().EmitEvent(sdk.NewEvent(
		types.EventTypeGovContractResult,
		sdk.NewAttribute(types.AttributeKeyResultDataHex, hex.EncodeToString(data)),
	))
	return nil
}

//nolint:staticcheck
func handleUpdateAdminProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.UpdateAdminProposal) error {
	if err := p.ValidateBasic(); err != nil {
		return err
	}
	contractAddr, err := sdk.AccAddressFromBech32(p.Contract)
	if err != nil {
		return errorsmod.Wrap(err, "contract")
	}
	newAdminAddr, err := sdk.AccAddressFromBech32(p.NewAdmin)
	if err != nil {
		return errorsmod.Wrap(err, "run as address")
	}

	return k.UpdateContractAdmin(ctx, contractAddr, nil, newAdminAddr)
}

//nolint:staticcheck
func handleClearAdminProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.ClearAdminProposal) error {
	if err := p.ValidateBasic(); err != nil {
		return err
	}

	contractAddr, err := sdk.AccAddressFromBech32(p.Contract)
	if err != nil {
		return errorsmod.Wrap(err, "contract")
	}
	err = k.ClearContractAdmin(ctx, contractAddr, nil)
	return err
}

//nolint:staticcheck
func handlePinCodesProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.PinCodesProposal) error {
	if err := p.ValidateBasic(); err != nil {
		return err
	}
	for _, v := range p.CodeIDs {
		if err := k.PinCode(ctx, v); err != nil {
			return errorsmod.Wrapf(err, "code id: %d", v)
		}
	}
	return nil
}

//nolint:staticcheck
func handleUnpinCodesProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.UnpinCodesProposal) error {
	if err := p.ValidateBasic(); err != nil {
		return err
	}
	for _, v := range p.CodeIDs {
		if err := k.UnpinCode(ctx, v); err != nil {
			return errorsmod.Wrapf(err, "code id: %d", v)
		}
	}
	return nil
}

//nolint:staticcheck
func handleUpdateInstantiateConfigProposal(ctx sdk.Context, k types.ContractOpsKeeper,
	p types.UpdateInstantiateConfigProposal) error {
	if err := p.ValidateBasic(); err != nil {
		return err
	}

	var emptyCaller sdk.AccAddress
	for _, accessConfigUpdate := range p.AccessConfigUpdates {
		if err := k.SetAccessConfig(ctx, accessConfigUpdate.CodeID, emptyCaller,
			accessConfigUpdate.InstantiatePermission); err != nil {
			return errorsmod.Wrapf(err, "code id: %d", accessConfigUpdate.CodeID)
		}
	}
	return nil
}
