using System.Security.Claims;
using FileAccessAgent.Application.Abstractions;
using FileAccessAgent.Application.Models;
using FileAccessAgent.Domain;
using FileAccessAgent.Infrastructure.Configuration;
using FileAccessAgent.Infrastructure.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.Configure<InstanceOptions>(builder.Configuration.GetSection(InstanceOptions.SectionName));
builder.Services.Configure<StorageOptions>(builder.Configuration.GetSection(StorageOptions.SectionName));
builder.Services.Configure<MinioOptions>(builder.Configuration.GetSection(MinioOptions.SectionName));
builder.Services.Configure<MemberCenterOptions>(builder.Configuration.GetSection(MemberCenterOptions.SectionName));
builder.Services.Configure<AuthOptions>(builder.Configuration.GetSection(AuthOptions.SectionName));

builder.Services.AddHttpClient();
var storageOptions = builder.Configuration.GetSection(StorageOptions.SectionName).Get<StorageOptions>() ?? new StorageOptions();
if (string.Equals(storageOptions.Provider, "minio", StringComparison.OrdinalIgnoreCase))
{
    builder.Services.AddSingleton<IFileStorage, MinioFileStorage>();
}
else
{
    builder.Services.AddSingleton<IFileStorage, LocalFileStorage>();
}
builder.Services.AddSingleton<IDelegatedDownloadTokenValidator, MemberCenterDelegatedDownloadTokenValidator>();

var authOptions = builder.Configuration.GetSection(AuthOptions.SectionName).Get<AuthOptions>() ?? new AuthOptions();
if (string.IsNullOrWhiteSpace(authOptions.Issuer))
{
    throw new InvalidOperationException("Auth:Issuer is required.");
}

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = authOptions.Issuer;
        if (!string.IsNullOrWhiteSpace(authOptions.MetadataAddress))
        {
            options.MetadataAddress = authOptions.MetadataAddress;
        }
        options.Audience = authOptions.Audience;
        options.RequireHttpsMetadata = authOptions.RequireHttpsMetadata;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = authOptions.Issuer,
            ValidateAudience = true,
            ValidAudience = authOptions.Audience,
            ValidateLifetime = true
        };
    });

builder.Services.AddAuthorization();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

var instanceOptions = app.Services.GetRequiredService<IOptions<InstanceOptions>>().Value;
if (instanceOptions.TenantId == Guid.Empty)
{
    throw new InvalidOperationException("Instance:TenantId is required.");
}

app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/health", () => Results.Ok(new { status = "ok" }))
    .WithName("Health")
    .WithOpenApi();

app.MapGet("/files/metadata/{**objectKey}", async (
    HttpContext context,
    string objectKey,
    IFileStorage storage,
    CancellationToken cancellationToken) =>
{
    var denied = EnsureJwtScopeAndTenant(context, FileAccessScopes.MetadataRead, instanceOptions.TenantId);
    if (denied is not null)
    {
        return denied;
    }

    FileObjectResult? metadata;
    try
    {
        metadata = await storage.GetMetadataAsync(instanceOptions.TenantId, objectKey, cancellationToken);
    }
    catch (InvalidOperationException ex)
    {
        return BadRequest(context, "invalid_object_key", ex.Message);
    }

    if (metadata is null)
    {
        return NotFound(context, "file_not_found", "File not found.");
    }

    return Results.Ok(ToResponse(metadata));
})
.RequireAuthorization()
.WithName("GetFileMetadata")
.WithOpenApi();

