using FluentValidation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
using SatelliteProvider.Api;
using SatelliteProvider.Api.Authentication;
using SatelliteProvider.Api.DTOs;
using SatelliteProvider.Api.Grpc;
using SatelliteProvider.Api.Swagger;
using SatelliteProvider.Api.Validators;
using SatelliteProvider.DataAccess;
using SatelliteProvider.DataAccess.Repositories;
using SatelliteProvider.DataAccess.TypeHandlers;
using SatelliteProvider.Common.Configs;
using SatelliteProvider.Common.DTO;
using SatelliteProvider.Common.Interfaces;
using SatelliteProvider.Services.RegionProcessing;
using SatelliteProvider.Services.RouteManagement;
using SatelliteProvider.Services.TileDownloader;
using Serilog;

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseSerilog((context, configuration) =>
    configuration.ReadFrom.Configuration(context.Configuration));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
    ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");

DapperEnumTypeHandlers.RegisterAll();

builder.Services.Configure<MapConfig>(builder.Configuration.GetSection("MapConfig"));
builder.Services.Configure<StorageConfig>(builder.Configuration.GetSection("StorageConfig"));
builder.Services.Configure<ProcessingConfig>(builder.Configuration.GetSection("ProcessingConfig"));
builder.Services.Configure<UavQualityConfig>(builder.Configuration.GetSection("UavQuality"));
builder.Services.Configure<TileProvisionConfig>(builder.Configuration.GetSection("TileProvision"));

var uavQuality = builder.Configuration.GetSection("UavQuality").Get<UavQualityConfig>() ?? new UavQualityConfig();
var uavBatchBodyLimit = checked((long)uavQuality.MaxBatchSize * uavQuality.MaxBytes);
builder.Services.Configure<KestrelServerOptions>(options =>
{
    options.Limits.MaxRequestBodySize = uavBatchBodyLimit;
    // AZ-505: enable HTTP/2 alongside HTTP/1.1 on every Kestrel endpoint so
    // programmatic clients (httpx http2=True, .NET HttpClient) can multiplex
    // tile reads on a single TCP connection. Kestrel requires TLS+ALPN for
    // HTTP/2 — the dev/test compose files mount a self-signed cert at
    // /app/certs/api.pfx and set ASPNETCORE_URLS=https://+:8080; production
    // is expected to terminate TLS at the same layer or upstream. Browsers
    // negotiate HTTP/2 via ALPN once TLS is present; legacy HTTP/1.1
    // callers continue to work over the same listener. HTTP/3/QUIC is
    // intentionally out of scope (see AZ-505 task spec § Excluded).
    options.ConfigureEndpointDefaults(listen =>
    {
        listen.Protocols = HttpProtocols.Http1AndHttp2;
    });
});
builder.Services.Configure<FormOptions>(options =>
{
    options.MultipartBodyLengthLimit = uavBatchBodyLimit;
    options.ValueLengthLimit = Math.Max(options.ValueLengthLimit, uavQuality.MaxBatchSize * 512);
});

builder.Services.AddSingleton<ITileRepository>(sp => new TileRepository(connectionString, sp.GetRequiredService<ILogger<TileRepository>>()));
builder.Services.AddSingleton<IRegionRepository>(sp => new RegionRepository(connectionString));
builder.Services.AddSingleton<IRouteRepository>(sp => new RouteRepository(connectionString));

builder.Services.AddHttpClient();

builder.Services.AddTileDownloader();
builder.Services.AddRegionProcessing();
builder.Services.AddRouteManagement();

builder.Services.AddSatelliteJwt(builder.Configuration);
builder.Services.AddSingleton<IAuthorizationHandler, PermissionsAuthorizationHandler>();
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy(SatellitePermissions.UavUploadPolicy, policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.Requirements.Add(new PermissionsRequirement(SatellitePermissions.Gps));
    });
});

var allowedOrigins = builder.Configuration.GetSection("CorsConfig:AllowedOrigins").Get<string[]>() ?? Array.Empty<string>();
var allowAnyOrigin = builder.Configuration.GetValue<bool>("CorsConfig:AllowAnyOrigin");
CorsConfigurationValidator.EnsureSafeForEnvironment(allowedOrigins, allowAnyOrigin, builder.Environment.EnvironmentName);

builder.Services.AddCors(options =>
{
    options.AddPolicy("TilesCors", policy =>
    {
        if (CorsConfigurationValidator.ShouldUsePermissivePolicy(allowedOrigins, allowAnyOrigin))
            policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod();
        else
            policy.WithOrigins(allowedOrigins).AllowAnyHeader().AllowAnyMethod();
    });
});

