using System.Globalization;
using System.Security.Claims;
using System.Text.RegularExpressions;
using GestionaDenunciasAN.Components;
using GestionaDenunciasAN.Configuration;
using GestionaDenunciasAN.Models;
using GestionaDenuncias.Shared.Models;
using GestionaDenunciasAN.Services;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.Options;
using System.Net.Http.Headers;

var builder = WebApplication.CreateBuilder(args);

CultureInfo.DefaultThreadCurrentCulture = CultureInfo.GetCultureInfo("es-ES");
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo("es-ES");

builder.Services.Configure<ApiDenunciasOptions>(builder.Configuration.GetSection(ApiDenunciasOptions.SectionName));

builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents(options =>
    {
        options.DetailedErrors = true;
        options.JSInteropDefaultCallTimeout = TimeSpan.FromSeconds(180);
    });
builder.Services.AddCascadingAuthenticationState();

builder.Services
    .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.Cookie.Name = "denuncias.auth";
        options.Cookie.HttpOnly = true;
        options.Cookie.SameSite = SameSiteMode.Lax;
        options.LoginPath = "/";
        options.LogoutPath = "/api/auth/logout";
        options.ExpireTimeSpan = TimeSpan.FromHours(8);
        options.SlidingExpiration = false;
    });

builder.Services.AddAuthorization();
builder.Services.AddDataProtection();
builder.Services.AddServerSideBlazor().AddCircuitOptions(option =>
{
    option.DetailedErrors = true;
    option.JSInteropDefaultCallTimeout = TimeSpan.FromSeconds(180);
});
builder.Services.AddHttpContextAccessor();
builder.Services.AddAntiforgery();
builder.Services.AddScoped<UserState>();
builder.Services.AddSingleton<AppSessionLifetime>();
builder.Services.AddSingleton<LoginRateLimiter>();
builder.Services.AddScoped<UiBusyService>();
builder.Services.AddScoped<ApiDenunciasClient>();
builder.Services.AddScoped<IDenunciaStore, ApiDenunciaStore>();
builder.Services.AddScoped<IInboxTrackingService, ApiInboxTrackingService>();

