using Cysharp.Threading.Tasks;
using ShrinkNetwork;
using ShrinkNetwork.ServerHost.Framework;

namespace ShrinkNetwork.ServerHost;

public sealed class AuthServerModule : IShrinkServerModule
{
    public const int LoginRequestOpcode = 1201;
    public const int LoginResponseOpcode = 1202;
    public const int RefreshRequestOpcode = 1203;
    public const int RefreshResponseOpcode = 1204;
    public const string LoginRoute = "server/auth/login";
    public const string RefreshRoute = "server/auth/refresh";
    private static readonly string[] DefaultGrantedPermissions = { "auth.ok", "room.access" };

    public string Name => "Auth";

    public void ConfigureService(ServerModuleContext context, ShrinkNetworkService service, string transportName)
    {
        service.RegisterMessage<ServerAuthLoginRequest>(LoginRequestOpcode, LoginRoute);
        service.RegisterMessage<ServerAuthLoginResponse>(LoginResponseOpcode, "server/auth/login_response");
        service.RegisterMessage<ServerAuthRefreshRequest>(RefreshRequestOpcode, RefreshRoute);
        service.RegisterMessage<ServerAuthRefreshResponse>(RefreshResponseOpcode, "server/auth/refresh_response");
        service.RegisterRequestHandler<ServerAuthLoginRequest, ServerAuthLoginResponse>((ctx, request) =>
            HandleLoginAsync(context, transportName, ctx, request));
        service.RegisterRequestHandler<ServerAuthRefreshRequest, ServerAuthRefreshResponse>((ctx, request) =>
            HandleRefreshAsync(context, ctx, request));
    }

    public UniTask StartAsync(ServerModuleContext context, CancellationToken cancellationToken)
    {
        return UniTask.CompletedTask;
    }

    private UniTask<ServerAuthLoginResponse> HandleLoginAsync(
        ServerModuleContext moduleContext,
        string transportName,
        ShrinkNetworkContext context,
        ServerAuthLoginRequest request)
    {
        var authToken = moduleContext.Options.SharedAuthToken;
        var requiresAuth = !string.IsNullOrWhiteSpace(authToken);
        if (!requiresAuth && !moduleContext.Options.AllowAnonymousWhenAuthTokenMissing)
        {
            return UniTask.FromResult(new ServerAuthLoginResponse
            {
                ErrorCode = 503,
                ErrorMessage = "服务端未配置 SHRINK_SERVER_AUTH_TOKEN，且未显式允许匿名登录。",
                IsAuthenticated = false
            });
        }

        if (requiresAuth && !string.Equals(authToken, request.Token, StringComparison.Ordinal))
        {
            return UniTask.FromResult(new ServerAuthLoginResponse
            {
                ErrorCode = ShrinkRpcErrorCode.AuthenticationFailed,
                ErrorMessage = "鉴权失败，口令不正确。",
                IsAuthenticated = false
            });
        }

        var authName = string.IsNullOrWhiteSpace(request.ClientName)
            ? $"{transportName}-{context.Session.SessionId}"
            : request.ClientName.Trim();
        var authState = moduleContext.AuthStore.MarkAuthenticated(context.Service, context.Session.SessionId, authName,
            BuildSessionTokenTtl(moduleContext.Options));

        context.Session.Items["auth.name"] = authName;
        context.Session.Items["auth.at_utc"] = DateTimeOffset.UtcNow;
        context.Session.SetSessionToken(authState.SessionToken, authState.SessionTokenExpiresAtUtc);
        foreach (var permission in DefaultGrantedPermissions)
            context.Session.GrantPermission(permission);

        return UniTask.FromResult(new ServerAuthLoginResponse
        {
            IsAuthenticated = true,
            AuthName = authName,
            SessionToken = authState.SessionToken,
            SessionTokenExpiresAtUnixTimeSeconds = authState.SessionTokenExpiresAtUtc.ToUnixTimeSeconds(),
            RefreshRecommendedAtUnixTimeSeconds = BuildRefreshRecommendedAtUnixTimeSeconds(
                authState.SessionTokenExpiresAtUtc,
                moduleContext.Options),
            GrantedPermissions = DefaultGrantedPermissions.ToArray(),
            ServerMessage = requiresAuth ? "鉴权成功。" : "服务端已显式允许匿名登录。"
        });
    }

