// Copyright 2018-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0

using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using ZB.MOM.NatsNet.Server.Internal;

namespace ZB.MOM.NatsNet.Server;

public sealed partial class NatsServer
{
    internal Exception? NewGateway(ServerOptions options)
    {
        var validationErr = GatewayHandler.ValidateGatewayOptions(options);
        if (validationErr != null)
            return validationErr;

        if (string.IsNullOrWhiteSpace(options.Gateway.Name) || options.Gateway.Port == 0)
            return null;

        _gateway.AcquireWriteLock();
        try
        {
            _gateway.Enabled = true;
            _gateway.Name = options.Gateway.Name;
            _gateway.RejectUnknown = options.Gateway.RejectUnknown;
            _gateway.Remotes.Clear();
            _gateway.Out.Clear();
            _gateway.Outo.Clear();
            _gateway.In.Clear();
            _gateway.OwnCfgUrls = [];

            foreach (var remote in options.Gateway.Gateways)
            {
                var cfg = new GatewayCfg
                {
                    RemoteOpts = remote.Clone(),
                    Hash = GatewayHandler.GetGWHash(remote.Name),
                    OldHash = GatewayHandler.GetOldHash(remote.Name),
                    Urls = remote.Urls.ToDictionary(u => u.ToString(), u => u, StringComparer.Ordinal),
                };

                _gateway.Remotes[remote.Name] = cfg;

                foreach (var url in remote.Urls)
                    _gateway.OwnCfgUrls.Add(url.ToString());
            }

            var info = CopyInfo();
            info.Gateway = options.Gateway.Name;
            info.GatewayUrl = _gateway.Url;
            info.GatewayUrls = [.. _gateway.Urls.GetAsStringSlice()];
            _gateway.Info = info;
            _gateway.InfoJson = GenerateInfoJson(info);

            _gateway.SIdHash = GatewayHandler.GetGWHash(_info.Id);
            _gateway.OldHash = GatewayHandler.GetOldHash(_info.Id);
            var clusterHash = GatewayHandler.GetGWHash(options.Gateway.Name);
            _gateway.ReplyPfx = Encoding.ASCII.GetBytes($"_GR_.{Encoding.ASCII.GetString(clusterHash)}.{Encoding.ASCII.GetString(_gateway.SIdHash)}.");
            _gateway.OldReplyPfx = Encoding.ASCII.GetBytes($"$GR.{Encoding.ASCII.GetString(_gateway.OldHash)}.");
        }
        finally
        {
            _gateway.ReleaseWriteLock();
        }

        return null;
    }

    internal Exception? StartGateways()
    {
        if (!_gateway.Enabled)
            return null;

        var hostPortErr = SetGatewayInfoHostPort();
        if (hostPortErr != null)
            return hostPortErr;

        SolicitGateways();
        return StartGatewayAcceptLoop();
    }

    internal Exception? StartGatewayAcceptLoop()
    {
        if (_gatewayListener == null)
        {
            var opts = GetOpts();
            var hp = $"{opts.Gateway.Host}:{opts.Gateway.Port}";

            _mu.EnterWriteLock();
            try
            {
                var parts = hp.Split(':', 2);
                var host = parts[0];
                var port = parts.Length > 1 ? int.Parse(parts[1]) : 0;
                var addr = string.IsNullOrWhiteSpace(host) || host == "0.0.0.0"
                    ? IPAddress.Any
                    : (host == "::" ? IPAddress.IPv6Any : IPAddress.Parse(host));

                _gatewayListener = new TcpListener(addr, port);
                _gatewayListener.Start();
            }
            catch (Exception ex)
            {
                _gatewayListenerErr = ex;
                return ex;
            }
            finally
            {
                _mu.ExitWriteLock();
            }
        }

        if (!StartGoRoutine(() => Noticef("Gateway accept loop started")))
            return new InvalidOperationException("unable to start gateway accept loop");

        return null;
    }

    internal Exception? SetGatewayInfoHostPort()
    {
        var opts = GetOpts();
        var host = opts.Gateway.Host;
        var port = opts.Gateway.Port;

        if (!string.IsNullOrWhiteSpace(opts.Gateway.Advertise))
        {
            var (advHost, advPort, advErr) = Internal.ServerUtilities.ParseHostPort(opts.Gateway.Advertise, port);
            if (advErr != null)
                return advErr;

            host = advHost;
            port = advPort;
        }

        var scheme = opts.Gateway.TlsConfig != null ? "tls" : "nats";
        var url = $"{scheme}://{host}:{port}";

        _gateway.AcquireWriteLock();
        try
        {
            _gateway.Url = url;
            _gateway.Info ??= CopyInfo();
            _gateway.Info.Gateway = opts.Gateway.Name;
            _gateway.Info.GatewayUrl = url;
            _gateway.Info.GatewayUrls = [.. _gateway.Urls.GetAsStringSlice()];
            _gateway.InfoJson = GenerateInfoJson(_gateway.Info);
        }
        finally
        {
            _gateway.ReleaseWriteLock();
        }

        return null;
    }

    internal void SolicitGateways()
    {
        if (!_gateway.Enabled)
            return;

        var delay = GatewayHandler.GetGatewaysSolicitDelay();
        if (delay > TimeSpan.Zero)
            Thread.Sleep(delay);

        List<GatewayCfg> remotes;
        _gateway.AcquireReadLock();
        try
        {
            remotes = [.. _gateway.Remotes.Values];
        }
        finally
        {
            _gateway.ReleaseReadLock();
        }

        foreach (var cfg in remotes)
            SolicitGateway(cfg, firstConnect: true);
    }

    internal void ReconnectGateway(GatewayCfg cfg)
    {
        SolicitGateway(cfg, firstConnect: false);
    }

    internal void SolicitGateway(GatewayCfg cfg, bool firstConnect)
    {
        _ = firstConnect;

        if (cfg.RemoteOpts == null || cfg.RemoteOpts.Urls.Count == 0)
            return;

        if (_gateway.HasInbound(cfg.RemoteOpts.Name))
            return;

        CreateGateway(cfg, cfg.RemoteOpts.Urls[0]);
    }

    internal ClientConnection? CreateGateway(GatewayCfg cfg, Uri? url = null)
    {
        if (cfg.RemoteOpts == null)
            return null;

        var connection = new ClientConnection(ClientKind.Gateway, this)
        {
            Gateway = new Gateway
            {
                Name = cfg.RemoteOpts.Name,
                Cfg = cfg,
                ConnectUrl = url,
                Outbound = true,
            },
        };

        _gateway.AcquireWriteLock();
        try
        {
            _gateway.Out[cfg.RemoteOpts.Name] = connection;
            _gateway.Outo = [.. _gateway.Out.Values];
        }
        finally
        {
            _gateway.ReleaseWriteLock();
        }

        return connection;
    }
}
