using System.Security.Claims;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.MxGateway.Server.Sessions;

namespace ZB.MOM.WW.MxGateway.Server.Dashboard;

/// <summary>
/// Default implementation of <see cref="IDashboardSessionAdminService"/>: gates
/// destructive session actions on the <see cref="DashboardRoles.Admin"/> role,
/// audit-logs successful operations, and converts <see cref="SessionManagerException"/>
/// (and any other unexpected exceptions) into <see cref="DashboardSessionAdminResult.Fail(string)"/>
/// so the Blazor pages never see a raw exception.
/// </summary>
/// <remarks>
///     The constant <c>dashboard-admin-kill</c> is the reason passed to
///     <see cref="ISessionManager.KillWorkerAsync"/> and forwarded as the
///     <c>reason</c> tag on the <c>mxgateway.workers.killed</c> counter and in
///     the worker-kill audit log entries.
/// </remarks>
public sealed class DashboardSessionAdminService(
    ISessionManager sessionManager,
    IHttpContextAccessor httpContextAccessor,
    ILogger<DashboardSessionAdminService>? logger = null) : IDashboardSessionAdminService
{
    private const string UnauthorizedMessage = "Sign in as an Admin to close sessions or kill workers.";
    private const string KillReason = "dashboard-admin-kill";

    private readonly ILogger<DashboardSessionAdminService> _logger =
        logger ?? NullLogger<DashboardSessionAdminService>.Instance;

    /// <inheritdoc />
    public bool CanManage(ClaimsPrincipal user)
    {
        ArgumentNullException.ThrowIfNull(user);

        return user.Identity?.IsAuthenticated == true
            && user.IsInRole(DashboardRoles.Admin);
    }

    /// <inheritdoc />
    public async Task<DashboardSessionAdminResult> CloseSessionAsync(
        ClaimsPrincipal user,
        string sessionId,
        CancellationToken cancellationToken)
    {
        if (!CanManage(user))
        {
            return DashboardSessionAdminResult.Fail(UnauthorizedMessage);
        }

        if (string.IsNullOrWhiteSpace(sessionId))
        {
            return DashboardSessionAdminResult.Fail("Session id is required.");
        }

        string actor = ResolveActor(user);
        try
        {
            SessionCloseResult result = await sessionManager
                .CloseSessionAsync(sessionId, cancellationToken)
                .ConfigureAwait(false);

            _logger.LogInformation(
                "Dashboard admin {Actor} closed session {SessionId} from {RemoteAddress}; alreadyClosed={AlreadyClosed}.",
                actor,
                sessionId,
                ResolveRemoteAddress(),
                result.AlreadyClosed);

            return DashboardSessionAdminResult.Success(
                result.AlreadyClosed
                    ? $"Session {sessionId} was already closed."
                    : $"Session {sessionId} closed.");
        }
        catch (SessionManagerException exception) when (exception.ErrorCode == SessionManagerErrorCode.SessionNotFound)
        {
            return DashboardSessionAdminResult.Fail($"Session {sessionId} was not found.");
        }
        catch (SessionManagerException exception)
        {
            _logger.LogWarning(
                exception,
                "Dashboard admin {Actor} close failed for session {SessionId}.",
                actor,
                sessionId);
            return DashboardSessionAdminResult.Fail(
                $"Close failed: {exception.Message}");
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch (Exception exception)
        {
            // Server-050: any non-SessionManagerException (e.g. an IOException or
            // InvalidOperationException from the session DisposeAsync / pipe teardown
            // path) used to propagate raw into Blazor's error boundary. Convert it to
            // a friendly failure so the Razor pages see only DashboardSessionAdminResult.
            _logger.LogWarning(
                exception,
                "Dashboard admin {Actor} close failed unexpectedly for session {SessionId}.",
                actor,
                sessionId);
            return DashboardSessionAdminResult.Fail(
                $"Close failed unexpectedly for session {sessionId}. See the gateway log for details.");
        }
    }

    /// <inheritdoc />
    public async Task<DashboardSessionAdminResult> KillWorkerAsync(
        ClaimsPrincipal user,
        string sessionId,
        CancellationToken cancellationToken)
    {
        if (!CanManage(user))
        {
            return DashboardSessionAdminResult.Fail(UnauthorizedMessage);
        }

        if (string.IsNullOrWhiteSpace(sessionId))
        {
            return DashboardSessionAdminResult.Fail("Session id is required.");
        }

        string actor = ResolveActor(user);
        try
        {
            SessionCloseResult result = await sessionManager
                .KillWorkerAsync(sessionId, KillReason, cancellationToken)
                .ConfigureAwait(false);

            _logger.LogInformation(
                "Dashboard admin {Actor} killed worker for session {SessionId} from {RemoteAddress}; alreadyClosed={AlreadyClosed}.",
                actor,
                sessionId,
                ResolveRemoteAddress(),
                result.AlreadyClosed);

            return DashboardSessionAdminResult.Success(
                result.AlreadyClosed
                    ? $"Session {sessionId} was already closed."
                    : $"Worker for session {sessionId} killed.");
        }
        catch (SessionManagerException exception) when (exception.ErrorCode == SessionManagerErrorCode.SessionNotFound)
        {
            return DashboardSessionAdminResult.Fail($"Session {sessionId} was not found.");
        }
        catch (SessionManagerException exception)
        {
            _logger.LogWarning(
                exception,
                "Dashboard admin {Actor} kill failed for session {SessionId}.",
                actor,
                sessionId);
            return DashboardSessionAdminResult.Fail(
                $"Kill failed: {exception.Message}");
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch (Exception exception)
        {
            // Server-050: any non-SessionManagerException (e.g. an IOException from
            // worker pipe teardown surfacing through session.DisposeAsync, or an
            // InvalidOperationException from a corrupted worker handle) used to
            // propagate raw into Blazor's error boundary. Convert it to a friendly
            // failure so the page renders the ResultMessage rather than the circuit
            // error page.
            _logger.LogWarning(
                exception,
                "Dashboard admin {Actor} kill failed unexpectedly for session {SessionId}.",
                actor,
                sessionId);
            return DashboardSessionAdminResult.Fail(
                $"Kill failed unexpectedly for session {sessionId}. See the gateway log for details.");
        }
    }

    private static string ResolveActor(ClaimsPrincipal user)
    {
        return user.Identity?.Name ?? "<unknown>";
    }

    private string? ResolveRemoteAddress()
    {
        return httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString();
    }
}