app.MapMethods("/files/{**objectKey}", new[] { "HEAD" }, async (
    HttpContext context,
    string objectKey,
    IFileStorage storage,
    CancellationToken cancellationToken) =>
{
    var denied = EnsureJwtScopeAndTenant(context, FileAccessScopes.MetadataRead, instanceOptions.TenantId);
    if (denied is not null)
    {
        return denied;
    }

    FileObjectResult? metadata;
    try
    {
        metadata = await storage.GetMetadataAsync(instanceOptions.TenantId, objectKey, cancellationToken);
    }
    catch (InvalidOperationException ex)
    {
        return BadRequest(context, "invalid_object_key", ex.Message);
    }

    if (metadata is null)
    {
        return NotFound(context, "file_not_found", "File not found.");
    }

    context.Response.ContentType = metadata.ContentType;
    context.Response.ContentLength = metadata.Length;
    context.Response.Headers.ETag = metadata.ETag;
    context.Response.Headers.LastModified = metadata.LastModified.ToString("R");
    return Results.Empty;
})
.RequireAuthorization()
.WithName("HeadFile")
.WithOpenApi();

app.MapPut("/files/{**objectKey}", async (
    HttpContext context,
    string objectKey,
    IFileStorage storage,
    CancellationToken cancellationToken) =>
{
    var denied = EnsureJwtScopeAndTenant(context, FileAccessScopes.UploadWrite, instanceOptions.TenantId);
    if (denied is not null)
    {
        return denied;
    }

    FileObjectResult file;
    try
    {
        file = await storage.UploadAsync(
            instanceOptions.TenantId,
            objectKey,
            context.Request.Body,
            context.Request.ContentLength,
            context.Request.ContentType,
            cancellationToken);
    }
    catch (InvalidOperationException ex)
    {
        return BadRequest(context, "invalid_object_key", ex.Message);
    }

    return Results.Created($"/files/{file.ObjectKey}", ToResponse(file));
})
.RequireAuthorization()
.WithName("UploadFile")
.WithOpenApi();

app.MapGet("/files/{**objectKey}", async (
    HttpContext context,
    string objectKey,
    IFileStorage storage,
    IDelegatedDownloadTokenValidator tokenValidator,
    CancellationToken cancellationToken) =>
{
    if (!TryReadAccessToken(context, out var delegatedToken))
    {
        return Unauthorized(context, "invalid_token", "Bearer token is required.");
    }

    DelegatedDownloadTokenValidationResult validationResult;
    try
    {
        validationResult = await tokenValidator.ValidateAsync(
            new DelegatedDownloadTokenValidationRequest(
                delegatedToken!,
                instanceOptions.TenantId,
                FileId: null,
                ObjectKey: objectKey,
                Method: "GET"),
            cancellationToken);
    }
    catch (Exception)
    {
        return Forbidden(context, "validation_unavailable", "Cannot validate download token.");
    }

    if (!validationResult.Active)
    {
        return Unauthorized(context, "invalid_token", "Download token is invalid.");
    }

    if (validationResult.TenantId.HasValue && validationResult.TenantId.Value != instanceOptions.TenantId)
    {
        return Forbidden(context, "tenant_mismatch", "Token tenant does not match instance tenant.");
    }

    if (!string.IsNullOrWhiteSpace(validationResult.ObjectKey)
        && !string.Equals(validationResult.ObjectKey, objectKey, StringComparison.Ordinal))
    {
        return Forbidden(context, "object_key_mismatch", "Token object key does not match request.");
    }

    if (!string.IsNullOrWhiteSpace(validationResult.Method)
        && !string.Equals(validationResult.Method, "GET", StringComparison.OrdinalIgnoreCase))
    {
        return Forbidden(context, "method_mismatch", "Token method does not match request.");
    }

    FileDownloadResult? file;
    try
    {
        file = await storage.OpenReadAsync(instanceOptions.TenantId, objectKey, cancellationToken);
    }
    catch (InvalidOperationException ex)
    {
        return BadRequest(context, "invalid_object_key", ex.Message);
    }

    if (file is null)
    {
        return NotFound(context, "file_not_found", "File not found.");
    }

    context.Response.Headers.ETag = file.ETag;
    return Results.File(file.Content, file.ContentType, enableRangeProcessing: true, lastModified: file.LastModified);
})
.WithName("DownloadFile")
.WithOpenApi();

