using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Feedser.Core.Jobs;
using StellaOps.Feedser.Storage.Mongo;
using StellaOps.Feedser.WebService.Diagnostics;
using Serilog;
using StellaOps.Feedser.Merge;
using StellaOps.Feedser.Merge.Services;
using StellaOps.Feedser.WebService.Extensions;
using StellaOps.Feedser.WebService.Jobs;
using StellaOps.Feedser.WebService.Options;
using StellaOps.Feedser.WebService.Filters;
using Serilog.Events;
using StellaOps.Plugin.DependencyInjection;
using StellaOps.Plugin.Hosting;
using StellaOps.Configuration;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.Client;
using StellaOps.Auth.ServerIntegration;

var builder = WebApplication.CreateBuilder(args);

const string JobsPolicyName = "Feedser.Jobs.Trigger";

builder.Configuration.AddStellaOpsDefaults(options =>
{
    options.BasePath = builder.Environment.ContentRootPath;
    options.EnvironmentPrefix = "FEEDSER_";
    options.ConfigureBuilder = configurationBuilder =>
    {
        configurationBuilder.AddFeedserYaml(Path.Combine(builder.Environment.ContentRootPath, "../etc/feedser.yaml"));
    };
});

var contentRootPath = builder.Environment.ContentRootPath;

var feedserOptions = builder.Configuration.BindOptions<FeedserOptions>(postConfigure: (opts, _) =>
{
    FeedserOptionsPostConfigure.Apply(opts, contentRootPath);
    FeedserOptionsValidator.Validate(opts);
});
builder.Services.AddOptions<FeedserOptions>()
    .Bind(builder.Configuration)
    .PostConfigure(options =>
    {
        FeedserOptionsPostConfigure.Apply(options, contentRootPath);
        FeedserOptionsValidator.Validate(options);
    })
    .ValidateOnStart();

builder.ConfigureFeedserTelemetry(feedserOptions);

builder.Services.AddMongoStorage(storageOptions =>
{
    storageOptions.ConnectionString = feedserOptions.Storage.Dsn;
    storageOptions.DatabaseName = feedserOptions.Storage.Database;
    storageOptions.CommandTimeout = TimeSpan.FromSeconds(feedserOptions.Storage.CommandTimeoutSeconds);
});

builder.Services.AddMergeModule(builder.Configuration);
builder.Services.AddJobScheduler();
builder.Services.AddBuiltInFeedserJobs();

builder.Services.AddSingleton<ServiceStatus>(sp => new ServiceStatus(sp.GetRequiredService<TimeProvider>()));

var authorityConfigured = feedserOptions.Authority is { Enabled: true };

