package com.ruoyi.common.core.cache;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.stereotype.Component;

@Component
public class InMemoryCacheStore
{
    private final ConcurrentHashMap<String, InMemoryCacheEntry> entries = new ConcurrentHashMap<>();
    private final AtomicLong hitCount = new AtomicLong();
    private final AtomicLong missCount = new AtomicLong();
    private final AtomicLong expiredCount = new AtomicLong();
    private final AtomicLong writeCount = new AtomicLong();

    public void set(String key, Object value)
    {
        putEntry(key, new InMemoryCacheEntry(value, null));
    }

    public void set(String key, Object value, long timeout, TimeUnit unit)
    {
        long expireAtMillis = System.currentTimeMillis() + Math.max(0L, unit.toMillis(timeout));
        putEntry(key, new InMemoryCacheEntry(value, expireAtMillis));
    }

    @SuppressWarnings("unchecked")
    public <T> T get(String key)
    {
        InMemoryCacheEntry entry = readEntry(key);
        return entry == null ? null : (T) entry.value();
    }

    public boolean hasKey(String key)
    {
        return readEntry(key) != null;
    }

    public boolean delete(String key)
    {
        return entries.remove(key) != null;
    }

    public boolean delete(Collection<String> keys)
    {
        boolean deleted = false;
        for (String key : keys)
        {
            deleted = delete(key) || deleted;
        }
        return deleted;
    }

    public Set<String> keys(String pattern)
    {
        purgeExpiredEntries();
        Set<String> matchedKeys = new TreeSet<>();
        entries.forEach((key, value) -> {
            if (matches(pattern, key))
            {
                matchedKeys.add(key);
            }
        });
        return matchedKeys;
    }

    public boolean expire(String key, long timeout, TimeUnit unit)
    {
        Objects.requireNonNull(unit, "TimeUnit must not be null");
        long expireAtMillis = System.currentTimeMillis() + Math.max(0L, unit.toMillis(timeout));
        return entries.computeIfPresent(key, (cacheKey, entry) -> entry.isExpired(System.currentTimeMillis())
                ? null
                : new InMemoryCacheEntry(entry.value(), expireAtMillis)) != null;
    }

    public long getExpire(String key)
    {
        return getExpire(key, TimeUnit.SECONDS);
    }

    public long getExpire(String key, TimeUnit unit)
    {
        InMemoryCacheEntry entry = readEntry(key);
        if (entry == null)
        {
            return -2L;
        }
        if (entry.expireAtMillis() == null)
        {
            return -1L;
        }
        long remainingMillis = Math.max(0L, entry.expireAtMillis() - System.currentTimeMillis());
        long unitMillis = Math.max(1L, unit.toMillis(1));
        return (remainingMillis + unitMillis - 1) / unitMillis;
    }

    public long increment(String key, long timeout, TimeUnit unit)
    {
        Objects.requireNonNull(unit, "TimeUnit must not be null");
        AtomicLong result = new AtomicLong();
        entries.compute(key, (cacheKey, currentEntry) -> {
            long now = System.currentTimeMillis();
            boolean missingOrExpired = currentEntry == null || currentEntry.isExpired(now);
            long nextValue = missingOrExpired ? 1L : toLong(currentEntry.value()) + 1L;
            Long expireAtMillis = missingOrExpired || currentEntry.expireAtMillis() == null
                    ? now + Math.max(0L, unit.toMillis(timeout))
                    : currentEntry.expireAtMillis();
            if (missingOrExpired && currentEntry != null)
            {
                expiredCount.incrementAndGet();
            }
            result.set(nextValue);
            return new InMemoryCacheEntry(nextValue, expireAtMillis);
        });
        writeCount.incrementAndGet();
        return result.get();
    }

    public void clear()
    {
        entries.clear();
    }

    public InMemoryCacheStats snapshot()
    {
        purgeExpiredEntries();
        return new InMemoryCacheStats(
                "IN_MEMORY",
                "single-instance",
                entries.size(),
                hitCount.get(),
                missCount.get(),
                expiredCount.get(),
                writeCount.get());
    }

    public <T> Map<String, T> getMap(String key)
    {
        Map<String, T> value = get(key);
        return value == null ? null : new HashMap<>(value);
    }

    public <T> void putMap(String key, Map<String, T> dataMap)
    {
        set(key, new HashMap<>(dataMap));
    }

    public <T> void putMapValue(String key, String mapKey, T value)
    {
        long ttl = getExpire(key, TimeUnit.MILLISECONDS);
        Map<String, T> current = getMap(key);
        if (current == null)
        {
            current = new HashMap<>();
        }
        current.put(mapKey, value);
        setWithOptionalTtl(key, current, ttl);
    }

    public boolean deleteMapValue(String key, String mapKey)
    {
        long ttl = getExpire(key, TimeUnit.MILLISECONDS);
        Map<String, Object> current = getMap(key);
        if (current == null || !current.containsKey(mapKey))
        {
            return false;
        }
        current.remove(mapKey);
        setWithOptionalTtl(key, current, ttl);
        return true;
    }

    public <T> Set<T> getSet(String key)
    {
        Set<T> value = get(key);
        return value == null ? null : new HashSet<>(value);
    }

    public <T> void putSet(String key, Set<T> dataSet)
    {
        set(key, new HashSet<>(dataSet));
    }

    public <T> java.util.List<T> getList(String key)
    {
        java.util.List<T> value = get(key);
        return value == null ? null : new java.util.ArrayList<>(value);
    }

    public <T> void putList(String key, java.util.List<T> dataList)
    {
        set(key, new java.util.ArrayList<>(dataList));
    }

    private void putEntry(String key, InMemoryCacheEntry entry)
    {
        entries.put(key, entry);
        writeCount.incrementAndGet();
    }

    private InMemoryCacheEntry readEntry(String key)
    {
        InMemoryCacheEntry entry = entries.get(key);
        if (entry == null)
        {
            missCount.incrementAndGet();
            return null;
        }
        if (entry.isExpired(System.currentTimeMillis()))
        {
            removeExpiredEntry(key, entry);
            missCount.incrementAndGet();
            return null;
        }
        hitCount.incrementAndGet();
        return entry;
    }

    private void purgeExpiredEntries()
    {
        long now = System.currentTimeMillis();
        entries.forEach((key, entry) -> {
            if (entry.isExpired(now))
            {
                removeExpiredEntry(key, entry);
            }
        });
    }

    private void removeExpiredEntry(String key, InMemoryCacheEntry expectedEntry)
    {
        if (entries.remove(key, expectedEntry))
        {
            expiredCount.incrementAndGet();
        }
    }

    private boolean matches(String pattern, String key)
    {
        if ("*".equals(pattern))
        {
            return true;
        }
        if (pattern.endsWith("*"))
        {
            return key.startsWith(pattern.substring(0, pattern.length() - 1));
        }
        return key.equals(pattern);
    }

    private long toLong(Object value)
    {
        if (value instanceof Number number)
        {
            return number.longValue();
        }
        return Long.parseLong(String.valueOf(value));
    }

    private void setWithOptionalTtl(String key, Object value, long ttlMillis)
    {
        if (ttlMillis > 0)
        {
            set(key, value, ttlMillis, TimeUnit.MILLISECONDS);
            return;
        }
        set(key, value);
    }
}
