package keeper

import (
	"fmt"
	"github.com/cosmos/cosmos-sdk/codec"
	cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
	sdk "github.com/cosmos/cosmos-sdk/types"
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
	auctiontypes "github.com/tharsis/ethermint/x/auction/types"
	"github.com/tharsis/ethermint/x/nameservice/helpers"
	"github.com/tharsis/ethermint/x/nameservice/types"
	"net/url"
	"strings"
	"time"
)

func getAuthorityPubKey(pubKey cryptotypes.PubKey) string {
	if pubKey != nil {
		return helpers.BytesToBase64(pubKey.Bytes())
	}
	return ""
}

// GetNameAuthorityIndexKey Generates name -> NameAuthority index key.
func GetNameAuthorityIndexKey(name string) []byte {
	return append(PrefixNameAuthorityRecordIndex, []byte(name)...)
}

// GetNameRecordIndexKey Generates WRN -> NameRecord index key.
func GetNameRecordIndexKey(wrn string) []byte {
	return append(PrefixWRNToNameRecordIndex, []byte(wrn)...)
}

func GetCIDToNamesIndexKey(id string) []byte {
	return append(PrefixCIDToNamesIndex, []byte(id)...)
}

func SetNameAuthority(ctx sdk.Context, store sdk.KVStore, codec codec.BinaryCodec, name string, authority *types.NameAuthority) {
	store.Set(GetNameAuthorityIndexKey(name), codec.MustMarshal(authority))
	updateBlockChangeSetForNameAuthority(ctx, codec, store, name)
}

// SetNameAuthority creates the NameAuthority record.
func (k Keeper) SetNameAuthority(ctx sdk.Context, name string, authority *types.NameAuthority) {
	SetNameAuthority(ctx, ctx.KVStore(k.storeKey), k.cdc, name, authority)
}

func removeAuctionToAuthorityMapping(store sdk.KVStore, auctionID string) {
	store.Delete(GetAuctionToAuthorityIndexKey(auctionID))
}

func (k Keeper) RemoveAuctionToAuthorityMapping(ctx sdk.Context, auctionID string) {
	removeAuctionToAuthorityMapping(ctx.KVStore(k.storeKey), auctionID)
}

// GetNameAuthority - gets a name authority from the store.
func GetNameAuthority(store sdk.KVStore, codec codec.BinaryCodec, name string) types.NameAuthority {
	authorityKey := GetNameAuthorityIndexKey(name)
	if !store.Has(authorityKey) {
		return types.NameAuthority{}
	}

	bz := store.Get(authorityKey)
	var obj types.NameAuthority
	codec.MustUnmarshal(bz, &obj)

	return obj
}

// GetNameAuthority - gets a name authority from the store.
func (k Keeper) GetNameAuthority(ctx sdk.Context, name string) types.NameAuthority {
	return GetNameAuthority(ctx.KVStore(k.storeKey), k.cdc, name)
}

// HasNameAuthority - checks if a name authority entry exists.
func HasNameAuthority(store sdk.KVStore, name string) bool {
	return store.Has(GetNameAuthorityIndexKey(name))
}

// HasNameAuthority - checks if a name/authority exists.
func (k Keeper) HasNameAuthority(ctx sdk.Context, name string) bool {
	return HasNameAuthority(ctx.KVStore(k.storeKey), name)
}

func getBondIDToAuthoritiesIndexKey(bondID string, name string) []byte {
	return append(append(PrefixBondIDToAuthoritiesIndex, []byte(bondID)...), []byte(name)...)
}

// AddBondToAuthorityIndexEntry adds the Bond ID -> [Authority] index entry.
func (k Keeper) AddBondToAuthorityIndexEntry(ctx sdk.Context, bondID string, name string) {
	store := ctx.KVStore(k.storeKey)
	store.Set(getBondIDToAuthoritiesIndexKey(bondID, name), []byte{})
}

