package repository

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"

	redislib "gateway/internal/library/redis"
	permission "gateway/internal/model/permission/domain"
	domrepo "gateway/internal/model/permission/domain/repository"

	"github.com/zeromicro/go-zero/core/stores/redis"
)

// CasbinRedisAdapter is a tenant-scoped Redis-backed Casbin policy store.
// Layout:
//
//	permission:casbin:rules:{tenant_id} → Redis Set of JSON-encoded
//	                                       []string rule rows.
//
// Atomicity: SaveAll uses a Pipelined DEL+SADD; AddPolicy/RemovePolicy
// rely on Redis Set semantics (idempotent inserts, single-shot removes).
type CasbinRedisAdapter struct {
	client *redis.Redis
}

// NewCasbinRedisAdapter returns a CasbinPolicyAdapter backed by Redis.
func NewCasbinRedisAdapter(client *redislib.Client) (domrepo.CasbinPolicyAdapter, error) {
	if client == nil || client.Zero() == nil {
		return nil, fmt.Errorf("permission: redis client is required for casbin adapter")
	}
	return &CasbinRedisAdapter{client: client.Zero()}, nil
}

func (a *CasbinRedisAdapter) key(tenantID string) string {
	return permission.GetCasbinRulesRedisKey(tenantID)
}

// LoadAll returns every rule for tenantID.
func (a *CasbinRedisAdapter) LoadAll(ctx context.Context, tenantID string) ([][]string, error) {
	raw, err := a.client.SmembersCtx(ctx, a.key(tenantID))
	if err != nil && !errors.Is(err, redis.Nil) {
		return nil, err
	}
	rules := make([][]string, 0, len(raw))
	for _, item := range raw {
		var rule []string
		if err := json.Unmarshal([]byte(item), &rule); err != nil {
			continue
		}
		if len(rule) == 0 {
			continue
		}
		rules = append(rules, rule)
	}
	return rules, nil
}

// SaveAll replaces all rules for tenantID with rules atomically.
func (a *CasbinRedisAdapter) SaveAll(ctx context.Context, tenantID string, rules [][]string) error {
	key := a.key(tenantID)
	encoded := make([]any, 0, len(rules))
	for _, rule := range rules {
		raw, err := json.Marshal(rule)
		if err != nil {
			return fmt.Errorf("permission: marshal casbin rule: %w", err)
		}
		encoded = append(encoded, string(raw))
	}
	return a.client.PipelinedCtx(ctx, func(p redis.Pipeliner) error {
		if err := p.Del(ctx, key).Err(); err != nil {
			return err
		}
		if len(encoded) == 0 {
			return nil
		}
		return p.SAdd(ctx, key, encoded...).Err()
	})
}

// AddPolicy inserts a single rule (idempotent).
func (a *CasbinRedisAdapter) AddPolicy(ctx context.Context, tenantID string, rule []string) error {
	raw, err := json.Marshal(rule)
	if err != nil {
		return err
	}
	_, err = a.client.SaddCtx(ctx, a.key(tenantID), string(raw))
	return err
}

// RemovePolicy removes a single rule.
func (a *CasbinRedisAdapter) RemovePolicy(ctx context.Context, tenantID string, rule []string) error {
	raw, err := json.Marshal(rule)
	if err != nil {
		return err
	}
	_, err = a.client.SremCtx(ctx, a.key(tenantID), string(raw))
	return err
}

// Clear empties all rules for tenantID.
func (a *CasbinRedisAdapter) Clear(ctx context.Context, tenantID string) error {
	_, err := a.client.DelCtx(ctx, a.key(tenantID))
	return err
}

var _ domrepo.CasbinPolicyAdapter = (*CasbinRedisAdapter)(nil)
