using System.Security.Cryptography;
using System.Text.Json.Nodes;
using Eis.Application.Authentication;
using QRCoder;

namespace Eis.Infrastructure.Authentication;

internal sealed class AuthenticationService(
    AuthenticationRepository repository,
    IAuthenticationStateStore state,
    PasswordCompatibilityService passwords,
    TotpCompatibilityService totp,
    AuthenticationOptions options) : IAuthenticationService
{
    private static readonly IReadOnlyDictionary<string, string[]> PermissionsByLevel =
        new Dictionary<string, string[]>(StringComparer.Ordinal)
        {
            ["super"] = ["*"],
            ["school"] =
            [
                "dashboard.read", "candidates.read", "candidates.write", "candidates.review",
                "registrations.read", "registrations.review", "payments.read", "payments.write",
                "results.read", "centers.read", "centers.write", "workflows.inbox"
            ],
            ["class"] =
            [
                "dashboard.read", "candidates.read", "candidates.review", "registrations.read",
                "registrations.review", "payments.read", "payments.write", "results.read", "workflows.inbox"
            ]
        };

    public async Task<AuthenticationEndpointResult> RegisterAsync(
        string name,
        string gender,
        string password,
        string schoolId,
        string classId,
        CancellationToken cancellationToken)
    {
        var normalizedName = Clean(name, 30);
        var normalizedGender = Clean(gender, 10);
        if (normalizedName.Length == 0 || normalizedGender is not ("男" or "女"))
        {
            return Error(400, "请填写姓名并选择性别");
        }

        if (password.Length < 8)
        {
            return Error(400, "密码至少需要 8 位");
        }

        var result = await repository.CreateSelfRegisteredCandidateAsync(
            normalizedName,
            normalizedGender,
            Clean(schoolId, 64),
            Clean(classId, 64),
            passwords.Hash(password),
            cancellationToken);
        if (result.RegistrationNumber is null)
        {
            return Error(result.StatusCode, result.ErrorMessage ?? "自主注册失败");
        }

        return new AuthenticationEndpointResult(
            201,
            new JsonObject
            {
                ["ok"] = true,
                ["registrationNumber"] = result.RegistrationNumber,
                ["message"] = "报名号已生成，请使用该号码登录并补全个人信息"
            });
    }

    public async Task<AuthenticationEndpointResult> GetCurrentUserAsync(
        string sessionToken,
        CancellationToken cancellationToken)
    {
        var user = await CurrentUserAsync(sessionToken, cancellationToken);
        if (user is null)
        {
            return Success(new JsonObject { ["ok"] = true, ["user"] = null });
        }

        var response = new JsonObject
        {
            ["ok"] = true,
            ["user"] = SafeUser(user),
            ["profile"] = user.Role == "candidate"
                ? await repository.GetCandidateProfileAsync(user.Id, cancellationToken)
                : null
        };
        if (user.Role == "admin")
        {
            var level = user.AdminLevel ?? "super";
            response["permissions"] = new JsonArray(
                (PermissionsByLevel.GetValueOrDefault(level) ?? [])
                    .Select(value => JsonValue.Create(value))
                    .ToArray());
            response["scopeLabel"] = await repository.GetAdminScopeLabelAsync(user, cancellationToken);
        }

        return Success(response);
    }

    public async Task<AuthenticationEndpointResult> LoginAsync(
        string username,
        string password,
        CancellationToken cancellationToken)
    {
        var account = Clean(username, 120).ToLowerInvariant();
        var user = await repository.FindUserByAccountAsync(account, cancellationToken);
        if (!CanLogin(user) || !passwords.Verify(password, user!.PasswordHash))
        {
            return Error(401, "账号或密码不正确");
        }

        if (user.TotpEnabled)
        {
            var challenge = Base64Url(RandomNumberGenerator.GetBytes(32));
            await state.CreateLoginChallengeAsync(challenge, user.Id);
            return Success(new JsonObject
            {
                ["ok"] = true,
                ["requiresTotp"] = true,
                ["challenge"] = challenge
            });
        }

        return await IssueSessionAsync(user);
    }

    public async Task<AuthenticationEndpointResult> CompleteTotpLoginAsync(
        string challenge,
        string code,
        CancellationToken cancellationToken)
    {
        var challengeState = await state.GetLoginChallengeAsync(challenge);
        if (challengeState is null || challengeState.Attempts >= 5)
        {
            await state.DeleteLoginChallengeAsync(challenge);
            return Error(401, "验证请求已过期，请重新输入账号和密码");
        }

        var user = await repository.FindUserByIdAsync(challengeState.UserId, cancellationToken);
        if (!CanLogin(user) || !user!.TotpEnabled)
        {
            await state.DeleteLoginChallengeAsync(challenge);
            return Error(401, "验证请求已失效，请重新登录");
        }

        var verified = VerifySecondFactor(user, code);
        if (verified is null)
        {
            var failure = await state.RecordLoginChallengeFailureAsync(challenge, 5);
            var message = failure is null
                ? "验证请求已过期，请重新输入账号和密码"
                : failure.Attempts >= 5
                    ? "验证失败次数过多，请重新登录"
                    : "验证码或恢复码不正确";
            return Error(401, message);
        }

        string? logId = null;
        string? action = null;
        string? detail = null;
        if (verified.Type == SecondFactorType.Totp)
        {
            user.TotpLastUsedStep = verified.Step;
        }
        else
        {
            user.TotpRecoveryCodes = verified.RecoveryCodes!;
            logId = Uid("log");
            action = "使用 TOTP 恢复码登录";
            detail = user.Username;
        }

        await repository.UpdateTotpSecurityAsync(user, logId, action, detail, cancellationToken);
        await state.DeleteLoginChallengeAsync(challenge);
        var issued = await IssueSessionAsync(user);
        issued.Body["usedRecoveryCode"] = verified.Type == SecondFactorType.Recovery;
        return issued;
    }

    public async Task<AuthenticationEndpointResult> ChangePasswordAsync(
        string sessionToken,
        string currentPassword,
        string newPassword,
        CancellationToken cancellationToken)
    {
        var user = await CurrentUserAsync(sessionToken, cancellationToken);
        if (user is null)
        {
            return Error(401, "请先登录");
        }

        if (!passwords.Verify(currentPassword, user.PasswordHash))
        {
            return Error(400, "当前密码不正确");
        }

        if (newPassword.Length < 8)
        {
            return Error(400, "新密码至少需要 8 位");
        }

        if (newPassword == currentPassword)
        {
            return Error(400, "新密码不能与当前密码相同");
        }

        user.PasswordHash = passwords.Hash(newPassword);
        user.MustChangePassword = false;
        var detail = user.Role == "candidate" ? $"报名号 {user.CandidateNumber}" : user.Username;
        await repository.UpdatePasswordAsync(user, Uid("log"), "修改登录密码", detail, cancellationToken);
        return Success(new JsonObject { ["ok"] = true, ["user"] = SafeUser(user) });
    }

    public async Task<AuthenticationEndpointResult> GetTotpStatusAsync(
        string sessionToken,
        CancellationToken cancellationToken)
    {
        var user = await CurrentUserAsync(sessionToken, cancellationToken);
        return user is null
            ? Error(401, "请先登录")
            : Success(new JsonObject
            {
                ["ok"] = true,
                ["enabled"] = user.TotpEnabled,
                ["recoveryCodesRemaining"] = user.TotpEnabled ? user.TotpRecoveryCodes.Count : 0
            });
    }

    public async Task<AuthenticationEndpointResult> BeginTotpSetupAsync(
        string sessionToken,
        string currentPassword,
        CancellationToken cancellationToken)
    {
        var user = await CurrentUserAsync(sessionToken, cancellationToken);
        if (user is null)
        {
            return Error(401, "请先登录");
        }

        if (user.MustChangePassword)
        {
            return Error(400, "请先修改初始密码，再启用二次验证");
        }

        if (user.TotpEnabled)
        {
            return Error(409, "当前账号已经启用 TOTP 二次验证");
        }

        if (!passwords.Verify(currentPassword, user.PasswordHash))
        {
            return Error(400, "当前密码不正确");
        }

        var issuer = Clean(await repository.GetOrganizationNameAsync(cancellationToken), 80);
        var secret = totp.CreateSecret();
        var uri = totp.BuildOtpAuthUri(secret, user.CandidateNumber ?? user.Username, issuer);
        await state.CreateTotpSetupAsync(sessionToken, user.Id, secret);
        return Success(new JsonObject
        {
            ["ok"] = true,
            ["secret"] = secret,
            ["uri"] = uri,
            ["qrCode"] = CreateQrCodeDataUrl(uri),
            ["expiresIn"] = 600
        });
    }

    public async Task<AuthenticationEndpointResult> EnableTotpAsync(
        string sessionToken,
        string code,
        CancellationToken cancellationToken)
    {
        var user = await CurrentUserAsync(sessionToken, cancellationToken);
        if (user is null)
        {
            return Error(401, "请先登录");
        }

        var setup = await state.GetTotpSetupAsync(sessionToken);
        if (setup is null || setup.UserId != user.Id)
        {
            await state.DeleteTotpSetupAsync(sessionToken);
            return Error(400, "绑定信息已过期，请重新开始");
        }

        var step = totp.Verify(code, setup.Secret);
        if (step is null)
        {
            return Error(400, "动态验证码不正确，请确认设备时间准确后重试");
        }

        var recoveryCodes = totp.CreateRecoveryCodes();
        user.TotpEnabled = true;
        user.TotpSecretEncrypted = totp.EncryptSecret(setup.Secret);
        user.TotpRecoveryCodes = recoveryCodes.Select(totp.HashRecoveryCode).ToArray();
        user.TotpLastUsedStep = step;
        await repository.UpdateTotpSecurityAsync(
            user,
            Uid("log"),
            "启用 TOTP 二次验证",
            user.Username,
            cancellationToken);
        await state.DeleteTotpSetupAsync(sessionToken);
        return Success(new JsonObject
        {
            ["ok"] = true,
            ["recoveryCodes"] = new JsonArray(recoveryCodes.Select(value => JsonValue.Create(value)).ToArray()),
            ["user"] = SafeUser(user)
        });
    }

    public async Task<AuthenticationEndpointResult> RegenerateRecoveryCodesAsync(
        string sessionToken,
        string currentPassword,
        string code,
        CancellationToken cancellationToken)
    {
        var user = await CurrentUserAsync(sessionToken, cancellationToken);
        if (user is null)
        {
            return Error(401, "请先登录");
        }

        if (!user.TotpEnabled)
        {
            return Error(400, "当前账号尚未启用 TOTP 二次验证");
        }

        if (!passwords.Verify(currentPassword, user.PasswordHash))
        {
            return Error(400, "当前密码不正确");
        }

        var verified = VerifySecondFactor(user, code);
        if (verified is null)
        {
            return Error(400, "动态验证码或恢复码不正确");
        }

        var recoveryCodes = totp.CreateRecoveryCodes();
        user.TotpRecoveryCodes = recoveryCodes.Select(totp.HashRecoveryCode).ToArray();
        if (verified.Type == SecondFactorType.Totp)
        {
            user.TotpLastUsedStep = verified.Step;
        }

        await repository.UpdateTotpSecurityAsync(
            user,
            Uid("log"),
            "重新生成 TOTP 恢复码",
            user.Username,
            cancellationToken);
        return Success(new JsonObject
        {
            ["ok"] = true,
            ["recoveryCodes"] = new JsonArray(recoveryCodes.Select(value => JsonValue.Create(value)).ToArray())
        });
    }

    public async Task<AuthenticationEndpointResult> DisableTotpAsync(
        string sessionToken,
        string currentPassword,
        string code,
        CancellationToken cancellationToken)
    {
        var user = await CurrentUserAsync(sessionToken, cancellationToken);
        if (user is null)
        {
            return Error(401, "请先登录");
        }

        if (!user.TotpEnabled)
        {
            return Error(400, "当前账号尚未启用 TOTP 二次验证");
        }

        if (!passwords.Verify(currentPassword, user.PasswordHash))
        {
            return Error(400, "当前密码不正确");
        }

        if (VerifySecondFactor(user, code) is null)
        {
            return Error(400, "动态验证码或恢复码不正确");
        }

        user.TotpEnabled = false;
        user.TotpSecretEncrypted = null;
        user.TotpRecoveryCodes = [];
        user.TotpLastUsedStep = null;
        await repository.UpdateTotpSecurityAsync(
            user,
            Uid("log"),
            "关闭 TOTP 二次验证",
            user.Username,
            cancellationToken);
        await state.DeleteTotpSetupAsync(sessionToken);
        return Success(new JsonObject { ["ok"] = true, ["user"] = SafeUser(user) });
    }

    public async Task<AuthenticationEndpointResult> LogoutAsync(
        string sessionToken,
        CancellationToken cancellationToken)
    {
        _ = cancellationToken;
        if (sessionToken.Length > 0)
        {
            await state.DeleteSessionAsync(sessionToken);
        }

        return new AuthenticationEndpointResult(
            200,
            new JsonObject { ["ok"] = true },
            "hz_session=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0");
    }

    private async Task<AuthenticationUser?> CurrentUserAsync(string sessionToken, CancellationToken cancellationToken)
    {
        if (sessionToken.Length == 0)
        {
            return null;
        }

        var userId = await state.GetSessionUserIdAsync(sessionToken);
        if (userId is null)
        {
            return null;
        }

        var user = await repository.FindUserByIdAsync(userId, cancellationToken);
        return CanLogin(user) ? user : null;
    }

    private async Task<AuthenticationEndpointResult> IssueSessionAsync(AuthenticationUser user)
    {
        var token = Convert.ToHexStringLower(RandomNumberGenerator.GetBytes(32));
        await state.CreateSessionAsync(token, user.Id);
        var secure = options.Production ? "; Secure" : string.Empty;
        var cookie = $"hz_session={token}; Path=/; HttpOnly; SameSite=Strict{secure}; Max-Age={state.SessionTtlSeconds}";
        return new AuthenticationEndpointResult(
            200,
            new JsonObject { ["ok"] = true, ["user"] = SafeUser(user) },
            cookie);
    }

    private SecondFactorResult? VerifySecondFactor(AuthenticationUser user, string code)
    {
        if (!user.TotpEnabled || string.IsNullOrWhiteSpace(user.TotpSecretEncrypted))
        {
            return null;
        }

        try
        {
            var normalized = code.Trim();
            if (normalized.Length == 6 && normalized.All(char.IsAsciiDigit))
            {
                var step = totp.Verify(normalized, totp.DecryptSecret(user.TotpSecretEncrypted), user.TotpLastUsedStep);
                return step is null ? null : new SecondFactorResult(SecondFactorType.Totp, step, null);
            }

            var recoveryCodes = totp.ConsumeRecoveryCode(normalized, user.TotpRecoveryCodes);
            return recoveryCodes is null
                ? null
                : new SecondFactorResult(SecondFactorType.Recovery, null, recoveryCodes);
        }
        catch (CryptographicException)
        {
            return null;
        }
        catch (FormatException)
        {
            return null;
        }
    }

    private static JsonObject SafeUser(AuthenticationUser user) => new()
    {
        ["id"] = user.Id,
        ["username"] = user.Username,
        ["role"] = user.Role,
        ["adminLevel"] = user.Role == "admin" ? user.AdminLevel ?? "super" : null,
        ["schoolId"] = JsonValue.Create(user.SchoolId),
        ["classId"] = JsonValue.Create(user.ClassId),
        ["displayName"] = user.DisplayName,
        ["candidateNumber"] = JsonValue.Create(user.CandidateNumber),
        ["mustChangePassword"] = user.MustChangePassword,
        ["totpEnabled"] = user.TotpEnabled,
        ["archived"] = user.ArchivedAt is not null
    };

    private static bool CanLogin(AuthenticationUser? user) => user is { Active: true, ArchivedAt: null };

    private static AuthenticationEndpointResult Success(JsonObject body) => new(200, body);

    private static AuthenticationEndpointResult Error(int statusCode, string message) => new(
        statusCode,
        new JsonObject { ["ok"] = false, ["message"] = message });

    private static string Clean(string? value, int maximum)
    {
        var normalized = (value ?? string.Empty).Trim();
        return normalized[..Math.Min(normalized.Length, maximum)];
    }

    private static string CreateQrCodeDataUrl(string uri)
    {
        using var generator = new QRCodeGenerator();
        using var data = generator.CreateQrCode(uri, QRCodeGenerator.ECCLevel.M);
        using var qrCode = new PngByteQRCode(data);
        return $"data:image/png;base64,{Convert.ToBase64String(qrCode.GetGraphic(10))}";
    }

    private static string Base64Url(byte[] value) => Convert.ToBase64String(value)
        .TrimEnd('=')
        .Replace('+', '-')
        .Replace('/', '_');

    private static string Uid(string prefix) =>
        $"{prefix}_{ToBase36(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds())}_{Convert.ToHexStringLower(RandomNumberGenerator.GetBytes(4))}";

    private static string ToBase36(long value)
    {
        const string alphabet = "0123456789abcdefghijklmnopqrstuvwxyz";
        Span<char> buffer = stackalloc char[16];
        var position = buffer.Length;
        do
        {
            buffer[--position] = alphabet[(int)(value % 36)];
            value /= 36;
        }
        while (value > 0);
        return new string(buffer[position..]);
    }

    private enum SecondFactorType
    {
        Totp,
        Recovery
    }

    private sealed record SecondFactorResult(
        SecondFactorType Type,
        long? Step,
        IReadOnlyList<string>? RecoveryCodes);
}
