<?php

namespace App\Http\Middleware;

use App\Models\ApiKey;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

/**
 * Bearer-token authentication for external integrations (AI Portal, custom
 * scripts, etc.). Tokens are issued by ApiKeyController, stored as SHA-256
 * hashes; the cleartext is shown once at creation time and never again.
 *
 * Pulls the token from `Authorization: Bearer <token>` (or `?api_token=` as
 * a fallback for things like webhooks that can't easily set headers).
 *
 * Auth is rejected for: missing token, unknown token, revoked, expired, or
 * if the route requires an ability the token doesn't have. On every match
 * we touch `last_used_at` so operators can see which keys are active.
 *
 * Successful auth puts the ApiKey on `$request->attributes->get('api_key')`
 * and resolves `$request->user()` to the key's owner so existing
 * permission checks (`isAdminOrAbove()`, `canManageServers()`, etc.) keep
 * working.
 */
class AuthenticateApiKey
{
    public function handle(Request $request, Closure $next, ?string $ability = null): Response
    {
        $token = $this->extractToken($request);
        if (!$token) {
            return response()->json([
                'error' => 'unauthorized',
                'message' => 'Bearer API token required. Set Authorization: Bearer <token>.',
            ], 401);
        }

        $hash = hash('sha256', $token);
        $key = ApiKey::with('user')->where('token_hash', $hash)->first();

        if (!$key) {
            return response()->json(['error' => 'invalid_token', 'message' => 'Token not recognized.'], 401);
        }
        if ($key->revoked) {
            return response()->json(['error' => 'token_revoked', 'message' => 'This token has been revoked.'], 401);
        }
        if ($key->expires_at && $key->expires_at->isPast()) {
            return response()->json(['error' => 'token_expired', 'message' => 'Token expired at ' . $key->expires_at->toIso8601String()], 401);
        }

        // Ability gate: routes can require a specific ability via middleware
        // arg, e.g. `auth.api-key:write`. Tokens default to `['read']` so a
        // mutate request from a read-only token returns 403.
        if ($ability) {
            $abilities = $key->abilities ?? [];
            if (!in_array($ability, $abilities, true) && !in_array('*', $abilities, true)) {
                return response()->json([
                    'error' => 'insufficient_scope',
                    'message' => "Token lacks '{$ability}' ability. Token has: " . implode(', ', $abilities),
                ], 403);
            }
        }

        if (!$key->user) {
            return response()->json(['error' => 'orphaned_token', 'message' => 'Token owner no longer exists.'], 401);
        }

        // Touch last_used_at so admins can see active keys at a glance. We
        // throttle the write to once per minute via a tight where clause to
        // avoid hitting the DB on every single request from a busy client.
        if (!$key->last_used_at || $key->last_used_at->lt(now()->subMinute())) {
            $key->forceFill(['last_used_at' => now()])->save();
        }

        // Authenticate as the token's owner so existing role/permission
        // checks (`canManageServers()`, etc.) keep working unchanged.
        auth()->setUser($key->user);
        $request->setUserResolver(fn () => $key->user);
        $request->attributes->set('api_key', $key);

        return $next($request);
    }

    private function extractToken(Request $request): ?string
    {
        $header = $request->header('Authorization', '');
        if (is_string($header) && str_starts_with($header, 'Bearer ')) {
            return trim(substr($header, 7));
        }
        // Fallback: ?api_token=xxx — useful for webhook URLs and one-off curls.
        return $request->query('api_token') ?: null;
    }
}
