/*
 * Copyright (c) 2025 SimcuTeam. All rights reserved.
 * 遵循 MIT 许可证。
 */

package simcu::simapi.helpers

import std.collection.*
import std.collection.concurrent.*
import std.convert.*
import std.time.*
import simcu::serialization.*
import redis.client.*
import simcu::simapi.communications.*
import simcu::simapi.configurations.*
import simcu::simapi.exceptions.*

/**
 * 认证助手：基于 Header Token 的登录态管理。
 * 支持两种存储模式：
 * - Redis 模式：配置了 RedisConfiguration 时使用，支持多实例共享（可带密码/DB 索引）。
 * - InMemory 模式：未配置 Redis 时自动使用，登录态带过期时间，重启后丢失。
 */

/// InMemory 存储项：登录信息 JSON + 过期时间（epoch 毫秒，0 表示不过期）
private struct TokenEntry {
    var json: String
    var expireAt: Int64

    public init(json: String, expireAt: Int64) {
        this.json = json
        this.expireAt = expireAt
    }
}

public class SimApiAuth {
    private static let tokenCachePrefix = "SimApi:Auth:Token:"
    private static let tokenSetCachePrefix = "SimApi:Auth:User:"

    private var _redis: ?RedisClient = None
    private var _redisHost: String = ""
    private var _redisPort: UInt16 = 6379

    // InMemory 模式：token → 登录信息（含过期时间，epoch 毫秒；0 表示不过期）
    private let _tokenStore = ConcurrentHashMap<String, TokenEntry>()
    // InMemory 模式：userId → token 集合
    private let _userTokens = ConcurrentHashMap<String, HashSet<String>>()

    /**
     * 创建认证助手（依赖注入 SimApiOptions）。
     * @param options SimApi 配置（redisConfiguration 非空时使用 Redis，否则 InMemory）。
     */
    public init(options: SimApiOptions) {
        let redisConfiguration = options.redisConfiguration
        if (!redisConfiguration.isEmpty()) {
            let (host, port, pwd, db) = parseRedisConfig(redisConfiguration)
            _redisHost = host
            _redisPort = port
            let client = if (pwd.isEmpty()) {
                // autoHello=false：跳过 HELLO 3 协商（Redis 8.x 的 RESP3 响应含 modules 等嵌套结构，
                // redis-client 库解析偶发失败），直接用 RESP2 协议
                RedisClient(host, port, autoHello: false)
            } else {
                RedisClient(host, port, autoHello: false, authPassword: Some(pwd))
            }
            // 指定 DB 索引（redis 客户端无 select 方法，直接执行 SELECT 命令）
            if (db > 0) {
                try {
                    client.executeString(["SELECT", db.toString()])
                } catch (_: Exception) {
                }
            }
            _redis = Some(client)
        }
    }

    /**
     * 登录：生成 token 并保存登录信息。
     * @param loginItem 登录信息。
     * @param expireSeconds 过期秒数，默认 7 天。
     * @param token 指定 token（可选，默认随机生成）。
     * @return 登录 token。
     */
    public func login(loginItem: SimApiLoginItem, expireSeconds!: Int64 = 604800, token!: String = ""): String {
        let newToken = if (token.isEmpty()) { generateToken() } else { token }
        let tokenKey = "${tokenCachePrefix}${newToken}"
        let setKey = "${tokenSetCachePrefix}${loginItem.id}"
        let json = loginItemJson(loginItem)

        if (let Some(redis) <- _redis) {
            redis.set(tokenKey, Blob.fromUtf8(json), ex: Some(expireSeconds))
            redis.sadd(setKey, [Blob.fromUtf8(newToken)])
            redis.expire(setKey, expireSeconds)
        } else {
            _tokenStore[newToken] = TokenEntry(json, nowMillis() + expireSeconds * 1000)
            var tokens = _userTokens.get(loginItem.id)
            if (tokens == None) {
                tokens = HashSet<String>()
                _userTokens[loginItem.id] = tokens.getOrThrow()
            }
            tokens.getOrThrow().add(newToken)
        }
        return newToken
    }

    /**
     * 更新登录信息（token 不变）。
     * @param loginItem 新的登录信息。
     * @param token 已有 token。
     * @return 原 token。
     */
    public func update(loginItem: SimApiLoginItem, token: String): String {
        let tokenKey = "${tokenCachePrefix}${token}"
        let json = loginItemJson(loginItem)
        if (let Some(redis) <- _redis) {
            // 保留原过期时间：先读旧 TTL，SET 后重新续期
            let ttl = redis.ttl(tokenKey)
            redis.set(tokenKey, Blob.fromUtf8(json))
            if (ttl > 0) {
                redis.expire(tokenKey, ttl)
            }
        } else {
            // 保留原过期时间
            let expireAt = match (_tokenStore.get(token)) {
                case Some(entry) => entry.expireAt
                case None => 0
            }
            _tokenStore[token] = TokenEntry(json, expireAt)
        }
        return token
    }