if (authorityConfigured)
{
    builder.Services.AddStellaOpsAuthClient(clientOptions =>
    {
        clientOptions.Authority = feedserOptions.Authority.Issuer;
        clientOptions.ClientId = feedserOptions.Authority.ClientId ?? string.Empty;
        clientOptions.ClientSecret = feedserOptions.Authority.ClientSecret;
        clientOptions.HttpTimeout = TimeSpan.FromSeconds(feedserOptions.Authority.BackchannelTimeoutSeconds);

        clientOptions.DefaultScopes.Clear();
        foreach (var scope in feedserOptions.Authority.ClientScopes)
        {
            clientOptions.DefaultScopes.Add(scope);
        }

        var resilience = feedserOptions.Authority.Resilience ?? new FeedserOptions.AuthorityOptions.ResilienceOptions();
        if (resilience.EnableRetries.HasValue)
        {
            clientOptions.EnableRetries = resilience.EnableRetries.Value;
        }

        if (resilience.RetryDelays is { Count: > 0 })
        {
            clientOptions.RetryDelays.Clear();
            foreach (var delay in resilience.RetryDelays)
            {
                clientOptions.RetryDelays.Add(delay);
            }
        }

        if (resilience.AllowOfflineCacheFallback.HasValue)
        {
            clientOptions.AllowOfflineCacheFallback = resilience.AllowOfflineCacheFallback.Value;
        }

        if (resilience.OfflineCacheTolerance.HasValue)
        {
            clientOptions.OfflineCacheTolerance = resilience.OfflineCacheTolerance.Value;
        }
    });

    builder.Services.AddStellaOpsResourceServerAuthentication(
        builder.Configuration,
        configurationSection: null,
        configure: resourceOptions =>
        {
            resourceOptions.Authority = feedserOptions.Authority.Issuer;
            resourceOptions.RequireHttpsMetadata = feedserOptions.Authority.RequireHttpsMetadata;
            resourceOptions.BackchannelTimeout = TimeSpan.FromSeconds(feedserOptions.Authority.BackchannelTimeoutSeconds);
            resourceOptions.TokenClockSkew = TimeSpan.FromSeconds(feedserOptions.Authority.TokenClockSkewSeconds);

            if (!string.IsNullOrWhiteSpace(feedserOptions.Authority.MetadataAddress))
            {
                resourceOptions.MetadataAddress = feedserOptions.Authority.MetadataAddress;
            }

            foreach (var audience in feedserOptions.Authority.Audiences)
            {
                resourceOptions.Audiences.Add(audience);
            }

            foreach (var scope in feedserOptions.Authority.RequiredScopes)
            {
                resourceOptions.RequiredScopes.Add(scope);
            }

            foreach (var network in feedserOptions.Authority.BypassNetworks)
            {
                resourceOptions.BypassNetworks.Add(network);
            }
        });

    builder.Services.AddAuthorization(options =>
    {
        options.AddStellaOpsScopePolicy(JobsPolicyName, feedserOptions.Authority.RequiredScopes.ToArray());
    });
}

var pluginHostOptions = BuildPluginOptions(feedserOptions, builder.Environment.ContentRootPath);
builder.Services.RegisterPluginRoutines(builder.Configuration, pluginHostOptions);

builder.Services.AddEndpointsApiExplorer();

var app = builder.Build();

var resolvedFeedserOptions = app.Services.GetRequiredService<IOptions<FeedserOptions>>().Value;
var resolvedAuthority = resolvedFeedserOptions.Authority ?? new FeedserOptions.AuthorityOptions();
authorityConfigured = resolvedAuthority.Enabled;
var enforceAuthority = resolvedAuthority.Enabled && !resolvedAuthority.AllowAnonymousFallback;

if (resolvedAuthority.Enabled && resolvedAuthority.AllowAnonymousFallback)
{
    app.Logger.LogWarning(
        "Authority authentication is configured but anonymous fallback remains enabled. Set authority.allowAnonymousFallback to false before 2025-12-31 to complete the rollout.");
}

var jsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
jsonOptions.Converters.Add(new JsonStringEnumConverter());

var loggingEnabled = feedserOptions.Telemetry?.EnableLogging ?? true;

if (loggingEnabled)
{
    app.UseSerilogRequestLogging(options =>
    {
        options.IncludeQueryInRequestPath = true;
        options.GetLevel = (httpContext, elapsedMs, exception) => exception is null ? LogEventLevel.Information : LogEventLevel.Error;
        options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
        {
            diagnosticContext.Set("RequestId", httpContext.TraceIdentifier);
            diagnosticContext.Set("UserAgent", httpContext.Request.Headers.UserAgent.ToString());
            if (Activity.Current is { TraceId: var traceId } && traceId != default)
            {
                diagnosticContext.Set("TraceId", traceId.ToString());
            }
        };
    });
}

app.UseExceptionHandler(errorApp =>
{
    errorApp.Run(async context =>
    {
        context.Response.ContentType = "application/problem+json";
        var feature = context.Features.Get<IExceptionHandlerFeature>();
        var error = feature?.Error;

        var extensions = new Dictionary<string, object?>(StringComparer.Ordinal)
        {
            ["traceId"] = Activity.Current?.TraceId.ToString() ?? context.TraceIdentifier,
        };

        var problem = Results.Problem(
            detail: error?.Message,
            instance: context.Request.Path,
            statusCode: StatusCodes.Status500InternalServerError,
            title: "Unexpected server error",
            type: ProblemTypes.JobFailure,
            extensions: extensions);

        await problem.ExecuteAsync(context);
    });
});

