// Package miniredis is a pure Go Redis test server, for use in Go unittests.
// There are no dependencies on system binaries, and every server you start
// will be empty.
//
// import "github.com/alicebob/miniredis/v2"
//
// Start a server with `s := miniredis.RunT(t)`, it'll be shutdown via a t.Cleanup().
// Or do everything manual: `s, err := miniredis.Run(); defer s.Close()`
//
// Point your Redis client to `s.Addr()` or `s.Host(), s.Port()`.
//
// Set keys directly via s.Set(...) and similar commands, or use a Redis client.
//
// For direct use you can select a Redis database with either `s.Select(12);
// s.Get("foo")` or `s.DB(12).Get("foo")`.
package miniredis

import (
	"context"
	"crypto/tls"
	"fmt"
	"math/rand"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/alicebob/miniredis/v2/proto"
	"github.com/alicebob/miniredis/v2/server"
)

var DumpMaxLineLen = 60

type hashKey map[string]string
type listKey []string
type setKey map[string]struct{}

// RedisDB holds a single (numbered) Redis database.
type RedisDB struct {
	master        *Miniredis               // pointer to the lock in Miniredis
	id            int                      // db id
	keys          map[string]string        // Master map of keys with their type
	stringKeys    map[string]string        // GET/SET &c. keys
	hashKeys      map[string]hashKey       // MGET/MSET &c. keys
	listKeys      map[string]listKey       // LPUSH &c. keys
	setKeys       map[string]setKey        // SADD &c. keys
	hllKeys       map[string]*hll          // PFADD &c. keys
	sortedsetKeys map[string]sortedSet     // ZADD &c. keys
	streamKeys    map[string]*streamKey    // XADD &c. keys
	ttl           map[string]time.Duration // effective TTL values
	lru           map[string]time.Time     // last recently used ( read or written to )
	keyVersion    map[string]uint          // used to watch values
}

// Miniredis is a Redis server implementation.
type Miniredis struct {
	sync.Mutex
	srv         *server.Server
	port        int
	passwords   map[string]string // username password
	dbs         map[int]*RedisDB
	selectedDB  int               // DB id used in the direct Get(), Set() &c.
	scripts     map[string]string // sha1 -> lua src
	signal      *sync.Cond
	now         time.Time // time.Now() if not set.
	subscribers map[*Subscriber]struct{}
	rand        *rand.Rand
	Ctx         context.Context
	CtxCancel   context.CancelFunc
}

type txCmd func(*server.Peer, *connCtx)

// database id + key combo
type dbKey struct {
	db  int
	key string
}

// connCtx has all state for a single connection.
// (this struct was named before context.Context existed)
type connCtx struct {
	selectedDB       int            // selected DB
	authenticated    bool           // auth enabled and a valid AUTH seen
	transaction      []txCmd        // transaction callbacks. Or nil.
	dirtyTransaction bool           // any error during QUEUEing
	watch            map[dbKey]uint // WATCHed keys
	subscriber       *Subscriber    // client is in PUBSUB mode if not nil
	nested           bool           // this is called via Lua
	nestedSHA        string         // set to the SHA of the nesting function
}

// NewMiniRedis makes a new, non-started, Miniredis object.
func NewMiniRedis() *Miniredis {
	m := Miniredis{
		dbs:         map[int]*RedisDB{},
		scripts:     map[string]string{},
		subscribers: map[*Subscriber]struct{}{},
	}
	m.Ctx, m.CtxCancel = context.WithCancel(context.Background())
	m.signal = sync.NewCond(&m)
	return &m
}

func newRedisDB(id int, m *Miniredis) RedisDB {
	return RedisDB{
		id:            id,
		master:        m,
		keys:          map[string]string{},
		lru:           map[string]time.Time{},
		stringKeys:    map[string]string{},
		hashKeys:      map[string]hashKey{},
		listKeys:      map[string]listKey{},
		setKeys:       map[string]setKey{},
		hllKeys:       map[string]*hll{},
		sortedsetKeys: map[string]sortedSet{},
		streamKeys:    map[string]*streamKey{},
		ttl:           map[string]time.Duration{},
		keyVersion:    map[string]uint{},
	}
}

// Run creates and Start()s a Miniredis.
func Run() (*Miniredis, error) {
	m := NewMiniRedis()
	return m, m.Start()
}

// Run creates and Start()s a Miniredis, TLS version.
func RunTLS(cfg *tls.Config) (*Miniredis, error) {
	m := NewMiniRedis()
	return m, m.StartTLS(cfg)
}

