using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.WebUtilities;

namespace Nexus.Api.Http;

public static class NexusProblemCodes
{
    public const string ValidationFailed = "validation_failed";
    public const string Unauthenticated = "unauthenticated";
    public const string Forbidden = "forbidden";
    public const string NotFound = "not_found";
    public const string Conflict = "conflict";
    public const string UnsupportedCapability = "unsupported_capability";
    public const string DependencyUnavailable = "dependency_unavailable";
    public const string Timeout = "timeout";
    public const string RateLimited = "rate_limited";
    public const string InternalError = "internal_error";

    public static string ForStatus(int statusCode) => statusCode switch
    {
        StatusCodes.Status400BadRequest or StatusCodes.Status422UnprocessableEntity => ValidationFailed,
        StatusCodes.Status401Unauthorized => Unauthenticated,
        StatusCodes.Status403Forbidden => Forbidden,
        StatusCodes.Status404NotFound => NotFound,
        StatusCodes.Status409Conflict => Conflict,
        StatusCodes.Status429TooManyRequests => RateLimited,
        StatusCodes.Status501NotImplemented => UnsupportedCapability,
        StatusCodes.Status502BadGateway or StatusCodes.Status503ServiceUnavailable => DependencyUnavailable,
        StatusCodes.Status504GatewayTimeout => Timeout,
        _ => InternalError
    };
}

public static class NexusProblemDetailsDefaults
{
    public static void Apply(ProblemDetails problem, HttpContext httpContext)
    {
        var statusCode = problem.Status
            ?? (httpContext.Response.StatusCode >= 400
                ? httpContext.Response.StatusCode
                : StatusCodes.Status500InternalServerError);

        problem.Status = statusCode;
        problem.Type ??= $"https://httpstatuses.com/{statusCode}";
        problem.Title ??= ReasonPhrases.GetReasonPhrase(statusCode);
        problem.Extensions.TryAdd("code", NexusProblemCodes.ForStatus(statusCode));
        problem.Extensions.TryAdd(
            "traceId",
            Activity.Current?.Id ?? httpContext.TraceIdentifier);
    }

    public static ProblemDetails? FromLegacyError(
        object? value,
        int? statusCode,
        HttpContext httpContext)
    {
        if (value is null || statusCode is null || statusCode < 400)
            return null;

        var errorProperty = value.GetType().GetProperty(
            "error",
            System.Reflection.BindingFlags.Public
            | System.Reflection.BindingFlags.Instance
            | System.Reflection.BindingFlags.IgnoreCase);
        if (errorProperty?.GetValue(value) is not string detail
            || string.IsNullOrWhiteSpace(detail))
        {
            return null;
        }

        var problem = new ProblemDetails
        {
            Status = statusCode,
            Detail = detail
        };
        Apply(problem, httpContext);
        return problem;
    }
}

public static class NexusHttpResults
{
    public static IResult Problem(
        int statusCode,
        string detail,
        string? code = null,
        string? title = null,
        IDictionary<string, object?>? extensions = null)
    {
        var values = extensions is null
            ? new Dictionary<string, object?>()
            : new Dictionary<string, object?>(extensions);
        values.TryAdd("code", code ?? NexusProblemCodes.ForStatus(statusCode));

        return Results.Problem(
            statusCode: statusCode,
            title: title,
            detail: detail,
            extensions: values);
    }
}

/// <summary>
/// Ensures MVC-produced ProblemDetails values use the same extensions as the
/// global exception and status-code writers.
/// </summary>
public sealed class NexusProblemDetailsFilter : IResultFilter
{
    public void OnResultExecuting(ResultExecutingContext context)
    {
        if (context.Result is not ObjectResult objectResult)
            return;

        if (objectResult.Value is ProblemDetails problem)
        {
            NexusProblemDetailsDefaults.Apply(problem, context.HttpContext);
            return;
        }

        var legacyProblem = NexusProblemDetailsDefaults.FromLegacyError(
            objectResult.Value,
            objectResult.StatusCode,
            context.HttpContext);
        if (legacyProblem is null)
            return;

        objectResult.Value = legacyProblem;
        objectResult.ContentTypes.Clear();
        objectResult.ContentTypes.Add("application/problem+json");
    }

    public void OnResultExecuted(ResultExecutedContext context)
    {
    }
}
