using FrameProcessor.Concurrency;
using FrameProcessor.Configuration;
using FrameProcessor.ImagePipeline;
using FrameProcessor.Middleware;
using FrameProcessor.Mqtt;
using FrameProcessor.Storage;
using FrameProcessor.UrlFetch;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi;
using Serilog;

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddJsonFile("frames.json", optional: false, reloadOnChange: true);

builder.Host.UseSerilog((context, services, configuration) => configuration
    .ReadFrom.Configuration(context.Configuration)
    .ReadFrom.Services(services)
    .Enrich.FromLogContext()
    .WriteTo.Console()
    .WriteTo.File(
        path: "logs/frame-processor-.log",
        rollingInterval: RollingInterval.Day));

builder.Services.AddControllers();

builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer((document, context, _) =>
    {
        document.Info.Title = "Frame Processor";
        document.Info.Version = "v1";

        document.Components ??= new OpenApiComponents();
        document.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>();
        document.Components.SecuritySchemes["ApiKey"] = new OpenApiSecurityScheme
        {
            Type = SecuritySchemeType.ApiKey,
            In = ParameterLocation.Header,
            Name = "X-Api-Key",
            Description = "Shared API key required for /api/* endpoints.",
        };

        document.Security =
        [
            new OpenApiSecurityRequirement
            {
                [new OpenApiSecuritySchemeReference("ApiKey", document)] = new List<string>(),
            },
        ];
        return Task.CompletedTask;
    });

    // Microsoft.AspNetCore.OpenApi (10.0) does not emit a multipart/form-data
    // request body for [FromForm] IFormFile parameters on MVC controllers, so
    // Swagger UI renders no upload widget. Synthesize one here for any operation
    // whose action takes form-bound parameters.
    options.AddOperationTransformer((operation, context, _) =>
    {
        var formParams = context.Description.ParameterDescriptions
            .Where(p => p.Source == BindingSource.FormFile || p.Source == BindingSource.Form)
            .ToList();

        if (formParams.Count == 0)
        {
            return Task.CompletedTask;
        }

        var properties = new Dictionary<string, IOpenApiSchema>();
        var required = new HashSet<string>();

        foreach (var p in formParams)
        {
            var isFile = p.Source == BindingSource.FormFile
                || typeof(IFormFile).IsAssignableFrom(p.ModelMetadata?.ModelType);

            properties[p.Name] = new OpenApiSchema
            {
                Type = JsonSchemaType.String,
                Format = isFile ? "binary" : null,
            };

            if (p.IsRequired)
            {
                required.Add(p.Name);
            }
        }

        operation.RequestBody = new OpenApiRequestBody
        {
            Required = true,
            Content = new Dictionary<string, OpenApiMediaType>
            {
                ["multipart/form-data"] = new OpenApiMediaType
                {
                    Schema = new OpenApiSchema
                    {
                        Type = JsonSchemaType.Object,
                        Properties = properties,
                        Required = required,
                    },
                },
            },
        };

        if (operation.Parameters is { Count: > 0 })
        {
            var formNames = formParams.Select(p => p.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
            var filtered = operation.Parameters.Where(p => p.Name is null || !formNames.Contains(p.Name)).ToList();
            operation.Parameters = filtered.Count > 0 ? filtered : null;
        }

        return Task.CompletedTask;
    });
});

builder.Services.AddOptions<MqttOptions>()
    .Bind(builder.Configuration.GetSection(MqttOptions.SectionName))
    .ValidateDataAnnotations()
    .ValidateOnStart();

builder.Services.AddOptions<StorageOptions>()
    .Bind(builder.Configuration.GetSection(StorageOptions.SectionName))
    .ValidateDataAnnotations()
    .ValidateOnStart();

builder.Services.AddOptions<UrlFetchOptions>()
    .Bind(builder.Configuration.GetSection(UrlFetchOptions.SectionName))
    .ValidateDataAnnotations()
    .ValidateOnStart();

builder.Services.AddOptions<ApiKeyOptions>()
    .Configure<IConfiguration>((opts, cfg) => opts.Value = cfg[ApiKeyOptions.SectionName] ?? string.Empty)
    .ValidateDataAnnotations()
    .ValidateOnStart();

// FramesOptions is bound but not validated via the options pipeline so that
// IOptionsMonitor<FramesOptions> can fire OnChange with invalid content during
// hot-reload without throwing. FramesRegistry takes responsibility for both
// strict startup validation (in its constructor) and lenient reload validation.
builder.Services.AddOptions<FramesOptions>()
    .Bind(builder.Configuration);
builder.Services.AddSingleton<FramesOptionsValidator>();
builder.Services.AddSingleton<FramesRegistry>();
builder.Services.AddSingleton<IImagePipeline, FrameProcessor.ImagePipeline.ImagePipeline>();
builder.Services.AddSingleton<ImageStore>();
builder.Services.AddSingleton<MqttPublisher>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<MqttPublisher>());
builder.Services.AddSingleton<FrameLockProvider>();

builder.Services.AddHttpClient<IImageUrlFetcher, ImageUrlFetcher>((sp, client) =>
{
    var opts = sp.GetRequiredService<IOptions<UrlFetchOptions>>().Value;
    client.Timeout = TimeSpan.FromSeconds(opts.TimeoutSeconds);
})
.ConfigurePrimaryHttpMessageHandler(sp =>
{
    var opts = sp.GetRequiredService<IOptions<UrlFetchOptions>>().Value;
    return new HttpClientHandler
    {
        AllowAutoRedirect = true,
        MaxAutomaticRedirections = opts.MaxRedirects,
    };
});

var app = builder.Build();

// Eagerly resolve FramesRegistry so an invalid frames.json fails startup fast.
_ = app.Services.GetRequiredService<FramesRegistry>();

app.MapOpenApi();
app.UseSwaggerUI(options =>
{
    options.SwaggerEndpoint("/openapi/v1.json", "Frame Processor v1");
    options.RoutePrefix = "swagger";
});

app.UseMiddleware<ApiKeyMiddleware>();

app.MapControllers();

app.Run();
