using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.OtOpcUa.Commons.Browsing;
using ZB.MOM.WW.OtOpcUa.Security.Auth;

namespace ZB.MOM.WW.OtOpcUa.AdminUI.Browsing;

/// <summary>
///     Default <see cref="IBrowserSessionService"/> implementation. Indexes injected
///     <see cref="IDriverBrowser"/>s by <see cref="IDriverBrowser.DriverType"/>
///     (case-insensitive) at construction, registers opened sessions, and wraps each
///     expand/attributes call in a 20-second linked CTS so a stuck driver cannot
///     stall the UI indefinitely.
/// </summary>
/// <param name="browsers">The bespoke per-driver browsers.</param>
/// <param name="registry">The open-session registry.</param>
/// <param name="logger">The logger.</param>
/// <param name="universalBrowser">The Discover-backed fallback browser.</param>
/// <param name="authorizationService">Policy evaluator for <see cref="RequestRebirthAsync"/>.</param>
/// <param name="authenticationStateProvider">Supplies the calling circuit's user.</param>
public sealed class BrowserSessionService(
    IEnumerable<IDriverBrowser> browsers,
    BrowseSessionRegistry registry,
    ILogger<BrowserSessionService> logger,
    IUniversalDriverBrowser universalBrowser,
    IAuthorizationService authorizationService,
    AuthenticationStateProvider authenticationStateProvider) : IBrowserSessionService
{
    /// <summary>Upper bound on a single root/expand/attributes call.</summary>
    public static readonly TimeSpan PerCallTimeout = TimeSpan.FromSeconds(20);

    private readonly IReadOnlyDictionary<string, IDriverBrowser> _browsersByType =
        browsers.ToDictionary(b => b.DriverType, StringComparer.OrdinalIgnoreCase);

    /// <inheritdoc />
    public async Task<BrowseOpenResult> OpenAsync(string driverType, string configJson, CancellationToken ct)
    {
        if (!_browsersByType.TryGetValue(driverType, out var browser))
            return await OpenUniversalAsync(driverType, configJson, ct).ConfigureAwait(false);
        try
        {
            var session = await browser.OpenAsync(configJson, ct).ConfigureAwait(false);
            registry.Register(session);
            return new(true, null, session.Token);
        }
        catch (Exception ex)
        {
            logger.LogInformation(ex,
                "Browser open failed for driverType={DriverType}: {Message}", driverType, ex.Message);
            return new(false, ex.Message, Guid.Empty);
        }
    }

    /// <inheritdoc />
    public bool CanBrowse(string driverType, string configJson) =>
        _browsersByType.ContainsKey(driverType) || universalBrowser.CanBrowse(driverType, configJson);

    private async Task<BrowseOpenResult> OpenUniversalAsync(string driverType, string configJson, CancellationToken ct)
    {
        if (!universalBrowser.CanBrowse(driverType, configJson))
            return new(false, $"No browser registered for driver type '{driverType}'.", Guid.Empty);
        try
        {
            var session = await universalBrowser.OpenAsync(driverType, configJson, ct).ConfigureAwait(false);
            registry.Register(session);
            return new(true, null, session.Token);
        }
        catch (Exception ex)
        {
            logger.LogInformation(ex,
                "Universal browse open failed for driverType={DriverType}: {Message}", driverType, ex.Message);
            return new(false, ex.Message, Guid.Empty);
        }
    }

    /// <inheritdoc />
    public Task<IReadOnlyList<BrowseNode>> RootAsync(Guid token, CancellationToken ct) =>
        InvokeAsync(token, ct, (s, c) => s.RootAsync(c));

    /// <inheritdoc />
    public Task<IReadOnlyList<BrowseNode>> ExpandAsync(Guid token, string nodeId, CancellationToken ct) =>
        InvokeAsync(token, ct, (s, c) => s.ExpandAsync(nodeId, c));

    /// <inheritdoc />
    public Task<IReadOnlyList<AttributeInfo>> AttributesAsync(Guid token, string nodeId, CancellationToken ct) =>
        InvokeAsync<IReadOnlyList<AttributeInfo>>(token, ct, (s, c) => s.AttributesAsync(nodeId, c));

    /// <inheritdoc />
    /// <remarks>
    ///     <para>
    ///         <b>The authorization check is the point of routing this through the service at all.</b>
    ///         Every other member here is read-only; this one causes the browse session to publish onto
    ///         a live plant broker, so it is gated on the same <c>DriverOperator</c> policy the picker
    ///         bodies evaluate before rendering their Browse affordance. A render-time check alone is
    ///         not a control — it decides what a page draws, not what a circuit may invoke — so the
    ///         check is made here, where the action actually happens, and it fails closed.
    ///     </para>
    ///     <para>
    ///         Deliberately <b>not</b> wrapped in <see cref="PerCallTimeout"/>-style swallowing: unlike
    ///         a failed expand, a failed or refused rebirth is something the operator must see, so the
    ///         exception propagates to the caller for display.
    ///     </para>
    /// </remarks>
    public async Task<int> RequestRebirthAsync(Guid token, string scope, CancellationToken ct)
    {
        if (!registry.TryGet(token, out var session))
            throw new BrowseSessionNotFoundException(token);

        if (session is not IRebirthCapableBrowseSession rebirthable)
        {
            throw new NotSupportedException(
                $"Browse session {token} ({session.GetType().Name}) has no re-announce action.");
        }

        var state = await authenticationStateProvider.GetAuthenticationStateAsync().ConfigureAwait(false);
        var authorized = await authorizationService
            .AuthorizeAsync(state.User, resource: null, AdminUiPolicies.DriverOperator)
            .ConfigureAwait(false);

        if (!authorized.Succeeded)
        {
            logger.LogWarning(
                "Rebirth request REFUSED for scope '{Scope}' on browse session {Token}: caller does not "
                + "satisfy the {Policy} policy.", scope, token, AdminUiPolicies.DriverOperator);
            throw new UnauthorizedAccessException(
                $"Requesting a rebirth requires the {AdminUiPolicies.DriverOperator} policy.");
        }

        using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        cts.CancelAfter(PerCallTimeout);

        var published = await rebirthable.RequestRebirthAsync(scope, cts.Token).ConfigureAwait(false);

        logger.LogInformation(
            "Rebirth requested by {User} for scope '{Scope}' on browse session {Token}: {Published} "
            + "message(s) published.",
            state.User.Identity?.Name ?? "(anonymous)", scope, token, published);

        return published;
    }

    /// <inheritdoc />
    public bool CanRequestRebirth(Guid token) =>
        registry.TryGet(token, out var session) && session is IRebirthCapableBrowseSession { RebirthAvailable: true };

    /// <inheritdoc />
    public async Task CloseAsync(Guid token)
    {
        if (!registry.TryRemove(token, out var session)) return;
        try { await session.DisposeAsync().ConfigureAwait(false); } catch { }
        logger.LogDebug("Browse session {Token} closed reason=user-close", token);
    }

    private async Task<T> InvokeAsync<T>(
        Guid token, CancellationToken callerCt, Func<IBrowseSession, CancellationToken, Task<T>> op)
    {
        if (!registry.TryGet(token, out var session))
            throw new BrowseSessionNotFoundException(token);
        using var cts = CancellationTokenSource.CreateLinkedTokenSource(callerCt);
        cts.CancelAfter(PerCallTimeout);
        return await op(session, cts.Token).ConfigureAwait(false);
    }
}