if (authorityConfigured)
{
    app.Use(async (context, next) =>
    {
        await next().ConfigureAwait(false);

        if (!context.Request.Path.StartsWithSegments("/jobs", StringComparison.OrdinalIgnoreCase))
        {
            return;
        }

        if (context.Response.StatusCode != StatusCodes.Status401Unauthorized)
        {
            return;
        }

        var optionsMonitor = context.RequestServices.GetRequiredService<IOptions<FeedserOptions>>().Value.Authority;
        if (optionsMonitor is null || !optionsMonitor.Enabled)
        {
            return;
        }

        var logger = context.RequestServices
            .GetRequiredService<ILoggerFactory>()
            .CreateLogger(JobAuthorizationAuditFilter.LoggerName);

        var matcher = new NetworkMaskMatcher(optionsMonitor.BypassNetworks);
        var remote = context.Connection.RemoteIpAddress;
        var bypassAllowed = matcher.IsAllowed(remote);

        logger.LogWarning(
            "Feedser authorization denied route={Route} remote={RemoteAddress} bypassAllowed={BypassAllowed} hasPrincipal={HasPrincipal}",
            context.Request.Path.Value ?? string.Empty,
            remote?.ToString() ?? "unknown",
            bypassAllowed,
            context.User?.Identity?.IsAuthenticated ?? false);
    });
}

if (authorityConfigured)
{
    app.UseAuthentication();
    app.UseAuthorization();
}

IResult JsonResult<T>(T value, int? statusCode = null)
{
    var payload = JsonSerializer.Serialize(value, jsonOptions);
    return Results.Content(payload, "application/json", Encoding.UTF8, statusCode);
}

IResult Problem(HttpContext context, string title, int statusCode, string type, string? detail = null, IDictionary<string, object?>? extensions = null)
{
    var traceId = Activity.Current?.TraceId.ToString() ?? context.TraceIdentifier;
    extensions ??= new Dictionary<string, object?>(StringComparer.Ordinal)
    {
        ["traceId"] = traceId,
    };

    if (!extensions.ContainsKey("traceId"))
    {
        extensions["traceId"] = traceId;
    }

    var problemDetails = new ProblemDetails
    {
        Type = type,
        Title = title,
        Detail = detail,
        Status = statusCode,
        Instance = context.Request.Path
    };

    foreach (var entry in extensions)
    {
        problemDetails.Extensions[entry.Key] = entry.Value;
    }

    var payload = JsonSerializer.Serialize(problemDetails, jsonOptions);
    return Results.Content(payload, "application/problem+json", Encoding.UTF8, statusCode);
}

static KeyValuePair<string, object?>[] BuildJobMetricTags(string jobKind, string trigger, string outcome)
    => new[]
    {
        new KeyValuePair<string, object?>("job.kind", jobKind),
        new KeyValuePair<string, object?>("job.trigger", trigger),
        new KeyValuePair<string, object?>("job.outcome", outcome),
    };

void ApplyNoCache(HttpResponse response)
{
    if (response is null)
    {
        return;
    }

    response.Headers.CacheControl = "no-store, no-cache, max-age=0, must-revalidate";
    response.Headers.Pragma = "no-cache";
    response.Headers["Expires"] = "0";
}

await InitializeMongoAsync(app);

app.MapGet("/health", (IOptions<FeedserOptions> opts, ServiceStatus status, HttpContext context) =>
{
    ApplyNoCache(context.Response);

    var snapshot = status.CreateSnapshot();
    var uptimeSeconds = Math.Max((snapshot.CapturedAt - snapshot.StartedAt).TotalSeconds, 0d);

    var storage = new StorageBootstrapHealth(
        Driver: opts.Value.Storage.Driver,
        Completed: snapshot.BootstrapCompletedAt is not null,
        CompletedAt: snapshot.BootstrapCompletedAt,
        DurationMs: snapshot.BootstrapDuration?.TotalMilliseconds);

    var telemetry = new TelemetryHealth(
        Enabled: opts.Value.Telemetry.Enabled,
        Tracing: opts.Value.Telemetry.EnableTracing,
        Metrics: opts.Value.Telemetry.EnableMetrics,
        Logging: opts.Value.Telemetry.EnableLogging);

    var response = new HealthDocument(
        Status: "healthy",
        StartedAt: snapshot.StartedAt,
        UptimeSeconds: uptimeSeconds,
        Storage: storage,
        Telemetry: telemetry);

    return JsonResult(response);
});

