package main

import (
	"context"
	"encoding/base64"
	"errors"
	"fmt"
	"strconv"
	"time"

	"github.com/faroedev/faroe"
	"github.com/redis/go-redis/v9"
)

type storageStruct struct {
	client *redis.Client
}

func newStorage(client *redis.Client) *storageStruct {
	storage := &storageStruct{
		client: client,
	}
	return storage
}

func (storage *storageStruct) Get(key string) ([]byte, int32, error) {
	values, err := storage.client.HMGet(context.Background(), key, "value", "counter").Result()
	if err != nil && errors.Is(err, redis.Nil) {
		return nil, 0, faroe.ErrStorageEntryNotFound
	}
	if err != nil {
		return nil, 0, fmt.Errorf("failed to execute command: %s", err.Error())
	}

	if values[0] == nil || values[1] == nil {
		_, err = storage.client.Del(context.Background(), key).Result()
		if err != nil {
			return nil, 0, fmt.Errorf("failed to execute command: %s", err.Error())
		}
		return nil, 0, faroe.ErrStorageEntryNotFound
	}

	value, err := base64.StdEncoding.DecodeString(values[0].(string))
	if err != nil {
		_, err = storage.client.Del(context.Background(), key).Result()
		if err != nil {
			return nil, 0, fmt.Errorf("failed to execute command: %s", err.Error())
		}
		return nil, 0, faroe.ErrStorageEntryNotFound
	}

	counter, err := strconv.ParseInt(values[1].(string), 10, 32)
	if err != nil {
		_, err = storage.client.Del(context.Background(), key).Result()
		if err != nil {
			return nil, 0, fmt.Errorf("failed to execute command: %s", err.Error())
		}
		return nil, 0, faroe.ErrStorageEntryNotFound
	}

	return value, int32(counter), nil
}

const storageAddRedisScript = `local key = KEYS[1]

local encoded_value = ARGV[1]
local encoded_expires_at = ARGV[2]

local count = redis.call("EXISTS", key)
if count == 1 then
	return "already_exists"
end

redis.call("HSET", key, "value", encoded_value, "counter", 0)
redis.call("EXPIREAT", key, encoded_expires_at)
return "ok"`

func (storage *storageStruct) Add(key string, value []byte, expiresAt time.Time) error {
	keys := make([]string, 1)
	keys[0] = key

	encodedValue := base64.StdEncoding.EncodeToString(value)
	result, err := storage.client.Eval(context.Background(), storageAddRedisScript, keys, encodedValue, expiresAt.Unix()).Result()
	if err != nil {
		return fmt.Errorf("failed to execute command: %s", err.Error())
	}
	resultCode := result.(string)
	if resultCode == "already_exists" {
		return faroe.ErrStorageEntryAlreadyExists
	}

	return nil
}

const storageUpdateRedisScript = `local key = KEYS[1]

local encoded_value = ARGV[1]
local encoded_expires_at = ARGV[2]
local counter = tonumber(ARGV[3])

local encoded_stored_counter = redis.call("HGET", key, "counter")
if encoded_stored_counter == nil then
	return "not_found"
end

if tonumber(encoded_stored_counter) ~= counter then
    return "not_found"
end

redis.call("HSET", key, "value", encoded_value, "counter", counter + 1)
redis.call("EXPIREAT", key, encoded_expires_at)
return "ok"`

func (storage *storageStruct) Update(key string, value []byte, expiresAt time.Time, counter int32) error {
	keys := make([]string, 1)
	keys[0] = key

	encodedValue := base64.StdEncoding.EncodeToString(value)
	result, err := storage.client.Eval(context.Background(), storageUpdateRedisScript, keys, encodedValue, expiresAt.Unix(), counter).Result()
	if err != nil {
		return fmt.Errorf("failed to execute command: %s", err.Error())
	}
	resultCode := result.(string)
	if resultCode == "not_found" {
		return faroe.ErrStorageEntryNotFound
	}

	return nil
}

func (storage *storageStruct) Delete(key string) error {
	count, err := storage.client.Del(context.Background(), key).Result()
	if err != nil {
		return fmt.Errorf("failed to execute command: %s", err.Error())
	}

	if count < 1 {
		return faroe.ErrStorageEntryNotFound
	}

	return nil
}