<?php

namespace App\Services\Fcm;

use Google\Auth\Credentials\ServiceAccountCredentials;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class FcmSender
{
    private const FCM_SCOPE = 'https://www.googleapis.com/auth/firebase.messaging';

    private ?string $accessToken = null;

    private ?int $tokenExpiresAt = null;

    /**
     * 단일 토큰에 푸시 발송
     */
    public function sendToToken(
        string $token,
        string $title,
        string $body,
        string $channelId = 'push_default',
        array $data = []
    ): FcmResponse {
        $message = $this->buildMessage($token, $title, $body, $channelId, $data);

        return $this->send($message);
    }

    /**
     * 다건 발송 (토큰 배열) - 단순 루프
     *
     * @param  array<string>  $tokens
     * @return array<FcmResponse>
     */
    public function sendToTokens(
        array $tokens,
        string $title,
        string $body,
        string $channelId = 'push_default',
        array $data = []
    ): array {
        $responses = [];

        foreach ($tokens as $token) {
            $responses[] = $this->sendToToken($token, $title, $body, $channelId, $data);
        }

        return $responses;
    }

    /**
     * 대량 발송 (chunk + rate limit 지원)
     *
     * @param  array<string>  $tokens
     */
    public function sendToMany(
        array $tokens,
        string $title,
        string $body,
        string $channelId = 'push_default',
        array $data = [],
        ?int $chunkSize = null,
        ?int $delayMs = null
    ): FcmBatchResult {
        $chunkSize = $chunkSize ?? config('fcm.batch.chunk_size', 200);
        $delayMs = $delayMs ?? config('fcm.batch.delay_ms', 100);

        $result = new FcmBatchResult;
        $chunks = array_chunk($tokens, $chunkSize);
        $totalChunks = count($chunks);

        foreach ($chunks as $index => $chunk) {
            foreach ($chunk as $token) {
                $response = $this->sendToToken($token, $title, $body, $channelId, $data);
                $result->addResponse($token, $response);
            }

            // 마지막 chunk가 아니면 rate limit delay
            if ($index < $totalChunks - 1 && $delayMs > 0) {
                usleep($delayMs * 1000);
            }
        }

        return $result;
    }

    /**
     * FCM 메시지 빌드
     */
    private function buildMessage(
        string $token,
        string $title,
        string $body,
        string $channelId,
        array $data
    ): array {
        $message = [
            'message' => [
                'token' => $token,
                'notification' => [
                    'title' => $title,
                    'body' => $body,
                ],
                'android' => [
                    'priority' => config('fcm.defaults.priority', 'high'),
                    'ttl' => config('fcm.defaults.ttl', '86400s'),
                    'notification' => [
                        'channel_id' => $channelId,
                        'sound' => $this->getSoundForChannel($channelId),
                    ],
                ],
                'apns' => [
                    'payload' => [
                        'aps' => [
                            'sound' => 'default',
                            'badge' => 1,
                        ],
                    ],
                ],
            ],
        ];

        // 커스텀 데이터 추가
        if (! empty($data)) {
            // FCM data는 모두 string이어야 함
            $stringData = array_map(fn ($v) => is_string($v) ? $v : json_encode($v), $data);
            $message['message']['data'] = $stringData;
        }

        return $message;
    }

    /**
     * FCM HTTP v1 API로 메시지 발송
     */
    private function send(array $message): FcmResponse
    {
        $projectId = config('fcm.project_id');

        if (empty($projectId)) {
            return FcmResponse::failure('FCM_PROJECT_ID not configured', 0, $message);
        }

        $endpoint = str_replace('{project_id}', $projectId, config('fcm.endpoint'));

        try {
            $accessToken = $this->getAccessToken();

            $response = Http::withHeaders([
                'Authorization' => 'Bearer '.$accessToken,
                'Content-Type' => 'application/json',
            ])->post($endpoint, $message);

            $statusCode = $response->status();
            $responseBody = $response->json() ?? [];

            $this->logRequest($message, $statusCode, $responseBody);

            if ($response->successful()) {
                return FcmResponse::success($responseBody['name'] ?? null, $statusCode, $responseBody);
            }

            $errorMessage = $responseBody['error']['message'] ?? 'Unknown FCM error';

            return FcmResponse::failure($errorMessage, $statusCode, $responseBody);

        } catch (\Exception $e) {
            $this->logError($message, $e);

            return FcmResponse::failure($e->getMessage(), 0, []);
        }
    }

    /**
     * OAuth2 Access Token 발급 (캐싱)
     */
    private function getAccessToken(): string
    {
        // 토큰이 유효하면 재사용
        if ($this->accessToken && $this->tokenExpiresAt && time() < $this->tokenExpiresAt - 60) {
            return $this->accessToken;
        }

        $saPath = $this->getServiceAccountPath();

        if (! file_exists($saPath)) {
            throw new FcmException("Service account file not found: {$saPath}");
        }

        $credentials = new ServiceAccountCredentials(
            self::FCM_SCOPE,
            json_decode(file_get_contents($saPath), true)
        );

        $token = $credentials->fetchAuthToken();

        if (empty($token['access_token'])) {
            throw new FcmException('Failed to fetch FCM access token');
        }

        $this->accessToken = $token['access_token'];
        $this->tokenExpiresAt = time() + ($token['expires_in'] ?? 3600);

        return $this->accessToken;
    }

    /**
     * Service Account JSON 파일 경로
     */
    private function getServiceAccountPath(): string
    {
        $path = config('fcm.service_account_path');

        // 절대 경로인 경우
        if (str_starts_with($path, '/')) {
            return $path;
        }

        // 상대 경로인 경우 storage_path 기준
        return storage_path($path);
    }

    /**
     * 요청/응답 로깅
     */
    private function logRequest(array $message, int $statusCode, array $response): void
    {
        if (! config('fcm.logging.enabled', true)) {
            return;
        }

        $channel = config('fcm.logging.channel', 'stack');
        $token = $message['message']['token'] ?? 'unknown';
        $maskedToken = substr($token, 0, 20).'...';

        if ($statusCode >= 200 && $statusCode < 300) {
            Log::channel($channel)->info('FCM message sent', [
                'token' => $maskedToken,
                'status' => $statusCode,
                'message_name' => $response['name'] ?? null,
            ]);
        } else {
            Log::channel($channel)->warning('FCM message failed', [
                'token' => $maskedToken,
                'status' => $statusCode,
                'error' => $response['error'] ?? $response,
            ]);
        }
    }

    /**
     * 에러 로깅
     */
    private function logError(array $message, \Exception $e): void
    {
        if (! config('fcm.logging.enabled', true)) {
            return;
        }

        $channel = config('fcm.logging.channel', 'stack');
        $token = $message['message']['token'] ?? 'unknown';
        $maskedToken = substr($token, 0, 20).'...';

        Log::channel($channel)->error('FCM send exception', [
            'token' => $maskedToken,
            'error' => $e->getMessage(),
            'trace' => $e->getTraceAsString(),
        ]);
    }

    /**
     * 채널 ID에 따른 사운드 파일명 반환
     */
    private function getSoundForChannel(string $channelId): string
    {
        return match ($channelId) {
            'push_vendor_register' => 'push_vendor_register',
            'push_approval_request' => 'push_approval_request',
            'push_income' => 'push_income',
            'push_sales_order' => 'push_sales_order',
            'push_purchase_order' => 'push_purchase_order',
            'push_contract' => 'push_contract',
            default => 'push_default',
        };
    }
}
