using Dapper;
using DexDemoBackend;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Tokens;
using Npgsql;
using System.Security.Claims;

var builder = WebApplication.CreateBuilder(args);

// Configure AppConfig using IConfiguration
builder.Services.Configure<AppConfig>(builder.Configuration.GetSection("AppConfig"));
builder.Services.AddSingleton(sp => sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<AppConfig>>().Value);

var config = builder.Configuration.GetSection("AppConfig").Get<AppConfig>() ?? new AppConfig();

ValidateConfiguration(config);

ConfigureJwtAuthentication(builder.Services, config);

builder.Services.AddAuthorization();

builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.SnakeCaseLower;
});

var app = builder.Build();
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;

// Get current config from DI (supports reload)
var currentConfig = app.Services.GetRequiredService<AppConfig>();

app.UseCors(policy =>
{
    policy.WithOrigins(currentConfig.AllowedOrigins)
          .AllowAnyMethod()
          .AllowAnyHeader()
          .AllowCredentials();
});
app.UseAuthentication();
app.UseAuthorization();

// Global error handling middleware
app.Use(async (context, next) =>
{
    try
    {
        await next();
    }
    catch (UnauthorizedAccessException)
    {
        context.Response.StatusCode = StatusCodes.Status401Unauthorized;
        await context.Response.WriteAsJsonAsync(new { error = "Unauthorized" });
    }
    catch (Exception)
    {
        context.Response.StatusCode = StatusCodes.Status500InternalServerError;
        await context.Response.WriteAsJsonAsync(new { error = "Internal server error" });
    }
});

app.MapGet("/api/health", () => Results.Ok(new HealthResponse("ok")));

app.MapGet("/api/user-identity", [Authorize] (HttpContext context) =>
{
    return Results.Ok(new UserIdentityResponse(
        context.User.Identity?.Name,
        context.User.Identity?.IsAuthenticated ?? false,
        context.User.Identity?.AuthenticationType,
        context.User.Claims.Select(c => new ClaimResponse(c.Type, c.Value)).ToList()
    ));
});

app.MapGet("/api/user-info", [Authorize] async (HttpContext context, [FromServices] AppConfig cfg) =>
{
    try
    {
        var email = await GetUserEmail(context, cfg);

        await using var conn = new NpgsqlConnection(cfg.ConnectionString);
        await conn.OpenAsync();

        var userData = await conn.QueryAsync<UserDataResult>(@"
            SELECT
                u.email, u.full_name,
                o.id as org_id, o.name as org_name,
                r.id as role_id, r.name as role_name, r.description as role_description,
                l.id as link_id, l.title as link_title, l.url as link_url, l.description as link_description
            FROM users u
            LEFT JOIN organizations o ON u.organization_id = o.id
            LEFT JOIN user_roles ur ON u.id = ur.user_id
            LEFT JOIN roles r ON ur.role_id = r.id
            LEFT JOIN role_links rl ON r.id = rl.role_id
            LEFT JOIN links l ON rl.link_id = l.id
            WHERE u.email = @email
            ORDER BY l.id",
            new { email });

        if (!userData.Any())
        {
            return Results.NotFound(new { detail = "User not found in database" });
        }

        var firstRecord = userData.First();
        var organization = firstRecord.OrgId.HasValue && firstRecord.OrgName != null
            ? new Organization(firstRecord.OrgId.Value, firstRecord.OrgName)
            : null;

        var roles = userData
            .Where(x => x.RoleId.HasValue)
            .GroupBy(x => x.RoleId)
            .Select(g => g.First())
            .Select(x => new Role(x.RoleId!.Value, x.RoleName!, x.RoleDescription))
            .ToList();

        var links = userData
            .Where(x => x.LinkId.HasValue)
            .GroupBy(x => x.LinkId)
            .Select(g => g.First())
            .Select(x => new Link(x.LinkId!.Value, x.LinkTitle!, x.LinkUrl!, x.LinkDescription))
            .ToList();

        return Results.Ok(new UserInfo(firstRecord.Email, firstRecord.FullName, organization, roles, links));
    }
    catch (NpgsqlException ex) when (ex.IsTransient)
    {
        return Results.Json(new { error = "Database temporarily unavailable" }, statusCode: 503);
    }
    catch (NpgsqlException)
    {
        return Results.Json(new { error = "Database error" }, statusCode: 500);
    }
});

app.Run();