app.MapGet("/ready", async (IMongoDatabase database, ServiceStatus status, HttpContext context, CancellationToken cancellationToken) =>
{
    ApplyNoCache(context.Response);

    var stopwatch = Stopwatch.StartNew();
    try
    {
        await database.RunCommandAsync((Command<BsonDocument>)"{ ping: 1 }", cancellationToken: cancellationToken).ConfigureAwait(false);
        stopwatch.Stop();
        status.RecordMongoCheck(success: true, latency: stopwatch.Elapsed, error: null);

        var snapshot = status.CreateSnapshot();
        var uptimeSeconds = Math.Max((snapshot.CapturedAt - snapshot.StartedAt).TotalSeconds, 0d);

        var mongo = new MongoReadyHealth(
            Status: "ready",
            LatencyMs: snapshot.LastMongoLatency?.TotalMilliseconds,
            CheckedAt: snapshot.LastReadyCheckAt,
            Error: null);

        var response = new ReadyDocument(
            Status: "ready",
            StartedAt: snapshot.StartedAt,
            UptimeSeconds: uptimeSeconds,
            Mongo: mongo);

        return JsonResult(response);
    }
    catch (Exception ex)
    {
        stopwatch.Stop();
        status.RecordMongoCheck(success: false, latency: stopwatch.Elapsed, error: ex.Message);

        var snapshot = status.CreateSnapshot();
        var uptimeSeconds = Math.Max((snapshot.CapturedAt - snapshot.StartedAt).TotalSeconds, 0d);

        var mongo = new MongoReadyHealth(
            Status: "unready",
            LatencyMs: snapshot.LastMongoLatency?.TotalMilliseconds,
            CheckedAt: snapshot.LastReadyCheckAt,
            Error: snapshot.LastMongoError ?? ex.Message);

        var response = new ReadyDocument(
            Status: "unready",
            StartedAt: snapshot.StartedAt,
            UptimeSeconds: uptimeSeconds,
            Mongo: mongo);

        var extensions = new Dictionary<string, object?>(StringComparer.Ordinal)
        {
            ["mongoLatencyMs"] = snapshot.LastMongoLatency?.TotalMilliseconds,
            ["mongoError"] = snapshot.LastMongoError ?? ex.Message,
        };

        return Problem(context, "Mongo unavailable", StatusCodes.Status503ServiceUnavailable, ProblemTypes.ServiceUnavailable, snapshot.LastMongoError ?? ex.Message, extensions);
    }
});

app.MapGet("/diagnostics/aliases/{seed}", async (string seed, AliasGraphResolver resolver, HttpContext context, CancellationToken cancellationToken) =>
{
    ApplyNoCache(context.Response);

    if (string.IsNullOrWhiteSpace(seed))
    {
        return Problem(context, "Seed advisory key is required.", StatusCodes.Status400BadRequest, ProblemTypes.Validation);
    }

    var component = await resolver.BuildComponentAsync(seed, cancellationToken).ConfigureAwait(false);

    var aliases = component.AliasMap.ToDictionary(
        static kvp => kvp.Key,
        static kvp => kvp.Value
            .Select(record => new
            {
                record.Scheme,
                record.Value,
                UpdatedAt = record.UpdatedAt
            })
            .ToArray());

    var response = new
    {
        Seed = component.SeedAdvisoryKey,
        Advisories = component.AdvisoryKeys,
        Collisions = component.Collisions
            .Select(collision => new
            {
                collision.Scheme,
                collision.Value,
                AdvisoryKeys = collision.AdvisoryKeys
            })
            .ToArray(),
        Aliases = aliases
    };

    return JsonResult(response);
});

