using System.Diagnostics;
using DodoSSH.Contracts;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.WebUtilities;

namespace DodoSSH.Api.Setup;

/// <summary>
/// The one shape every error this API returns takes.
/// </summary>
/// <remarks>
/// RFC 9457 with a root-level <c>code</c>. The prose is for whoever reads a log; the code is what the
/// client branches on, which is why it is a constant in <see cref="ProblemCodes"/> and never a literal
/// at the call site. Shared rather than duplicated per feature because there are now several endpoint
/// classes per file, and a second copy is how two of them end up disagreeing.
/// </remarks>
internal static class Problems
{
    /// <summary>An error a handler decided on.</summary>
    internal static ProblemHttpResult Coded(int statusCode, string code, string detail) =>
        TypedResults.Problem(
            detail: detail,
            statusCode: statusCode,
            type: ProblemCodes.TypeBaseUri + code,
            extensions: new Dictionary<string, object?>(StringComparer.Ordinal) { ["code"] = code });

    /// <summary>The same shape, for the one path that cannot return an <see cref="IResult"/>.</summary>
    /// <remarks>
    /// FastEndpoints answers a request whose body will not deserialise before any handler runs, through
    /// a builder that returns a plain object rather than a result — so <see cref="Coded"/> cannot be
    /// reused and the shape has to be spelled out. Replacing the default is not cosmetic: it announces
    /// <c>application/problem+json</c> while sending something that is not a problem document, and it
    /// puts the failing .NET type name on the wire, which is exactly what <c>IncludeErrorDetails =
    /// false</c> on the bearer handler exists to prevent.
    /// </remarks>
    internal static CodedProblem ForBindingFailure(HttpContext context, int statusCode)
    {
        ArgumentNullException.ThrowIfNull(context);

        return new CodedProblem(
            Type: ProblemCodes.TypeBaseUri + ProblemCodes.MalformedRequest,
            Title: ReasonPhrases.GetReasonPhrase(statusCode),
            Status: statusCode,

            // Deliberately says nothing about which member failed. The binder knows, but naming it
            // describes the server's types rather than the caller's request.
            Detail: "The request body could not be read. Check it against the contract for the server "
                + "version reported by GET /api/v1/meta.",
            Code: ProblemCodes.MalformedRequest,
            TraceId: Activity.Current?.Id ?? context.TraceIdentifier);
    }
}

/// <summary>A problem document written by something other than <c>TypedResults.Problem</c>.</summary>
/// <remarks>
/// Member order here is member order on the wire, and the names match what
/// <see cref="Microsoft.AspNetCore.Mvc.ProblemDetails"/> serialises to, so a client cannot tell which
/// of the two produced a given response.
/// </remarks>
/// <param name="Type">The problem type URI, formed under <see cref="ProblemCodes.TypeBaseUri"/>.</param>
/// <param name="Title">The status code's reason phrase.</param>
/// <param name="Status">The HTTP status code.</param>
/// <param name="Detail">Human-readable explanation. Never a secret and never a .NET type name.</param>
/// <param name="Code">The stable code the client switches on.</param>
/// <param name="TraceId">Correlates the response with the server's trace.</param>
internal sealed record CodedProblem(
    string Type,
    string Title,
    int Status,
    string Detail,
    string Code,
    string? TraceId);