// Tester is a minimal version of a testing.T
type Tester interface {
	Fatalf(string, ...interface{})
	Cleanup(func())
	Logf(format string, args ...interface{})
}

// RunT start a new miniredis, pass it a testing.T. It also registers the cleanup after your test is done.
func RunT(t Tester) *Miniredis {
	m := NewMiniRedis()
	if err := m.Start(); err != nil {
		t.Fatalf("could not start miniredis: %s", err)
		// not reached
	}
	t.Cleanup(m.Close)
	return m
}

func runWithClient(t Tester) (*Miniredis, *proto.Client) {
	m := RunT(t)

	c, err := proto.Dial(m.Addr())
	if err != nil {
		t.Fatalf("could not connect to miniredis: %s", err)
	}
	t.Cleanup(func() {
		if err = c.Close(); err != nil {
			t.Logf("error closing connection to miniredis: %s", err)
		}
	})

	return m, c
}

// Start starts a server. It listens on a random port on localhost. See also
// Addr().
func (m *Miniredis) Start() error {
	s, err := server.NewServer(fmt.Sprintf("127.0.0.1:%d", m.port))
	if err != nil {
		return err
	}
	return m.start(s)
}

// Start starts a server, TLS version.
func (m *Miniredis) StartTLS(cfg *tls.Config) error {
	s, err := server.NewServerTLS(fmt.Sprintf("127.0.0.1:%d", m.port), cfg)
	if err != nil {
		return err
	}
	return m.start(s)
}

// StartAddr runs miniredis with a given addr. Examples: "127.0.0.1:6379",
// ":6379", or "127.0.0.1:0"
func (m *Miniredis) StartAddr(addr string) error {
	s, err := server.NewServer(addr)
	if err != nil {
		return err
	}
	return m.start(s)
}

// StartAddrTLS runs miniredis with a given addr, TLS version.
func (m *Miniredis) StartAddrTLS(addr string, cfg *tls.Config) error {
	s, err := server.NewServerTLS(addr, cfg)
	if err != nil {
		return err
	}
	return m.start(s)
}

func (m *Miniredis) start(s *server.Server) error {
	m.Lock()
	defer m.Unlock()
	m.srv = s
	m.port = s.Addr().Port

	commandsConnection(m)
	commandsGeneric(m)
	commandsServer(m)
	commandsString(m)
	commandsHash(m)
	commandsList(m)
	commandsPubsub(m)
	commandsSet(m)
	commandsSortedSet(m)
	commandsStream(m)
	commandsTransaction(m)
	commandsScripting(m)
	commandsGeo(m)
	commandsCluster(m)
	commandsHll(m)
	commandsClient(m)
	commandsObject(m)

	return nil
}

// Restart restarts a Close()d server on the same port. Values will be
// preserved.
func (m *Miniredis) Restart() error {
	return m.Start()
}

// Close shuts down a Miniredis.
func (m *Miniredis) Close() {
	m.Lock()

	if m.srv == nil {
		m.Unlock()
		return
	}
	srv := m.srv
	m.srv = nil
	m.CtxCancel()
	m.Unlock()

	// the OnDisconnect callbacks can lock m, so run Close() outside the lock.
	srv.Close()

}

// RequireAuth makes every connection need to AUTH first. This is the old 'AUTH [password] command.
// Remove it by setting an empty string.
func (m *Miniredis) RequireAuth(pw string) {
	m.RequireUserAuth("default", pw)
}

// Add a username/password, for use with 'AUTH [username] [password]'.
// There are currently no access controls for commands implemented.
// Disable access for the user with an empty password.
func (m *Miniredis) RequireUserAuth(username, pw string) {
	m.Lock()
	defer m.Unlock()
	if m.passwords == nil {
		m.passwords = map[string]string{}
	}
	if pw == "" {
		delete(m.passwords, username)
		return
	}
	m.passwords[username] = pw
}

// DB returns a DB by ID.
func (m *Miniredis) DB(i int) *RedisDB {
	m.Lock()
	defer m.Unlock()
	return m.db(i)
}

// get DB. No locks!
func (m *Miniredis) db(i int) *RedisDB {
	if db, ok := m.dbs[i]; ok {
		return db
	}
	db := newRedisDB(i, m) // main miniredis has our mutex.
	m.dbs[i] = &db
	return &db
}

// SwapDB swaps DBs by IDs.
func (m *Miniredis) SwapDB(i, j int) bool {
	m.Lock()
	defer m.Unlock()
	return m.swapDB(i, j)
}