builder.Services.AddProblemDetails();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();

// AZ-795: strict JSON parsing — unknown fields are rejected at the deserializer
// level instead of being silently dropped. Pairs with the per-endpoint
// FluentValidation filter (`WithValidation<T>()`) so the API has a single
// uniform RFC 7807 error contract for both wire-format failures and
// business-rule failures (`_docs/02_document/contracts/api/error-shape.md`).
builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase;
    options.SerializerOptions.PropertyNameCaseInsensitive = true;
    options.SerializerOptions.UnmappedMemberHandling = System.Text.Json.Serialization.JsonUnmappedMemberHandling.Disallow;
    options.SerializerOptions.Converters.Add(
        new System.Text.Json.Serialization.JsonStringEnumConverter(System.Text.Json.JsonNamingPolicy.CamelCase));
});

// AZ-795: register every IValidator<T> in this assembly with DI so the
// generic ValidationEndpointFilter<T> can resolve them at request time.
// GlobalValidatorConfig.ApplyOnce() centralizes process-wide FluentValidation
// configuration (camelCase property paths, etc.) so the API host and the
// unit-test fixture share one source of truth — see error-shape.md Inv-4.
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
GlobalValidatorConfig.ApplyOnce();

// AZ-810: explicit registration so `.AddEndpointFilter<UavUploadValidationFilter>()`
// on the UAV upload endpoint resolves the filter with its `IValidator<…>` + JSON
// options constructor deps. Transient so each request gets a fresh instance.
builder.Services.AddTransient<UavUploadValidationFilter>();

builder.Services.AddGrpc(options =>
{
    options.MaxReceiveMessageSize = 16 * 1024 * 1024;
    options.MaxSendMessageSize = 64 * 1024 * 1024;
});

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "Satellite Provider API", Version = "v1" });

    c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Name = "Authorization",
        Type = SecuritySchemeType.Http,
        Scheme = "bearer",
        BearerFormat = "JWT",
        In = ParameterLocation.Header,
        Description = "JWT Authorization header using the Bearer scheme. Example: 'Bearer {token}'"
    });

    c.AddSecurityRequirement(_ => new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecuritySchemeReference("Bearer"),
            new List<string>()
        }
    });

    c.MapType<UavTileBatchUploadRequest>(() => new OpenApiSchema
    {
        Type = JsonSchemaType.Object,
        Properties = new Dictionary<string, IOpenApiSchema>
        {
            ["metadata"] = new OpenApiSchema
            {
                Type = JsonSchemaType.String,
                Description = "JSON document `{ \"items\": [ { \"latitude\", \"longitude\", \"tileZoom\", \"tileSizeMeters\", \"capturedAt\" } ] }` where item ordinal index aligns with the matching file in `files`."
            },
            ["files"] = new OpenApiSchema
            {
                Type = JsonSchemaType.Array,
                Description = "UAV tile JPEG files in the same order as `metadata.items`.",
                Items = new OpenApiSchema { Type = JsonSchemaType.String, Format = "binary" }
            }
        },
        Required = new HashSet<string> { "metadata", "files" }
    });

    c.OperationFilter<ParameterDescriptionFilter>();
});

var app = builder.Build();

if (CorsConfigurationValidator.ShouldWarnAboutPermissiveDefault(allowedOrigins, allowAnyOrigin))
{
    app.Services
        .GetRequiredService<ILogger<Program>>()
        .LogWarning(CorsConfigurationValidator.PermissiveDefaultWarning, app.Environment.EnvironmentName);
}

var migratorLogger = app.Services.GetRequiredService<ILogger<DatabaseMigrator>>();
var migrator = new DatabaseMigrator(connectionString, migratorLogger);
if (!migrator.RunMigrations())
{
    throw new Exception("Database migration failed. Application cannot start.");
}

var storageConfig = app.Configuration.GetSection("StorageConfig").Get<StorageConfig>() ?? new StorageConfig();
Directory.CreateDirectory(storageConfig.TilesDirectory);
Directory.CreateDirectory(storageConfig.ReadyDirectory);

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseExceptionHandler();
app.UseHttpsRedirection();
app.UseCors("TilesCors");
app.UseAuthentication();
app.UseAuthorization();

app.MapGrpcService<RouteTileDeliveryGrpcService>();