// RemoveBondToAuthorityIndexEntry removes the Bond ID -> [Authority] index entry.
func (k Keeper) RemoveBondToAuthorityIndexEntry(ctx sdk.Context, bondID string, name string) {
	RemoveBondToAuthorityIndexEntry(ctx.KVStore(k.storeKey), bondID, name)
}

func RemoveBondToAuthorityIndexEntry(store sdk.KVStore, bondID string, name string) {
	store.Delete(getBondIDToAuthoritiesIndexKey(bondID, name))
}

func (k Keeper) updateBlockChangeSetForName(ctx sdk.Context, wrn string) {
	changeSet := k.getOrCreateBlockChangeSet(ctx, ctx.BlockHeight())
	changeSet.Names = append(changeSet.Names, wrn)
	k.saveBlockChangeSet(ctx, changeSet)
}

func (k Keeper) getAuthority(ctx sdk.Context, wrn string) (string, *url.URL, *types.NameAuthority, error) {
	parsedWRN, err := url.Parse(wrn)
	if err != nil {
		return "", nil, nil, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "Invalid WRN.")
	}

	name := parsedWRN.Host
	if !k.HasNameAuthority(ctx, name) {
		return name, nil, nil, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "Name authority not found.")
	}
	authority := k.GetNameAuthority(ctx, name)
	return name, parsedWRN, &authority, nil
}

func (k Keeper) checkWRNAccess(ctx sdk.Context, signer sdk.AccAddress, wrn string) error {
	name, parsedWRN, authority, err := k.getAuthority(ctx, wrn)
	if err != nil {
		return err
	}

	formattedWRN := fmt.Sprintf("wrn://%s%s", name, parsedWRN.RequestURI())
	if formattedWRN != wrn {
		return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "Invalid WRN.")
	}

	if authority.OwnerAddress != signer.String() {
		return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "Access denied.")
	}

	if authority.Status != types.AuthorityActive {
		return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "Authority is not active.")
	}

	if authority.BondId == "" || len(authority.BondId) == 0 {
		return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "Authority bond not found.")
	}

	if authority.OwnerPublicKey == "" {
		// Try to set owner public key if account has it available now.
		ownerAccount := k.accountKeeper.GetAccount(ctx, signer)
		pubKey := ownerAccount.GetPubKey()
		if pubKey != nil {
			// Update public key in authority record.
			authority.OwnerPublicKey = getAuthorityPubKey(pubKey)
			k.SetNameAuthority(ctx, name, authority)
		}
	}

	return nil
}

// HasNameRecord - checks if a name record exists.
func (k Keeper) HasNameRecord(ctx sdk.Context, wrn string) bool {
	store := ctx.KVStore(k.storeKey)
	return store.Has(GetNameRecordIndexKey(wrn))
}

// GetNameRecord - gets a name record from the store.
func GetNameRecord(store sdk.KVStore, codec codec.BinaryCodec, wrn string) *types.NameRecord {
	nameRecordKey := GetNameRecordIndexKey(wrn)
	if !store.Has(nameRecordKey) {
		return nil
	}

	bz := store.Get(nameRecordKey)
	var obj types.NameRecord
	codec.MustUnmarshal(bz, &obj)

	return &obj
}

// GetNameRecord - gets a name record from the store.
func (k Keeper) GetNameRecord(ctx sdk.Context, wrn string) *types.NameRecord {
	_, _, authority, err := k.getAuthority(ctx, wrn)
	if err != nil || authority.Status != types.AuthorityActive {
		// If authority is not active (or any other error), lookup fails.
		return nil
	}

	nameRecord := GetNameRecord(ctx.KVStore(k.storeKey), k.cdc, wrn)

	// Name record may not exist.
	if nameRecord == nil {
		return nil
	}

	// Name lookup should fail if the name record is stale.
	// i.e. authority was registered later than the name.
	if authority.Height > nameRecord.Latest.Height {
		return nil
	}

	return nameRecord
}