// swap DB. No locks!
func (m *Miniredis) swapDB(i, j int) bool {
	db1 := m.db(i)
	db2 := m.db(j)

	db1.id = j
	db2.id = i

	m.dbs[i] = db2
	m.dbs[j] = db1

	return true
}

// Addr returns '127.0.0.1:12345'. Can be given to a Dial(). See also Host()
// and Port(), which return the same things.
func (m *Miniredis) Addr() string {
	m.Lock()
	defer m.Unlock()
	return m.srv.Addr().String()
}

// Host returns the host part of Addr().
func (m *Miniredis) Host() string {
	m.Lock()
	defer m.Unlock()
	return m.srv.Addr().IP.String()
}

// Port returns the (random) port part of Addr().
func (m *Miniredis) Port() string {
	m.Lock()
	defer m.Unlock()
	return strconv.Itoa(m.srv.Addr().Port)
}

// CommandCount returns the number of processed commands.
func (m *Miniredis) CommandCount() int {
	m.Lock()
	defer m.Unlock()
	return int(m.srv.TotalCommands())
}

// CurrentConnectionCount returns the number of currently connected clients.
func (m *Miniredis) CurrentConnectionCount() int {
	m.Lock()
	defer m.Unlock()
	return m.srv.ClientsLen()
}

// TotalConnectionCount returns the number of client connections since server start.
func (m *Miniredis) TotalConnectionCount() int {
	m.Lock()
	defer m.Unlock()
	return int(m.srv.TotalConnections())
}

// FastForward decreases all TTLs by the given duration. All TTLs <= 0 will be
// expired.
func (m *Miniredis) FastForward(duration time.Duration) {
	m.Lock()
	defer m.Unlock()
	for _, db := range m.dbs {
		db.fastForward(duration)
	}
}

// Server returns the underlying server to allow custom commands to be implemented
func (m *Miniredis) Server() *server.Server {
	return m.srv
}

// IsReadOnlyCommand checks if a command is marked as read-only
func (m *Miniredis) IsReadOnlyCommand(cmd string) bool {
	if m.srv == nil {
		return false
	}
	return m.srv.IsReadOnlyCommand(cmd)
}

// Dump returns a text version of the selected DB, usable for debugging.
//
// Dump limits the maximum length of each key:value to "DumpMaxLineLen" characters.
// To increase that, call something like:
//
//	miniredis.DumpMaxLineLen = 1024
//	mr, _ = miniredis.Run()
//	mr.Dump()
func (m *Miniredis) Dump() string {
	m.Lock()
	defer m.Unlock()

	var (
		maxLen = DumpMaxLineLen
		indent = "   "
		db     = m.db(m.selectedDB)
		r      = ""
		v      = func(s string) string {
			suffix := ""
			if len(s) > maxLen {
				suffix = fmt.Sprintf("...(%d)", len(s))
				s = s[:maxLen-len(suffix)]
			}
			return fmt.Sprintf("%q%s", s, suffix)
		}
	)

	for _, k := range db.allKeys() {
		r += fmt.Sprintf("- %s\n", k)
		t := db.t(k)
		switch t {
		case keyTypeString:
			r += fmt.Sprintf("%s%s\n", indent, v(db.stringKeys[k]))
		case keyTypeHash:
			for _, hk := range db.hashFields(k) {
				r += fmt.Sprintf("%s%s: %s\n", indent, hk, v(db.hashGet(k, hk)))
			}
		case keyTypeList:
			for _, lk := range db.listKeys[k] {
				r += fmt.Sprintf("%s%s\n", indent, v(lk))
			}
		case keyTypeSet:
			for _, mk := range db.setMembers(k) {
				r += fmt.Sprintf("%s%s\n", indent, v(mk))
			}
		case keyTypeSortedSet:
			for _, el := range db.ssetElements(k) {
				r += fmt.Sprintf("%s%f: %s\n", indent, el.score, v(el.member))
			}
		case keyTypeStream:
			for _, entry := range db.streamKeys[k].entries {
				r += fmt.Sprintf("%s%s\n", indent, entry.ID)
				ev := entry.Values
				for i := 0; i < len(ev)/2; i++ {
					r += fmt.Sprintf("%s%s%s: %s\n", indent, indent, v(ev[2*i]), v(ev[2*i+1]))
				}
			}
		case keyTypeHll:
			for _, entry := range db.hllKeys {
				r += fmt.Sprintf("%s%s\n", indent, v(string(entry.Bytes())))
			}
		default:
			r += fmt.Sprintf("%s(a %s, fixme!)\n", indent, t)
		}
	}
	return r
}