app.MapDelete("/files/{**objectKey}", async (
    HttpContext context,
    string objectKey,
    IFileStorage storage,
    CancellationToken cancellationToken) =>
{
    var denied = EnsureJwtScopeAndTenant(context, FileAccessScopes.Delete, instanceOptions.TenantId);
    if (denied is not null)
    {
        return denied;
    }

    bool deleted;
    try
    {
        deleted = await storage.DeleteAsync(instanceOptions.TenantId, objectKey, cancellationToken);
    }
    catch (InvalidOperationException ex)
    {
        return BadRequest(context, "invalid_object_key", ex.Message);
    }

    if (!deleted)
    {
        return NotFound(context, "file_not_found", "File not found.");
    }

    return Results.NoContent();
})
.RequireAuthorization()
.WithName("DeleteFile")
.WithOpenApi();

app.Run();

static IResult? EnsureJwtScopeAndTenant(HttpContext context, string requiredScope, Guid instanceTenantId)
{
    if (context.User.Identity?.IsAuthenticated != true)
    {
        return Unauthorized(context, "invalid_token", "Authentication required.");
    }

    if (!HasScope(context.User, requiredScope))
    {
        return Forbidden(context, "insufficient_scope", $"Required scope '{requiredScope}'.");
    }

    var tenantRaw = context.User.FindFirst("tenant_id")?.Value;
    if (!Guid.TryParse(tenantRaw, out var tokenTenantId))
    {
        return Forbidden(context, "tenant_missing", "tenant_id claim is required.");
    }

    if (tokenTenantId != instanceTenantId)
    {
        return Forbidden(context, "tenant_mismatch", "Token tenant does not match instance tenant.");
    }

    return null;
}

static bool HasScope(ClaimsPrincipal user, string expectedScope)
{
    var scopes = user.FindAll("scope")
        .Concat(user.FindAll("scp"))
        .SelectMany(claim => claim.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries));
    return scopes.Contains(expectedScope, StringComparer.Ordinal);
}

static bool TryReadAccessToken(HttpContext context, out string? token)
{
    token = null;
    var value = context.Request.Headers.Authorization.ToString();
    if (value.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
    {
        token = value["Bearer ".Length..].Trim();
        return !string.IsNullOrWhiteSpace(token);
    }

    if (context.Request.Query.TryGetValue("access_token", out var queryToken))
    {
        token = queryToken.ToString().Trim();
        return !string.IsNullOrWhiteSpace(token);
    }

    return false;
}

static object ToResponse(FileObjectResult file)
{
    return new
    {
        tenant_id = file.TenantId,
        file_id = file.FileId,
        object_key = file.ObjectKey,
        content_type = file.ContentType,
        size = file.Length,
        etag = file.ETag,
        last_modified_at = file.LastModified
    };
}

static IResult BadRequest(HttpContext context, string error, string message)
{
    return Results.Json(new { error, message, request_id = context.TraceIdentifier }, statusCode: StatusCodes.Status400BadRequest);
}

static IResult Unauthorized(HttpContext context, string error, string message)
{
    return Results.Json(new { error, message, request_id = context.TraceIdentifier }, statusCode: StatusCodes.Status401Unauthorized);
}

static IResult Forbidden(HttpContext context, string error, string message)
{
    return Results.Json(new { error, message, request_id = context.TraceIdentifier }, statusCode: StatusCodes.Status403Forbidden);
}

static IResult NotFound(HttpContext context, string error, string message)
{
    return Results.Json(new { error, message, request_id = context.TraceIdentifier }, statusCode: StatusCodes.Status404NotFound);
}

public sealed class AuthOptions
{
    public const string SectionName = "Auth";

    public string Issuer { get; set; } = string.Empty;
    public string? MetadataAddress { get; set; }
    public string Audience { get; set; } = "file_access_api";
    public bool RequireHttpsMetadata { get; set; } = true;
}
