using System.Security.Claims;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.Audit;
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,
/// records each attempt as a canonical <see cref="AuditEvent"/> through <see cref="IAuditWriter"/>
/// (in addition to the operational <see cref="ILogger"/> line), 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. Destructive dashboard actions write durable,
///     queryable audit rows (actions <c>dashboard-close-session</c> / <c>dashboard-kill-worker</c>)
///     to the canonical <c>audit_event</c> store, mirroring the API-key management path so a
///     worker killed mid-production is not visible only in a rotatable <see cref="ILogger"/> line.
/// </remarks>
public sealed class DashboardSessionAdminService(
    ISessionManager sessionManager,
    IHttpContextAccessor httpContextAccessor,
    IAuditWriter auditWriter,
    ILogger<DashboardSessionAdminService>? logger = null) : IDashboardSessionAdminService
{
    /// <summary>Canonical <see cref="AuditEvent.Category"/> for dashboard session-admin actions.</summary>
    internal const string SessionAdminCategory = "SessionAdmin";

    /// <summary>Canonical <see cref="AuditEvent.Action"/> for a dashboard session close.</summary>
    internal const string CloseSessionAction = "dashboard-close-session";

    /// <summary>Canonical <see cref="AuditEvent.Action"/> for a dashboard worker kill.</summary>
    internal const string KillWorkerAction = "dashboard-kill-worker";

    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))
        {
            await WriteAuditAsync(user, CloseSessionAction, sessionId, AuditOutcome.Denied, "unauthorized", cancellationToken)
                .ConfigureAwait(false);
            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);

            await WriteAuditAsync(
                    user,
                    CloseSessionAction,
                    sessionId,
                    AuditOutcome.Success,
                    result.AlreadyClosed ? "alreadyClosed" : "closed",
                    cancellationToken)
                .ConfigureAwait(false);

            return DashboardSessionAdminResult.Success(
                result.AlreadyClosed
                    ? $"Session {sessionId} was already closed."
                    : $"Session {sessionId} closed.");
        }
        catch (SessionManagerException exception) when (exception.ErrorCode == SessionManagerErrorCode.SessionNotFound)
        {
            await WriteAuditAsync(user, CloseSessionAction, sessionId, AuditOutcome.Failure, "not-found", cancellationToken)
                .ConfigureAwait(false);
            return DashboardSessionAdminResult.Fail($"Session {sessionId} was not found.");
        }
        catch (SessionManagerException exception)
        {
            _logger.LogWarning(
                exception,
                "Dashboard admin {Actor} close failed for session {SessionId}.",
                actor,
                sessionId);
            await WriteAuditAsync(user, CloseSessionAction, sessionId, AuditOutcome.Failure, exception.Message, cancellationToken)
                .ConfigureAwait(false);
            return DashboardSessionAdminResult.Fail(
                $"Close failed: {exception.Message}");
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch (Exception exception)
        {
            _logger.LogWarning(
                exception,
                "Dashboard admin {Actor} close failed unexpectedly for session {SessionId}.",
                actor,
                sessionId);
            await WriteAuditAsync(user, CloseSessionAction, sessionId, AuditOutcome.Failure, "unexpected", cancellationToken)
                .ConfigureAwait(false);
            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))
        {
            await WriteAuditAsync(user, KillWorkerAction, sessionId, AuditOutcome.Denied, "unauthorized", cancellationToken)
                .ConfigureAwait(false);
            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);

            await WriteAuditAsync(
                    user,
                    KillWorkerAction,
                    sessionId,
                    AuditOutcome.Success,
                    result.AlreadyClosed ? "alreadyClosed" : "killed",
                    cancellationToken)
                .ConfigureAwait(false);

            return DashboardSessionAdminResult.Success(
                result.AlreadyClosed
                    ? $"Session {sessionId} was already closed."
                    : $"Worker for session {sessionId} killed.");
        }
        catch (SessionManagerException exception) when (exception.ErrorCode == SessionManagerErrorCode.SessionNotFound)
        {
            await WriteAuditAsync(user, KillWorkerAction, sessionId, AuditOutcome.Failure, "not-found", cancellationToken)
                .ConfigureAwait(false);
            return DashboardSessionAdminResult.Fail($"Session {sessionId} was not found.");
        }
        catch (SessionManagerException exception)
        {
            _logger.LogWarning(
                exception,
                "Dashboard admin {Actor} kill failed for session {SessionId}.",
                actor,
                sessionId);
            await WriteAuditAsync(user, KillWorkerAction, sessionId, AuditOutcome.Failure, exception.Message, cancellationToken)
                .ConfigureAwait(false);
            return DashboardSessionAdminResult.Fail(
                $"Kill failed: {exception.Message}");
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch (Exception exception)
        {
            _logger.LogWarning(
                exception,
                "Dashboard admin {Actor} kill failed unexpectedly for session {SessionId}.",
                actor,
                sessionId);
            await WriteAuditAsync(user, KillWorkerAction, sessionId, AuditOutcome.Failure, "unexpected", cancellationToken)
                .ConfigureAwait(false);
            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();
    }

    /// <summary>
    /// Writes a canonical <see cref="AuditEvent"/> for a dashboard session-admin action through the
    /// best-effort <see cref="IAuditWriter"/> (failures are swallowed/logged by the writer, so this
    /// never throws and never masks the operation result). <see cref="AuditEvent.Actor"/> is the LDAP
    /// operator, <see cref="AuditEvent.Target"/> the session id, and <paramref name="detail"/> is wrapped
    /// as the <c>detail</c> field of the JSON extension bag.
    /// </summary>
    private async Task WriteAuditAsync(
        ClaimsPrincipal user,
        string action,
        string sessionId,
        AuditOutcome outcome,
        string? detail,
        CancellationToken cancellationToken)
    {
        AuditEvent auditEvent = new()
        {
            EventId = Guid.NewGuid(),
            OccurredAtUtc = DateTimeOffset.UtcNow,
            Actor = ResolveActor(user),
            Action = action,
            Outcome = outcome,
            Category = SessionAdminCategory,
            Target = sessionId,
            SourceNode = ResolveRemoteAddress(),
            CorrelationId = ParseCorrelationId(),
            DetailsJson = WrapDetail(detail),
        };

        await auditWriter.WriteAsync(auditEvent, cancellationToken).ConfigureAwait(false);
    }

    /// <summary>
    /// Derives a correlation id from the request trace identifier when it is a well-formed GUID;
    /// otherwise null (the default <c>HttpContext.TraceIdentifier</c> is the connection:request form,
    /// not a GUID, so it correlates to null rather than fabricating one).
    /// </summary>
    private Guid? ParseCorrelationId() =>
        Guid.TryParse(httpContextAccessor.HttpContext?.TraceIdentifier, out Guid correlationId)
            ? correlationId
            : null;

    private static string? WrapDetail(string? detail) =>
        detail is null
            ? null
            : JsonSerializer.Serialize(new Dictionary<string, string> { ["detail"] = detail });
}
