package cache

import (
	"github.com/micro/go-micro/v3/store"
	"github.com/micro/go-micro/v3/store/memory"
)

// cache store is a store with caching to reduce IO where applicable.
// A memory store is used to cache reads from the given backing store.
// Reads are read through, writes are write-through
type cache struct {
	m       store.Store // the memory store
	b       store.Store // the backing store, could be file, cockroach etc
	options store.Options
}

// NewStore returns a new cache store
func NewStore(store store.Store, opts ...store.Option) store.Store {
	cf := &cache{
		m: memory.NewStore(opts...),
		b: store,
	}
	return cf

}

func (c *cache) init(opts ...store.Option) error {
	for _, o := range opts {
		o(&c.options)
	}
	return nil
}

// Init initialises the underlying stores
func (c *cache) Init(opts ...store.Option) error {
	if err := c.init(opts...); err != nil {
		return err
	}
	if err := c.m.Init(opts...); err != nil {
		return err
	}
	return c.b.Init(opts...)
}

// Options allows you to view the current options.
func (c *cache) Options() store.Options {
	return c.options
}

// Read takes a single key name and optional ReadOptions. It returns matching []*Record or an error.
func (c *cache) Read(key string, opts ...store.ReadOption) ([]*store.Record, error) {
	recs, err := c.m.Read(key, opts...)
	if err != nil && err != store.ErrNotFound {
		return nil, err
	}
	if len(recs) > 0 {
		return recs, nil
	}
	recs, err = c.b.Read(key, opts...)
	if err == nil {
		for _, rec := range recs {
			if err := c.m.Write(rec); err != nil {
				return nil, err
			}
		}
	}
	return recs, err
}

// Write() writes a record to the store, and returns an error if the record was not written.
// If the write succeeds in writing to memory but fails to write through to file, you'll receive an error
// but the value may still reside in memory so appropriate action should be taken.
func (c *cache) Write(r *store.Record, opts ...store.WriteOption) error {
	if err := c.m.Write(r, opts...); err != nil {
		return err
	}
	return c.b.Write(r, opts...)
}

// Delete removes the record with the corresponding key from the store.
// If the delete succeeds in writing to memory but fails to write through to file, you'll receive an error
// but the value may still reside in memory so appropriate action should be taken.
func (c *cache) Delete(key string, opts ...store.DeleteOption) error {
	if err := c.m.Delete(key, opts...); err != nil {
		return err
	}
	return c.b.Delete(key, opts...)
}

// List returns any keys that match, or an empty list with no error if none matched.
func (c *cache) List(opts ...store.ListOption) ([]string, error) {
	keys, err := c.m.List(opts...)
	if err != nil && err != store.ErrNotFound {
		return nil, err
	}
	if len(keys) > 0 {
		return keys, nil
	}
	keys, err = c.b.List(opts...)
	if err == nil {
		for _, key := range keys {
			recs, err := c.b.Read(key)
			if err != nil {
				return nil, err
			}
			for _, r := range recs {
				if err := c.m.Write(r); err != nil {
					return nil, err
				}
			}

		}
	}
	return keys, err
}

// Close the store and the underlying store
func (c *cache) Close() error {
	if err := c.m.Close(); err != nil {
		return err
	}
	return c.b.Close()
}

// String returns the name of the implementation.
func (c *cache) String() string {
	return "cache"
}