// RemoveRecordToNameMapping removes a name from the record ID -> []names index.
func RemoveRecordToNameMapping(store sdk.KVStore, id string, wrn string) {
	reverseNameIndexKey := GetCIDToNamesIndexKey(id)

	names, _ := helpers.BytesArrToStringArr(store.Get(reverseNameIndexKey))
	nameSet := helpers.SliceToSet(names)
	nameSet.Remove(wrn)

	if nameSet.Cardinality() == 0 {
		// Delete as storing empty slice throws error from baseapp.
		store.Delete(reverseNameIndexKey)
	} else {
		data, _ := helpers.StrArrToBytesArr(helpers.SetToSlice(nameSet))
		store.Set(reverseNameIndexKey, data)
	}
}

// AddRecordToNameMapping adds a name to the record ID -> []names index.
func AddRecordToNameMapping(store sdk.KVStore, id string, wrn string) {
	reverseNameIndexKey := GetCIDToNamesIndexKey(id)

	var names []string
	if store.Has(reverseNameIndexKey) {
		names, _ = helpers.BytesArrToStringArr(store.Get(reverseNameIndexKey))
	}

	nameSet := helpers.SliceToSet(names)
	nameSet.Add(wrn)
	bz, _ := helpers.StrArrToBytesArr(helpers.SetToSlice(nameSet))
	store.Set(reverseNameIndexKey, bz)
}

// SetNameRecord - sets a name record.
func SetNameRecord(store sdk.KVStore, codec codec.BinaryCodec, wrn string, id string, height int64) {
	nameRecordIndexKey := GetNameRecordIndexKey(wrn)

	var nameRecord types.NameRecord
	if store.Has(nameRecordIndexKey) {
		bz := store.Get(nameRecordIndexKey)
		codec.MustUnmarshal(bz, &nameRecord)
		nameRecord.History = append(nameRecord.History, nameRecord.Latest)

		// Update old CID -> []Name index.
		if nameRecord.Latest.Id != "" || len(nameRecord.Latest.Id) != 0 {
			RemoveRecordToNameMapping(store, nameRecord.Latest.Id, wrn)
		}
	}

	nameRecord.Latest = &types.NameRecordEntry{
		Id:     id,
		Height: uint64(height),
	}

	store.Set(nameRecordIndexKey, codec.MustMarshal(&nameRecord))

	// Update new CID -> []Name index.
	if id != "" {
		AddRecordToNameMapping(store, id, wrn)
	}
}

// SetNameRecord - sets a name record.
func (k Keeper) SetNameRecord(ctx sdk.Context, wrn string, id string) {
	SetNameRecord(ctx.KVStore(k.storeKey), k.cdc, wrn, id, ctx.BlockHeight())

	// Update changeSet for name.
	k.updateBlockChangeSetForName(ctx, wrn)
}

// ProcessSetName creates a WRN -> Record ID mapping.
func (k Keeper) ProcessSetName(ctx sdk.Context, msg types.MsgSetName) error {
	signerAddress, err := sdk.AccAddressFromBech32(msg.Signer)
	if err != nil {
		return err
	}
	err = k.checkWRNAccess(ctx, signerAddress, msg.Wrn)
	if err != nil {
		return err
	}

	nameRecord := k.GetNameRecord(ctx, msg.Wrn)
	if nameRecord != nil && nameRecord.Latest.Id == msg.Cid {
		return nil
	}

	k.SetNameRecord(ctx, msg.Wrn, msg.Cid)

	return nil
}

