// Package redis provides Redis cache provisioning for projects.
// Uses Redis ACLs to isolate each project to its own key prefix.
package redis

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"log/slog"
	"strings"
	"time"

	"github.com/redis/go-redis/v9"

	"github.com/orchard9/rdev/internal/domain"
)

// Provisioner implements port.CacheProvisioner using Redis ACLs.
type Provisioner struct {
	client    *redis.Client
	host      string
	port      int
	keyPrefix string
	logger    *slog.Logger
}

// Config holds Redis provisioner configuration.
type Config struct {
	Host      string
	Port      int
	Password  string
	KeyPrefix string // Base prefix for project keys, default "project:"
}

// NewProvisioner creates a new Redis cache provisioner.
func NewProvisioner(cfg Config, logger *slog.Logger) (*Provisioner, error) {
	if cfg.KeyPrefix == "" {
		cfg.KeyPrefix = "project:"
	}

	client := redis.NewClient(&redis.Options{
		Addr:     fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
		Password: cfg.Password,
		DB:       0,
	})

	// Verify connection
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	if err := client.Ping(ctx).Err(); err != nil {
		return nil, fmt.Errorf("redis connection failed: %w", err)
	}

	return &Provisioner{
		client:    client,
		host:      cfg.Host,
		port:      cfg.Port,
		keyPrefix: cfg.KeyPrefix,
		logger:    logger,
	}, nil
}

// CreateProjectCache provisions isolated cache access for a project.
func (p *Provisioner) CreateProjectCache(ctx context.Context, projectID string) (*domain.CacheCredentials, error) {
	username := p.usernameFor(projectID)
	password, err := generateToken(32)
	if err != nil {
		return nil, fmt.Errorf("generate password: %w", err)
	}
	prefix := p.prefixFor(projectID)

	// Check if user already exists
	existing, err := p.client.Do(ctx, "ACL", "GETUSER", username).Result()
	if err == nil && existing != nil {
		p.logger.Warn("cache user already exists, recreating",
			"project_id", projectID,
			"username", username)
		// Delete existing user to recreate with new password
		if err := p.client.Do(ctx, "ACL", "DELUSER", username).Err(); err != nil {
			return nil, fmt.Errorf("delete existing user: %w", err)
		}
	}

	// Create ACL user with scoped permissions:
	// - on: user is active
	// - >password: set password
	// - ~prefix*: can only access keys matching this pattern
	// - +@all: allow all command categories
	// - -@dangerous: deny dangerous commands (FLUSHALL, SHUTDOWN, DEBUG, etc.)
	// - -@admin: deny admin commands (CONFIG, ACL, SLAVEOF, etc.)
	err = p.client.Do(ctx,
		"ACL", "SETUSER", username,
		"on",
		">"+password,
		"~"+prefix+"*",
		"+@all",
		"-@dangerous",
		"-@admin",
	).Err()
	if err != nil {
		return nil, fmt.Errorf("create ACL user: %w", err)
	}

	// Persist ACL changes to disk
	if err := p.client.Do(ctx, "ACL", "SAVE").Err(); err != nil {
		p.logger.Warn("failed to persist ACL to disk", "error", err)
		// Non-fatal: ACLs will still work until Redis restarts
	}

	p.logger.Info("created project cache",
		"project_id", projectID,
		"username", username,
		"prefix", prefix)

	url := fmt.Sprintf("redis://%s:%s@%s:%d", username, password, p.host, p.port)
	return &domain.CacheCredentials{
		ProjectID:  projectID,
		URL:        url,
		URLStaging: url, // Same for now; separate staging instance in future
		Prefix:     prefix,
		Username:   username,
		Host:       p.host,
		Port:       p.port,
		CreatedAt:  time.Now().UTC(),
	}, nil
}

