using System.Net;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Ikar.Server.Data;
using Ikar.Server.Infrastructure;
using Ikar.Server.Infrastructure.Admin;
using Ikar.Server.Infrastructure.Auth;
using Ikar.Server.Infrastructure.Bots;
using Ikar.Server.Infrastructure.Calls;
using Ikar.Server.Infrastructure.Hubs;
using Ikar.Server.Infrastructure.Push;
using Ikar.Server.Infrastructure.Storage;
using Ikar.Server.Infrastructure.Updates;
using Ikar.Shared;
using System.Text;

var builder = WebApplication.CreateBuilder(args);
var sessionActivityWriteInterval = TimeSpan.FromMinutes(1);
var seedDemoData = builder.Configuration.GetValue<bool?>("SeedDemoData") ?? builder.Environment.IsDevelopment();
var configuredUrls =
    builder.Configuration["ASPNETCORE_URLS"] ??
    builder.Configuration["URLS"] ??
    "http://0.0.0.0:5099";

builder.WebHost.UseUrls(configuredUrls);

builder.Services.AddControllers();
builder.Services.AddOpenApi();
builder.Services.AddSignalR();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddHttpClient();

builder.Services.AddDbContext<IkarDbContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("IkarDb") ??
                      "Data Source=Data/ikar.db"));

builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
    options.KnownProxies.Add(IPAddress.Loopback);
    options.KnownProxies.Add(IPAddress.IPv6Loopback);
});

builder.Services.Configure<JwtOptions>(builder.Configuration.GetSection(JwtOptions.SectionName));
builder.Services.Configure<PushOptions>(builder.Configuration.GetSection(PushOptions.SectionName));
builder.Services.Configure<AdminOptions>(builder.Configuration.GetSection(AdminOptions.SectionName));
builder.Services.Configure<WebRtcOptions>(builder.Configuration.GetSection(WebRtcOptions.SectionName));

var jwtOptions = builder.Configuration
    .GetSection(JwtOptions.SectionName)
    .Get<JwtOptions>() ?? new JwtOptions();
var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.SigningKey));

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateIssuerSigningKey = true,
            ValidateLifetime = true,
            ValidIssuer = jwtOptions.Issuer,
            ValidAudience = jwtOptions.Audience,
            IssuerSigningKey = signingKey,
            ClockSkew = TimeSpan.FromSeconds(30)
        };

        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                var accessToken = context.Request.Query["access_token"];
                var path = context.HttpContext.Request.Path;
                if (!string.IsNullOrWhiteSpace(accessToken) &&
                    path.StartsWithSegments("/hubs/messenger"))
                {
                    context.Token = accessToken;
                }

                return Task.CompletedTask;
            },
            OnTokenValidated = async context =>
            {
                var principal = context.Principal;
                if (principal is null)
                {
                    context.Fail("Token principal is missing.");
                    return;
                }

                var sessionId = principal.GetRequiredSessionId();
                var dbContext = context.HttpContext.RequestServices.GetRequiredService<IkarDbContext>();
                var session = await dbContext.UserSessions
                    .Include(x => x.User)
                    .SingleOrDefaultAsync(x => x.Id == sessionId, context.HttpContext.RequestAborted);
                if (session is null)
                {
                    context.Fail("Session is no longer active.");
                    return;
                }

                if (session.User.IsBlocked)
                {
                    context.Fail("User is blocked.");
                    return;
                }

                var now = DateTimeOffset.UtcNow;
                var changed = ClientVersionRequestReader.Apply(
                    session,
                    ClientVersionRequestReader.Read(context.HttpContext.Request));
                if (session.LastSeenAt <= now.Subtract(sessionActivityWriteInterval))
                {
                    session.LastSeenAt = now;
                    changed = true;
                }

                if (changed)
                {
                    await dbContext.SaveChangesAsync(context.HttpContext.RequestAborted);
                }
            }
        };
    })
    .AddCookie(AdminAuthConstants.Scheme, options =>
    {
        options.Cookie.Name = "Ikar.Admin";
        options.Cookie.HttpOnly = true;
        options.Cookie.SameSite = SameSiteMode.Strict;
        options.Cookie.SecurePolicy = builder.Environment.IsDevelopment()
            ? CookieSecurePolicy.SameAsRequest
            : CookieSecurePolicy.Always;
        options.ExpireTimeSpan = TimeSpan.FromHours(12);
        options.SlidingExpiration = true;
        options.Events = new CookieAuthenticationEvents
        {
            OnRedirectToLogin = context =>
            {
                context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                return Task.CompletedTask;
            },
            OnRedirectToAccessDenied = context =>
            {
                context.Response.StatusCode = StatusCodes.Status403Forbidden;
                return Task.CompletedTask;
            }
        };
    });

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy(AdminAuthConstants.Policy, policy =>
    {
        policy.AuthenticationSchemes.Add(AdminAuthConstants.Scheme);
        policy.RequireAuthenticatedUser();
        policy.RequireClaim(ClaimTypes.Role, "Admin");
    });
});
builder.Services.AddSingleton<PresenceTracker>();
builder.Services.AddSingleton<CallSessionStore>();
builder.Services.AddSingleton(new ServerRuntimeState(DateTimeOffset.UtcNow));
builder.Services.AddScoped<TokenService>();
builder.Services.AddSingleton<BotTokenService>();
builder.Services.AddSingleton<PhoneAuthChallengeStore>();
builder.Services.AddScoped<DatabaseSeeder>();
builder.Services.AddScoped<DatabaseSchemaBootstrapper>();
builder.Services.AddScoped<AttachmentStorageService>();
builder.Services.AddScoped<AvatarStorageService>();
builder.Services.AddScoped<BotUpdateService>();
builder.Services.AddScoped<PushNotificationDispatcher>();
builder.Services.AddScoped<AndroidUpdateCompatibilityTrackingService>();
builder.Services.AddScoped<AdminService>();
builder.Services.AddSingleton<AndroidAppUpdateService>();
builder.Services.AddHostedService<AndroidAppUpdateMonitorService>();
builder.Services.AddSingleton<PushDispatchQueue>();
builder.Services.AddHostedService<PushDispatchBackgroundService>();
builder.Services.AddHttpClient<IPushNotificationSender, FirebasePushNotificationSender>();

