using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using Serilog;
using Serilog.Events;
using StellaOps.Auth.Client;
using StellaOps.Auth.ServerIntegration;
using StellaOps.Configuration;
using StellaOps.Plugin.DependencyInjection;
using StellaOps.Scanner.WebService.Diagnostics;
using StellaOps.Scanner.WebService.Endpoints;
using StellaOps.Scanner.WebService.Extensions;
using StellaOps.Scanner.WebService.Hosting;
using StellaOps.Scanner.WebService.Options;
using StellaOps.Scanner.WebService.Services;
using StellaOps.Scanner.WebService.Security;

var builder = WebApplication.CreateBuilder(args);

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

var contentRoot = builder.Environment.ContentRootPath;

var bootstrapOptions = builder.Configuration.BindOptions<ScannerWebServiceOptions>(
    ScannerWebServiceOptions.SectionName,
    (opts, _) =>
    {
        ScannerWebServiceOptionsPostConfigure.Apply(opts, contentRoot);
        ScannerWebServiceOptionsValidator.Validate(opts);
    });

builder.Services.AddOptions<ScannerWebServiceOptions>()
    .Bind(builder.Configuration.GetSection(ScannerWebServiceOptions.SectionName))
    .PostConfigure(options =>
    {
        ScannerWebServiceOptionsPostConfigure.Apply(options, contentRoot);
        ScannerWebServiceOptionsValidator.Validate(options);
    })
    .ValidateOnStart();

builder.Host.UseSerilog((context, services, loggerConfiguration) =>
{
    loggerConfiguration
        .MinimumLevel.Information()
        .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
        .Enrich.FromLogContext()
        .WriteTo.Console();
});

builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddSingleton<ServiceStatus>();
builder.Services.AddHttpContextAccessor();
builder.Services.AddSingleton<ScanProgressStream>();
builder.Services.AddSingleton<IScanProgressPublisher>(sp => sp.GetRequiredService<ScanProgressStream>());
builder.Services.AddSingleton<IScanProgressReader>(sp => sp.GetRequiredService<ScanProgressStream>());
builder.Services.AddSingleton<IScanCoordinator, InMemoryScanCoordinator>();

var pluginHostOptions = ScannerPluginHostFactory.Build(bootstrapOptions, contentRoot);
builder.Services.RegisterPluginRoutines(builder.Configuration, pluginHostOptions);

builder.Services.AddEndpointsApiExplorer();

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

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

        var resilience = bootstrapOptions.Authority.Resilience ?? new ScannerWebServiceOptions.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 = bootstrapOptions.Authority.Issuer;
            resourceOptions.RequireHttpsMetadata = bootstrapOptions.Authority.RequireHttpsMetadata;
            resourceOptions.MetadataAddress = bootstrapOptions.Authority.MetadataAddress;
            resourceOptions.BackchannelTimeout = TimeSpan.FromSeconds(bootstrapOptions.Authority.BackchannelTimeoutSeconds);
            resourceOptions.TokenClockSkew = TimeSpan.FromSeconds(bootstrapOptions.Authority.TokenClockSkewSeconds);

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

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

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

    builder.Services.AddAuthorization(options =>
    {
        options.AddStellaOpsScopePolicy(ScannerPolicies.ScansEnqueue, bootstrapOptions.Authority.RequiredScopes.ToArray());
        options.AddStellaOpsScopePolicy(ScannerPolicies.ScansRead, ScannerAuthorityScopes.ScansRead);
        options.AddStellaOpsScopePolicy(ScannerPolicies.Reports, ScannerAuthorityScopes.ReportsRead);
    });
}
else
{
    builder.Services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = "Anonymous";
        options.DefaultChallengeScheme = "Anonymous";
    })
    .AddScheme<AuthenticationSchemeOptions, AnonymousAuthenticationHandler>("Anonymous", _ => { });

    builder.Services.AddAuthorization(options =>
    {
        options.AddPolicy(ScannerPolicies.ScansEnqueue, policy => policy.RequireAssertion(_ => true));
        options.AddPolicy(ScannerPolicies.ScansRead, policy => policy.RequireAssertion(_ => true));
        options.AddPolicy(ScannerPolicies.Reports, policy => policy.RequireAssertion(_ => true));
    });
}

var app = builder.Build();

var resolvedOptions = app.Services.GetRequiredService<IOptions<ScannerWebServiceOptions>>().Value;
var authorityConfigured = resolvedOptions.Authority.Enabled;
if (authorityConfigured && resolvedOptions.Authority.AllowAnonymousFallback)
{
    app.Logger.LogWarning(
        "Scanner authority authentication is enabled but anonymous fallback remains allowed. Disable fallback before production rollout.");
}

if (resolvedOptions.Telemetry.EnableLogging && resolvedOptions.Telemetry.EnableRequestLogging)
{
    app.UseSerilogRequestLogging(options =>
    {
        options.GetLevel = (httpContext, elapsed, 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: "https://stellaops.org/problems/internal-error",
            extensions: extensions);

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

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

app.MapHealthEndpoints();

var apiGroup = app.MapGroup(resolvedOptions.Api.BasePath);

if (app.Environment.IsEnvironment("Testing"))
{
    apiGroup.MapGet("/__auth-probe", () => Results.Ok("ok"))
        .RequireAuthorization(ScannerPolicies.ScansEnqueue)
        .WithName("scanner.auth-probe");
}

apiGroup.MapScanEndpoints();
await app.RunAsync().ConfigureAwait(false);
