using System.Net;
using Microsoft.AspNetCore.Server.Kestrel.Core;

namespace ZB.MOM.WW.OtOpcUa.Host.Configuration;

/// <summary>
///     One HTTP endpoint the host was asked to serve, parsed out of the configured URL list so it
///     can be re-bound explicitly.
/// </summary>
/// <param name="Host">The host component as written — <c>+</c>, <c>*</c>, a hostname, or an IP.</param>
/// <param name="Port">The port.</param>
/// <param name="IsSecure">True when the URL used the <c>https</c> scheme.</param>
public sealed record KestrelHttpBinding(string Host, int Port, bool IsSecure)
{
    /// <summary>
    ///     Parses a semicolon-separated URL list (the <c>ASPNETCORE_URLS</c> / <c>urls</c> format)
    ///     into bindings. Unparseable entries are skipped rather than throwing — a malformed URL
    ///     must not take the process down at startup.
    /// </summary>
    /// <param name="urls">The configured URL list, or null/empty when none is configured.</param>
    /// <returns>The parsed bindings, in the order given; empty when nothing is configured.</returns>
    public static IReadOnlyList<KestrelHttpBinding> Parse(string? urls)
    {
        if (string.IsNullOrWhiteSpace(urls))
            return [];

        var result = new List<KestrelHttpBinding>();
        foreach (var raw in urls.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
        {
            // Uri cannot parse the "+"/"*" wildcard hosts Kestrel accepts, so substitute a
            // placeholder host purely to get scheme/port out, then keep the original host token.
            var isWildcard = raw.Contains("://+", StringComparison.Ordinal)
                          || raw.Contains("://*", StringComparison.Ordinal);
            var probe = isWildcard
                ? raw.Replace("://+", "://placeholder", StringComparison.Ordinal)
                     .Replace("://*", "://placeholder", StringComparison.Ordinal)
                : raw;

            if (!Uri.TryCreate(probe, UriKind.Absolute, out var uri))
                continue;

            var host = isWildcard
                ? raw.Contains("://+", StringComparison.Ordinal) ? "+" : "*"
                : uri.Host;

            result.Add(new KestrelHttpBinding(host, uri.Port, uri.Scheme == Uri.UriSchemeHttps));
        }

        return result;
    }

    /// <summary>
    ///     Parses the bare-port list format of <c>ASPNETCORE_HTTP_PORTS</c> / <c>HTTP_PORTS</c> (and
    ///     the <c>HTTPS</c> variants) into wildcard (<c>+</c>, all-interfaces) bindings — the shape
    ///     Kestrel itself gives those variables. Modern .NET base images (aspnet:8.0+) set
    ///     <c>ASPNETCORE_HTTP_PORTS=8080</c> as the container default <b>instead of</b>
    ///     <c>ASPNETCORE_URLS</c>, so a node that never sets <c>URLS</c> explicitly still has a real
    ///     configured surface here that must be re-bound, not replaced with Kestrel's localhost:5000.
    /// </summary>
    /// <param name="ports">A <c>;</c>- or <c>,</c>-separated list of bare ports, or null/empty.</param>
    /// <param name="isSecure">True to mark the parsed bindings <c>https</c> (the <c>HTTPS_PORTS</c> vars).</param>
    /// <returns>One all-interfaces binding per parseable port; empty when nothing is configured.</returns>
    public static IReadOnlyList<KestrelHttpBinding> ParsePorts(string? ports, bool isSecure)
    {
        if (string.IsNullOrWhiteSpace(ports))
            return [];

        var result = new List<KestrelHttpBinding>();
        foreach (var token in ports.Split([';', ','], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
        {
            // Skip a malformed port rather than throwing — a bad env var must not down startup.
            if (int.TryParse(token, out var port) && port is > 0 and <= 65535)
                result.Add(new KestrelHttpBinding("+", port, isSecure));
        }

        return result;
    }

    /// <summary>
    ///     Applies this binding to Kestrel, choosing the narrowest listen call the host component
    ///     allows.
    /// </summary>
    /// <remarks>
    ///     A hostname that is neither a literal IP nor <c>localhost</c> cannot be reliably mapped to
    ///     a local interface, so it falls back to <see cref="KestrelServerOptions.ListenAnyIP"/> —
    ///     the safe superset. Narrowing it and guessing wrong would silently stop serving.
    /// </remarks>
    /// <param name="options">The Kestrel options being configured.</param>
    public void Apply(KestrelServerOptions options)
    {
        ArgumentNullException.ThrowIfNull(options);

        if (Host is "+" or "*")
            options.ListenAnyIP(Port);
        else if (IPAddress.TryParse(Host, out var address))
            options.Listen(address, Port);
        else if (string.Equals(Host, "localhost", StringComparison.OrdinalIgnoreCase))
            options.ListenLocalhost(Port);
        else
            options.ListenAnyIP(Port);
    }
}