app.MapGet("/tiles/{z:int}/{x:int}/{y:int}", ServeTile)
    .RequireAuthorization()
    .WithOpenApi(op => new(op) { Summary = "Get satellite tile image by z/x/y coordinates (Slippy Map tile server)" });

app.MapGet("/api/satellite/tiles/latlon", GetTileByLatLon)
    .RequireAuthorization()
    .AddEndpointFilter(new RejectUnknownQueryParamsEndpointFilter(new[] { "lat", "lon", "zoom" }))
    .WithValidation<GetTileByLatLonQuery>()
    .Produces<DownloadTileResponse>(StatusCodes.Status200OK)
    .ProducesProblem(StatusCodes.Status400BadRequest)
    .WithOpenApi(op => new(op) { Summary = "Get satellite tile by latitude and longitude coordinates" });

app.MapGet("/api/satellite/tiles/mgrs", GetSatelliteTilesByMgrs)
    .RequireAuthorization()
    .ProducesProblem(StatusCodes.Status501NotImplemented)
    .WithOpenApi(op => new(op) { Summary = "Get satellite tiles by MGRS coordinates (NOT IMPLEMENTED)" });

app.MapPost("/api/satellite/tiles/inventory", GetTilesInventory)
    .RequireAuthorization()
    .WithValidation<TileInventoryRequest>()
    .Accepts<TileInventoryRequest>("application/json")
    .Produces<TileInventoryResponse>(StatusCodes.Status200OK)
    .ProducesProblem(StatusCodes.Status400BadRequest)
    .WithOpenApi(op => new(op)
    {
        Summary = "Bulk tile inventory lookup by (z,x,y) coords or location_hash",
        Description = "Body MUST populate exactly one of `tiles` (array of `{z, x, y}` slippy-map coordinates) OR `locationHashes` (array of UUIDv5 hashes) — sending both, or neither, is HTTP 400. Response order matches request order; each entry reports `present: true|false`, and when present includes `id`, `capturedAt`, `source`, `flightId`, `resolutionMPerPx`. Hard cap: 5000 entries per request."
    });

app.MapPost("/api/satellite/upload", UploadUavTileBatch)
    .RequireAuthorization(SatellitePermissions.UavUploadPolicy)
    .AddEndpointFilter<UavUploadValidationFilter>()
    .Accepts<UavTileBatchUploadRequest>("multipart/form-data")
    .Produces<UavTileBatchUploadResponse>(StatusCodes.Status200OK)
    .ProducesProblem(StatusCodes.Status400BadRequest)
    .WithOpenApi(op => new(op)
    {
        Summary = "Upload a batch of UAV-captured satellite tiles",
        Description = "Multipart form: a JSON `metadata` field and an aligned `files` collection. Each item is graded by the 5-rule quality gate and persisted with `source='uav'` when accepted. Returns 200 with per-item results (mixed accept/reject), 400 for envelope-level errors (malformed metadata, missing files, oversized batch), 401 without a valid JWT, 403 without the `GPS` permission claim."
    })
    .DisableAntiforgery();

app.MapPost("/api/satellite/request", RequestRegion)
    .RequireAuthorization()
    .WithValidation<RequestRegionRequest>()
    .Accepts<RequestRegionRequest>("application/json")
    .Produces<RegionStatusResponse>(StatusCodes.Status200OK)
    .ProducesProblem(StatusCodes.Status400BadRequest)
    .WithOpenApi(op => new(op)
    {
        Summary = "Request tiles for a region",
        Description = "Idempotent: POSTing the same `id` twice returns the existing region resource with HTTP 200 and does not enqueue duplicate background processing.",
    });

app.MapGet("/api/satellite/region/{id:guid}", GetRegionStatus)
    .RequireAuthorization()
    .WithOpenApi(op => new(op) { Summary = "Get region status and file paths" });

app.MapPost("/api/satellite/route", CreateRoute)
    .RequireAuthorization()
    .WithValidation<CreateRouteRequest>()
    .Accepts<CreateRouteRequest>("application/json")
    .Produces<RouteResponse>(StatusCodes.Status200OK)
    .ProducesProblem(StatusCodes.Status400BadRequest)
    .WithOpenApi(op => new(op)
    {
        Summary = "Create a route with intermediate points",
        Description = "Idempotent: POSTing the same `id` twice returns the existing route resource with HTTP 200 and does not regenerate intermediate points or re-queue geofence regions.",
    });

app.MapGet("/api/satellite/route/{id:guid}", GetRoute)
    .RequireAuthorization()
    .WithOpenApi(op => new(op) { Summary = "Get route information with calculated points" });