// ListNameRecords - get all name records.
func (k Keeper) ListNameRecords(ctx sdk.Context) []types.NameEntry {
	var nameEntries []types.NameEntry
	store := ctx.KVStore(k.storeKey)
	itr := sdk.KVStorePrefixIterator(store, PrefixWRNToNameRecordIndex)
	defer itr.Close()
	for ; itr.Valid(); itr.Next() {
		bz := store.Get(itr.Key())
		if bz != nil {
			var record types.NameRecord
			k.cdc.MustUnmarshal(bz, &record)
			nameEntries = append(nameEntries, types.NameEntry{
				Name:  string(itr.Key()[len(PrefixWRNToNameRecordIndex):]),
				Entry: &record,
			})
		}
	}

	return nameEntries
}

// ProcessReserveSubAuthority reserves a sub-authority.
func (k Keeper) ProcessReserveSubAuthority(ctx sdk.Context, name string, msg types.MsgReserveAuthority) error {
	// Get parent authority name.
	names := strings.Split(name, ".")
	parent := strings.Join(names[1:], ".")

	// Check if parent authority exists.
	if !k.HasNameAuthority(ctx, parent) {
		return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "Parent authority not found.")
	}
	parentAuthority := k.GetNameAuthority(ctx, parent)

	// Sub-authority creator needs to be the owner of the parent authority.
	if parentAuthority.OwnerAddress != msg.Signer {
		return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "Access denied.")
	}

	// Sub-authority owner defaults to parent authority owner.
	subAuthorityOwner := msg.Signer
	if len(msg.Owner) != 0 {
		// Override sub-authority owner if provided in message.
		subAuthorityOwner = msg.Owner
	}

	sdkErr := k.createAuthority(ctx, name, subAuthorityOwner, false)
	if sdkErr != nil {
		return sdkErr
	}

	return nil
}

func GetAuctionToAuthorityIndexKey(auctionID string) []byte {
	return append(PrefixAuctionToAuthorityNameIndex, []byte(auctionID)...)
}

func (k Keeper) AddAuctionToAuthorityMapping(ctx sdk.Context, auctionID string, name string) {
	store := ctx.KVStore(k.storeKey)
	store.Set(GetAuctionToAuthorityIndexKey(auctionID), []byte(name))
}

func (k Keeper) createAuthority(ctx sdk.Context, name string, owner string, isRoot bool) error {
	moduleParams := k.GetParams(ctx)

	if k.HasNameAuthority(ctx, name) {
		authority := k.GetNameAuthority(ctx, name)
		if authority.Status != types.AuthorityExpired {
			return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "Name already reserved.")
		}
	}

	ownerAddress, err := sdk.AccAddressFromBech32(owner)
	if err != nil {
		return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "Invalid owner address.")
	}
	ownerAccount := k.accountKeeper.GetAccount(ctx, ownerAddress)
	if ownerAccount == nil {
		return sdkerrors.Wrap(sdkerrors.ErrUnknownAddress, "Account not found.")
	}

	authority := types.NameAuthority{
		OwnerPublicKey: getAuthorityPubKey(ownerAccount.GetPubKey()),
		OwnerAddress:   owner,
		Height:         uint64(ctx.BlockHeight()),
		Status:         types.AuthorityActive,
		AuctionId:      "",
		BondId:         "",
		ExpiryTime:     ctx.BlockTime().Add(moduleParams.AuthorityGracePeriod),
	}

	if isRoot && moduleParams.AuthorityAuctionEnabled {
		// If auctions are enabled, clear out owner fields. They will be set after a winner is picked.
		authority.OwnerAddress = ""
		authority.OwnerPublicKey = ""
		// Reset bond ID if required.
		if authority.BondId != "" || len(authority.BondId) != 0 {
			k.RemoveBondToAuthorityIndexEntry(ctx, authority.BondId, name)
			authority.BondId = ""
		}

		params := auctiontypes.Params{
			CommitsDuration: moduleParams.AuthorityAuctionCommitsDuration,
			RevealsDuration: moduleParams.AuthorityAuctionRevealsDuration,
			CommitFee:       moduleParams.AuthorityAuctionCommitFee,
			RevealFee:       moduleParams.AuthorityAuctionRevealFee,
			MinimumBid:      moduleParams.AuthorityAuctionMinimumBid,
		}

		// Create an auction.
		msg := auctiontypes.NewMsgCreateAuction(params, ownerAddress)

		auction, sdkErr := k.auctionKeeper.CreateAuction(ctx, msg)
		if sdkErr != nil {
			return sdkErr
		}

		// Create auction ID -> authority name index.
		k.AddAuctionToAuthorityMapping(ctx, auction.Id, name)

		authority.Status = types.AuthorityUnderAuction
		authority.AuctionId = auction.Id
		authority.ExpiryTime = auction.RevealsEndTime.Add(moduleParams.AuthorityGracePeriod)
	}

	k.SetNameAuthority(ctx, name, &authority)
	k.InsertAuthorityExpiryQueue(ctx, name, authority.ExpiryTime)

	return nil
}