// SetTime sets the time against which EXPIREAT values are compared, and the
// time used in stream entry IDs.  Will use time.Now() if this is not set.
func (m *Miniredis) SetTime(t time.Time) {
	m.Lock()
	defer m.Unlock()
	m.now = t
}

// make every command return this message. For example:
//
//	LOADING Redis is loading the dataset in memory
//	MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'.
//
// Clear it with an empty string. Don't add newlines.
func (m *Miniredis) SetError(msg string) {
	cb := server.Hook(nil)
	if msg != "" {
		cb = func(c *server.Peer, cmd string, args ...string) bool {
			c.WriteError(msg)
			return true
		}
	}
	m.srv.SetPreHook(cb)
}

type argRequirements struct {
	minimum int
	maximum *int
}

func atLeast(n int) argRequirements {
	return argRequirements{n, nil}
}

func between(a int, b int) argRequirements {
	return argRequirements{a, &b}
}

func exactly(n int) argRequirements {
	return argRequirements{n, &n}
}

// isValidCMD returns true if command is valid and can be executed.
func (m *Miniredis) isValidCMD(c *server.Peer, cmd string, args []string, argReqs argRequirements) bool {
	if len(args) < argReqs.minimum || (argReqs.maximum != nil && len(args) > *argReqs.maximum) {
		setDirty(c)
		c.WriteError(errWrongNumber(cmd))
		return false
	}

	if !m.handleAuth(c) {
		return false
	}
	if m.checkPubsub(c, cmd) {
		return false
	}

	return true
}

// handleAuth returns false if connection has no access. It sends the reply.
func (m *Miniredis) handleAuth(c *server.Peer) bool {
	if getCtx(c).nested {
		return true
	}

	m.Lock()
	defer m.Unlock()
	if len(m.passwords) == 0 {
		return true
	}
	if !getCtx(c).authenticated {
		c.WriteError("NOAUTH Authentication required.")
		return false
	}
	return true
}

// handlePubsub sends an error to the user if the connection is in PUBSUB mode.
// It'll return true if it did.
func (m *Miniredis) checkPubsub(c *server.Peer, cmd string) bool {
	if getCtx(c).nested {
		return false
	}

	m.Lock()
	defer m.Unlock()

	ctx := getCtx(c)
	if ctx.subscriber == nil {
		return false
	}

	prefix := "ERR "
	if strings.ToLower(cmd) == "exec" {
		prefix = "EXECABORT Transaction discarded because of: "
	}
	c.WriteError(fmt.Sprintf(
		"%sCan't execute '%s': only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT are allowed in this context",
		prefix,
		strings.ToLower(cmd),
	))
	return true
}

func getCtx(c *server.Peer) *connCtx {
	if c.Ctx == nil {
		c.Ctx = &connCtx{}
	}
	return c.Ctx.(*connCtx)
}

func startTx(ctx *connCtx) {
	ctx.transaction = []txCmd{}
	ctx.dirtyTransaction = false
}

func stopTx(ctx *connCtx) {
	ctx.transaction = nil
	unwatch(ctx)
}

func inTx(ctx *connCtx) bool {
	return ctx.transaction != nil
}

func addTxCmd(ctx *connCtx, cb txCmd) {
	ctx.transaction = append(ctx.transaction, cb)
}

func watch(db *RedisDB, ctx *connCtx, key string) {
	if ctx.watch == nil {
		ctx.watch = map[dbKey]uint{}
	}
	ctx.watch[dbKey{db: db.id, key: key}] = db.keyVersion[key] // Can be 0.
}

func unwatch(ctx *connCtx) {
	ctx.watch = nil
}

// setDirty can be called even when not in an tx. Is an no-op then.
func setDirty(c *server.Peer) {
	if c.Ctx == nil {
		// No transaction. Not relevant.
		return
	}
	getCtx(c).dirtyTransaction = true
}

func (m *Miniredis) addSubscriber(s *Subscriber) {
	m.subscribers[s] = struct{}{}
}

// closes and remove the subscriber.
func (m *Miniredis) removeSubscriber(s *Subscriber) {
	_, ok := m.subscribers[s]
	delete(m.subscribers, s)
	if ok {
		s.Close()
	}
}

func (m *Miniredis) publish(c, msg string) int {
	n := 0
	for s := range m.subscribers {
		n += s.Publish(c, msg)
	}
	return n
}

