using Microsoft.Extensions.Logging;
using ZB.MOM.WW.OtOpcUa.Commons.Browsing;

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>
public sealed class BrowserSessionService(
    IEnumerable<IDriverBrowser> browsers,
    BrowseSessionRegistry registry,
    ILogger<BrowserSessionService> logger,
    IUniversalDriverBrowser universalBrowser) : 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 />
    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);
    }
}
