using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using CalledIt.Api.Authentication;
using CalledIt.Api.OutputCaching;
using CalledIt.DataAccess;
using CalledIt.Domain;
using Npgsql;
using CalledIt.Domain.Services;
using CalledIt.Util;
using DotNetEnv;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;

namespace CalledIt.Api;

public static class Program
{
    public static async Task Main(string[] args)
    {
        Env.TraversePath().Load();
        var builder = WebApplication.CreateBuilder(args);

        var baseConnectionString = builder.Configuration.GetConnectionString("DefaultConnection");
        var password = Environment.GetEnvironmentVariable("DB_PASSWORD");

        if (!string.IsNullOrEmpty(password))
        {
            var connBuilder = new NpgsqlConnectionStringBuilder(baseConnectionString)
            {
                Password = password
            };
            var finalConnectionString = connBuilder.ConnectionString;
            builder.Configuration.GetSection("ConnectionStrings")["DefaultConnection"] = finalConnectionString;
        }

        builder.Services.RegisterModule<DataAccessModule>(builder.Configuration);
        builder.Services.RegisterModule<DomainModule>(builder.Configuration);
        builder.Services.Configure<JwtOptions>(
            builder.Configuration.GetSection(JwtOptions.SectionName));
        builder.Services.AddScoped<IJwtTokenService, JwtTokenService>();
        builder.Services.AddHostedService<TokenRevocationCacheLoader>();
        builder.Services.AddHostedService<TokenRevocationListener>();

        var jwtOptions = builder.Configuration
            .GetSection(JwtOptions.SectionName)
            .Get<JwtOptions>() ?? new JwtOptions();

        if (string.IsNullOrWhiteSpace(jwtOptions.Key) || jwtOptions.Key.Length < 64)
        {
            throw new InvalidOperationException(
                "JWT signing key must be configured and at least 64 characters long.");
        }

        builder.Services
            .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                options.MapInboundClaims = false;
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateIssuerSigningKey = true,
                    ValidateLifetime = true,
                    ValidIssuer = jwtOptions.Issuer,
                    ValidAudience = jwtOptions.Audience,
                    IssuerSigningKey = new SymmetricSecurityKey(
                        Encoding.UTF8.GetBytes(jwtOptions.Key)),
                    ClockSkew = TimeSpan.FromMinutes(1),
                    RoleClaimType = "role"
                };
                options.Events = new JwtBearerEvents
                {
                    OnTokenValidated = async ctx =>
                    {
                        // Check revocation blocklist
                        var revocationSvc = ctx.HttpContext.RequestServices
                            .GetRequiredService<ITokenRevocationService>();
                        var jti = ctx.Principal?.FindFirstValue(JwtRegisteredClaimNames.Jti);
                        if (jti is not null && await revocationSvc.IsRevokedAsync(jti))
                        {
                            ctx.Fail("Token has been revoked.");
                            return;
                        }

                        // Validate fingerprint binding
                        var fgpClaim = ctx.Principal?.FindFirstValue("fgp");
                        if (fgpClaim is not null)
                        {
                            var isDev = ctx.HttpContext.RequestServices
                                .GetRequiredService<IWebHostEnvironment>()
                                .IsDevelopment();
                            var cookieName = isDev ? "Fgp" : "__Host-Fgp";
                            var cookie = ctx.HttpContext.Request.Cookies[cookieName];

                            if (cookie is null)
                            {
                                ctx.Fail("Fingerprint cookie missing.");
                                return;
                            }

                            var cookieHash = Convert.ToBase64String(
                                SHA256.HashData(Encoding.UTF8.GetBytes(cookie)));

                            if (!CryptographicOperations.FixedTimeEquals(
                                    Encoding.UTF8.GetBytes(cookieHash),
                                    Encoding.UTF8.GetBytes(fgpClaim)))
                            {
                                ctx.Fail("Fingerprint mismatch.");
                            }
                        }
                    }
                };
            });
        builder.Services.AddAuthorization();

        builder.Services.AddCors(options =>
        {
            options.AddPolicy("AllowFrontend", policy =>
            {
                policy.WithOrigins("http://localhost:5173", "http://localhost:3000", "http://localhost")
                      .AllowAnyHeader()
                      .AllowAnyMethod()
                      .AllowCredentials();
            });
        });

        builder.Services.AddResponseCompression(options => options.EnableForHttps = true);
        builder.Services.AddOutputCache(options =>
        {
            options.AddPolicy("Leaderboard", LeaderboardCachePolicy.Instance);
        });
        builder.Services.AddControllers();
        builder.Services.AddEndpointsApiExplorer();
        builder.Services.AddOpenApi();

        var app = builder.Build();

        if (app.Environment.IsDevelopment() || app.Environment.IsEnvironment("Docker"))
        {
            using var scope = app.Services.CreateScope();
            var dbContext = scope.ServiceProvider.GetRequiredService<CalledItDbContext>();
            await dbContext.Database.MigrateAsync();

            if (Environment.GetEnvironmentVariable("MIGRATE_ONLY") == "true")
            {
                return;
            }
        }

        app.UseResponseCompression();

        var uploadsPath = builder.Configuration["Uploads:BasePath"] ?? "uploads";
        var absoluteUploadsPath = Path.IsPathRooted(uploadsPath)
            ? uploadsPath
            : Path.Combine(app.Environment.ContentRootPath, uploadsPath);
        Directory.CreateDirectory(absoluteUploadsPath);
        app.UseStaticFiles(new StaticFileOptions
        {
            FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(absoluteUploadsPath),
            RequestPath = "/uploads"
        });

        var swaggerEnabled =
            app.Environment.IsDevelopment() ||
            string.Equals(Environment.GetEnvironmentVariable("ENABLE_SWAGGER"), "true", StringComparison.OrdinalIgnoreCase);

        if (swaggerEnabled)
        {
            app.MapOpenApi();
            app.UseSwaggerUI(options =>
            {
                options.SwaggerEndpoint("/openapi/v1.json", "CalledIt.Api v1");
                options.RoutePrefix = "swagger";
            });
        }

        app.UseCors("AllowFrontend");
        app.UseAuthentication();
        app.UseAuthorization();

        // UnauthorizedAccessException unambiguously means 401 (missing/invalid
        // credentials or claims). "Authenticated but forbidden" uses
        // ForbiddenAccessException, mapped to 403 inside the relevant controllers.
        app.Use(async (context, next) =>
        {
            try
            {
                await next();
            }
            catch (UnauthorizedAccessException) when (!context.Response.HasStarted)
            {
                context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            }
        });

        app.UseOutputCache();

        app.MapControllers();

        await app.RunAsync();
    }
}
