package asset

import (
	"context"
	"encoding/json"
	"strings"
	"sync"

	"github.com/golang/groupcache/lru"

	"github.com/vapor/consensus"
	dbm "github.com/vapor/database/leveldb"
	chainjson "github.com/vapor/encoding/json"
	"github.com/vapor/errors"
	"github.com/vapor/protocol"
	"github.com/vapor/protocol/bc"
)

// DefaultNativeAsset native BTM asset
var DefaultNativeAsset *Asset

const (
	maxAssetCache = 1000
)

var (
	assetIndexKey  = []byte("AssetIndex")
	assetPrefix    = []byte("Asset:")
	aliasPrefix    = []byte("AssetAlias:")
	extAssetPrefix = []byte("EXA:")
)

func initNativeAsset() {
	alias := consensus.BTMAlias

	definitionBytes, _ := serializeAssetDef(consensus.BTMDefinitionMap)
	DefaultNativeAsset = &Asset{
		AssetID:           *consensus.BTMAssetID,
		Alias:             &alias,
		VMVersion:         1,
		DefinitionMap:     consensus.BTMDefinitionMap,
		RawDefinitionByte: definitionBytes,
	}
}

// AliasKey store asset alias prefix
func aliasKey(name string) []byte {
	return append(aliasPrefix, []byte(name)...)
}

// Key store asset prefix
func Key(id *bc.AssetID) []byte {
	return append(assetPrefix, id.Bytes()...)
}

// ExtAssetKey return store external assets key
func ExtAssetKey(id *bc.AssetID) []byte {
	return append(extAssetPrefix, id.Bytes()...)
}

// pre-define errors for supporting bytom errorFormatter
var (
	ErrDuplicateAlias = errors.New("duplicate asset alias")
	ErrDuplicateAsset = errors.New("duplicate asset id")
	ErrSerializing    = errors.New("serializing asset definition")
	ErrMarshalAsset   = errors.New("failed marshal asset")
	ErrFindAsset      = errors.New("fail to find asset")
	ErrInternalAsset  = errors.New("btm has been defined as the internal asset")
	ErrNullAlias      = errors.New("null asset alias")
)

//NewRegistry create new registry
func NewRegistry(db dbm.DB, chain *protocol.Chain) *Registry {
	initNativeAsset()
	return &Registry{
		db:         db,
		chain:      chain,
		cache:      lru.New(maxAssetCache),
		aliasCache: lru.New(maxAssetCache),
	}
}

// Registry tracks and stores all known assets on a blockchain.
type Registry struct {
	db    dbm.DB
	chain *protocol.Chain

	cacheMu    sync.Mutex
	cache      *lru.Cache
	aliasCache *lru.Cache

	assetIndexMu sync.Mutex
	assetMu      sync.Mutex
}

//Asset describe asset on bytom chain
type Asset struct {
	AssetID           bc.AssetID             `json:"id"`
	Alias             *string                `json:"alias"`
	VMVersion         uint64                 `json:"vm_version"`
	RawDefinitionByte chainjson.HexBytes     `json:"raw_definition_byte"`
	DefinitionMap     map[string]interface{} `json:"definition"`
}

// SaveAsset store asset
func (reg *Registry) SaveAsset(a *Asset, alias string) error {
	reg.assetMu.Lock()
	defer reg.assetMu.Unlock()

	aliasKey := aliasKey(alias)
	if existed := reg.db.Get(aliasKey); existed != nil {
		return ErrDuplicateAlias
	}

	assetKey := Key(&a.AssetID)
	if existAsset := reg.db.Get(assetKey); existAsset != nil {
		return ErrDuplicateAsset
	}

	rawAsset, err := json.Marshal(a)
	if err != nil {
		return ErrMarshalAsset
	}

	storeBatch := reg.db.NewBatch()
	storeBatch.Set(aliasKey, []byte(a.AssetID.String()))
	storeBatch.Set(assetKey, rawAsset)
	storeBatch.Write()
	return nil
}

// FindByID retrieves an Asset record along with its signer, given an assetID.
func (reg *Registry) FindByID(ctx context.Context, id *bc.AssetID) (*Asset, error) {
	reg.cacheMu.Lock()
	cached, ok := reg.cache.Get(id.String())
	reg.cacheMu.Unlock()
	if ok {
		return cached.(*Asset), nil
	}

	bytes := reg.db.Get(Key(id))
	if bytes == nil {
		return nil, ErrFindAsset
	}

	asset := &Asset{}
	if err := json.Unmarshal(bytes, asset); err != nil {
		return nil, err
	}

	reg.cacheMu.Lock()
	reg.cache.Add(id.String(), asset)
	reg.cacheMu.Unlock()
	return asset, nil
}

