package cache

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"os"
	"time"

	"github.com/go-redis/redis/v7"
)

type ValKeyCacheConfig struct {
	Addrs          []string
	Password       string
	ReadOnly       bool
	DialTimeout    time.Duration
	PoolSize       int
	DefaultTTL     time.Duration
	RootCaFilePath string
}

type valkeyClient interface {
	Set(key string, value any, expiration time.Duration) *redis.StatusCmd
	Get(key string) *redis.StringCmd
	Del(keys ...string) *redis.IntCmd
	Exists(keys ...string) *redis.IntCmd
	Ping() *redis.StatusCmd
}

type valKeyCache struct {
	config ValKeyCacheConfig
	client valkeyClient
}

func newValKeyCache(c Config) (*valKeyCache, error) {
	cfg, ok := c.(ValKeyCacheConfig)
	if !ok {
		return nil, ErrInvalidConfig
	}

	caCert, err := os.ReadFile(cfg.RootCaFilePath)
	if err != nil {
		return nil, fmt.Errorf("%w: %s", ErrTlsConfig, err.Error())
	}

	caCertPool := x509.NewCertPool()

	if ok := caCertPool.AppendCertsFromPEM(caCert); !ok {
		return nil, fmt.Errorf("%w: %s", ErrTlsConfig, "failed to append CA cert to pool")
	}

	tlsConfig := &tls.Config{
		RootCAs:    caCertPool,
		MinVersion: tls.VersionTLS12,
	}

	client := redis.NewUniversalClient(&redis.UniversalOptions{
		Addrs:       cfg.Addrs,
		Password:    cfg.Password,
		ReadOnly:    cfg.ReadOnly,
		DialTimeout: cfg.DialTimeout,
		PoolSize:    cfg.PoolSize,
		TLSConfig:   tlsConfig,
	})

	if err := client.Ping().Err(); err != nil {
		return nil, fmt.Errorf("%w: %v", ErrConnect, err)
	}

	return &valKeyCache{
		config: cfg,
		client: client,
	}, nil
}

func (c *valKeyCache) Get(ctx context.Context, key string, valueType ValueType) (string, error) {
	if err := c.checkInstance(); err != nil {
		return "", fmt.Errorf("%w: %v", ErrGet, err)
	}

	val, err := c.client.Get(getKey(key, valueType)).Result()
	if err == redis.Nil {
		return "", ErrKeyNotFound
	}

	if err != nil {
		return "", fmt.Errorf("%w: %v", ErrGet, err)
	}

	return val, nil
}

func (c *valKeyCache) Set(ctx context.Context, key string, valueType ValueType, value any, expiration time.Duration) error {
	if err := c.checkInstance(); err != nil {
		return fmt.Errorf("%w: %v", ErrSet, err)
	}

	key = getKey(key, valueType)

	if exists, err := c.client.Exists(key).Result(); err != nil || exists == 1 {
		if exists == 1 {
			return ErrKeyExists
		}

		return fmt.Errorf("%w: %v", ErrSet, err)
	}

	if err := c.client.Set(key, value, expiration).Err(); err != nil {
		return fmt.Errorf("%w: %v", ErrSet, err)
	}

	return nil
}

func (c *valKeyCache) Del(ctx context.Context, key string, valueType ValueType) error {
	if err := c.checkInstance(); err != nil {
		return fmt.Errorf("%w: %v", ErrDel, err.Error())
	}

	if res, err := c.client.Del(getKey(key, valueType)).Result(); err != nil || res == 0 {
		if res == 0 {
			return ErrKeyNotFound
		}

		return fmt.Errorf("%w: %s", ErrDel, err.Error())
	}

	return nil
}

func (c *valKeyCache) checkInstance() error {
	if c == nil {
		return ErrInvalidInstance
	}

	if s := c.client.Ping(); s.Err() != nil {
		return fmt.Errorf("%w: %v", ErrInvalidInstance, s.Err().Error())
	}

	return nil
}