app.Run();

async Task<IResult> ServeTile(int z, int x, int y, HttpContext httpContext, ITileService tileService)
{
    var tile = await tileService.GetOrDownloadTileAsync(z, x, y, httpContext.RequestAborted);
    httpContext.Response.Headers.CacheControl = $"public, max-age={(long)tile.MaxAge.TotalSeconds}";
    httpContext.Response.Headers.ETag = tile.ETag;
    return Results.Bytes(tile.Bytes, tile.ContentType);
}

async Task<IResult> GetTileByLatLon([AsParameters] GetTileByLatLonQuery query, HttpContext httpContext, ITileService tileService)
{
    // AZ-811: GetTileByLatLonQueryValidator guarantees lat/lon/zoom are non-null
    // by the time the handler runs (CascadeMode.Stop + NotNull rules).
    var tile = await tileService.DownloadAndStoreSingleTileAsync(query.Lat!.Value, query.Lon!.Value, query.Zoom!.Value, httpContext.RequestAborted);

    var response = new DownloadTileResponse
    {
        Id = tile.Id,
        ZoomLevel = tile.TileZoom,
        Latitude = tile.Latitude,
        Longitude = tile.Longitude,
        TileSizeMeters = tile.TileSizeMeters,
        TileSizePixels = tile.TileSizePixels,
        ImageType = tile.ImageType,
        Version = tile.Version,
        FilePath = tile.FilePath,
        CreatedAt = tile.CreatedAt,
        UpdatedAt = tile.UpdatedAt
    };

    return Results.Ok(response);
}

IResult GetSatelliteTilesByMgrs(string mgrs, double squareSideMeters)
{
    return Results.Problem(
        statusCode: StatusCodes.Status501NotImplemented,
        title: "Not implemented",
        detail: "MGRS-based tile retrieval is not implemented.");
}

async Task<IResult> GetTilesInventory(
    [FromBody] TileInventoryRequest request,
    HttpContext httpContext,
    ITileService tileService)
{
    var response = await tileService.GetInventoryAsync(request, httpContext.RequestAborted);
    return Results.Ok(response);
}

async Task<IResult> UploadUavTileBatch(
    HttpContext httpContext,
    IUavTileUploadHandler handler,
    [FromForm] UavTileBatchUploadRequest request)
{
    ArgumentNullException.ThrowIfNull(request);

    var files = request.Files ?? (IFormFileCollection)new FormFileCollection();
    var uploadFiles = new List<UavUploadFile>(files.Count);
    foreach (var file in files)
    {
        await using var stream = file.OpenReadStream();
        using var buffer = new MemoryStream(checked((int)file.Length));
        await stream.CopyToAsync(buffer, httpContext.RequestAborted);
        uploadFiles.Add(new UavUploadFile(file.FileName, file.ContentType, buffer.ToArray()));
    }

    var result = await handler.HandleAsync(request.Metadata, uploadFiles, httpContext.RequestAborted);
    if (result.EnvelopeRejected)
    {
        return Results.Problem(
            statusCode: StatusCodes.Status400BadRequest,
            title: "Invalid UAV tile batch",
            detail: result.EnvelopeError);
    }

    return Results.Ok(result.Response);
}

async Task<IResult> RequestRegion([FromBody] RequestRegionRequest request, IRegionService regionService)
{
    var status = await regionService.RequestRegionAsync(
        request.Id,
        request.Lat,
        request.Lon,
        request.SizeMeters,
        request.ZoomLevel,
        request.StitchTiles);

    return Results.Ok(status);
}

async Task<IResult> GetRegionStatus(Guid id, IRegionService regionService)
{
    var status = await regionService.GetRegionStatusAsync(id);

    if (status == null)
    {
        return Results.NotFound(new { error = $"Region {id} not found" });
    }

    return Results.Ok(status);
}

async Task<IResult> CreateRoute([FromBody] CreateRouteRequest request, IRouteService routeService, ILogger<Program> logger)
{
    try
    {
        var route = await routeService.CreateRouteAsync(request);
        return Results.Ok(route);
    }
    catch (ArgumentException ex)
    {
        logger.LogWarning(ex, "Invalid route request");
        return Results.BadRequest(new { error = ex.Message });
    }
}

async Task<IResult> GetRoute(Guid id, IRouteService routeService)
{
    var route = await routeService.GetRouteAsync(id);

    if (route == null)
    {
        return Results.NotFound(new { error = $"Route {id} not found" });
    }

    return Results.Ok(route);
}
