{%- import "util.jinja.include" as util -%}
{{ util.CommonFileHeader() }}

using CloudNative.CloudEvents;
using CloudNative.CloudEvents.SystemTextJson;
using CloudNative.CloudEvents.Amqp;
using Amqp;
using Amqp.Sasl;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;

namespace {{ project_name | pascal }}
{
    {{ util.EndpointCredentialClasses() | indent(4) }}

    internal class AmqpProducer : IDisposable
    {
        private readonly ILogger _logger;
        private readonly EndpointCredential _credential;
        private readonly List<Uri> _endpoints;
        private readonly ConcurrentDictionary<Uri, Tuple<Connection, Session, SenderLink>> endpointConnections = new();
        private readonly string? _node;
        private static readonly CloudEventFormatter _formatter = new JsonEventFormatter();
        private bool _disposed = false;
        
        public AmqpProducer(ILogger logger, EndpointCredential credential, Dictionary<string, string> options, List<Uri> endpoints)
        {
            this._logger = logger ?? throw new ArgumentNullException(nameof(logger));
            this._credential = credential ?? throw new ArgumentNullException(nameof(credential));
            this._endpoints = endpoints ?? throw new ArgumentNullException(nameof(endpoints));
            if (options != null && options.TryGetValue("node", out var node))
            {
                _node = node;
            }
        }

        public async Task SendAsync(CloudEvent cloudEvent, ContentMode contentMode, CloudEventFormatter? formatter)
        {
            for (int i = _endpoints.Count - 1; i >= 0; i--)
            {
                var endpoint = _endpoints[i];
                var connectionTuple = await GetEndpointConnectionAsync(endpoint).ConfigureAwait(false);
                var sender = connectionTuple.Item3;
                try
                {
                    await sender.SendAsync(cloudEvent.ToAmqpMessage(contentMode, formatter??AmqpProducer._formatter)).ConfigureAwait(false);
                }
                catch (AmqpException ex)
                {
                    _logger.LogError(ex, "Error sending message to endpoint {Endpoint}", endpoint);
                    _endpoints.RemoveAt(i);
                    throw;
                }
            }
        }

        public async Task SendAsync(Message amqpMessage)
        {
            for (int i = _endpoints.Count - 1; i >= 0; i--)
            {
                var endpoint = _endpoints[i];
                var connectionTuple = await GetEndpointConnectionAsync(endpoint).ConfigureAwait(false);
                var sender = connectionTuple.Item3;
                try
                {
                    await sender.SendAsync(amqpMessage).ConfigureAwait(false);
                }
                catch (AmqpException ex)
                {
                    _logger.LogError(ex, "Error sending message to endpoint {Endpoint}", endpoint);
                    _endpoints.RemoveAt(i);
                    throw;
                }
            }
        }

        /// <summary>
        /// Gets the connection to the endpoint.
        /// </summary>
        /// <param name="endpoint">The endpoint to connect to.</param>
        /// <returns>The connection to the endpoint.</returns>
        private async Task<Tuple<Connection, Session, SenderLink>> GetEndpointConnectionAsync(Uri endpoint)
        {
            try
            {
                if (endpointConnections.TryGetValue(endpoint, out var connectionTuple))
                {
                    return connectionTuple;
                }

                var pathValue = _node != null 
                    ? (_node.StartsWith("/") ? _node : "/" + _node)
                    : endpoint.AbsolutePath;

                Address address = new Address(
                   endpoint.Host,
                   endpoint.Port == -1 ? endpoint.Scheme == "amqps" ? 5671 : 5672 : endpoint.Port,
                   path: pathValue, scheme: endpoint.Scheme.ToUpper(),
                   user: (_credential as PlainEndpointCredential)?.ClientId,
                   password: (_credential as PlainEndpointCredential)?.ClientSecret);

                ConnectionFactory factory = new ConnectionFactory();
                if (_credential is TokenEndpointCredential tokenCredential)
                {
                    factory.SASL.Profile = SaslProfile.Anonymous;
                }

                var connection = await factory.CreateAsync(address).ConfigureAwait(false);
                var session = new Session(connection);
                if (_credential is TokenEndpointCredential)
                {
                    var token = await ((TokenEndpointCredential)_credential).GetTokenAsync().ConfigureAwait(false);
                    var cbsSender = new SenderLink(session, "$cbs", "$cbs");
                    var request = new global::Amqp.Message(token);
                    request.Properties.MessageId = Guid.NewGuid().ToString();
                    request.ApplicationProperties["operation"] = "put-token";
                    request.ApplicationProperties["type"] = "amqp:jwt";
                    request.ApplicationProperties["name"] = string.Format("amqp://{0}/{1}", address.Host, address.Path);
                    await cbsSender.SendAsync(request).ConfigureAwait(false);
                    await cbsSender.CloseAsync().ConfigureAwait(false);
                }
                var sender = new SenderLink(session, "sender-link", address.Path);
                connectionTuple = new Tuple<Connection, Session, SenderLink>(connection, session, sender);
                endpointConnections.TryAdd(endpoint, connectionTuple);
                return connectionTuple;
            }
            catch( Exception ex)
            {
                _logger.LogError(ex, "Error establishing connection to endpoint {Endpoint}", endpoint);
                for (int i = _endpoints.Count - 1; i >= 0; i--)
                {
                    if (_endpoints[i] == endpoint)
                    {
                        _endpoints.RemoveAt(i);
                        break;
                    }
                }
                throw;
            }
        }

        public void Dispose()
        {
            if (_disposed) return;
            
            foreach (var tuple in endpointConnections.Values)
            {
                try
                {
                    tuple.Item3?.Close(TimeSpan.FromSeconds(5));  // SenderLink with shorter timeout
                    tuple.Item2?.Close(TimeSpan.FromSeconds(5));  // Session with shorter timeout
                    tuple.Item1?.Close(TimeSpan.FromSeconds(5));  // Connection with shorter timeout
                }
                catch (TimeoutException)
                {
                    // Timeout during close is acceptable - resources will be cleaned up by the broker
                    _logger.LogDebug("Timeout while closing AMQP connection during disposal (acceptable)");
                }
                catch (Exception ex)
                {
                    _logger.LogWarning(ex, "Error closing AMQP connection during disposal");
                }
            }
            endpointConnections.Clear();
            _disposed = true;
        }
    }
}