var jobsListEndpoint = app.MapGet("/jobs", async (string? kind, int? limit, IJobCoordinator coordinator, HttpContext context, CancellationToken cancellationToken) =>
{
    ApplyNoCache(context.Response);

    var take = Math.Clamp(limit.GetValueOrDefault(50), 1, 200);
    var runs = await coordinator.GetRecentRunsAsync(kind, take, cancellationToken).ConfigureAwait(false);
    var payload = runs.Select(JobRunResponse.FromSnapshot).ToArray();
    return JsonResult(payload);
}).AddEndpointFilter<JobAuthorizationAuditFilter>();
if (enforceAuthority)
{
    jobsListEndpoint.RequireAuthorization(JobsPolicyName);
}

var jobByIdEndpoint = app.MapGet("/jobs/{runId:guid}", async (Guid runId, IJobCoordinator coordinator, HttpContext context, CancellationToken cancellationToken) =>
{
    ApplyNoCache(context.Response);

    var run = await coordinator.GetRunAsync(runId, cancellationToken).ConfigureAwait(false);
    if (run is null)
    {
        return Problem(context, "Job run not found", StatusCodes.Status404NotFound, ProblemTypes.NotFound, $"Job run '{runId}' was not found.");
    }

    return JsonResult(JobRunResponse.FromSnapshot(run));
}).AddEndpointFilter<JobAuthorizationAuditFilter>();
if (enforceAuthority)
{
    jobByIdEndpoint.RequireAuthorization(JobsPolicyName);
}

var jobDefinitionsEndpoint = app.MapGet("/jobs/definitions", async (IJobCoordinator coordinator, HttpContext context, CancellationToken cancellationToken) =>
{
    ApplyNoCache(context.Response);

    var definitions = await coordinator.GetDefinitionsAsync(cancellationToken).ConfigureAwait(false);
    if (definitions.Count == 0)
    {
        return JsonResult(Array.Empty<JobDefinitionResponse>());
    }

    var definitionKinds = definitions.Select(static definition => definition.Kind).ToArray();
    var lastRuns = await coordinator.GetLastRunsAsync(definitionKinds, cancellationToken).ConfigureAwait(false);

    var responses = new List<JobDefinitionResponse>(definitions.Count);
    foreach (var definition in definitions)
    {
        lastRuns.TryGetValue(definition.Kind, out var lastRun);
        responses.Add(JobDefinitionResponse.FromDefinition(definition, lastRun));
    }

    return JsonResult(responses);
}).AddEndpointFilter<JobAuthorizationAuditFilter>();
if (enforceAuthority)
{
    jobDefinitionsEndpoint.RequireAuthorization(JobsPolicyName);
}

var jobDefinitionEndpoint = app.MapGet("/jobs/definitions/{kind}", async (string kind, IJobCoordinator coordinator, HttpContext context, CancellationToken cancellationToken) =>
{
    ApplyNoCache(context.Response);

    var definition = (await coordinator.GetDefinitionsAsync(cancellationToken).ConfigureAwait(false))
        .FirstOrDefault(d => string.Equals(d.Kind, kind, StringComparison.Ordinal));

    if (definition is null)
    {
        return Problem(context, "Job definition not found", StatusCodes.Status404NotFound, ProblemTypes.NotFound, $"Job kind '{kind}' is not registered.");
    }

    var lastRuns = await coordinator.GetLastRunsAsync(new[] { definition.Kind }, cancellationToken).ConfigureAwait(false);
    lastRuns.TryGetValue(definition.Kind, out var lastRun);

    var response = JobDefinitionResponse.FromDefinition(definition, lastRun);
    return JsonResult(response);
}).AddEndpointFilter<JobAuthorizationAuditFilter>();
if (enforceAuthority)
{
    jobDefinitionEndpoint.RequireAuthorization(JobsPolicyName);
}