builder.Services.AddHttpClient(ApiDenunciasClient.HttpClientName, (sp, client) =>
{
    var opts = sp.GetRequiredService<IOptions<ApiDenunciasOptions>>().Value;
    client.BaseAddress = new Uri(opts.BaseUrl.TrimEnd('/') + "/");
    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.Use(async (context, next) =>
{
    context.Response.Headers.XFrameOptions = "DENY";
    context.Response.Headers.XContentTypeOptions = "nosniff";
    context.Response.Headers["Referrer-Policy"] = "no-referrer";
    context.Response.Headers.ContentSecurityPolicy =
        "default-src 'self'; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; font-src 'self' data: https://cdn.jsdelivr.net; img-src 'self' data:; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net";
    await next();
});

if (builder.Configuration.GetValue("ForceHttpsRedirection", false))
{
    app.UseHttpsRedirection();
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.Use(async (context, next) =>
{
    var path = context.Request.Path;
    var isPublic =
        path == "/" ||
        path.StartsWithSegments("/api") ||
        path.StartsWithSegments("/_blazor") ||
        path.StartsWithSegments("/_framework") ||
        path.StartsWithSegments("/bootstrap") ||
        path.StartsWithSegments("/Content") ||
        path.StartsWithSegments("/Scripts") ||
        path.StartsWithSegments("/css") ||
        path.StartsWithSegments("/js") ||
        path.StartsWithSegments("/favicon") ||
        Path.HasExtension(path.Value);

    if (context.User.Identity?.IsAuthenticated == true)
    {
        var username = context.User.Identity?.Name?.Trim();
        var appSessionLifetime = context.RequestServices.GetRequiredService<AppSessionLifetime>();
        var cookieStartupStamp = context.User.FindFirst("app_startup_stamp")?.Value;
        var hasApiToken = !string.IsNullOrWhiteSpace(context.User.FindFirst(ApiDenunciasClient.AccessTokenClaim)?.Value);
        var tokenIsStillValid = true;
        var cookieBelongsToCurrentStartup =
            !string.IsNullOrWhiteSpace(cookieStartupStamp) &&
            string.Equals(cookieStartupStamp, appSessionLifetime.StartupStamp, StringComparison.Ordinal);

        var tokenExpiresAt = context.User.FindFirst(ApiDenunciasClient.TokenExpiresAtClaim)?.Value;
        if (!string.IsNullOrWhiteSpace(tokenExpiresAt) &&
            DateTimeOffset.TryParse(tokenExpiresAt, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var expiresAt))
        {
            tokenIsStillValid = expiresAt > DateTimeOffset.UtcNow;
        }

        if (string.IsNullOrWhiteSpace(username) || !hasApiToken || !tokenIsStillValid || !cookieBelongsToCurrentStartup)
        {
            await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

            if (!isPublic)
            {
                var returnUrl = $"{context.Request.Path}{context.Request.QueryString}";
                context.Response.Redirect($"/?returnUrl={Uri.EscapeDataString(returnUrl)}");
                return;
            }
        }
    }

    if (isPublic || context.User.Identity?.IsAuthenticated == true)
    {
        await next();
        return;
    }

    var loginReturnUrl = $"{context.Request.Path}{context.Request.QueryString}";
    context.Response.Redirect($"/?returnUrl={Uri.EscapeDataString(loginReturnUrl)}");
});
app.UseAntiforgery();

var api = app.MapGroup("/api");

api.MapPost("/auth/prepare", async (
    ApiLoginPrepareRequest request,
    ApiDenunciasClient apiClient,
    IOptions<ApiDenunciasOptions> apiOptions,
    CancellationToken cancellationToken) =>
{
    if (string.IsNullOrWhiteSpace(request.Username) ||
        string.IsNullOrWhiteSpace(request.Password))
    {
        return Results.Json(
            new ApiError("Debes indicar usuario y contrasena."),
            statusCode: StatusCodes.Status400BadRequest);
    }

    try
    {
        var loginTimeoutSeconds = Math.Clamp(apiOptions.Value.LoginTimeoutSeconds, 15, 300);
        using var loginTimeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        loginTimeout.CancelAfter(TimeSpan.FromSeconds(loginTimeoutSeconds));

        var prepared = await apiClient.PrepareLoginAsync(
            request with { Username = request.Username.Trim() },
            loginTimeout.Token);

        return Results.Ok(prepared);
    }
    catch (UnauthorizedAccessException ex)
    {
        return Results.Json(new ApiError(ex.Message), statusCode: StatusCodes.Status401Unauthorized);
    }
    catch (InvalidOperationException ex)
    {
        return Results.Json(new ApiError(ex.Message), statusCode: StatusCodes.Status400BadRequest);
    }
    catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
    {
        return Results.Json(
            new ApiError($"La API de denuncias no ha respondido en {apiOptions.Value.LoginTimeoutSeconds} segundos ({apiOptions.Value.BaseUrl})."),
            statusCode: StatusCodes.Status504GatewayTimeout);
    }
    catch (HttpRequestException ex)
    {
        return Results.Json(
            new ApiError($"No se ha podido conectar con la API de denuncias ({apiOptions.Value.BaseUrl}). Detalle: {ex.Message}"),
            statusCode: StatusCodes.Status503ServiceUnavailable);
    }
}).DisableAntiforgery();

api.MapPost("/auth/login", async (
    LoginRequest request,
    HttpContext httpContext,
    ApiDenunciasClient apiClient,
    LoginRateLimiter rateLimiter,
    IOptions<ApiDenunciasOptions> apiOptions,
    CancellationToken cancellationToken) =>
{
    var ip = httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
    var appSessionLifetime = httpContext.RequestServices.GetRequiredService<AppSessionLifetime>();
    if (!rateLimiter.AllowAttempt(ip))
    {
        return Results.Json(
            new ApiError("Demasiados intentos. Espera un minuto."),
            statusCode: StatusCodes.Status429TooManyRequests);
    }

    if (string.IsNullOrWhiteSpace(request.Username) ||
        string.IsNullOrWhiteSpace(request.Password) ||
        string.IsNullOrWhiteSpace(request.Authcode))
    {
        return Results.Json(
            new ApiError("Debes indicar usuario, contraseña y código 2FA."),
            statusCode: StatusCodes.Status400BadRequest);
    }

    if (!Regex.IsMatch(request.Authcode.Trim(), @"^\d{6}$"))
    {
        return Results.Json(
            new ApiError("El código 2FA debe tener exactamente 6 dígitos."),
            statusCode: StatusCodes.Status400BadRequest);
    }

    try
    {
        var loginTimeoutSeconds = Math.Clamp(apiOptions.Value.LoginTimeoutSeconds, 15, 300);
        using var loginTimeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        loginTimeout.CancelAfter(TimeSpan.FromSeconds(loginTimeoutSeconds));

        var login = await apiClient.LoginAsync(
            request with
            {
                Username = request.Username.Trim(),
                Authcode = request.Authcode.Trim()
            },
            loginTimeout.Token);

        var claims = new List<Claim>
        {
            new(ClaimTypes.Name, login.Username),
            new("app_startup_stamp", appSessionLifetime.StartupStamp),
            new(ApiDenunciasClient.AccessTokenClaim, login.AccessToken),
            new(ApiDenunciasClient.TokenExpiresAtClaim, login.ExpiresAtUtc.ToString("O", CultureInfo.InvariantCulture)),
        };

        if (!string.IsNullOrWhiteSpace(login.Role))
        {
            claims.Add(new Claim("gl_role", login.Role));
        }

        var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
        var principal = new ClaimsPrincipal(identity);
        var authProperties = new AuthenticationProperties
        {
            IsPersistent = false,
            AllowRefresh = true,
        };

        await httpContext.SignInAsync(
            CookieAuthenticationDefaults.AuthenticationScheme,
            principal,
            authProperties);

        return Results.Ok(new LoginResponse(login.Username));
    }
    catch (UnauthorizedAccessException ex)
    {
        return Results.Json(new ApiError(ex.Message), statusCode: StatusCodes.Status401Unauthorized);
    }
    catch (InvalidOperationException ex)
    {
        return Results.Json(
            new ApiError(ex.Message),
            statusCode: StatusCodes.Status400BadRequest);
    }
    catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
    {
        return Results.Json(
            new ApiError($"La API de denuncias no ha respondido en {apiOptions.Value.LoginTimeoutSeconds} segundos ({apiOptions.Value.BaseUrl}). Comprueba los logs de ApiDenuncias: probablemente esta esperando a GlobalLeaks o a una dependencia externa."),
            statusCode: StatusCodes.Status504GatewayTimeout);
    }
    catch (HttpRequestException ex)
    {
        return Results.Json(
            new ApiError($"No se ha podido conectar con la API de denuncias ({apiOptions.Value.BaseUrl}). Detalle: {ex.Message}"),
            statusCode: StatusCodes.Status503ServiceUnavailable);
    }
}).DisableAntiforgery();

api.MapPost("/auth/complete", async (
    ApiLoginCompleteRequest request,
    HttpContext httpContext,
    ApiDenunciasClient apiClient,
    IOptions<ApiDenunciasOptions> apiOptions,
    CancellationToken cancellationToken) =>
{
    var appSessionLifetime = httpContext.RequestServices.GetRequiredService<AppSessionLifetime>();

    if (string.IsNullOrWhiteSpace(request.PendingLoginId) ||
        string.IsNullOrWhiteSpace(request.Authcode))
    {
        return Results.Json(
            new ApiError("Debes indicar el codigo 2FA."),
            statusCode: StatusCodes.Status400BadRequest);
    }

    if (!Regex.IsMatch(request.Authcode.Trim(), @"^\d{6}$"))
    {
        return Results.Json(
            new ApiError("El codigo 2FA debe tener exactamente 6 digitos."),
            statusCode: StatusCodes.Status400BadRequest);
    }

    try
    {
        var loginTimeoutSeconds = Math.Clamp(apiOptions.Value.LoginTimeoutSeconds, 15, 300);
        using var loginTimeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        loginTimeout.CancelAfter(TimeSpan.FromSeconds(loginTimeoutSeconds));

        var login = await apiClient.CompleteLoginAsync(
            request with { Authcode = request.Authcode.Trim() },
            loginTimeout.Token);

        var claims = new List<Claim>
        {
            new(ClaimTypes.Name, login.Username),
            new("app_startup_stamp", appSessionLifetime.StartupStamp),
            new(ApiDenunciasClient.AccessTokenClaim, login.AccessToken),
            new(ApiDenunciasClient.TokenExpiresAtClaim, login.ExpiresAtUtc.ToString("O", CultureInfo.InvariantCulture)),
        };

        if (!string.IsNullOrWhiteSpace(login.Role))
        {
            claims.Add(new Claim("gl_role", login.Role));
        }

        var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
        var principal = new ClaimsPrincipal(identity);
        var authProperties = new AuthenticationProperties
        {
            IsPersistent = false,
            AllowRefresh = true,
        };

        await httpContext.SignInAsync(
            CookieAuthenticationDefaults.AuthenticationScheme,
            principal,
            authProperties);

        return Results.Ok(new LoginResponse(login.Username));
    }
    catch (UnauthorizedAccessException ex)
    {
        return Results.Json(new ApiError(ex.Message), statusCode: StatusCodes.Status401Unauthorized);
    }
    catch (InvalidOperationException ex)
    {
        return Results.Json(
            new ApiError(ex.Message),
            statusCode: StatusCodes.Status400BadRequest);
    }
    catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
    {
        return Results.Json(
            new ApiError($"La API de denuncias no ha respondido en {apiOptions.Value.LoginTimeoutSeconds} segundos ({apiOptions.Value.BaseUrl})."),
            statusCode: StatusCodes.Status504GatewayTimeout);
    }
    catch (HttpRequestException ex)
    {
        return Results.Json(
            new ApiError($"No se ha podido conectar con la API de denuncias ({apiOptions.Value.BaseUrl}). Detalle: {ex.Message}"),
            statusCode: StatusCodes.Status503ServiceUnavailable);
    }
}).DisableAntiforgery();

api.MapPost("/auth/logout", async (
    HttpContext httpContext,
    ApiDenunciasClient apiClient,
    CancellationToken cancellationToken) =>
{
    if (httpContext.User.Identity?.IsAuthenticated == true)
    {
        try
        {
            await apiClient.LogoutAsync(cancellationToken);
        }
        catch
        {
            // Aunque la API no responda, el usuario debe poder cerrar la sesion local.
        }
    }

    await httpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    return Results.Ok(new { ok = true });
}).DisableAntiforgery();

api.MapGet("/denuncias/{denunciaId:int}/ficheros/content", async (
    int denunciaId,
    string fileName,
    HttpContext httpContext,
    IHttpClientFactory httpClientFactory,
    CancellationToken cancellationToken) =>
{
    if (string.IsNullOrWhiteSpace(fileName))
    {
        return Results.BadRequest(new ApiError("Nombre de fichero obligatorio."));
    }

    var token = httpContext.User.FindFirst(ApiDenunciasClient.AccessTokenClaim)?.Value;
    if (string.IsNullOrWhiteSpace(token))
    {
        return Results.Unauthorized();
    }

    var client = httpClientFactory.CreateClient(ApiDenunciasClient.HttpClientName);
    var path = $"api/denuncias/{denunciaId}/ficheros/content?fileName={Uri.EscapeDataString(fileName)}";
    using var request = new HttpRequestMessage(HttpMethod.Get, path);
    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);

    using var response = await client.SendAsync(request, cancellationToken);
    if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
    {
        return Results.NotFound();
    }

    if (!response.IsSuccessStatusCode)
    {
        var message = await response.Content.ReadAsStringAsync(cancellationToken);
        return Results.Problem(message, statusCode: (int)response.StatusCode);
    }

    var bytes = await response.Content.ReadAsByteArrayAsync(cancellationToken);
    var contentType = response.Content.Headers.ContentType?.ToString() ?? "application/octet-stream";

    httpContext.Response.Headers.CacheControl = "no-store";
    return Results.File(bytes, contentType, enableRangeProcessing: true);
}).RequireAuthorization();

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

app.Run();