// FindByAlias retrieves an Asset record along with its signer,
// given an asset alias.
func (reg *Registry) FindByAlias(alias string) (*Asset, error) {
	reg.cacheMu.Lock()
	cachedID, ok := reg.aliasCache.Get(alias)
	reg.cacheMu.Unlock()
	if ok {
		return reg.FindByID(nil, cachedID.(*bc.AssetID))
	}

	rawID := reg.db.Get(aliasKey(alias))
	if rawID == nil {
		return nil, errors.Wrapf(ErrFindAsset, "no such asset, alias: %s", alias)
	}

	assetID := &bc.AssetID{}
	if err := assetID.UnmarshalText(rawID); err != nil {
		return nil, err
	}

	reg.cacheMu.Lock()
	reg.aliasCache.Add(alias, assetID)
	reg.cacheMu.Unlock()
	return reg.FindByID(nil, assetID)
}

//GetAliasByID return asset alias string by AssetID string
func (reg *Registry) GetAliasByID(id string) string {
	//btm
	if id == consensus.BTMAssetID.String() {
		return consensus.BTMAlias
	}

	assetID := &bc.AssetID{}
	if err := assetID.UnmarshalText([]byte(id)); err != nil {
		return ""
	}

	asset, err := reg.FindByID(nil, assetID)
	if err != nil {
		return ""
	}

	return *asset.Alias
}

// GetAsset get asset by assetID
func (reg *Registry) GetAsset(id string) (*Asset, error) {
	var assetID bc.AssetID
	if err := assetID.UnmarshalText([]byte(id)); err != nil {
		return nil, err
	}

	if assetID.String() == DefaultNativeAsset.AssetID.String() {
		return DefaultNativeAsset, nil
	}

	asset := &Asset{}
	if interAsset := reg.db.Get(Key(&assetID)); interAsset != nil {
		if err := json.Unmarshal(interAsset, asset); err != nil {
			return nil, err
		}
		return asset, nil
	}

	if extAsset := reg.db.Get(ExtAssetKey(&assetID)); extAsset != nil {
		definitionMap := make(map[string]interface{})
		if err := json.Unmarshal(extAsset, &definitionMap); err != nil {
			return nil, err
		}
		alias := strings.ToUpper(assetID.String())
		asset.Alias = &alias
		asset.AssetID = assetID
		asset.DefinitionMap = definitionMap
		return asset, nil
	}

	return nil, errors.WithDetailf(ErrFindAsset, "no such asset, assetID: %s", id)
}

// ListAssets returns the accounts in the db
func (reg *Registry) ListAssets(id string) ([]*Asset, error) {
	assets := []*Asset{DefaultNativeAsset}

	assetIDStr := strings.TrimSpace(id)
	if assetIDStr == DefaultNativeAsset.AssetID.String() {
		return assets, nil
	}

	if assetIDStr != "" {
		assetID := &bc.AssetID{}
		if err := assetID.UnmarshalText([]byte(assetIDStr)); err != nil {
			return nil, err
		}

		asset := &Asset{}
		interAsset := reg.db.Get(Key(assetID))
		if interAsset != nil {
			if err := json.Unmarshal(interAsset, asset); err != nil {
				return nil, err
			}
			return []*Asset{asset}, nil
		}

		return []*Asset{}, nil
	}

	assetIter := reg.db.IteratorPrefix(assetPrefix)
	defer assetIter.Release()

	for assetIter.Next() {
		asset := &Asset{}
		if err := json.Unmarshal(assetIter.Value(), asset); err != nil {
			return nil, err
		}
		assets = append(assets, asset)
	}

	return assets, nil
}

// serializeAssetDef produces a canonical byte representation of an asset
// definition. Currently, this is implemented using pretty-printed JSON.
// As is the standard for Go's map[string] serialization, object keys will
// appear in lexicographic order. Although this is mostly meant for machine
// consumption, the JSON is pretty-printed for easy reading.
func serializeAssetDef(def map[string]interface{}) ([]byte, error) {
	if def == nil {
		def = make(map[string]interface{}, 0)
	}
	return json.MarshalIndent(def, "", "  ")
}

//UpdateAssetAlias updates asset alias
func (reg *Registry) UpdateAssetAlias(id, newAlias string) error {
	oldAlias := reg.GetAliasByID(id)
	newAlias = strings.ToUpper(strings.TrimSpace(newAlias))

	if oldAlias == consensus.BTMAlias || newAlias == consensus.BTMAlias {
		return ErrInternalAsset
	}

	if oldAlias == "" || newAlias == "" {
		return ErrNullAlias
	}

	reg.assetMu.Lock()
	defer reg.assetMu.Unlock()

	if _, err := reg.FindByAlias(newAlias); err == nil {
		return ErrDuplicateAlias
	}

	findAsset, err := reg.FindByAlias(oldAlias)
	if err != nil {
		return err
	}

	storeBatch := reg.db.NewBatch()
	findAsset.Alias = &newAlias
	assetID := &findAsset.AssetID
	rawAsset, err := json.Marshal(findAsset)
	if err != nil {
		return err
	}

	storeBatch.Set(Key(assetID), rawAsset)
	storeBatch.Set(aliasKey(newAlias), []byte(assetID.String()))
	storeBatch.Delete(aliasKey(oldAlias))
	storeBatch.Write()

	reg.cacheMu.Lock()
	reg.aliasCache.Add(newAlias, assetID)
	reg.aliasCache.Remove(oldAlias)
	reg.cacheMu.Unlock()

	return nil
}
