<?php

namespace App\Http\Middleware;

use App\Facades\Settings;
use App\Master\Models\ApiToken;
use App\Services\ApiUsageTracker;
use Carbon\Carbon;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Mtc\MercuryDataModels\ApiUsageEndpoint;
use Symfony\Component\HttpFoundation\Response;

class ApiAuthenticatedRequest
{
    /**
     * How long a successful (token, tenant) authorisation is cached, in seconds.
     * A token revoked/expired or with tenant access removed stays accepted for at
     * most this window — acceptable for these high-volume API integrations.
     */
    private const TOKEN_CACHE_TTL = 60;

    private ApiToken $apiToken;

    /**
     * Handle an incoming request.
     *
     * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
     */
    public function handle(Request $request, Closure $next, ?string $legacyScenario = null): Response
    {
        if ($this->failsAuthToken($request, $legacyScenario)) {
            return \response('Access Denied', 401);
        }

        if ($this->failsRouteAccess($request, $legacyScenario)) {
            return \response('Forbidden', 403);
        }

        $start = microtime(true);
        $response = $next($request);
        $request_length = microtime(true) - $start;

        if (isset($this->apiToken)) {
            // Buffer the request stats in Redis (one atomic op) rather than writing
            // the hot api_usage row per request; api-usage:flush drains it to the DB.
            app(ApiUsageTracker::class)->record(
                tenant('id'),
                ApiUsageEndpoint::fromRequest($request),
                $request->input(),
                $request->method(),
                $response->getStatusCode(),
                $request_length,
                $this->apiToken,
            );
        }
        return $response;
    }

    private function failsAuthToken(Request $request, ?string $legacyScenario): bool
    {
        if (!$request->hasHeader('Authorization') && empty($legacyScenario)) {
            return true;
        }

        if (!empty($legacyScenario)) {
            return $this->authenticatesLegacyScenario($request, $legacyScenario) === false;
        }

        $token = str_replace('Bearer ', '', $request->header('Authorization'));
        return $this->matchesApiToken($token) === false;
    }

    private function authenticatesLegacyScenario(Request $request, string $legacyScenario): bool
    {
        return match ($legacyScenario) {
            'auto-imaging' => $request->input('key') === config('services.image-sync.auto-imaging.key')
                || $this->matchesApiToken($request->input('key')),
            'export-map' => $this->matchesApiToken($request->input('key'))
                || $this->matchesApiToken(str_replace('Bearer ', '', $request->header('Authorization'))),
            default => false
        };
    }

    private function matchesApiToken(?string $token): bool
    {
        if (empty($token)) {
            return false;
        }

        // Cache the successful lookup per (token, tenant) so a single token hitting a
        // high-volume endpoint doesn't run the token + tenant-access queries on every
        // request. Only positive results are cached (the closure returns null on
        // failure, which Cache::remember does not persist), so a newly created or
        // re-granted token is honoured immediately.
        $cacheKey = 'api-auth:' . hash('sha256', $token) . ':' . tenant('id');

        $apiToken = Cache::remember(
            $cacheKey,
            self::TOKEN_CACHE_TTL,
            fn () => $this->lookupApiToken($token)
        );

        if (!$apiToken) {
            return false;
        }

        $this->apiToken = $apiToken;
        return true;
    }

    private function lookupApiToken(string $token): ?ApiToken
    {
        $apiToken = ApiToken::query()
            ->whereHas('apiUser', fn ($query) => $query->where('is_active', 1))
            ->where('token', $token)
            ->where('expires_at', '>=', Carbon::now())
            ->first();
        if (!$apiToken) {
            return null;
        }

        $has_access_to_tenant = $apiToken->apiUser->tenants()->where('tenant_id', tenant('id'))->exists();
        if (!$has_access_to_tenant) {
            return null;
        }

        return $apiToken;
    }

    private function failsRouteAccess(Request $request, ?string $legacyScenario): bool
    {
        if (!isset($this->apiToken)) {
            // apiToken may not be set in the auto-imaging legacy scenario.
            // route access should fail if apiToken is missing,
            // unless the legacy scenario is 'auto-imaging'.
            return $legacyScenario != 'auto-imaging';
        }

        $routeName = $request->route()?->getName();

        if (!$routeName) {
            return true;
        }

        // Per-tenant kill-switch for the route allow-list. Defaults to enabled so a
        // tenant that has never been seeded the setting stays restricted. Token and
        // tenant access are still enforced regardless of this toggle.
        if (!Settings::get('api-users-restrict-routes', true)) {
            return false;
        }

        return !$this->apiToken->apiUser->allowedRoutes()
            ->where('route_name', $routeName)
            ->exists();
    }
}