    /**
     * 获取登录信息。
     * @param token 登录 token。
     * @return 登录信息；token 无效返回 None。
     */
    public func getLogin(token: String): ?SimApiLoginItem {
        let tokenKey = "${tokenCachePrefix}${token}"
        let json: ?String
        if (let Some(redis) <- _redis) {
            json = match (redis.get(tokenKey)) {
                case Some(blob) => Some(blob.toUtf8())
                case None => None
            }
        } else {
            json = match (_tokenStore.get(token)) {
                case Some(entry) =>
                    // InMemory 过期检查：超过 expireAt 则移除并视为无效
                    if (entry.expireAt > 0 && entry.expireAt < nowMillis()) {
                        _tokenStore.remove(token)
                        None
                    } else {
                        Some(entry.json)
                    }
                case None => None
            }
        }
        return match (json) {
            case Some(j) => parseLoginItemSafe(j)
            case None => None
        }
    }

    /// 解析登录信息；JSON 无效/非对象时返回 None（视为 token 无效）
    private static func parseLoginItemSafe(json: String): ?SimApiLoginItem {
        try {
            Some(JsonSerializer.deserialize<SimApiLoginItem>(json))
        } catch (_: Exception) {
            None
        }
    }

    /**
     * 获取某用户全部登录信息。
     * @param userId 用户 ID。
     * @return 登录信息数组。
     */
    public func getAllLogins(userId: String): Array<SimApiLoginItem> {
        let tokens = getTokensOfUser(userId)
        var result = ArrayList<SimApiLoginItem>()
        for (token in tokens) {
            if (let Some(item) <- getLogin(token)) {
                result.add(item)
            } else {
                removeTokenOfUser(userId, token)
            }
        }
        return result.toArray()
    }

    /**
     * 退出登录。
     * @param token 登录 token。
     */
    public func logout(token: String): Unit {
        let item = getLogin(token)
        if (let Some(item) <- item) {
            removeTokenOfUser(item.id, token)
        }
        let tokenKey = "${tokenCachePrefix}${token}"
        if (let Some(redis) <- _redis) {
            redis.del([tokenKey])
        } else {
            _tokenStore.remove(token)
        }
    }

    /**
     * 退出某用户全部登录。
     * @param userId 用户 ID。
     */
    public func logoutAll(userId: String): Unit {
        let tokens = getTokensOfUser(userId)
        if (let Some(redis) <- _redis) {
            for (token in tokens) {
                redis.del(["${tokenCachePrefix}${token}"])
            }
            redis.del(["${tokenSetCachePrefix}${userId}"])
        } else {
            _userTokens.remove(userId)
            for (token in tokens) {
                _tokenStore.remove(token)
            }
        }
    }

    private func getTokensOfUser(userId: String): ArrayList<String> {
        var result = ArrayList<String>()
        if (let Some(redis) <- _redis) {
            let members = redis.smembers("${tokenSetCachePrefix}${userId}")
            for (m in members) {
                result.add(m.toUtf8())
            }
        } else {
            if (let Some(tokens) <- _userTokens.get(userId)) {
                for (t in tokens) {
                    result.add(t)
                }
            }
        }
        return result
    }

    private func removeTokenOfUser(userId: String, token: String): Unit {
        if (let Some(redis) <- _redis) {
            redis.srem("${tokenSetCachePrefix}${userId}", [Blob.fromUtf8(token)])
        } else {
            if (let Some(tokens) <- _userTokens.get(userId)) {
                tokens.remove(token)
            }
        }
    }

    private static func generateToken(): String {
        // token = Guid.NewGuid().ToString()（小写、8-4-4-4-12）
        SimApiUtil.newGuid()
    }

    /// 解析 Redis 连接串，支持：host:port | host:port,password=xxx | host:port,password=xxx,db=2
    private static func parseRedisConfig(config: String): (String, UInt16, String, Int64) {
        var host = "127.0.0.1"
        var port = 6379u16
        var pwd = ""
        var db: Int64 = 0
        let segments = config.split(",")
        let hp = segments[0].split(":")
        if (hp.size == 2) {
            host = hp[0]
            port = UInt16.parse(hp[1])
        } else {
            host = config
        }
        for (i in 1..segments.size) {
            let seg = segments[i].trimAscii()
            match (seg.indexOf("=")) {
                case Some(idx) =>
                    let key = seg[0..idx].trimAscii().toAsciiLower()
                    let value = seg[idx + 1..].trimAscii()
                    match (key) {
                        case "password" | "pwd" => pwd = value
                        case "db" | "database" | "defaultdatabase" => db = Int64.parse(value)
                        case _ => ()
                    }
                case None => ()
            }
        }
        (host, port, pwd, db)
    }

    /// 当前时间（epoch 毫秒）
    private static func nowMillis(): Int64 {
        DateTime.nowUTC().toUnixTimeStamp().toMilliseconds()
    }

    private static func loginItemJson(item: SimApiLoginItem): String {
        // 统一 JSON 序列化：JsonSerializer.Serialize(item)
        JsonSerializer.serialize(item)
    }
}
