<?php

namespace App\Services;

use App\Jobs\SendPushNotification;
use App\Models\Customer;
use App\Models\User;
use Firebase\JWT\JWT;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use RuntimeException;

/**
 * Sends push notifications through the FCM HTTP v1 API.
 *
 * Auth is a standard OAuth2 service-account JWT bearer flow: sign a short
 * lived assertion with the service account's private key, exchange it at
 * Google's token endpoint for an access token, then call the v1 send
 * endpoint with that bearer token. The service-account file itself is read
 * from a local path outside the webroot (see config/services.php) — it is
 * never fetched over HTTP, unlike the old project's implementation.
 */
class FcmService
{
    private const TOKEN_URL = 'https://oauth2.googleapis.com/token';

    private const SCOPE = 'https://www.googleapis.com/auth/firebase.messaging';

    private readonly ?string $credentialsPath;

    private readonly ?string $projectId;

    public function __construct(?string $credentialsPath = null, ?string $projectId = null)
    {
        $this->credentialsPath = $credentialsPath ?? config('services.fcm.credentials_path');
        $this->projectId = $projectId ?? config('services.fcm.project_id');
    }

    public function isConfigured(): bool
    {
        return (bool) $this->projectId && $this->credentialsPath && is_file($this->credentialsPath);
    }

    /**
     * PERF-03: queues the actual send (OAuth token exchange + one HTTP call
     * per device token) rather than making the caller — a booking-status
     * request/response cycle, in every existing call site — wait on it.
     * None of those call sites inspect the return value, so this always
     * reports a dispatched, not-yet-known outcome; call sendToOwnerNow()
     * directly if a caller ever needs the real sent/failed counts.
     *
     * @param  array<string, string>  $data
     * @return array{sent: int, failed: int, invalid_tokens: string[]}
     */
    public function sendToOwner(User|Customer $owner, string $title, string $body, array $data = []): array
    {
        // NT-01: optional notification types the recipient has muted never
        // even get queued — job-critical types aren't in that catalog, so
        // they're unaffected by this check.
        if ($owner instanceof User && ! $owner->wantsNotification($data['type'] ?? null)) {
            return ['sent' => 0, 'failed' => 0, 'invalid_tokens' => []];
        }

        SendPushNotification::dispatch($owner::class, $owner->getKey(), $title, $body, $data);

        return ['sent' => 0, 'failed' => 0, 'invalid_tokens' => []];
    }

    /**
     * The actual synchronous send sendToOwner() above used to do inline —
     * still called directly by SendPushNotification's queued handle().
     *
     * @param  array<string, string>  $data
     * @return array{sent: int, failed: int, invalid_tokens: string[]}
     */
    public function sendToOwnerNow(User|Customer $owner, string $title, string $body, array $data = []): array
    {
        $tokens = $owner->deviceTokens()->where('is_active', true)->pluck('token')->all();

        $result = $this->sendToTokens($tokens, $title, $body, $data);

        if ($result['invalid_tokens'] !== []) {
            $owner->deviceTokens()->whereIn('token', $result['invalid_tokens'])->update(['is_active' => false]);
        }

        return $result;
    }

    /**
     * @param  array<string, string>  $data
     * @return array{sent: int, failed: int, invalid_tokens: string[]}
     */
    public function sendToTokens(array $tokens, string $title, string $body, array $data = []): array
    {
        $tokens = array_values(array_unique(array_filter($tokens)));

        if ($tokens === []) {
            return ['sent' => 0, 'failed' => 0, 'invalid_tokens' => []];
        }

        if (! $this->isConfigured()) {
            Log::warning('FcmService: skipped send, FCM is not configured.', ['title' => $title]);

            return ['sent' => 0, 'failed' => count($tokens), 'invalid_tokens' => []];
        }

        $accessToken = $this->getAccessToken();
        $endpoint = sprintf('https://fcm.googleapis.com/v1/projects/%s/messages:send', $this->projectId);

        $sent = 0;
        $failed = 0;
        $invalidTokens = [];

        foreach ($tokens as $token) {
            $response = Http::withToken($accessToken)->post($endpoint, [
                'message' => [
                    'token' => $token,
                    'notification' => ['title' => $title, 'body' => $body],
                    'data' => array_map('strval', $data),
                ],
            ]);

            if ($response->successful()) {
                $sent++;

                continue;
            }

            $failed++;
            $status = $response->json('error.status');

            if (in_array($status, ['UNREGISTERED', 'NOT_FOUND', 'INVALID_ARGUMENT'], true)) {
                $invalidTokens[] = $token;
            }

            Log::warning('FcmService: send failed.', [
                'status' => $response->status(),
                'error' => $response->json('error'),
            ]);
        }

        return ['sent' => $sent, 'failed' => $failed, 'invalid_tokens' => $invalidTokens];
    }

    private function getAccessToken(): string
    {
        return Cache::remember('fcm.access_token', 3300, function () {
            $credentials = json_decode(file_get_contents($this->credentialsPath), true);

            if (! is_array($credentials) || ! isset($credentials['private_key'], $credentials['client_email'])) {
                throw new RuntimeException('FCM credentials file is missing required fields.');
            }

            $now = time();
            $assertion = JWT::encode([
                'iss' => $credentials['client_email'],
                'scope' => self::SCOPE,
                'aud' => self::TOKEN_URL,
                'iat' => $now,
                'exp' => $now + 3600,
            ], $credentials['private_key'], 'RS256');

            $response = Http::asForm()->post(self::TOKEN_URL, [
                'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
                'assertion' => $assertion,
            ]);

            $response->throw();

            return $response->json('access_token');
        });
    }
}