// ProcessReserveAuthority reserves a name authority.
func (k Keeper) ProcessReserveAuthority(ctx sdk.Context, msg types.MsgReserveAuthority) error {
	wrn := fmt.Sprintf("wrn://%s", msg.GetName())
	parsedWrn, err := url.Parse(wrn)
	if err != nil {
		return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "Invalid name")
	}
	name := parsedWrn.Host
	if fmt.Sprintf("wrn://%s", name) != wrn {
		return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "Invalid name")
	}
	if strings.Contains(name, ".") {
		return k.ProcessReserveSubAuthority(ctx, name, msg)
	}
	err = k.createAuthority(ctx, name, msg.GetSigner(), true)
	if err != nil {
		return err
	}
	return nil
}

func (k Keeper) ProcessSetAuthorityBond(ctx sdk.Context, msg types.MsgSetAuthorityBond) error {
	name := msg.GetName()
	signer := msg.GetSigner()
	if !k.HasNameAuthority(ctx, name) {
		return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "Name authority not found.")
	}
	authority := k.GetNameAuthority(ctx, name)
	if authority.OwnerAddress != signer {
		return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "Access denied")
	}

	if !k.bondKeeper.HasBond(ctx, msg.BondId) {
		return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "Bond not found.")
	}
	//
	bond := k.bondKeeper.GetBond(ctx, msg.BondId)
	if bond.Owner != signer {
		return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "Bond owner mismatch.")
	}

	// No-op if bond hasn't changed.
	if authority.BondId == msg.BondId {
		return nil
	}

	// Remove old bond ID mapping, if any.
	if authority.BondId != "" {
		k.RemoveBondToAuthorityIndexEntry(ctx, authority.BondId, name)
	}

	// Update bond ID for authority.
	authority.BondId = bond.Id
	k.SetNameAuthority(ctx, name, &authority)
	// Add new bond ID mapping.
	k.AddBondToAuthorityIndexEntry(ctx, authority.BondId, name)
	return nil
}

// ProcessDeleteName removes a WRN -> Record ID mapping.
func (k Keeper) ProcessDeleteName(ctx sdk.Context, msg types.MsgDeleteNameAuthority) error {
	signerAddress, err := sdk.AccAddressFromBech32(msg.Signer)
	if err != nil {
		return err
	}
	err = k.checkWRNAccess(ctx, signerAddress, msg.Wrn)
	if err != nil {
		return err
	}

	if !k.HasNameRecord(ctx, msg.Wrn) {
		return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "Name not found.")
	}

	// Set CID to empty string.
	k.SetNameRecord(ctx, msg.Wrn, "")

	return nil
}

func (k Keeper) GetAuthorityExpiryQueue(ctx sdk.Context) []*types.ExpiryQueueRecord {
	var authorities []*types.ExpiryQueueRecord

	store := ctx.KVStore(k.storeKey)
	itr := sdk.KVStorePrefixIterator(store, PrefixExpiryTimeToAuthoritiesIndex)
	defer itr.Close()
	for ; itr.Valid(); itr.Next() {
		var record []string
		record, err := helpers.BytesArrToStringArr(itr.Value())
		if err != nil {
			return authorities
		}
		authorities = append(authorities, &types.ExpiryQueueRecord{
			Id:    string(itr.Key()[len(PrefixExpiryTimeToAuthoritiesIndex):]),
			Value: record,
		})
	}

	return authorities
}