static void ConfigureJwtAuthentication(IServiceCollection services, AppConfig config)
{
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.Authority = config.Issuer;
            options.RequireHttpsMetadata = false;
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidIssuer = config.Issuer,
                ValidateAudience = false,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                ClockSkew = OidcConfigConstants.ClockSkew,
                NameClaimType = OidcConfigConstants.NameClaimType,
                RoleClaimType = OidcConfigConstants.RoleClaimType
            };
            
            options.Events = new JwtBearerEvents
            {
                OnTokenValidated = context =>
                {
                    var nameClaim = context.Principal?.FindFirst(OidcConfigConstants.NameClaimType);
                    var emailClaim = context.Principal?.FindFirst(OidcConfigConstants.EmailClaimType);

                    if (nameClaim == null && emailClaim != null)
                    {
                        var identity = context.Principal?.Identity as ClaimsIdentity;
                        identity?.AddClaim(new Claim(OidcConfigConstants.NameClaimType, emailClaim.Value));
                    }

                    return Task.CompletedTask;
                }
            };
            
            var httpClientHandler = new HttpClientHandler();
            
            if (config.InsecureDevMode)
            {
                httpClientHandler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true;
            }
            
            var discoveryUrl = $"{config.Issuer}.well-known/openid-configuration";
            
            options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
                discoveryUrl,
                new OpenIdConnectConfigurationRetriever(),
                new HttpDocumentRetriever(new HttpClient(httpClientHandler)) 
                { 
                    RequireHttps = !config.InsecureDevMode 
                }
            );
        });
}


static void ValidateConfiguration(AppConfig config)
{
    if (string.IsNullOrWhiteSpace(config.DbHost))
        throw new InvalidOperationException("Configuration parameter 'AppConfig:DbHost' is required");

    if (string.IsNullOrWhiteSpace(config.DbPort))
        throw new InvalidOperationException("Configuration parameter 'AppConfig:DbPort' is required");

    if (string.IsNullOrWhiteSpace(config.DbName))
        throw new InvalidOperationException("Configuration parameter 'AppConfig:DbName' is required");

    if (string.IsNullOrWhiteSpace(config.DbUser))
        throw new InvalidOperationException("Configuration parameter 'AppConfig:DbUser' is required");

    if (string.IsNullOrWhiteSpace(config.DbPassword))
        throw new InvalidOperationException("Configuration parameter 'AppConfig:DbPassword' is required");

    if (string.IsNullOrWhiteSpace(config.Issuer))
        throw new InvalidOperationException("Configuration parameter 'AppConfig:Issuer' is required");

    if (!Uri.TryCreate(config.Issuer, UriKind.Absolute, out _))
        throw new InvalidOperationException("Configuration parameter 'AppConfig:Issuer' must be a valid URL");

    if (config.InsecureDevMode && string.IsNullOrWhiteSpace(config.InsecureDevEmail))
        throw new InvalidOperationException("Configuration parameter 'AppConfig:InsecureDevEmail' is required when 'AppConfig:InsecureDevMode' is enabled");
}

static Task<string> GetUserEmail(HttpContext context, AppConfig config)
{
    if (config.InsecureDevMode)
    {
        return Task.FromResult(config.InsecureDevEmail!);
    }

    if (context.User.Identity?.IsAuthenticated == true)
    {
        var emailClaim = context.User.FindFirst(OidcConfigConstants.EmailClaimType);
        if (emailClaim != null)
        {
            return Task.FromResult(emailClaim.Value);
        }
    }

    if (context.Request.Headers.TryGetValue(OidcConfigConstants.AuthRequestEmailHeader, out var emailHeader))
    {
        return Task.FromResult(emailHeader.ToString());
    }

    throw new UnauthorizedAccessException("No authentication information found");
}

record UserDataResult(
    string Email,
    string FullName,
    int? OrgId,
    string? OrgName,
    int? RoleId,
    string? RoleName,
    string? RoleDescription,
    int? LinkId,
    string? LinkTitle,
    string? LinkUrl,
    string? LinkDescription
);

// API Response Models
record HealthResponse(string Status);

record ClaimResponse(string Type, string Value);

record UserIdentityResponse(
    string? Name,
    bool IsAuthenticated,
    string? AuthenticationType,
    List<ClaimResponse> Claims
);

static class OidcConfigConstants
{
    // Default environment variable values
    public const string DefaultDbHost = "postgres";
    public const string DefaultDbPort = "5440";
    public const string DefaultDbName = "dexdemo";
    public const string DefaultDbUser = "dexdemo";
    public const string DefaultDbPassword = "dexdemo";
    public const string DefaultDexIssuer = "https://dex.127.0.0.1.sslip.io/";

    // JWT settings
    public const string NameClaimType = "name";
    public const string RoleClaimType = "role";
    public const string EmailClaimType = "email";
    public const string AuthRequestEmailHeader = "X-Auth-Request-Email";

    // Time settings
    public static readonly TimeSpan ClockSkew = TimeSpan.FromMinutes(5);
}
