package org.apereo.cas.ticket.registry;

import org.apereo.cas.redis.core.util.RedisUtils;
import org.apereo.cas.ticket.Ticket;
import org.apereo.cas.util.LoggingUtils;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.apache.commons.io.IOUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;

import java.util.Collection;
import java.util.Objects;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

/**
 * Key-value ticket registry implementation that stores tickets in redis keyed on the ticket ID.
 *
 * @author serv
 * @since 5.1.0
 */
@Slf4j
@RequiredArgsConstructor
public class RedisTicketRegistry extends AbstractTicketRegistry {
    private static final String CAS_TICKET_PREFIX = "CAS_TICKET:";

    private final RedisTemplate<String, Ticket> client;

    private final long scanCount;

    @Override
    @SuppressWarnings("java:S2583")
    public long deleteAll() {
        val redisKeys = RedisUtils.keys(this.client, getPatternTicketRedisKey(), this.scanCount)
            .collect(Collectors.toSet());
        val size = Objects.requireNonNull(redisKeys).size();
        this.client.delete(redisKeys);
        return size;
    }

    @Override
    public boolean deleteSingleTicket(final String ticketId) {
        val redisKey = getTicketRedisKey(encodeTicketId(ticketId));
        this.client.delete(redisKey);
        return true;
    }

    @Override
    public void addTicketInternal(final Ticket ticket) {
        try {
            LOGGER.debug("Adding ticket [{}]", ticket);
            val redisKey = getTicketRedisKey(encodeTicketId(ticket.getId()));
            val encodeTicket = encodeTicket(ticket);
            val timeout = getTimeout(ticket);
            this.client.boundValueOps(redisKey).set(encodeTicket, timeout, TimeUnit.SECONDS);
        } catch (final Exception e) {
            LOGGER.error("Failed to add [{}]", ticket);
            LoggingUtils.error(LOGGER, e);
        }
    }

    @Override
    public Ticket getTicket(final String ticketId, final Predicate<Ticket> predicate) {
        try {
            val redisKey = getTicketRedisKey(encodeTicketId(ticketId));
            val t = this.client.boundValueOps(redisKey).get();
            if (t != null) {
                val result = decodeTicket(t);
                if (predicate.test(result)) {
                    return result;
                }
                LOGGER.trace("The condition enforced by [{}] cannot successfully accept/test the ticket id [{}]", ticketId,
                    predicate.getClass().getSimpleName());
                return null;
            }
        } catch (final Exception e) {
            LOGGER.error("Failed fetching [{}]", ticketId);
            LoggingUtils.error(LOGGER, e);
        }
        return null;
    }

    @Override
    public Collection<? extends Ticket> getTickets() {
        try (val ticketsStream = stream()) {
            return ticketsStream.collect(Collectors.toSet());
        }
    }

    @Override
    public Stream<? extends Ticket> stream() {
        return getKeysStream()
            .map(redisKey -> {
                val ticket = this.client.boundValueOps(redisKey).get();
                if (ticket == null) {
                    this.client.delete(redisKey);
                    return null;
                }
                return ticket;
            })
            .filter(Objects::nonNull)
            .map(this::decodeTicket)
            .filter(Objects::nonNull);
    }

    @Override
    public Ticket updateTicket(final Ticket ticket) {
        try {
            LOGGER.debug("Updating ticket [{}]", ticket);
            val encodeTicket = this.encodeTicket(ticket);
            val redisKey = getTicketRedisKey(encodeTicketId(ticket.getId()));
            LOGGER.debug("Fetched redis key [{}] for ticket [{}]", redisKey, ticket);

            val timeout = getTimeout(ticket);
            client.boundValueOps(redisKey).set(encodeTicket, timeout, TimeUnit.SECONDS);
            return encodeTicket;
        } catch (final Exception e) {
            LOGGER.error("Failed to update [{}]", ticket);
            LoggingUtils.error(LOGGER, e);
        }
        return null;
    }

    /**
     * If not time out value is specified, expire the ticket immediately.
     *
     * @param ticket the ticket
     * @return timeout
     */
    private static Long getTimeout(final Ticket ticket) {
        val ttl = ticket.getExpirationPolicy().getTimeToLive();
        if (ttl > Integer.MAX_VALUE) {
            return (long) Integer.MAX_VALUE;
        } else if (ttl <= 0) {
            return 1L;
        }
        return ttl;
    }

    private static String getTicketRedisKey(final String ticketId) {
        return CAS_TICKET_PREFIX + ticketId;
    }

    private static String getPatternTicketRedisKey() {
        return CAS_TICKET_PREFIX + '*';
    }

    /**
     * Get a stream of all CAS-related keys from Redis DB.
     *
     * @return stream of all CAS-related keys from Redis DB
     */
    private Stream<String> getKeysStream() {
        val cursor = Objects.requireNonNull(client.getConnectionFactory()).getConnection()
            .scan(ScanOptions.scanOptions().match(getPatternTicketRedisKey()).build());
        return StreamSupport
            .stream(Spliterators.spliteratorUnknownSize(cursor, Spliterator.ORDERED), false)
            .map(key -> (String) client.getKeySerializer().deserialize(key))
            .collect(Collectors.toSet())
            .stream()
        .onClose(() -> IOUtils.closeQuietly(cursor));
    }
}

@@ -259,46 +259,8 @@
 al;%0A
-import org.apache.commons.io.IOUtils;%0A
 impo
@@ -317,178 +317,61 @@
 te;%0A
-import org.springframework.data.redis.core.ScanOptions;%0A%0Aimport java.util.Collection;%0Aimport java.util.Objects;%0Aimport java.util.Spliterator;%0Aimport java.util.Spliterator
+%0Aimport java.util.Collection;%0Aimport java.util.Object
 s;%0Ai
@@ -515,47 +515,8 @@
 eam;
-%0Aimport java.util.stream.StreamSupport;
 %0A%0A/*
@@ -1019,93 +1019,23 @@
 s = 
-RedisUtils.keys(this.client, getPatternTicketRedisKey(), this.scanCount)%0A            
+getKeysStream()
 .col
@@ -5069,492 +5069,86 @@
     
-val cursor = Objects.requireNonNull(client.getConnectionFactory()).getConnection()%0A            .scan(ScanOptions.scanOptions().match(getPatternTicketRedisKey()).build());%0A        return StreamSupport%0A            .stream(Spliterators.spliteratorUnknownSize(cursor, Spliterator.ORDERED), false)%0A            .map(key -%3E (String) client.getKeySerializer().deserialize(key))%0A            .collect(Collectors.toSet())%0A            .stream()%0A        .onClose(() -%3E IOUtils.closeQuietly(cursor)
+return RedisUtils.keys(this.client, getPatternTicketRedisKey(), this.scanCount
 );%0A 

simplify RedisTicketRegistry by using RedisUtils