// ResolveWRN resolves a WRN to a record.
func (k Keeper) ResolveWRN(ctx sdk.Context, wrn string) *types.Record {
	_, _, authority, err := k.getAuthority(ctx, wrn)
	if err != nil || authority.Status != types.AuthorityActive {
		// If authority is not active (or any other error), resolution fails.
		return nil
	}

	// Name should not resolve if it's stale.
	// i.e. authority was registered later than the name.
	record, nameRecord := ResolveWRN(ctx.KVStore(k.storeKey), wrn, k, ctx)
	if authority.Height > nameRecord.Latest.Height {
		return nil
	}

	return record
}

// ResolveWRN resolves a WRN to a record.
func ResolveWRN(store sdk.KVStore, wrn string, k Keeper, c sdk.Context) (*types.Record, *types.NameRecord) {
	nameKey := GetNameRecordIndexKey(wrn)

	if store.Has(nameKey) {
		bz := store.Get(nameKey)
		var obj types.NameRecord
		k.cdc.MustUnmarshal(bz, &obj)

		recordExists := k.HasRecord(c, obj.Latest.Id)
		if !recordExists || obj.Latest.Id == "" {
			return nil, &obj
		}

		record := k.GetRecord(c, obj.Latest.Id)
		return &record, &obj
	}

	return nil, nil
}

func getAuthorityExpiryQueueTimeKey(timestamp time.Time) []byte {
	timeBytes := sdk.FormatTimeBytes(timestamp)
	return append(PrefixExpiryTimeToAuthoritiesIndex, timeBytes...)
}

func (k Keeper) InsertAuthorityExpiryQueue(ctx sdk.Context, name string, expiryTime time.Time) {
	timeSlice := k.GetAuthorityExpiryQueueTimeSlice(ctx, expiryTime)
	timeSlice = append(timeSlice, name)
	k.SetAuthorityExpiryQueueTimeSlice(ctx, expiryTime, timeSlice)
}

func (k Keeper) GetAuthorityExpiryQueueTimeSlice(ctx sdk.Context, timestamp time.Time) []string {
	store := ctx.KVStore(k.storeKey)

	bz := store.Get(getAuthorityExpiryQueueTimeKey(timestamp))
	if bz == nil {
		return []string{}
	}

	names, err := helpers.BytesArrToStringArr(bz)
	if err != nil {
		return []string{}
	}

	return names
}

func (k Keeper) SetAuthorityExpiryQueueTimeSlice(ctx sdk.Context, timestamp time.Time, names []string) {
	store := ctx.KVStore(k.storeKey)
	bz, _ := helpers.StrArrToBytesArr(names)
	store.Set(getAuthorityExpiryQueueTimeKey(timestamp), bz)
}

// ListNameAuthorityRecords - get all name authority records.
func (k Keeper) ListNameAuthorityRecords(ctx sdk.Context) map[string]types.NameAuthority {
	nameAuthorityRecords := make(map[string]types.NameAuthority)
	store := ctx.KVStore(k.storeKey)

	itr := sdk.KVStorePrefixIterator(store, PrefixNameAuthorityRecordIndex)
	defer itr.Close()
	for ; itr.Valid(); itr.Next() {
		bz := store.Get(itr.Key())
		if bz != nil {
			var record types.NameAuthority
			k.cdc.MustUnmarshal(bz, &record)
			nameAuthorityRecords[string(itr.Key()[len(PrefixNameAuthorityRecordIndex):])] = record
		}
	}

	return nameAuthorityRecords
}