    private UniTask<ServerAuthRefreshResponse> HandleRefreshAsync(
        ServerModuleContext moduleContext,
        ShrinkNetworkContext context,
        ServerAuthRefreshRequest request)
    {
        var sessionToken = string.IsNullOrWhiteSpace(request.SessionToken)
            ? context.Packet.SessionToken
            : request.SessionToken.Trim();
        if (!moduleContext.AuthStore.TryRefreshSessionToken(context.Service, context.Session.SessionId, sessionToken,
                BuildSessionTokenTtl(moduleContext.Options), BuildRefreshWindow(moduleContext.Options), out var authState,
                out var failureReason))
        {
            return UniTask.FromResult(new ServerAuthRefreshResponse
            {
                ErrorCode = string.Equals(failureReason, "会话令牌已过期。", StringComparison.Ordinal)
                    ? ShrinkRpcErrorCode.SessionTokenExpired
                    : ShrinkRpcErrorCode.AuthenticationFailed,
                ErrorMessage = failureReason,
                IsAuthenticated = false
            });
        }

        context.Session.SetSessionToken(authState.SessionToken, authState.SessionTokenExpiresAtUtc);
        context.Session.Items["auth.name"] = authState.AuthName;
        foreach (var permission in DefaultGrantedPermissions)
            context.Session.GrantPermission(permission);

        return UniTask.FromResult(new ServerAuthRefreshResponse
        {
            IsAuthenticated = true,
            AuthName = authState.AuthName,
            SessionToken = authState.SessionToken,
            SessionTokenExpiresAtUnixTimeSeconds = authState.SessionTokenExpiresAtUtc.ToUnixTimeSeconds(),
            RefreshRecommendedAtUnixTimeSeconds = BuildRefreshRecommendedAtUnixTimeSeconds(
                authState.SessionTokenExpiresAtUtc,
                moduleContext.Options),
            ServerMessage = "会话令牌已刷新。"
        });
    }

    private static TimeSpan BuildSessionTokenTtl(ServerHostOptions options)
    {
        return TimeSpan.FromSeconds(Math.Max(60, options.SessionTokenTtlSeconds));
    }

    private static TimeSpan BuildRefreshWindow(ServerHostOptions options)
    {
        return TimeSpan.FromSeconds(Math.Max(0, options.SessionTokenRefreshWindowSeconds));
    }

    private static long BuildRefreshRecommendedAtUnixTimeSeconds(DateTimeOffset expiresAtUtc, ServerHostOptions options)
    {
        var refreshWindow = BuildRefreshWindow(options);
        if (refreshWindow <= TimeSpan.Zero)
            return 0;

        return expiresAtUtc.Subtract(refreshWindow).ToUnixTimeSeconds();
    }
}

[ShrinkNetworkMessage(AuthServerModule.LoginRequestOpcode, AuthServerModule.LoginRoute)]
public sealed class ServerAuthLoginRequest : IShrinkNetworkRequest
{
    public string ClientName { get; set; } = string.Empty;
    public string Token { get; set; } = string.Empty;
}

[ShrinkNetworkMessage(AuthServerModule.LoginResponseOpcode, "server/auth/login_response")]
public sealed class ServerAuthLoginResponse : ShrinkRpcResponseBase
{
    public bool IsAuthenticated { get; set; }
    public string AuthName { get; set; } = string.Empty;
    public string SessionToken { get; set; } = string.Empty;
    public long SessionTokenExpiresAtUnixTimeSeconds { get; set; }
    public long RefreshRecommendedAtUnixTimeSeconds { get; set; }
    public string[] GrantedPermissions { get; set; } = Array.Empty<string>();
    public string ServerMessage { get; set; } = string.Empty;
}

[ShrinkNetworkMessage(AuthServerModule.RefreshRequestOpcode, AuthServerModule.RefreshRoute)]
public sealed class ServerAuthRefreshRequest : IShrinkNetworkRequest
{
    public string SessionToken { get; set; } = string.Empty;
}

[ShrinkNetworkMessage(AuthServerModule.RefreshResponseOpcode, "server/auth/refresh_response")]
public sealed class ServerAuthRefreshResponse : ShrinkRpcResponseBase
{
    public bool IsAuthenticated { get; set; }
    public string AuthName { get; set; } = string.Empty;
    public string SessionToken { get; set; } = string.Empty;
    public long SessionTokenExpiresAtUnixTimeSeconds { get; set; }
    public long RefreshRecommendedAtUnixTimeSeconds { get; set; }
    public string ServerMessage { get; set; } = string.Empty;
}
