<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;

/**
 * Builds the CORS origin allowlist per request instead of trusting every origin.
 *
 * The customer API is public and multi-tenant, and CORS runs with
 * supports_credentials enabled. A static '*' allowlist would make php-cors reflect
 * any Origin back with Access-Control-Allow-Credentials: true (the OWASP
 * reflect-any-origin-with-credentials anti-pattern). Instead we validate the
 * incoming Origin and only then add it to cors.allowed_origins, which HandleCors
 * re-reads from config on every request.
 *
 * Scoping is per tenant. On an actual request the tenant is known from the
 * x-tenant header (which holds a domain, resolved by DomainTenantResolver), so the
 * Origin must belong to that same tenant. A CORS preflight (OPTIONS) does not carry
 * custom headers such as x-tenant, so it can only be validated against the global
 * domains table; the browser re-checks the actual response, where the tenant-scoped
 * rule applies, so cross-tenant reflection is still blocked end to end.
 *
 * This runs as a global middleware ordered before HandleCors, because tenancy is
 * not yet initialised at that point; the domains table is on the central
 * connection and is queried directly.
 */
class ResolveCorsAllowedOrigins
{
    public function handle(Request $request, Closure $next): Response
    {
        $origin = $request->headers->get('Origin');

        if ($origin !== null && $this->isOriginAllowed($request, $origin)) {
            Config::set('cors.allowed_origins', array_values(array_unique(
                array_merge((array) config('cors.allowed_origins', []), [$origin])
            )));
        }

        $response = $next($request);

        // php-cors omits Vary: Origin on its single-origin fast path; add it so a
        // shared cache never serves one origin's CORS headers to another origin.
        if ($origin !== null) {
            $vary = $response->getVary();

            if (!in_array('Origin', $vary, true)) {
                $response->setVary(array_merge($vary, ['Origin']));
            }
        }

        return $response;
    }

    private function isOriginAllowed(Request $request, string $origin): bool
    {
        $originHost = $this->normaliseHost($origin);

        if ($originHost === '') {
            return false;
        }

        $tenantHost = $this->normaliseHost((string) $request->headers->get('x-tenant'));

        // One lookup for both hosts, keyed as domain => tenant_id, then compare in memory.
        try {
            $model = config('tenancy.domain_model');
            $tenantIds = $model::query()
                ->whereIn('domain', array_filter([$originHost, $tenantHost]))
                ->pluck('tenant_id', 'domain');
        } catch (\Throwable) {
            // Never let an allowlist lookup failure turn into a 500 for every request.
            return false;
        }

        // The Origin must be a registered tenant domain.
        if (!$tenantIds->has($originHost)) {
            return false;
        }

        // Preflight (no x-tenant): any registered tenant domain is accepted. The actual
        // response is re-checked by the browser against the tenant-scoped rule below.
        if ($tenantHost === '') {
            return true;
        }

        // Actual request: the Origin and x-tenant must resolve to the same tenant.
        return $tenantIds->has($tenantHost) && $tenantIds[$originHost] === $tenantIds[$tenantHost];
    }

    private function normaliseHost(string $value): string
    {
        return (string) Str::of($value)
            ->after('://')
            ->before('/')
            ->before(':')
            ->replaceFirst('www.', '')
            ->lower()
            ->trim();
    }
}
