package keeper

import (
	"encoding/hex"
	"io"

	errorsmod "cosmossdk.io/errors"
	snapshot "cosmossdk.io/store/snapshots/types"
	storetypes "cosmossdk.io/store/types"

	sdk "git.cw.tr/mukan-network/mukan-sdk/types"

	cmtproto "git.cw.tr/mukan-network/mukan-consensus/proto/tendermint/types"

	"github.com/cosmos/ibc-go/modules/light-clients/08-wasm/v10/types"
)

var _ snapshot.ExtensionSnapshotter = &WasmSnapshotter{}

// SnapshotFormat defines the default snapshot extension encoding format.
// SnapshotFormat 1 is gzipped wasm byte code for each item payload. No protobuf envelope, no metadata.
const SnapshotFormat = 1

// WasmSnapshotter implements the snapshot.ExtensionSnapshotter interface and is used to
// import and export state maintained within the wasmvm cache.
// NOTE: The following ExtensionSnapshotter has been adapted from CosmWasm's x/wasm:
// https://github.com/CosmWasm/wasmd/blob/v0.43.0/x/wasm/keeper/snapshotter.go
type WasmSnapshotter struct {
	cms    storetypes.MultiStore
	keeper *Keeper
}

// NewWasmSnapshotter creates and returns a new snapshot.ExtensionSnapshotter implementation for the 08-wasm module.
func NewWasmSnapshotter(cms storetypes.MultiStore, keeper *Keeper) snapshot.ExtensionSnapshotter {
	return &WasmSnapshotter{
		cms:    cms,
		keeper: keeper,
	}
}

// SnapshotName implements the snapshot.ExtensionSnapshotter interface.
// A unique name should be provided such that the implementation can be identified by the manager.
func (*WasmSnapshotter) SnapshotName() string {
	return types.ModuleName
}

// SnapshotFormat implements the snapshot.ExtensionSnapshotter interface.
// This is the default format used for encoding payloads when taking a snapshot.
func (*WasmSnapshotter) SnapshotFormat() uint32 {
	return SnapshotFormat
}

// SupportedFormats implements the snapshot.ExtensionSnapshotter interface.
// This defines a list of supported formats the snapshotter extension can restore from.
func (*WasmSnapshotter) SupportedFormats() []uint32 {
	return []uint32{SnapshotFormat}
}

// SnapshotExtension implements the snapshot.ExntensionSnapshotter interface.
// SnapshotExtension is used to write data payloads into the underlying protobuf stream from the 08-wasm module.
func (ws *WasmSnapshotter) SnapshotExtension(height uint64, payloadWriter snapshot.ExtensionPayloadWriter) error {
	cacheMS, err := ws.cms.CacheMultiStoreWithVersion(int64(height))
	if err != nil {
		return err
	}

	ctx := sdk.NewContext(cacheMS, cmtproto.Header{}, false, nil)

	checksums, err := ws.keeper.GetAllChecksums(ctx)
	if err != nil {
		return err
	}

	for _, checksum := range checksums {
		wasmCode, err := ws.keeper.GetVM().GetCode(checksum)
		if err != nil {
			return err
		}

		compressedWasm, err := types.GzipIt(wasmCode)
		if err != nil {
			return err
		}

		if err = payloadWriter(compressedWasm); err != nil {
			return err
		}
	}

	return nil
}

// RestoreExtension implements the snapshot.ExtensionSnapshotter interface.
// RestoreExtension is used to read data from an existing extension state snapshot into the 08-wasm module.
// The payload reader returns io.EOF when it has reached the end of the extension state snapshot.
func (ws *WasmSnapshotter) RestoreExtension(height uint64, format uint32, payloadReader snapshot.ExtensionPayloadReader) error {
	if format == ws.SnapshotFormat() {
		return ws.processAllItems(height, payloadReader, restoreV1)
	}

	return errorsmod.Wrapf(snapshot.ErrUnknownFormat, "expected %d, got %d", ws.SnapshotFormat(), format)
}

func restoreV1(ctx sdk.Context, k *Keeper, compressedCode []byte) error {
	if !types.IsGzip(compressedCode) {
		return errorsmod.Wrap(types.ErrInvalidData, "expected wasm code is not gzip format")
	}

	wasmCode, err := types.Uncompress(compressedCode, types.MaxWasmSize)
	if err != nil {
		return errorsmod.Wrap(err, "failed to uncompress wasm code")
	}

	checksum, err := k.GetVM().StoreCodeUnchecked(wasmCode)
	if err != nil {
		return errorsmod.Wrap(err, "failed to store wasm code")
	}

	if err := k.GetVM().Pin(checksum); err != nil {
		return errorsmod.Wrapf(err, "failed to pin checksum: %s to in-memory cache", hex.EncodeToString(checksum))
	}

	return nil
}

func (ws *WasmSnapshotter) processAllItems(
	height uint64,
	payloadReader snapshot.ExtensionPayloadReader,
	cb func(sdk.Context, *Keeper, []byte) error,
) error {
	ctx := sdk.NewContext(ws.cms, cmtproto.Header{Height: int64(height)}, false, nil)
	for {
		payload, err := payloadReader()
		if err == io.EOF {
			break
		} else if err != nil {
			return err
		}

		if err := cb(ctx, ws.keeper, payload); err != nil {
			return errorsmod.Wrap(err, "failure processing snapshot item")
		}
	}

	return nil
}