var jobDefinitionRunsEndpoint = app.MapGet("/jobs/definitions/{kind}/runs", async (string kind, int? limit, IJobCoordinator coordinator, HttpContext context, CancellationToken cancellationToken) =>
{
    ApplyNoCache(context.Response);

    var definition = (await coordinator.GetDefinitionsAsync(cancellationToken).ConfigureAwait(false))
        .FirstOrDefault(d => string.Equals(d.Kind, kind, StringComparison.Ordinal));

    if (definition is null)
    {
        return Problem(context, "Job definition not found", StatusCodes.Status404NotFound, ProblemTypes.NotFound, $"Job kind '{kind}' is not registered.");
    }

    var take = Math.Clamp(limit.GetValueOrDefault(20), 1, 200);
    var runs = await coordinator.GetRecentRunsAsync(kind, take, cancellationToken).ConfigureAwait(false);
    var payload = runs.Select(JobRunResponse.FromSnapshot).ToArray();
    return JsonResult(payload);
}).AddEndpointFilter<JobAuthorizationAuditFilter>();
if (enforceAuthority)
{
    jobDefinitionRunsEndpoint.RequireAuthorization(JobsPolicyName);
}

var activeJobsEndpoint = app.MapGet("/jobs/active", async (IJobCoordinator coordinator, HttpContext context, CancellationToken cancellationToken) =>
{
    ApplyNoCache(context.Response);

    var runs = await coordinator.GetActiveRunsAsync(cancellationToken).ConfigureAwait(false);
    var payload = runs.Select(JobRunResponse.FromSnapshot).ToArray();
    return JsonResult(payload);
}).AddEndpointFilter<JobAuthorizationAuditFilter>();
if (enforceAuthority)
{
    activeJobsEndpoint.RequireAuthorization(JobsPolicyName);
}

var triggerJobEndpoint = app.MapPost("/jobs/{*jobKind}", async (string jobKind, JobTriggerRequest request, IJobCoordinator coordinator, HttpContext context) =>
{
    ApplyNoCache(context.Response);

    request ??= new JobTriggerRequest();
    request.Parameters ??= new Dictionary<string, object?>(StringComparer.Ordinal);
    var trigger = string.IsNullOrWhiteSpace(request.Trigger) ? "api" : request.Trigger;

    var lifetime = context.RequestServices.GetRequiredService<IHostApplicationLifetime>();
    var result = await coordinator.TriggerAsync(jobKind, request.Parameters, trigger, lifetime.ApplicationStopping).ConfigureAwait(false);

    var outcome = result.Outcome;
    var tags = BuildJobMetricTags(jobKind, trigger, outcome.ToString().ToLowerInvariant());

    switch (outcome)
    {
        case JobTriggerOutcome.Accepted:
            JobMetrics.TriggerCounter.Add(1, tags);
            if (result.Run is null)
            {
                return Results.StatusCode(StatusCodes.Status202Accepted);
            }

            var acceptedRun = JobRunResponse.FromSnapshot(result.Run);
            context.Response.Headers.Location = $"/jobs/{acceptedRun.RunId}";
            return JsonResult(acceptedRun, StatusCodes.Status202Accepted);

        case JobTriggerOutcome.NotFound:
            JobMetrics.TriggerConflictCounter.Add(1, tags);
            return Problem(context, "Job not found", StatusCodes.Status404NotFound, ProblemTypes.NotFound, result.ErrorMessage ?? $"Job '{jobKind}' is not registered.");

        case JobTriggerOutcome.Disabled:
            JobMetrics.TriggerConflictCounter.Add(1, tags);
            return Problem(context, "Job disabled", StatusCodes.Status423Locked, ProblemTypes.Locked, result.ErrorMessage ?? $"Job '{jobKind}' is disabled.");

        case JobTriggerOutcome.AlreadyRunning:
            JobMetrics.TriggerConflictCounter.Add(1, tags);
            return Problem(context, "Job already running", StatusCodes.Status409Conflict, ProblemTypes.Conflict, result.ErrorMessage ?? $"Job '{jobKind}' already has an active run.");

        case JobTriggerOutcome.LeaseRejected:
            JobMetrics.TriggerConflictCounter.Add(1, tags);
            return Problem(context, "Job lease rejected", StatusCodes.Status409Conflict, ProblemTypes.LeaseRejected, result.ErrorMessage ?? $"Job '{jobKind}' could not acquire a lease.");

        case JobTriggerOutcome.InvalidParameters:
        {
            JobMetrics.TriggerConflictCounter.Add(1, tags);
            var extensions = new Dictionary<string, object?>(StringComparer.Ordinal)
            {
                ["parameters"] = request.Parameters,
            };
            return Problem(context, "Invalid job parameters", StatusCodes.Status400BadRequest, ProblemTypes.Validation, result.ErrorMessage, extensions);
        }

        case JobTriggerOutcome.Cancelled:
        {
            JobMetrics.TriggerConflictCounter.Add(1, tags);
            var extensions = new Dictionary<string, object?>(StringComparer.Ordinal)
            {
                ["run"] = result.Run is null ? null : JobRunResponse.FromSnapshot(result.Run),
            };

            return Problem(context, "Job cancelled", StatusCodes.Status409Conflict, ProblemTypes.Conflict, result.ErrorMessage ?? $"Job '{jobKind}' was cancelled before completion.", extensions);
        }

        case JobTriggerOutcome.Failed:
        {
            JobMetrics.TriggerFailureCounter.Add(1, tags);
            var extensions = new Dictionary<string, object?>(StringComparer.Ordinal)
            {
                ["run"] = result.Run is null ? null : JobRunResponse.FromSnapshot(result.Run),
            };

            return Problem(context, "Job execution failed", StatusCodes.Status500InternalServerError, ProblemTypes.JobFailure, result.ErrorMessage, extensions);
        }

        default:
            JobMetrics.TriggerFailureCounter.Add(1, tags);
            return Problem(context, "Unexpected job outcome", StatusCodes.Status500InternalServerError, ProblemTypes.JobFailure, $"Job '{jobKind}' returned outcome '{outcome}'.");
    }
}).AddEndpointFilter<JobAuthorizationAuditFilter>();
if (enforceAuthority)
{
    triggerJobEndpoint.RequireAuthorization(JobsPolicyName);
}