// enter 'subscribed state', or return the existing one.
func (m *Miniredis) subscribedState(c *server.Peer) *Subscriber {
	ctx := getCtx(c)
	sub := ctx.subscriber
	if sub != nil {
		return sub
	}

	sub = newSubscriber()
	m.addSubscriber(sub)

	c.OnDisconnect(func() {
		m.Lock()
		m.removeSubscriber(sub)
		m.Unlock()
	})

	ctx.subscriber = sub

	go monitorPublish(c, sub.publish)
	go monitorPpublish(c, sub.ppublish)

	return sub
}

// whenever the p?sub count drops to 0 subscribed state should be stopped, and
// all redis commands are allowed again.
func endSubscriber(m *Miniredis, c *server.Peer) {
	ctx := getCtx(c)
	if sub := ctx.subscriber; sub != nil {
		m.removeSubscriber(sub) // will Close() the sub
	}
	ctx.subscriber = nil
}

// Start a new pubsub subscriber. It can (un) subscribe to channels and
// patterns, and has a channel to get published messages. Close it with
// Close().
// Does not close itself when there are no subscriptions left.
func (m *Miniredis) NewSubscriber() *Subscriber {
	sub := newSubscriber()

	m.Lock()
	m.addSubscriber(sub)
	m.Unlock()

	return sub
}

func (m *Miniredis) allSubscribers() []*Subscriber {
	var subs []*Subscriber
	for s := range m.subscribers {
		subs = append(subs, s)
	}
	return subs
}

func (m *Miniredis) Seed(seed int) {
	m.Lock()
	defer m.Unlock()

	// m.rand is not safe for concurrent use.
	m.rand = rand.New(rand.NewSource(int64(seed)))
}

func (m *Miniredis) randIntn(n int) int {
	if m.rand == nil {
		return rand.Intn(n)
	}
	return m.rand.Intn(n)
}

// shuffle shuffles a list of strings. Kinda.
func (m *Miniredis) shuffle(l []string) {
	for range l {
		i := m.randIntn(len(l))
		j := m.randIntn(len(l))
		l[i], l[j] = l[j], l[i]
	}
}

func (m *Miniredis) effectiveNow() time.Time {
	if !m.now.IsZero() {
		return m.now
	}
	return time.Now().UTC()
}

// convert a unixtimestamp to a duration, to use an absolute time as TTL.
// d can be either time.Second or time.Millisecond.
func (m *Miniredis) at(i int, d time.Duration) time.Duration {
	var ts time.Time
	switch d {
	case time.Millisecond:
		ts = time.Unix(int64(i/1000), 1000000*int64(i%1000))
	case time.Second:
		ts = time.Unix(int64(i), 0)
	default:
		panic("invalid time unit (d). Fixme!")
	}
	now := m.effectiveNow()
	return ts.Sub(now)
}

// copy does not mind if dst already exists.
func (m *Miniredis) copy(
	srcDB *RedisDB, src string,
	destDB *RedisDB, dst string,
) error {
	if !srcDB.exists(src) {
		return ErrKeyNotFound
	}

	switch srcDB.t(src) {
	case keyTypeString:
		destDB.stringKeys[dst] = srcDB.stringKeys[src]
	case keyTypeHash:
		destDB.hashKeys[dst] = copyHashKey(srcDB.hashKeys[src])
	case keyTypeList:
		destDB.listKeys[dst] = copyListKey(srcDB.listKeys[src])
	case keyTypeSet:
		destDB.setKeys[dst] = copySetKey(srcDB.setKeys[src])
	case keyTypeSortedSet:
		destDB.sortedsetKeys[dst] = copySortedSet(srcDB.sortedsetKeys[src])
	case keyTypeStream:
		destDB.streamKeys[dst] = srcDB.streamKeys[src].copy()
	case keyTypeHll:
		destDB.hllKeys[dst] = srcDB.hllKeys[src].copy()
	default:
		panic("missing case")
	}
	destDB.keys[dst] = srcDB.keys[src]
	destDB.incr(dst)
	if v, ok := srcDB.ttl[src]; ok {
		destDB.ttl[dst] = v
	}
	return nil
}

func copyHashKey(orig hashKey) hashKey {
	cpy := hashKey{}
	for k, v := range orig {
		cpy[k] = v
	}
	return cpy
}

func copyListKey(orig listKey) listKey {
	cpy := make(listKey, len(orig))
	copy(cpy, orig)
	return cpy
}

func copySetKey(orig setKey) setKey {
	cpy := setKey{}
	for k, v := range orig {
		cpy[k] = v
	}
	return cpy
}

func copySortedSet(orig sortedSet) sortedSet {
	cpy := sortedSet{}
	for k, v := range orig {
		cpy[k] = v
	}
	return cpy
}
