package com.lingniu.ingest.session;

import com.lingniu.ingest.api.ProtocolId;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.time.Duration;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.UnaryOperator;

/**
 * Redis-backed session store for multi-instance command routing metadata.
 *
 * <p>The Netty Channel itself stays local to the protocol instance. Redis keeps
 * the stable terminal indexes so HTTP callers can resolve sessionId / VIN /
 * phone consistently across restarts and replicas.
 */
public final class RedisSessionStore implements SessionStore {

    private static final String PREFIX = "vehicle:session:";
    private static final String ID_SET_KEY = PREFIX + "index:ids";

    private final StringRedisTemplate redis;
    private final Duration ttl;

    public RedisSessionStore(StringRedisTemplate redis, Duration ttl) {
        this.redis = redis;
        this.ttl = ttl;
    }

    @Override
    public void put(DeviceSession session) {
        // 同一个 sessionId 重新写入时先清理旧 VIN/phone 索引，避免旧外部号继续指向新会话。
        findBySessionId(session.sessionId()).ifPresent(old -> deleteIndexes(old));
        redis.opsForHash().putAll(sessionKey(session.sessionId()), toHash(session));
        redis.expire(sessionKey(session.sessionId()), ttl);
        if (hasText(session.vin())) {
            redis.opsForValue().set(vinIndexKey(session.vin()), session.sessionId(), ttl);
        }
        if (hasText(session.phone())) {
            redis.opsForValue().set(phoneIndexKey(session.phone()), session.sessionId(), ttl);
        }
        redis.opsForSet().add(ID_SET_KEY, session.sessionId());
        redis.expire(ID_SET_KEY, ttl);
    }

    @Override
    public Optional<DeviceSession> findBySessionId(String sessionId) {
        if (!hasText(sessionId)) {
            return Optional.empty();
        }
        Map<Object, Object> values = redis.opsForHash().entries(sessionKey(sessionId));
        if (values == null || values.isEmpty()) {
            return Optional.empty();
        }
        redis.expire(sessionKey(sessionId), ttl);
        return Optional.of(fromHash(values));
    }

    @Override
    public Optional<DeviceSession> findByVin(String vin) {
        if (!hasText(vin)) {
            return Optional.empty();
        }
        String sessionId = redis.opsForValue().get(vinIndexKey(vin));
        Optional<DeviceSession> session = findBySessionId(sessionId);
        if (session.isEmpty() && hasText(sessionId)) {
            // 二级索引可能比主 hash 晚过期；发现悬挂索引时立即清理。
            redis.delete(vinIndexKey(vin));
        }
        return session;
    }

    @Override
    public Optional<DeviceSession> findByPhone(String phone) {
        if (!hasText(phone)) {
            return Optional.empty();
        }
        String sessionId = redis.opsForValue().get(phoneIndexKey(phone));
        Optional<DeviceSession> session = findBySessionId(sessionId);
        if (session.isEmpty() && hasText(sessionId)) {
            // 二级索引可能比主 hash 晚过期；发现悬挂索引时立即清理。
            redis.delete(phoneIndexKey(phone));
        }
        return session;
    }

    @Override
    public synchronized Optional<DeviceSession> update(String sessionId, UnaryOperator<DeviceSession> updater) {
        Optional<DeviceSession> current = findBySessionId(sessionId);
        if (current.isEmpty()) {
            return Optional.empty();
        }
        DeviceSession next = updater.apply(current.orElseThrow());
        put(next);
        return Optional.of(next);
    }

    @Override
    public void remove(String sessionId) {
        Optional<DeviceSession> current = findBySessionId(sessionId);
        redis.delete(sessionKey(sessionId));
        current.ifPresent(this::deleteIndexes);
        redis.opsForSet().remove(ID_SET_KEY, sessionId);
    }

    @Override
    public int size() {
        Long size = redis.opsForSet().size(ID_SET_KEY);
        if (size == null || size <= 0) {
            return 0;
        }
        return size > Integer.MAX_VALUE ? Integer.MAX_VALUE : size.intValue();
    }

    private void deleteIndexes(DeviceSession session) {
        if (hasText(session.vin())) {
            redis.delete(vinIndexKey(session.vin()));
        }
        if (hasText(session.phone())) {
            redis.delete(phoneIndexKey(session.phone()));
        }
    }

    private static Map<String, String> toHash(DeviceSession session) {
        Map<String, String> values = new LinkedHashMap<>();
        // attr.* 保留协议侧附加字段，后续扩展不需要改 Redis schema。
        put(values, "sessionId", session.sessionId());
        put(values, "protocol", session.protocol() == null ? null : session.protocol().name());
        put(values, "vin", session.vin());
        put(values, "phone", session.phone());
        put(values, "token", session.token());
        put(values, "peerAddress", session.peerAddress());
        put(values, "registeredAt", session.registeredAt() == null ? null : session.registeredAt().toString());
        put(values, "lastSeenAt", session.lastSeenAt() == null ? null : session.lastSeenAt().toString());
        if (session.attributes() != null) {
            session.attributes().forEach((key, value) -> put(values, "attr." + key, value));
        }
        return values;
    }

    private static DeviceSession fromHash(Map<Object, Object> values) {
        Map<String, String> attributes = new LinkedHashMap<>();
        values.forEach((key, value) -> {
            String k = String.valueOf(key);
            if (k.startsWith("attr.") && value != null) {
                attributes.put(k.substring("attr.".length()), String.valueOf(value));
            }
        });
        return new DeviceSession(
                value(values, "sessionId"),
                ProtocolId.valueOf(value(values, "protocol")),
                value(values, "vin"),
                value(values, "phone"),
                value(values, "token"),
                value(values, "peerAddress"),
                Instant.parse(value(values, "registeredAt")),
                Instant.parse(value(values, "lastSeenAt")),
                Map.copyOf(attributes));
    }

    private static void put(Map<String, String> values, String key, String value) {
        if (value != null) {
            values.put(key, value);
        }
    }

    private static String value(Map<Object, Object> values, String key) {
        Object value = values.get(key);
        return value == null ? null : String.valueOf(value);
    }

    private static String sessionKey(String sessionId) {
        return PREFIX + sessionId;
    }

    private static String vinIndexKey(String vin) {
        return PREFIX + "index:vin:" + vin;
    }

    private static String phoneIndexKey(String phone) {
        return PREFIX + "index:phone:" + phone;
    }

    private static boolean hasText(String value) {
        return value != null && !value.isBlank();
    }
}
