// Copyright 2019-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");

using System.Net;
using System.Text;
using ZB.MOM.NatsNet.Server.Auth;
using ZB.MOM.NatsNet.Server.Internal;

namespace ZB.MOM.NatsNet.Server;

internal static class LeafNodeHandler
{
    internal static readonly TimeSpan LeafNodeReconnectDelayAfterLoopDetected = TimeSpan.FromSeconds(30);
    internal static readonly TimeSpan LeafNodeReconnectAfterPermViolation = TimeSpan.FromSeconds(30);
    internal static readonly TimeSpan LeafNodeReconnectDelayAfterClusterNameSame = TimeSpan.FromSeconds(30);

    internal const string LeafNodeLoopDetectionSubjectPrefix = "$LDS.";

    public static Exception? ValidateLeafNode(ServerOptions options)
    {
        var authErr = ValidateLeafNodeAuthOptions(options);
        if (authErr != null)
            return authErr;

        foreach (var remote in options.LeafNode.Remotes)
        {
            if (string.IsNullOrWhiteSpace(remote.LocalAccount))
                remote.LocalAccount = ServerConstants.DefaultGlobalAccount;

            var (warnings, proxyErr) = ValidateLeafNodeProxyOptions(remote);
            _ = warnings;
            if (proxyErr != null)
                return proxyErr;

        }

        if (!string.IsNullOrWhiteSpace(options.LeafNode.MinVersion))
        {
            var minVersionErr = CheckLeafMinVersionConfig(options.LeafNode.MinVersion);
            if (minVersionErr != null)
                return minVersionErr;
        }

        return null;
    }

    public static Exception? CheckLeafMinVersionConfig(string minVersion)
    {
        var (ok, err) = ServerUtilities.VersionAtLeastCheckError(minVersion, 2, 8, 0);
        if (err != null)
            return new InvalidOperationException($"invalid leafnode minimum version: {err.Message}", err);

        if (!ok)
            return new InvalidOperationException("the minimum version should be at least 2.8.0");

        return null;
    }

    public static Exception? ValidateLeafNodeAuthOptions(ServerOptions options)
    {
        var users = options.LeafNode.Users;
        if (users is null || users.Count == 0)
            return null;

        if (!string.IsNullOrWhiteSpace(options.LeafNode.Username))
            return new InvalidOperationException("can not have a single user/pass and a users array");

        if (!string.IsNullOrWhiteSpace(options.LeafNode.Nkey))
            return new InvalidOperationException("can not have a single nkey and a users array");

        var seen = new HashSet<string>(StringComparer.Ordinal);
        foreach (var user in users)
        {
            if (!seen.Add(user.Username))
                return new InvalidOperationException($"duplicate user {user.Username} detected in leafnode authorization");
        }

        return null;
    }

    public static (List<string> Warnings, Exception? Error) ValidateLeafNodeProxyOptions(RemoteLeafOpts remote)
    {
        var warnings = new List<string>();
        if (string.IsNullOrWhiteSpace(remote.Proxy.Url))
            return (warnings, null);

        if (!Uri.TryCreate(remote.Proxy.Url, UriKind.Absolute, out var proxyUri))
            return (warnings, new InvalidOperationException("invalid proxy URL"));

        if (!string.Equals(proxyUri.Scheme, "http", StringComparison.OrdinalIgnoreCase)
            && !string.Equals(proxyUri.Scheme, "https", StringComparison.OrdinalIgnoreCase))
            return (warnings, new InvalidOperationException($"proxy URL scheme must be http or https, got: {proxyUri.Scheme}"));

        if (string.IsNullOrWhiteSpace(proxyUri.Host))
            return (warnings, new InvalidOperationException("proxy URL must specify a host"));

        if (remote.Proxy.Timeout < TimeSpan.Zero)
            return (warnings, new InvalidOperationException("proxy timeout must be >= 0"));

        var userPresent = !string.IsNullOrWhiteSpace(remote.Proxy.Username);
        var passPresent = !string.IsNullOrWhiteSpace(remote.Proxy.Password);
        if (userPresent != passPresent)
            return (warnings, new InvalidOperationException("proxy username and password must both be specified or both be empty"));

        return (warnings, null);
    }

    public static LeafNodeCfg NewLeafNodeCfg(RemoteLeafOpts remote)
    {
        var cfg = new LeafNodeCfg
        {
            RemoteOpts = remote,
            Urls = [.. remote.Urls],
            CurUrl = remote.Urls.Count > 0 ? remote.Urls[0] : null,
            Perms = new Permissions
            {
                Publish = new SubjectPermission { Deny = [.. remote.DenyImports] },
                Subscribe = new SubjectPermission { Deny = [.. remote.DenyExports] },
            },
        };

        cfg.SaveUserPassword(cfg.CurUrl);
        cfg.SaveTLSHostname(cfg.CurUrl);
        return cfg;
    }

    public static Exception? EstablishHTTPProxyTunnel(Stream stream, Uri target, RemoteLeafProxyOpts proxy, CancellationToken cancellationToken = default)
    {
        _ = cancellationToken;
        if (stream is null)
            return new InvalidOperationException("proxy tunnel requires an open stream");
        if (target is null)
            return new InvalidOperationException("proxy tunnel requires a target URL");

        var hostPort = target.IsDefaultPort ? target.Host : $"{target.Host}:{target.Port}";
        var builder = new StringBuilder();
        builder.Append("CONNECT ").Append(hostPort).Append(" HTTP/1.1\r\n");
        builder.Append("Host: ").Append(hostPort).Append("\r\n");
        if (!string.IsNullOrWhiteSpace(proxy.Username) || !string.IsNullOrWhiteSpace(proxy.Password))
        {
            var raw = Encoding.UTF8.GetBytes($"{proxy.Username}:{proxy.Password}");
            builder.Append("Proxy-Authorization: Basic ")
                .Append(Convert.ToBase64String(raw))
                .Append("\r\n");
        }

        builder.Append("\r\n");
        var data = Encoding.ASCII.GetBytes(builder.ToString());
        stream.Write(data, 0, data.Length);
        stream.Flush();
        return null;
    }

    public static string KeyFromSub(Internal.Subscription sub)
    {
        var subject = Encoding.ASCII.GetString(sub.Subject);
        if (sub.Queue is not { Length: > 0 })
            return subject;

        return $"{subject} {Encoding.ASCII.GetString(sub.Queue)}";
    }

    public static string KeyFromSubWithOrigin(Internal.Subscription sub, string? origin = null)
    {
        var subject = Encoding.ASCII.GetString(sub.Subject);
        var queue = sub.Queue is { Length: > 0 } q ? Encoding.ASCII.GetString(q) : string.Empty;
        var hasOrigin = !string.IsNullOrWhiteSpace(origin);

        return (hasOrigin, queue.Length > 0) switch
        {
            (false, false) => $"R {subject}",
            (false, true) => $"R {subject} {queue}",
            (true, false) => $"L {subject} {origin}",
            (true, true) => $"L {subject} {queue} {origin}",
        };
    }
}