// DeleteProjectCache removes cache access for a project.
func (p *Provisioner) DeleteProjectCache(ctx context.Context, projectID string, purgeKeys bool) error {
	username := p.usernameFor(projectID)
	prefix := p.prefixFor(projectID)

	// Delete ACL user
	result, err := p.client.Do(ctx, "ACL", "DELUSER", username).Result()
	if err != nil {
		return fmt.Errorf("delete ACL user: %w", err)
	}

	// ACL DELUSER returns number of users deleted
	deleted, ok := result.(int64)
	if !ok || deleted == 0 {
		p.logger.Warn("cache user did not exist", "project_id", projectID, "username", username)
	}

	// Optionally purge all project keys
	if purgeKeys {
		if err := p.purgeKeys(ctx, prefix); err != nil {
			p.logger.Warn("failed to purge project keys",
				"project_id", projectID,
				"prefix", prefix,
				"error", err)
			// Non-fatal: user is already deleted
		}
	}

	// Persist ACL changes
	if err := p.client.Do(ctx, "ACL", "SAVE").Err(); err != nil {
		p.logger.Warn("failed to persist ACL to disk", "error", err)
	}

	p.logger.Info("deleted project cache",
		"project_id", projectID,
		"username", username,
		"purged_keys", purgeKeys)

	return nil
}

// GetProjectCache retrieves cache credentials for a project.
// Note: Password cannot be retrieved from Redis ACL, only verified.
// Returns nil if user doesn't exist.
func (p *Provisioner) GetProjectCache(ctx context.Context, projectID string) (*domain.CacheCredentials, error) {
	username := p.usernameFor(projectID)
	prefix := p.prefixFor(projectID)

	// Check if user exists
	result, err := p.client.Do(ctx, "ACL", "GETUSER", username).Result()
	if err != nil {
		if strings.Contains(err.Error(), "User") {
			return nil, nil // User doesn't exist
		}
		return nil, fmt.Errorf("get ACL user: %w", err)
	}
	if result == nil {
		return nil, nil
	}

	// User exists but we can't retrieve password
	// Caller should use stored credentials from credential store
	return &domain.CacheCredentials{
		ProjectID: projectID,
		URL:       "", // Password not available
		Prefix:    prefix,
		Username:  username,
		Host:      p.host,
		Port:      p.port,
	}, nil
}

// TestConnection verifies Redis connectivity.
func (p *Provisioner) TestConnection(ctx context.Context) error {
	return p.client.Ping(ctx).Err()
}

// Close closes the Redis connection.
func (p *Provisioner) Close() error {
	return p.client.Close()
}

// usernameFor returns the Redis username for a project.
func (p *Provisioner) usernameFor(projectID string) string {
	// Sanitize project ID for Redis username (alphanumeric + hyphen)
	safe := strings.Map(func(r rune) rune {
		if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' {
			return r
		}
		return '-'
	}, projectID)
	return "proj-" + safe
}

// prefixFor returns the key prefix for a project.
func (p *Provisioner) prefixFor(projectID string) string {
	return p.keyPrefix + projectID + ":"
}

// purgeKeys deletes all keys matching the project prefix.
func (p *Provisioner) purgeKeys(ctx context.Context, prefix string) error {
	var cursor uint64
	var deleted int64

	for {
		keys, nextCursor, err := p.client.Scan(ctx, cursor, prefix+"*", 100).Result()
		if err != nil {
			return fmt.Errorf("scan keys: %w", err)
		}

		if len(keys) > 0 {
			n, err := p.client.Del(ctx, keys...).Result()
			if err != nil {
				return fmt.Errorf("delete keys: %w", err)
			}
			deleted += n
		}

		cursor = nextCursor
		if cursor == 0 {
			break
		}
	}

	p.logger.Debug("purged project keys", "prefix", prefix, "count", deleted)
	return nil
}

// generateToken generates a cryptographically secure random token.
func generateToken(length int) (string, error) {
	bytes := make([]byte, length)
	if _, err := rand.Read(bytes); err != nil {
		return "", err
	}
	return hex.EncodeToString(bytes), nil
}
