package infra

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"sync"
	"time"

	_ "github.com/mattn/go-sqlite3"
)

// SQLiteCache es un cache key-value persistido en SQLite con soporte de TTL.
// Valores almacenados como JSON serializado. El caller es responsable de
// deserializar el []byte retornado por Get.
// Seguro para uso concurrente.
type SQLiteCache struct {
	db        *sql.DB
	namespace string
	mu        sync.Mutex
}

const sqliteCacheSchema = `
CREATE TABLE IF NOT EXISTS cache (
    namespace TEXT NOT NULL,
    key       TEXT NOT NULL,
    value     TEXT NOT NULL,
    created_at REAL NOT NULL,
    expires_at REAL,
    PRIMARY KEY (namespace, key)
);`

// CacheToSQLite abre (o crea) una base de datos SQLite en dbPath y retorna
// un SQLiteCache para el namespace dado.
func CacheToSQLite(dbPath, namespace string) (*SQLiteCache, error) {
	db, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL")
	if err != nil {
		return nil, fmt.Errorf("cache_to_sqlite: open db: %w", err)
	}
	if _, err := db.Exec(sqliteCacheSchema); err != nil {
		db.Close()
		return nil, fmt.Errorf("cache_to_sqlite: create schema: %w", err)
	}
	return &SQLiteCache{db: db, namespace: namespace}, nil
}

// evictExpired elimina las entradas expiradas del namespace. Debe llamarse
// con el mutex ya tomado.
func (c *SQLiteCache) evictExpired() {
	now := float64(time.Now().UnixNano()) / 1e9
	c.db.Exec(
		"DELETE FROM cache WHERE namespace = ? AND expires_at IS NOT NULL AND expires_at <= ?",
		c.namespace, now,
	)
}

// Get retorna el valor asociado a key y true, o nil y false si no existe o
// esta expirado. El []byte contiene JSON que el caller puede deserializar.
func (c *SQLiteCache) Get(key string) ([]byte, bool) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.evictExpired()
	var value string
	err := c.db.QueryRow(
		"SELECT value FROM cache WHERE namespace = ? AND key = ?",
		c.namespace, key,
	).Scan(&value)
	if err != nil {
		return nil, false
	}
	return []byte(value), true
}

// Set almacena value (JSON bytes) bajo key. ttl=0 significa sin expiracion.
func (c *SQLiteCache) Set(key string, value []byte, ttl time.Duration) error {
	c.mu.Lock()
	defer c.mu.Unlock()
	now := float64(time.Now().UnixNano()) / 1e9
	var expiresAt any
	if ttl > 0 {
		expiresAt = now + ttl.Seconds()
	}
	_, err := c.db.Exec(
		`INSERT INTO cache (namespace, key, value, created_at, expires_at)
		 VALUES (?, ?, ?, ?, ?)
		 ON CONFLICT(namespace, key) DO UPDATE SET
		     value = excluded.value,
		     created_at = excluded.created_at,
		     expires_at = excluded.expires_at`,
		c.namespace, key, string(value), now, expiresAt,
	)
	if err != nil {
		return fmt.Errorf("cache set: %w", err)
	}
	return nil
}

// Delete elimina la entrada asociada a key. Retorna error si falla la query.
func (c *SQLiteCache) Delete(key string) error {
	c.mu.Lock()
	defer c.mu.Unlock()
	_, err := c.db.Exec(
		"DELETE FROM cache WHERE namespace = ? AND key = ?",
		c.namespace, key,
	)
	if err != nil {
		return fmt.Errorf("cache delete: %w", err)
	}
	return nil
}

// Clear elimina todas las entradas del namespace. Retorna el numero de filas
// eliminadas.
func (c *SQLiteCache) Clear() (int64, error) {
	c.mu.Lock()
	defer c.mu.Unlock()
	res, err := c.db.Exec(
		"DELETE FROM cache WHERE namespace = ?",
		c.namespace,
	)
	if err != nil {
		return 0, fmt.Errorf("cache clear: %w", err)
	}
	n, _ := res.RowsAffected()
	return n, nil
}

// GetOrSet retorna el valor cacheado o llama factory() para obtenerlo,
// lo almacena con el ttl dado y lo retorna.
func (c *SQLiteCache) GetOrSet(key string, factory func() ([]byte, error), ttl time.Duration) ([]byte, error) {
	if v, ok := c.Get(key); ok {
		return v, nil
	}
	value, err := factory()
	if err != nil {
		return nil, fmt.Errorf("cache get_or_set factory: %w", err)
	}
	if err := c.Set(key, value, ttl); err != nil {
		return nil, err
	}
	return value, nil
}

// SetJSON serializa v como JSON y lo almacena bajo key.
func (c *SQLiteCache) SetJSON(key string, v any, ttl time.Duration) error {
	b, err := json.Marshal(v)
	if err != nil {
		return fmt.Errorf("cache set_json marshal: %w", err)
	}
	return c.Set(key, b, ttl)
}

// Close cierra la conexion a la base de datos.
func (c *SQLiteCache) Close() error {
	return c.db.Close()
}
