package db

import (
	"encoding/json"
	"fmt"
	"time"

	"github.com/gomodule/redigo/redis"
)

type RedisKV struct {
	Key       string    `json:"key"`
	Value     string    `json:"value"`
	Type      string    `json:"type"`
	Size      int64     `json:"size"`
	Length    int64     `json:"length"`
	TTL       int64     `json:"ttl"`
	UpdatedAt time.Time `json:"updated_at"`
}

type Redis struct {
	conn     redis.Conn
	username string
	password string
	address  string
}

func NewRedis(username, password, address string) (*Redis, error) {
	conn, err := redis.Dial("tcp", address, redis.DialUsername(username), redis.DialPassword(password))
	if err != nil {
		return nil, err
	}

	return &Redis{
		conn:     conn,
		username: username,
		password: password,
		address:  address,
	}, nil
}

func (r *Redis) Close() error {
	return r.conn.Close()
}

func (r *Redis) Exec(command string, args ...any) (any, error) {
	return r.conn.Do(command, args...)
}

func (r *Redis) Database() (int, error) {
	return redis.Int(r.conn.Do("CONFIG", "GET", "databases"))
}

func (r *Redis) Select(db int) error {
	_, err := r.conn.Do("SELECT", db)
	return err
}

func (r *Redis) Size() (int, error) {
	return redis.Int(r.conn.Do("DBSIZE"))
}

func (r *Redis) Data(page, pageSize int) ([]RedisKV, error) {
	result := make([]RedisKV, 0)
	cursor := 0
	var keys []string
	for {
		values, err := redis.Values(r.conn.Do("SCAN", cursor, "COUNT", 100))
		if err != nil {
			return nil, fmt.Errorf("failed to SCAN: %v", err)
		}
		var batch []string
		_, err = redis.Scan(values, &cursor, &batch)
		if err != nil {
			return nil, fmt.Errorf("failed to parse SCAN result: %v", err)
		}
		keys = append(keys, batch...)
		if cursor == 0 {
			break
		}
	}

	start := (page - 1) * pageSize
	end := start + pageSize
	if start >= len(keys) {
		return []RedisKV{}, nil
	}
	if end > len(keys) {
		end = len(keys)
	}

	paged := keys[start:end]
	for _, key := range paged {
		kv := RedisKV{
			Key: key,
		}

		keyType, err := redis.String(r.conn.Do("TYPE", key))
		if err != nil {
			continue
		}
		kv.Type = keyType

		ttl, err := redis.Int64(r.conn.Do("TTL", key))
		if err == nil {
			kv.TTL = ttl
		}
		idleTime, err := redis.Int64(r.conn.Do("OBJECT", "IDLETIME", key))
		if err == nil {
			kv.UpdatedAt = time.Now().Add(-time.Duration(idleTime) * time.Second)
		} else {
			kv.UpdatedAt = time.Now()
		}
		memory, err := redis.Int64(r.conn.Do("MEMORY", "USAGE", key))
		if err == nil {
			kv.Size = memory
		}

		var value any
		switch keyType {
		case "string":
			if value, err = redis.String(r.conn.Do("GET", key)); err == nil {
				kv.Length = int64(len(value.(string)))
			}
		case "list":
			if value, err = redis.Strings(r.conn.Do("LRANGE", key, 0, -1)); err == nil {
				kv.Length, _ = redis.Int64(r.conn.Do("LLEN", key))
			}
		case "set":
			if value, err = redis.Strings(r.conn.Do("SMEMBERS", key)); err == nil {
				kv.Length, _ = redis.Int64(r.conn.Do("SCARD", key))
			}
		case "zset":
			if members, err := redis.Strings(r.conn.Do("ZRANGE", key, 0, -1, "WITHSCORES")); err == nil {
				kv.Length, _ = redis.Int64(r.conn.Do("ZCARD", key))
				zsetMap := make(map[string]string)
				for i := 0; i < len(members); i += 2 {
					zsetMap[members[i]] = members[i+1]
				}
				value = zsetMap
			}
		case "hash":
			if value, err = redis.StringMap(r.conn.Do("HGETALL", key)); err == nil {
				kv.Length, _ = redis.Int64(r.conn.Do("HLEN", key))
			}
		default:
			continue
		}

		if err != nil {
			continue
		}
		if kv.Length > 500 {
			value = "data is too long, can't display"
		}

		if str, ok := value.(string); ok {
			kv.Value = str
		} else {
			encoded, err := json.Marshal(value)
			if err != nil {
				continue
			}
			kv.Value = string(encoded)
		}

		result = append(result, kv)
	}

	return result, nil
}

func (r *Redis) Clear() error {
	_, err := r.conn.Do("FLUSHDB")
	return err
}
