using System.Security.Claims;
using System.Text;
using CoreUpload.Models;
using CoreUpload.Services;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;

namespace CoreUpload.Demos.DemoSecurity;

public static class DemoSecurityEndpoints
{
    public const string RateLimitPolicyName = "DemoSecurityUploadPolicy";

    private static readonly string[] UserImageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp"];
    private const long UserMaxBytes = 2 * 1024 * 1024;
    private const string DemoThreatName = "EICAR-Demo-Test";
    private const string EicarSignature = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";

    public static IEndpointRouteBuilder MapDemoSecurityEndpoints(this IEndpointRouteBuilder endpoints)
    {
        var group = endpoints.MapGroup("/demo/security");

        group.MapGet("/auth/state", (ClaimsPrincipal user) => Results.Ok(BuildAuthState(user)));

        group.MapPost("/auth/signin/{role}", async (HttpContext context, string role) =>
        {
            var normalizedRole = role.Equals("admin", StringComparison.OrdinalIgnoreCase)
                ? "Admin"
                : role.Equals("user", StringComparison.OrdinalIgnoreCase)
                    ? "User"
                    : null;

            if (normalizedRole == null)
            {
                return Results.BadRequest(UploadResult.Error("Role must be 'user' or 'admin'."));
            }

            var displayName = normalizedRole == "Admin" ? "Demo Admin" : "Demo User";
            var claims = new[]
            {
                new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString("N")),
                new Claim(ClaimTypes.Name, displayName),
                new Claim(ClaimTypes.Role, normalizedRole)
            };

            var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme));
            await context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);

            return Results.Ok(BuildAuthState(principal));
        });

        group.MapPost("/auth/signout", async (HttpContext context) =>
        {
            await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
            return Results.Ok(BuildAuthState(new ClaimsPrincipal(new ClaimsIdentity())));
        });

        group.MapPost("/antiforgery-upload", async (HttpContext context, IAntiforgery antiforgery, IUploadService uploadService, CancellationToken ct) =>
        {
            try
            {
                await antiforgery.ValidateRequestAsync(context);
            }
            catch (AntiforgeryValidationException)
            {
                return Results.Ok(UploadResult.Error("Antiforgery token validation failed. Enable the X-XSRF-TOKEN header to upload in this demo."));
            }

            return await UploadSingleFileAsync(
                context,
                uploadService,
                ct,
                successExtras: _ => new Dictionary<string, object?>
                {
                    ["message"] = "Antiforgery token accepted."
                });
        });

        group.MapPost("/authenticated-upload", async (HttpContext context, IUploadService uploadService, CancellationToken ct) =>
        {
            return await UploadSingleFileAsync(
                context,
                uploadService,
                ct,
                successExtras: user => new Dictionary<string, object?>
                {
                    ["uploadedBy"] = user.Identity?.Name,
                    ["role"] = user.FindFirstValue(ClaimTypes.Role)
                });
        }).RequireAuthorization();

        group.MapPost("/role-upload", async (HttpContext context, IUploadService uploadService, CancellationToken ct) =>
        {
            return await UploadSingleFileAsync(
                context,
                uploadService,
                ct,
                validateFile: file =>
                {
                    if (context.User.IsInRole("Admin"))
                    {
                        return null;
                    }

                    var ext = Path.GetExtension(file.FileName).ToLowerInvariant();
                    if (!UserImageExtensions.Contains(ext))
                    {
                        return "Demo user policy: only image files are allowed.";
                    }

                    if (file.Length > UserMaxBytes)
                    {
                        return "Demo user policy: image uploads must be 2 MB or smaller.";
                    }

                    return null;
                },
                successExtras: user => new Dictionary<string, object?>
                {
                    ["uploadedBy"] = user.Identity?.Name,
                    ["role"] = user.FindFirstValue(ClaimTypes.Role),
                    ["policy"] = user.IsInRole("Admin")
                        ? "Admin policy: full access"
                        : "User policy: images only, up to 2 MB"
                });
        }).RequireAuthorization();

        group.MapPost("/rate-limited-upload", async (HttpContext context, IUploadService uploadService, CancellationToken ct) =>
        {
            return await UploadSingleFileAsync(
                context,
                uploadService,
                ct,
                successExtras: _ => new Dictionary<string, object?>
                {
                    ["message"] = "Accepted by the rate-limited endpoint."
                });
        }).RequireRateLimiting(RateLimitPolicyName);

        group.MapPost("/virus-scan-upload", async (HttpContext context, IUploadService uploadService, CancellationToken ct) =>
        {
            if (!context.Request.HasFormContentType)
            {
                return Results.BadRequest(UploadResult.Error("Expected multipart form data."));
            }

            var form = await context.Request.ReadFormAsync(ct);
            var file = form.Files.FirstOrDefault();
            if (file == null || file.Length == 0)
            {
                return Results.BadRequest(UploadResult.Error("No file provided."));
            }

            await using var source = file.OpenReadStream();
            await using var buffer = new MemoryStream();
            await source.CopyToAsync(buffer, ct);

            var threat = DetectThreat(file.FileName, buffer);
            if (threat != null)
            {
                return Results.Ok(new Dictionary<string, object?>
                {
                    ["success"] = false,
                    ["errorMessage"] = $"File rejected by the demo scanner: {threat}.",
                    ["threat"] = threat
                });
            }

            buffer.Position = 0;
            var result = await uploadService.UploadFileAsync(buffer, file.FileName, file.Length, file.ContentType, ct);
            return result.Success
                ? Results.Ok(BuildPayload(result, new Dictionary<string, object?>
                {
                    ["scanStatus"] = "clean",
                    ["threat"] = null
                }))
                : Results.BadRequest(result);
        });

        return endpoints;
    }

    private static async Task<IResult> UploadSingleFileAsync(
        HttpContext context,
        IUploadService uploadService,
        CancellationToken ct,
        Func<IFormFile, string?>? validateFile = null,
        Func<ClaimsPrincipal, IDictionary<string, object?>>? successExtras = null)
    {
        if (!context.Request.HasFormContentType)
        {
            return Results.BadRequest(UploadResult.Error("Expected multipart form data."));
        }

        var form = await context.Request.ReadFormAsync(ct);
        var file = form.Files.FirstOrDefault();
        if (file == null || file.Length == 0)
        {
            return Results.BadRequest(UploadResult.Error("No file provided."));
        }

        if (validateFile != null)
        {
            var validationError = validateFile(file);
            if (!string.IsNullOrEmpty(validationError))
            {
                return Results.Ok(UploadResult.Error(validationError));
            }
        }

        await using var stream = file.OpenReadStream();
        var result = await uploadService.UploadFileAsync(stream, file.FileName, file.Length, file.ContentType, ct);
        if (!result.Success)
        {
            return Results.BadRequest(result);
        }

        return Results.Ok(BuildPayload(result, successExtras?.Invoke(context.User)));
    }

    private static Dictionary<string, object?> BuildPayload(UploadResult result, IDictionary<string, object?>? extras = null)
    {
        var payload = new Dictionary<string, object?>
        {
            ["success"] = result.Success,
            ["fileGuid"] = result.FileGuid,
            ["fileName"] = result.FileName,
            ["fileSize"] = result.FileSize,
            ["errorMessage"] = result.ErrorMessage
        };

        if (extras != null)
        {
            foreach (var pair in extras)
            {
                payload[pair.Key] = pair.Value;
            }
        }

        return payload;
    }

    private static object BuildAuthState(ClaimsPrincipal user)
    {
        var isAuthenticated = user.Identity?.IsAuthenticated ?? false;
        var roles = user.FindAll(ClaimTypes.Role).Select(claim => claim.Value).ToArray();

        return new
        {
            isAuthenticated,
            userName = isAuthenticated ? user.Identity?.Name : "Guest",
            role = roles.FirstOrDefault() ?? "Guest",
            roles
        };
    }

    private static string? DetectThreat(string fileName, MemoryStream buffer)
    {
        if (fileName.Contains("eicar", StringComparison.OrdinalIgnoreCase) ||
            fileName.Contains("virus", StringComparison.OrdinalIgnoreCase) ||
            fileName.Contains("malware", StringComparison.OrdinalIgnoreCase) ||
            fileName.Contains("infected", StringComparison.OrdinalIgnoreCase))
        {
            return DemoThreatName;
        }

        var sampleLength = (int)Math.Min(buffer.Length, 128 * 1024);
        buffer.Position = 0;
        var bytes = new byte[sampleLength];
        _ = buffer.Read(bytes, 0, sampleLength);
        buffer.Position = 0;

        var ascii = Encoding.ASCII.GetString(bytes);
        return ascii.Contains(EicarSignature, StringComparison.Ordinal)
            ? DemoThreatName
            : null;
    }
}
