using Microsoft.AspNetCore.Http;

namespace ErsatzTV.Middleware;

/// <summary>
///     Adds security response headers to every response (API, IPTV, artwork, static, and error
///     responses alike — which is why this is middleware rather than an MVC filter).
///     <c>nosniff</c> in particular blunts the MIME-sniffing half of the artwork content-type
///     reflection risk (issue #197). A Content-Security-Policy is enforced everywhere except the
///     Scalar API-reference UI (<c>/docs</c>) and the OpenAPI document (<c>/openapi</c>) — see
///     <see cref="InvokeAsync" />. HSTS is intentionally left out: it is a deployment/TLS decision
///     that belongs with the go-live reverse-proxy posture, not this host middleware.
///     Baseline headers landed for #197; the CSP + Permissions-Policy were added for #319 (ZAP
///     baseline hardening). Cross-Origin-Resource-Policy was added for #330 to prevent browsers
///     from embedding responses through cross-origin no-cors requests.
/// </summary>
public class SecurityHeadersMiddleware(RequestDelegate next)
{
    /// <summary>
    ///     sha256 of the inline theme-bootstrap <c>&lt;script&gt;</c> in the built SPA
    ///     <c>index.html</c> (<c>ErsatzTV/wwwroot/app</c>). The SPA is served as a static file, so a
    ///     per-response CSP nonce is not possible — the one inline script is allow-listed by hash
    ///     instead. If <c>web/index.html</c>'s inline script changes, this hash must change with it;
    ///     <c>SecurityHeadersMiddlewareTests.Csp_Script_Hash_Should_Match_The_Spa_Index</c>
    ///     guards the two against drift.
    /// </summary>
    public const string SpaInlineScriptHash = "sha256-5qq06L57XDT8IqaUeyjnI1l7bZ+ggOaE4GXF4TpnGwM=";

    /// <summary>
    ///     Enforcing CSP for the SPA plus the API/artwork/IPTV responses. Notes on the choices:
    ///     <list type="bullet">
    ///         <item><c>script-src 'self'</c> (the Vite module bundle) + the inline theme-bootstrap
    ///         hash — no <c>'unsafe-inline'</c>/<c>'unsafe-eval'</c> (the SPA has no eval/CSS-in-JS).</item>
    ///         <item><c>style-src 'unsafe-inline'</c>: React writes inline <c>style=""</c> attributes
    ///         (there is no styled-components/emotion in the SPA to hash <c>&lt;style&gt;</c> blocks).
    ///         <c>fonts.googleapis.com</c> is the Geist web-font stylesheet the SPA CSS <c>@import</c>s
    ///         (with <c>fonts.gstatic.com</c> in <c>font-src</c> for the font files). Self-hosting the
    ///         font to drop the Google dependency is a follow-on hardening, not this issue.</item>
    ///         <item><c>img-src data: blob:</c>: favicon/generated-image data URIs and object-URL
    ///         upload previews.</item>
    ///         <item><c>connect-src 'self'</c>: the SPA only ever fetches same-origin
    ///         <c>/api</c>, <c>/artwork</c>, <c>/iptv</c>.</item>
    ///     </list>
    ///     Non-HTML responses (JSON, images, m3u) never exercise the script/style directives, so the
    ///     same policy is safe to apply to them.
    /// </summary>
    private static readonly string ContentSecurityPolicy = string.Join(
        "; ",
        "default-src 'self'",
        "script-src 'self' '" + SpaInlineScriptHash + "'",
        "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
        "img-src 'self' data: blob:",
        "font-src 'self' data: https://fonts.gstatic.com",
        "connect-src 'self'",
        "object-src 'none'",
        "base-uri 'self'",
        "frame-ancestors 'none'",
        "form-action 'self'");

    public Task InvokeAsync(HttpContext context)
    {
        IHeaderDictionary headers = context.Response.Headers;
        headers["X-Content-Type-Options"] = "nosniff";
        headers["X-Frame-Options"] = "DENY";
        headers["Referrer-Policy"] = "strict-origin-when-cross-origin";
        headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=(), payment=(), usb=()";
        headers["Cross-Origin-Resource-Policy"] = "same-origin";

        // The Scalar API-reference UI (/docs) and the OpenAPI document (/openapi) rely on inline
        // bootstrap scripts/styles that a strict policy would break; hardening that admin surface is
        // a #197 follow-up. Everything else — the SPA, /api, /artwork, /iptv — gets the CSP.
        PathString path = context.Request.Path;
        if (!path.StartsWithSegments("/docs") && !path.StartsWithSegments("/openapi"))
        {
            headers["Content-Security-Policy"] = ContentSecurityPolicy;
        }

        return next(context);
    }
}
