using FrameProcessor.Configuration;
using FrameProcessor.Domain;
using Microsoft.Extensions.Options;
using MQTTnet;
using MQTTnet.Client;
using MQTTnet.Protocol;

namespace FrameProcessor.Mqtt;

/// <summary>
/// Persistent MQTT client wrapped in a hosted service. Connects on startup and
/// reconnects in the background after disconnects. Connection state is surfaced
/// via <see cref="IsConnected"/> for the <c>/health</c> endpoint.
/// </summary>
public sealed class MqttPublisher : BackgroundService
{
    private static readonly TimeSpan ReconnectDelay = TimeSpan.FromSeconds(5);

    private readonly IOptions<MqttOptions> _options;
    private readonly ILogger<MqttPublisher> _logger;
    private readonly IMqttClient _client;

    public MqttPublisher(IOptions<MqttOptions> options, ILogger<MqttPublisher> logger)
    {
        _options = options;
        _logger = logger;
        _client = new MqttFactory().CreateMqttClient();
    }

    public bool IsConnected => _client.IsConnected;

    public override async Task StartAsync(CancellationToken cancellationToken)
    {
        try
        {
            await _client.ConnectAsync(BuildClientOptions(), cancellationToken).ConfigureAwait(false);
            _logger.LogInformation(
                "MQTT connected to {Host}:{Port}",
                _options.Value.Host,
                _options.Value.Port);
        }
        catch (Exception ex) when (!cancellationToken.IsCancellationRequested)
        {
            _logger.LogWarning(
                ex,
                "Initial MQTT connect to {Host}:{Port} failed; background reconnect will retry",
                _options.Value.Host,
                _options.Value.Port);
        }

        await base.StartAsync(cancellationToken).ConfigureAwait(false);
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await Task.Delay(ReconnectDelay, stoppingToken).ConfigureAwait(false);
            }
            catch (OperationCanceledException)
            {
                return;
            }

            if (_client.IsConnected)
            {
                continue;
            }

            try
            {
                await _client.ConnectAsync(BuildClientOptions(), stoppingToken).ConfigureAwait(false);
                _logger.LogInformation(
                    "MQTT reconnected to {Host}:{Port}",
                    _options.Value.Host,
                    _options.Value.Port);
            }
            catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
            {
                _logger.LogWarning(
                    ex,
                    "MQTT reconnect to {Host}:{Port} failed",
                    _options.Value.Host,
                    _options.Value.Port);
            }
        }
    }

    /// <summary>
    /// Publish an <c>update</c> notification for the given frame. Returns
    /// <see cref="PublishResult.Failure"/> on any error (disconnected, broker reject,
    /// transport fault) — per SPEC.md §3.5, publish failure must not fail the upload,
    /// so this method never throws on MQTT errors. <see cref="OperationCanceledException"/>
    /// still propagates so callers can honor cooperative cancellation.
    /// </summary>
    public async Task<PublishResult> PublishAsync(MacAddress mac, CancellationToken cancellationToken)
    {
        var opts = _options.Value;
        var topic = $"{opts.BaseTopic}/{mac}";
        var message = new MqttApplicationMessageBuilder()
            .WithTopic(topic)
            .WithPayload("update")
            .WithQualityOfServiceLevel((MqttQualityOfServiceLevel)opts.PublishQos)
            .WithRetainFlag(false)
            .Build();

        try
        {
            var result = await _client.PublishAsync(message, cancellationToken).ConfigureAwait(false);
            if (result.IsSuccess)
            {
                _logger.LogInformation("MQTT publish to {Topic} succeeded", topic);
                return PublishResult.Success;
            }

            _logger.LogWarning("MQTT publish to {Topic} returned {ReasonCode}", topic, result.ReasonCode);
            return PublishResult.Failure;
        }
        catch (Exception ex) when (!cancellationToken.IsCancellationRequested)
        {
            _logger.LogWarning(ex, "MQTT publish to {Topic} failed", topic);
            return PublishResult.Failure;
        }
    }

    public override async Task StopAsync(CancellationToken cancellationToken)
    {
        await base.StopAsync(cancellationToken).ConfigureAwait(false);

        if (_client.IsConnected)
        {
            try
            {
                await _client.DisconnectAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "MQTT disconnect on shutdown failed");
            }
        }
    }

    public override void Dispose()
    {
        _client.Dispose();
        base.Dispose();
    }

    private MqttClientOptions BuildClientOptions()
    {
        var opts = _options.Value;
        var builder = new MqttClientOptionsBuilder()
            .WithTcpServer(opts.Host, opts.Port)
            .WithClientId(opts.ClientId)
            .WithCleanSession(false);

        if (!string.IsNullOrEmpty(opts.Username))
        {
            builder = builder.WithCredentials(opts.Username, opts.Password);
        }

        if (opts.UseTls)
        {
            builder = builder.WithTlsOptions(_ => { });
        }

        return builder.Build();
    }
}
