using System.Globalization;

namespace Eis.Infrastructure.Authentication;

public sealed class AuthenticationOptions
{
    private AuthenticationOptions(
        bool nativeEnabled,
        bool production,
        string? cacheRedisUrl,
        string? sessionRedisUrl,
        int sessionRedisDatabase,
        string redisPrefix,
        int sessionTtlSeconds,
        int loginChallengeTtlSeconds,
        int totpSetupTtlSeconds,
        int redisConnectTimeoutMilliseconds,
        string totpEncryptionMaterial)
    {
        NativeEnabled = nativeEnabled;
        Production = production;
        CacheRedisUrl = cacheRedisUrl;
        SessionRedisUrl = sessionRedisUrl;
        SessionRedisDatabase = sessionRedisDatabase;
        RedisPrefix = redisPrefix;
        SessionTtlSeconds = sessionTtlSeconds;
        LoginChallengeTtlSeconds = loginChallengeTtlSeconds;
        TotpSetupTtlSeconds = totpSetupTtlSeconds;
        RedisConnectTimeoutMilliseconds = redisConnectTimeoutMilliseconds;
        TotpEncryptionMaterial = totpEncryptionMaterial;
    }

    public bool NativeEnabled { get; }

    public bool Production { get; }

    public string? CacheRedisUrl { get; }

    public string? SessionRedisUrl { get; }

    public int SessionRedisDatabase { get; }

    public string RedisPrefix { get; }

    public int SessionTtlSeconds { get; }

    public int LoginChallengeTtlSeconds { get; }

    public int TotpSetupTtlSeconds { get; }

    public int RedisConnectTimeoutMilliseconds { get; }

    public string TotpEncryptionMaterial { get; }

    public bool UsesRedis => !string.IsNullOrWhiteSpace(SessionRedisUrl);

    public bool SharesLegacySessions => UsesRedis;

    public static AuthenticationOptions FromEnvironment(bool production, bool configuredNativeEnabled = false)
    {
        var nativeEnabled = configuredNativeEnabled ||
            ParseBoolean(Environment.GetEnvironmentVariable("AUTH_NATIVE_ENABLED"), false);
        var cacheUrl = Clean(Environment.GetEnvironmentVariable("REDIS_URL"));
        var explicitSessionUrl = Clean(Environment.GetEnvironmentVariable("REDIS_SESSION_URL"));
        var sessionUrl = explicitSessionUrl ?? cacheUrl;
        var cacheDatabase = RedisDatabase(cacheUrl);
        var sessionDatabaseText = Clean(Environment.GetEnvironmentVariable("REDIS_SESSION_DB"));
        var sessionDatabase = sessionDatabaseText is not null
            ? ParseNonNegativeInteger(sessionDatabaseText, cacheDatabase == 0 ? 1 : 0, 1024)
            : explicitSessionUrl is not null
                ? RedisDatabase(explicitSessionUrl)
                : cacheDatabase == 0 ? 1 : 0;

        if (cacheUrl is not null && sessionUrl is not null &&
            string.Equals(RedisEndpoint(cacheUrl), RedisEndpoint(sessionUrl), StringComparison.OrdinalIgnoreCase) &&
            cacheDatabase == sessionDatabase)
        {
            throw new InvalidOperationException(
                "Redis 认证状态必须使用与普通缓存不同的逻辑数据库；请配置 REDIS_SESSION_DB 或 REDIS_SESSION_URL");
        }

        var configuredTotpKey = Environment.GetEnvironmentVariable("TOTP_ENCRYPTION_KEY") ?? string.Empty;
        if (nativeEnabled && production && configuredTotpKey.Length < 32)
        {
            throw new InvalidOperationException("生产环境启用原生认证前必须设置至少 32 个字符的 TOTP_ENCRYPTION_KEY");
        }

        var initialPassword = Environment.GetEnvironmentVariable("INITIAL_ADMIN_PASSWORD") ?? "local-exam-system";
        var keyMaterial = configuredTotpKey.Length > 0 ? configuredTotpKey : $"development-only:{initialPassword}";
        var prefix = NormalizePrefix(Environment.GetEnvironmentVariable("REDIS_SESSION_PREFIX"));

        return new AuthenticationOptions(
            nativeEnabled,
            production,
            cacheUrl,
            sessionUrl,
            sessionDatabase,
            prefix,
            ParsePositiveInteger(Environment.GetEnvironmentVariable("AUTH_SESSION_TTL_SECONDS"), 8 * 60 * 60, 30 * 24 * 60 * 60),
            ParsePositiveInteger(Environment.GetEnvironmentVariable("AUTH_LOGIN_CHALLENGE_TTL_SECONDS"), 5 * 60, 60 * 60),
            ParsePositiveInteger(Environment.GetEnvironmentVariable("AUTH_TOTP_SETUP_TTL_SECONDS"), 10 * 60, 60 * 60),
            ParsePositiveInteger(Environment.GetEnvironmentVariable("REDIS_CONNECT_TIMEOUT_MS"), 1500, 30000),
            keyMaterial);
    }

    internal static AuthenticationOptions CreateForTests(string totpEncryptionMaterial) => new(
        nativeEnabled: true,
        production: false,
        cacheRedisUrl: null,
        sessionRedisUrl: null,
        sessionRedisDatabase: 1,
        redisPrefix: "exam-information:auth",
        sessionTtlSeconds: 8 * 60 * 60,
        loginChallengeTtlSeconds: 5 * 60,
        totpSetupTtlSeconds: 10 * 60,
        redisConnectTimeoutMilliseconds: 1500,
        totpEncryptionMaterial: totpEncryptionMaterial);

    private static string NormalizePrefix(string? value)
    {
        var source = string.IsNullOrWhiteSpace(value) ? "exam-information:auth" : value.Trim();
        var normalized = string.Concat(source.Select(character =>
            char.IsAsciiLetterOrDigit(character) || character is ':' or '_' or '-' ? character : '-'));
        return normalized.Length > 0 ? normalized : "exam-information:auth";
    }

    private static string? Clean(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim();

    private static bool ParseBoolean(string? value, bool fallback) => value?.Trim().ToLowerInvariant() switch
    {
        "1" or "true" or "yes" or "on" => true,
        "0" or "false" or "no" or "off" => false,
        _ => fallback
    };

    private static int ParsePositiveInteger(string? value, int fallback, int maximum) =>
        int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) && parsed > 0
            ? Math.Min(parsed, maximum)
            : fallback;

    private static int ParseNonNegativeInteger(string? value, int fallback, int maximum) =>
        int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) && parsed >= 0
            ? Math.Min(parsed, maximum)
            : fallback;

    private static int RedisDatabase(string? value)
    {
        if (!Uri.TryCreate(value, UriKind.Absolute, out var uri))
        {
            return 0;
        }

        return int.TryParse(uri.AbsolutePath.Trim('/'), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) && parsed >= 0
            ? parsed
            : 0;
    }

    private static string RedisEndpoint(string value)
    {
        if (!Uri.TryCreate(value, UriKind.Absolute, out var uri))
        {
            return string.Empty;
        }

        var port = uri.IsDefaultPort ? 6379 : uri.Port;
        return $"{uri.Scheme}://{uri.Host}:{port}";
    }
}