var app = builder.Build();

Directory.CreateDirectory(Path.Combine(app.Environment.ContentRootPath, "Data"));
app.Services.GetRequiredService<AndroidAppUpdateService>().EnsureStorage();

await using (var scope = app.Services.CreateAsyncScope())
{
    var db = scope.ServiceProvider.GetRequiredService<IkarDbContext>();
    await db.Database.EnsureCreatedAsync();
    await scope.ServiceProvider.GetRequiredService<DatabaseSchemaBootstrapper>().EnsureLatestAsync();
    if (seedDemoData)
    {
        await scope.ServiceProvider.GetRequiredService<DatabaseSeeder>().SeedAsync();
    }
}

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.Use(async (context, next) =>
{
    if (context.Request.Path == "/" || context.Request.Path == "/index.html")
    {
        context.Response.Redirect("https://argus.kusoft.xyz/#catalog", permanent: false);
        return;
    }

    if (context.Request.Path == "/admin" || context.Request.Path == "/admin/")
    {
        context.Response.Redirect("/admin/index.html");
        return;
    }

    await next();
});
app.UseForwardedHeaders();
app.UseStaticFiles();
app.Use(async (context, next) =>
{
    if (!ShouldEnforceAndroidClientVersion(context.Request))
    {
        await next();
        return;
    }

    var release = await context.RequestServices
        .GetRequiredService<AndroidAppUpdateService>()
        .GetLatestReleaseAsync(context.RequestAborted);

    var minimumSupportedVersionCode = release?.MinimumSupportedVersionCode ?? 0;
    if (release is null || minimumSupportedVersionCode <= 0)
    {
        await next();
        return;
    }

    if (!TryGetClientPlatform(context.Request, out var clientPlatform) ||
        !string.Equals(clientPlatform, ClientPlatformNames.Android, StringComparison.OrdinalIgnoreCase))
    {
        await next();
        return;
    }

    if (!TryGetClientVersionCode(context.Request, out var clientVersionCode) ||
        clientVersionCode >= minimumSupportedVersionCode)
    {
        await next();
        return;
    }

    context.Response.StatusCode = StatusCodes.Status426UpgradeRequired;
    context.Response.ContentType = "application/json; charset=utf-8";
    await context.Response.WriteAsJsonAsync(release, cancellationToken: context.RequestAborted);
});
app.UseAuthentication();
app.UseMiddleware<ApiRateLimitMiddleware>();
app.UseAuthorization();

app.MapControllers();
app.MapGet("/health", async (IkarDbContext dbContext, CancellationToken cancellationToken) =>
{
    var canConnect = await dbContext.Database.CanConnectAsync(cancellationToken);
    return canConnect
        ? Results.Ok(new { status = "ok" })
        : Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
});
app.MapHub<MessengerHub>("/hubs/messenger");

app.Run();

static bool ShouldEnforceAndroidClientVersion(HttpRequest request)
{
    if (request.Path.StartsWithSegments("/health", StringComparison.OrdinalIgnoreCase) ||
        request.Path.StartsWithSegments("/api/app-updates", StringComparison.OrdinalIgnoreCase))
    {
        return false;
    }

    return request.Path.StartsWithSegments("/api", StringComparison.OrdinalIgnoreCase) ||
           request.Path.StartsWithSegments("/hubs/messenger", StringComparison.OrdinalIgnoreCase);
}

static bool TryGetClientPlatform(HttpRequest request, out string? clientPlatform)
{
    return ClientVersionRequestReader.TryGetClientPlatform(request, out clientPlatform);
}

static bool TryGetClientVersionCode(HttpRequest request, out int clientVersionCode)
{
    return ClientVersionRequestReader.TryGetClientVersionCode(request, out clientVersionCode);
}
