using Opc.Ua;
using Opc.Ua.Client;
using Serilog;

namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Adapters;

/// <summary>
///     Production endpoint discovery that queries the real server.
/// </summary>
internal sealed class DefaultEndpointDiscovery : IEndpointDiscovery
{
    private static readonly ILogger Logger = Log.ForContext<DefaultEndpointDiscovery>();

    /// <inheritdoc />
    public EndpointDescription SelectEndpoint(ApplicationConfiguration config, string endpointUrl,
        MessageSecurityMode requestedMode)
    {
        if (requestedMode == MessageSecurityMode.None)
        {
#pragma warning disable CS0618 // Acceptable for endpoint selection
            return CoreClientUtils.SelectEndpoint(config, endpointUrl, false);
#pragma warning restore CS0618
        }

        using var client = DiscoveryClient.Create(new Uri(endpointUrl));
        var allEndpoints = client.GetEndpoints(null);

        return EndpointSelector.SelectBest(allEndpoints, endpointUrl, requestedMode);
    }
}

/// <summary>
///     Pure best-endpoint selection logic, extracted from <see cref="DefaultEndpointDiscovery"/>
///     so it can be unit tested without standing up a real <see cref="DiscoveryClient"/>.
/// </summary>
internal static class EndpointSelector
{
    private static readonly ILogger Logger = Log.ForContext(typeof(EndpointSelector));

    /// <summary>
    ///     Picks the best endpoint from the discovery response that matches the requested
    ///     security mode, preferring <c>Basic256Sha256</c>, and rewrites the endpoint URL
    ///     host to match the user-supplied URL when the discovery response advertises a
    ///     different hostname.
    /// </summary>
    /// <param name="allEndpoints">Endpoints returned by the discovery query, in any order.</param>
    /// <param name="endpointUrl">The endpoint URL the operator supplied; supplies the hostname rewrite target.</param>
    /// <param name="requestedMode">The requested OPC UA message security mode.</param>
    /// <exception cref="InvalidOperationException">
    ///     Thrown when no endpoint matches <paramref name="requestedMode"/>; the message lists the
    ///     security mode + policy combinations the server returned so operators can diagnose mismatches.
    /// </exception>
    /// <returns>The best-matching endpoint, with its URL host rewritten to match <paramref name="endpointUrl"/> if needed.</returns>
    public static EndpointDescription SelectBest(
        IEnumerable<EndpointDescription> allEndpoints,
        string endpointUrl,
        MessageSecurityMode requestedMode)
    {
        ArgumentNullException.ThrowIfNull(allEndpoints);
        if (string.IsNullOrWhiteSpace(endpointUrl))
            throw new ArgumentException("Endpoint URL must not be null or empty.", nameof(endpointUrl));

        // Materialise once so we can both iterate and produce a diagnostic message
        // without re-running the underlying discovery enumeration.
        var endpoints = allEndpoints.ToList();

        EndpointDescription? best = null;

        foreach (var ep in endpoints)
        {
            if (ep.SecurityMode != requestedMode)
                continue;

            if (best == null)
            {
                best = ep;
                continue;
            }

            // Prefer Basic256Sha256 when multiple endpoints match the requested mode.
            if (ep.SecurityPolicyUri == SecurityPolicies.Basic256Sha256)
                best = ep;
        }

        if (best == null)
        {
            var available = string.Join(", ", endpoints.Select(e => $"{e.SecurityMode}/{e.SecurityPolicyUri}"));
            throw new InvalidOperationException(
                $"No endpoint found with security mode '{requestedMode}'. Available endpoints: {available}");
        }

        // Rewrite endpoint URL hostname to match user-supplied hostname. Necessary
        // when the OPC UA server returns a discovery URL using a different hostname
        // (e.g. internal DNS name) than the one the operator routed to.
        var serverUri = new Uri(best.EndpointUrl);
        var requestedUri = new Uri(endpointUrl);
        if (serverUri.Host != requestedUri.Host)
        {
            var builder = new UriBuilder(best.EndpointUrl) { Host = requestedUri.Host };
            best.EndpointUrl = builder.ToString();
            Logger.Debug("Rewrote endpoint host from {ServerHost} to {RequestedHost}", serverUri.Host,
                requestedUri.Host);
        }

        return best;
    }
}