await app.RunAsync();

static PluginHostOptions BuildPluginOptions(FeedserOptions options, string contentRoot)
{
    var pluginOptions = new PluginHostOptions
    {
        BaseDirectory = options.Plugins.BaseDirectory ?? contentRoot,
        PluginsDirectory = options.Plugins.Directory ?? Path.Combine(contentRoot, "PluginBinaries"),
        EnsureDirectoryExists = true,
        RecursiveSearch = false,
    };

    if (options.Plugins.SearchPatterns.Count == 0)
    {
        pluginOptions.SearchPatterns.Add("StellaOps.Feedser.Plugin.*.dll");
    }
    else
    {
        foreach (var pattern in options.Plugins.SearchPatterns)
        {
            if (!string.IsNullOrWhiteSpace(pattern))
            {
                pluginOptions.SearchPatterns.Add(pattern);
            }
        }
    }

    return pluginOptions;
}

static async Task InitializeMongoAsync(WebApplication app)
{
    await using var scope = app.Services.CreateAsyncScope();
    var bootstrapper = scope.ServiceProvider.GetRequiredService<MongoBootstrapper>();
    var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("MongoBootstrapper");
    var status = scope.ServiceProvider.GetRequiredService<ServiceStatus>();

    var stopwatch = Stopwatch.StartNew();

    try
    {
        await bootstrapper.InitializeAsync(app.Lifetime.ApplicationStopping).ConfigureAwait(false);
        stopwatch.Stop();
        status.MarkBootstrapCompleted(stopwatch.Elapsed);
        logger.LogInformation("Mongo bootstrap completed in {ElapsedMs} ms", stopwatch.Elapsed.TotalMilliseconds);
    }
    catch (Exception ex)
    {
        stopwatch.Stop();
        status.RecordMongoCheck(success: false, latency: stopwatch.Elapsed, error: ex.Message);
        logger.LogCritical(ex, "Mongo bootstrap failed after {ElapsedMs} ms", stopwatch.Elapsed.TotalMilliseconds);
        throw;
    }
}

public partial class Program;
