package blockstore

import (
	"context"
	"io"

	"golang.org/x/xerrors"

	blocks "github.com/ipfs/go-block-format"
	cid "github.com/ipfs/go-cid"
	mh "github.com/multiformats/go-multihash"
)

var _ Blockstore = (*idstore)(nil)

type idstore struct {
	bs Blockstore
}

func NewIDStore(bs Blockstore) Blockstore {
	return &idstore{bs: bs}
}

func decodeCid(cid cid.Cid) (inline bool, data []byte, err error) {
	if cid.Prefix().MhType != mh.IDENTITY {
		return false, nil, nil
	}

	dmh, err := mh.Decode(cid.Hash())
	if err != nil {
		return false, nil, err
	}

	if dmh.Code == mh.IDENTITY {
		return true, dmh.Digest, nil
	}

	return false, nil, err
}

func (b *idstore) Has(cid cid.Cid) (bool, error) {
	inline, _, err := decodeCid(cid)
	if err != nil {
		return false, xerrors.Errorf("error decoding Cid: %w", err)
	}

	if inline {
		return true, nil
	}

	return b.bs.Has(cid)
}

func (b *idstore) Get(cid cid.Cid) (blocks.Block, error) {
	inline, data, err := decodeCid(cid)
	if err != nil {
		return nil, xerrors.Errorf("error decoding Cid: %w", err)
	}

	if inline {
		return blocks.NewBlockWithCid(data, cid)
	}

	return b.bs.Get(cid)
}

func (b *idstore) GetSize(cid cid.Cid) (int, error) {
	inline, data, err := decodeCid(cid)
	if err != nil {
		return 0, xerrors.Errorf("error decoding Cid: %w", err)
	}

	if inline {
		return len(data), err
	}

	return b.bs.GetSize(cid)
}

func (b *idstore) View(cid cid.Cid, cb func([]byte) error) error {
	inline, data, err := decodeCid(cid)
	if err != nil {
		return xerrors.Errorf("error decoding Cid: %w", err)
	}

	if inline {
		return cb(data)
	}

	return b.bs.View(cid, cb)
}

func (b *idstore) Put(blk blocks.Block) error {
	inline, _, err := decodeCid(blk.Cid())
	if err != nil {
		return xerrors.Errorf("error decoding Cid: %w", err)
	}

	if inline {
		return nil
	}

	return b.bs.Put(blk)
}

func (b *idstore) PutMany(blks []blocks.Block) error {
	toPut := make([]blocks.Block, 0, len(blks))
	for _, blk := range blks {
		inline, _, err := decodeCid(blk.Cid())
		if err != nil {
			return xerrors.Errorf("error decoding Cid: %w", err)
		}

		if inline {
			continue
		}
		toPut = append(toPut, blk)
	}

	if len(toPut) > 0 {
		return b.bs.PutMany(toPut)
	}

	return nil
}

func (b *idstore) DeleteBlock(cid cid.Cid) error {
	inline, _, err := decodeCid(cid)
	if err != nil {
		return xerrors.Errorf("error decoding Cid: %w", err)
	}

	if inline {
		return nil
	}

	return b.bs.DeleteBlock(cid)
}

func (b *idstore) DeleteMany(cids []cid.Cid) error {
	toDelete := make([]cid.Cid, 0, len(cids))
	for _, cid := range cids {
		inline, _, err := decodeCid(cid)
		if err != nil {
			return xerrors.Errorf("error decoding Cid: %w", err)
		}

		if inline {
			continue
		}
		toDelete = append(toDelete, cid)
	}

	if len(toDelete) > 0 {
		return b.bs.DeleteMany(toDelete)
	}

	return nil
}

func (b *idstore) AllKeysChan(ctx context.Context) (<-chan cid.Cid, error) {
	return b.bs.AllKeysChan(ctx)
}

func (b *idstore) HashOnRead(enabled bool) {
	b.bs.HashOnRead(enabled)
}

func (b *idstore) Close() error {
	if c, ok := b.bs.(io.Closer); ok {
		return c.Close()
	}
	return nil
}