using System.Text.Json;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Mbproxy.Options;

namespace Mbproxy.Admin;

/// <summary>
/// Hosted service that owns the Kestrel-backed admin HTTP endpoint.
///
/// <para>Lifecycle:</para>
/// <list type="bullet">
///   <item><see cref="StartAsync"/> builds a <see cref="WebApplication"/> bound to
///         <c>Mbproxy.AdminPort</c> and starts it non-blocking.</item>
///   <item>If the bind fails (port in use, etc.), logs <c>mbproxy.admin.bind.failed</c>
///         at Error and continues — the proxy listeners are unaffected.</item>
///   <item>If <c>AdminPort</c> changes via hot-reload, the current app is stopped and a
///         new one is started on the new port. Other config changes are ignored here.</item>
///   <item><see cref="StopAsync"/> shuts down the current Kestrel app with a 2 s deadline.</item>
/// </list>
///
/// <para>Routes: exactly two — <c>GET /</c> (HTML) and <c>GET /status.json</c> (JSON).</para>
/// </summary>
internal sealed partial class AdminEndpointHost : IHostedService, IAsyncDisposable
{
    private readonly IOptionsMonitor<MbproxyOptions> _optionsMonitor;
    private readonly StatusSnapshotBuilder _builder;
    private readonly ILoggerFactory _loggerFactory;
    private readonly ILogger<AdminEndpointHost> _logger;

    // The currently-running Kestrel app; null when stopped or when bind failed.
    private WebApplication? _app;

    // Protects concurrent Start/Stop calls (hot-reload + StopAsync racing).
    private readonly SemaphoreSlim _lock = new(1, 1);

    // Current configured port — used to detect changes on hot-reload.
    private int _currentPort;

    // Subscription token for IOptionsMonitor.OnChange.
    private IDisposable? _optionsChangeRegistration;

    public AdminEndpointHost(
        IOptionsMonitor<MbproxyOptions> optionsMonitor,
        StatusSnapshotBuilder builder,
        ILoggerFactory loggerFactory)
    {
        _optionsMonitor = optionsMonitor;
        _builder        = builder;
        _loggerFactory  = loggerFactory;
        _logger         = loggerFactory.CreateLogger<AdminEndpointHost>();
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        _currentPort = _optionsMonitor.CurrentValue.AdminPort;

        await StartAppAsync(_currentPort, cancellationToken).ConfigureAwait(false);

        // Subscribe to config changes: if AdminPort changes, re-bind.
        _optionsChangeRegistration = _optionsMonitor.OnChange(opts =>
        {
            int newPort = opts.AdminPort;
            if (newPort == _currentPort) return; // Only care about AdminPort changes.

            // Fire-and-forget: re-bind is async; we can't await in OnChange.
            _ = Task.Run(async () =>
            {
                await _lock.WaitAsync().ConfigureAwait(false);
                try
                {
                    if (newPort == _currentPort) return; // double-check under lock

                    // Stop the old app.
                    await StopCurrentAppAsync().ConfigureAwait(false);

                    _currentPort = newPort;

                    // Start on the new port.
                    await StartAppAsync(newPort, CancellationToken.None).ConfigureAwait(false);
                }
                finally
                {
                    _lock.Release();
                }
            });
        });
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        _optionsChangeRegistration?.Dispose();
        _optionsChangeRegistration = null;

        await _lock.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            await StopCurrentAppAsync().ConfigureAwait(false);
        }
        finally
        {
            _lock.Release();
        }
    }

    // ── Internal helpers ─────────────────────────────────────────────────────

    /// <summary>
    /// Builds and starts a Kestrel <see cref="WebApplication"/> on <paramref name="port"/>.
    /// On bind failure, logs the error and sets <c>_app = null</c> — does NOT throw.
    /// Caller must hold <c>_lock</c> or be in a single-threaded context (StartAsync).
    /// </summary>
    private async Task StartAppAsync(int port, CancellationToken ct)
    {
        try
        {
            // Use CreateSlimBuilder with explicit args (empty) to avoid inheriting
            // process-level environment variables like ASPNETCORE_URLS.
            var builder = WebApplication.CreateSlimBuilder(new WebApplicationOptions
            {
                Args = [],
            });

            // Suppress Kestrel/ASP.NET Core built-in logging; forward to the outer host's
            // logger factory so that admin-endpoint errors appear in the proxy's log stream.
            builder.Logging.ClearProviders();
            builder.Logging.AddProvider(new ForwardingLoggerProvider(_loggerFactory));

            // Explicit Kestrel listen — overrides any ASPNETCORE_URLS that leaked in.
            builder.WebHost.UseKestrel(k =>
            {
                k.Listen(System.Net.IPAddress.Any, port);
            });

            var app = builder.Build();

            // ── Routes ───────────────────────────────────────────────────────
            app.MapGet("/", (HttpContext ctx) =>
            {
                var snapshot = _builder.Build();
                string html  = StatusHtmlRenderer.Render(snapshot);
                return Results.Content(html, "text/html; charset=utf-8");
            });

            app.MapGet("/status.json", (HttpContext ctx) =>
            {
                var snapshot = _builder.Build();
                string json  = JsonSerializer.Serialize(snapshot, StatusJsonContext.Default.StatusResponse);
                return Results.Content(json, "application/json");
            });

            await app.StartAsync(ct).ConfigureAwait(false);
            _app = app;

            LogAdminStarted(_logger, port);
        }
        catch (Exception ex) when (ex is not OperationCanceledException)
        {
            // Bind failed — log and continue. Proxy listeners are unaffected.
            LogAdminBindFailed(_logger, port, ex.Message);
            _app = null;
        }
    }

    /// <summary>
    /// Stops the current <see cref="WebApplication"/> with a 2 s deadline, then disposes it.
    /// </summary>
    private async Task StopCurrentAppAsync()
    {
        if (_app is null) return;

        var app = _app;
        _app = null;

        try
        {
            using var stopCts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
            await app.StopAsync(stopCts.Token).ConfigureAwait(false);
        }
        catch
        {
            // Best-effort.
        }

        await app.DisposeAsync().ConfigureAwait(false);
    }

    // ── IAsyncDisposable ─────────────────────────────────────────────────────

    public async ValueTask DisposeAsync()
    {
        _optionsChangeRegistration?.Dispose();
        _lock.Dispose();

        if (_app is { } app)
        {
            _app = null;
            await app.DisposeAsync().ConfigureAwait(false);
        }
    }

    // ── Logging ──────────────────────────────────────────────────────────────

    [LoggerMessage(EventId = 70, EventName = "mbproxy.admin.started",
        Level = LogLevel.Information,
        Message = "Admin endpoint started on port {Port}")]
    private static partial void LogAdminStarted(ILogger logger, int port);

    [LoggerMessage(EventId = 71, EventName = "mbproxy.admin.bind.failed",
        Level = LogLevel.Error,
        Message = "Admin endpoint bind failed — admin page will be unavailable: Port={Port} Reason={Reason}")]
    private static partial void LogAdminBindFailed(ILogger logger, int port, string reason);

    // ── Inner logger provider (forwards Kestrel/ASP.NET logs to the proxy's factory) ────

    private sealed class ForwardingLoggerProvider : ILoggerProvider
    {
        private readonly ILoggerFactory _factory;
        public ForwardingLoggerProvider(ILoggerFactory factory) => _factory = factory;
        public ILogger CreateLogger(string categoryName) => _factory.CreateLogger(categoryName);
        public void Dispose() { }
    }
}
