<?php

namespace App\Libs\Services;


use App\Models\User;
use App\Exceptions\ApiHttpException;
use App\Models\Collection;
use App\Models\Exhibition;
use App\Models\Location;
use App\Models\NewsItem;
use App\Services\UserDataService;
use Illuminate\Support\Collection as SupportCollection;
use Kreait\Firebase\Messaging;
use Kreait\Firebase\Messaging\AppInstance;
use Kreait\Firebase\Messaging\CloudMessage;
use Kreait\Firebase\Messaging\Topic;
use Kreait\Firebase\Messaging\TopicSubscription;
use Kreait\Firebase\Messaging\TopicSubscriptions;
use Throwable;
use Illuminate\Support\Facades\Log;

class FcmService
{

    private Messaging $messaging;
    private UserDataService $userDataService;

    public const TOPIC_NEW_EXHIBITIONS = 'new_exhibitions';
    public const TOPIC_NEW_COLLECTIONS = 'new_collections';
    public const TOPIC_NEW_LOCATIONS   = 'new_locations';
    public const TOPIC_NEW_NEWS_ITEM   = 'new_news_item';
    public const TOPIC_WEEKLY_OPENINGS = 'weekly_openings';


    public function __construct(UserDataService $userDataService)
    {
        $this->messaging       = app('firebase.messaging');
        $this->userDataService = $userDataService;
    }


    /**
     * Gets a list of translated allowed topics
     * @return array
     */
    public function getFcmTopics(): array
    {
        return [
            static::TOPIC_NEW_EXHIBITIONS => trans('fcm.topics.' . static::TOPIC_NEW_EXHIBITIONS . '.topic_name'),
            static::TOPIC_NEW_COLLECTIONS => trans('fcm.topics.' . static::TOPIC_NEW_COLLECTIONS . '.topic_name'),
            static::TOPIC_NEW_LOCATIONS   => trans('fcm.topics.' . static::TOPIC_NEW_LOCATIONS . '.topic_name'),
            static::TOPIC_NEW_NEWS_ITEM   => trans('fcm.topics.' . static::TOPIC_NEW_NEWS_ITEM . '.topic_name'),
            static::TOPIC_WEEKLY_OPENINGS => trans('fcm.' . static::TOPIC_WEEKLY_OPENINGS . '.topic_name'),
        ];
    }

    /**
     * Gets the correct topic for a model
     * @return string
     */
    public static function getFcmTopicFromModel($model): string
    {
        $topics     = [
            Exhibition::class => static::TOPIC_NEW_EXHIBITIONS,
            Collection::class => static::TOPIC_NEW_COLLECTIONS,
            Location::class   => static::TOPIC_NEW_LOCATIONS,
            NewsItem::class   => static::TOPIC_NEW_NEWS_ITEM,
        ];
        $modelClass = get_class($model);
        return isset($topics[$modelClass]) ? $topics[$modelClass] : '';
    }


    /**
     * Subscribes a user to a specific topic in FCM according to the defined language
     * @param string $topic
     * @param User $user
     * @throws ApiHttpException
     * 
     * @return bool
     */
    public function subscribeToTopic(string $topic, User $user): bool
    {

        $devices = $user->devices;
        $topics  = $this->getFcmTopics();

        if (!isset($topics[$topic])) {
            throw new ApiHttpException(500, 'invalid_fcm_topic');
        }

        //adjust the topic to the correct user language to send to FCM
        $lang         = !empty($user->language_id) ? $user->language_id : 'pt';
        $langFcmTopic = $topic . '_' . $lang;

        $subscriptionLocallyStored = false;
        foreach ($devices as $device) {

            if (empty($device->fcm_token)) {
                continue;
            }
            $deviceToken = $device->fcm_token;
            $res         = $this->messaging->subscribeToTopic($langFcmTopic, $device->fcm_token);

            if (!$subscriptionLocallyStored && isset($res[$langFcmTopic][$deviceToken]) && $res[$langFcmTopic][$deviceToken] === 'OK') {
                $this->userDataService->createUserFcmSubscription($user, $topic);
                $subscriptionLocallyStored = true;
            }
        }

        return $subscriptionLocallyStored;
    }



    /**
     * Unsubscribes a user to a specific topic in FCM according to the defined language
     * @param User $user
     * @param string $topic
     * @throws ApiHttpException
     * 
     * @return bool
     */
    public function unsubscribeToTopic(User $user, string $topic): bool
    {

        $devices             = $user->devices;
        $availableTopics     = $this->getFcmTopics();
        $tokensToUnsubscribe = [];
        $langCodes           = getAvailableLanguageCodes();
        $topicsToUnsubscribe = [];

        if (!isset($availableTopics[$topic])) {
            throw new ApiHttpException(500, 'invalid_fcm_topic');
        }

        //create the structure for the topics in all languages
        foreach ($langCodes as $langCode) {
            $topicsToUnsubscribe[] = $topic . '_' . $langCode;
        }


        foreach ($devices as $device) {

            if (empty($device->fcm_token)) {
                continue;
            }

            $tokensToUnsubscribe[] = $device->fcm_token;
        }


        //unsubscribe to the topics in FCM
        $res                      = $this->messaging->unsubscribeFromTopics($topicsToUnsubscribe, $tokensToUnsubscribe);
        $unsubscribed             = false;
        if (count($res) > 0) {
            foreach ($res as $tokenResult) {
                foreach ($tokensToUnsubscribe as $tokenToUnsubscribe) {

                    if (isset($tokenResult[$tokenToUnsubscribe]) && $tokenResult[$tokenToUnsubscribe] === 'OK') {
                        $unsubscribed = true;
                    }
                }
            }
        }

        return $unsubscribed;
    }


    /**
     * Subscribes a user to a group of topics in FCM according to the defined language in the user
     * @param User $user
     * @param array $topics
     * @throws ApiHttpException
     */
    public function subscribeToTopicsInFCM(User $user, array $topics)
    {

        $devices       = $user->devices;
        $allowedTopics = $this->getFcmTopics();
        $diff          = array_diff($topics, array_keys($allowedTopics));

        if (!empty($diff)) {
            throw new ApiHttpException(500, 'invalid_fcm_topic');
        }


        //clean slate of all the topics for this user
        $this->unsubscribeToAllTopics($user);


        //adjust the topic to the correct user language
        $lang = !empty($user->language_id) ? $user->language_id : 'pt';
        $langTopics = [];
        foreach ($topics as $idx => $topic) {
            $langTopic = $topic . '_' . $lang;
            $langTopics[] = $langTopic;
        }

        $deviceTokens = [];
        foreach ($devices as $device) {
            if (empty($device->fcm_token)) {
                continue;
            }
            $deviceTokens[] = $device->fcm_token;
        }
        if (count($deviceTokens) > 0 && count($langTopics) > 0) {
            try {
                $resSubs = $this->messaging->subscribeToTopics($langTopics, $deviceTokens);
            } catch (Throwable $t) {
                Log::error('Error in subscribing topics: ' . $t->getMessage());
            }
        }
    }

    /**
     * Unsubscribes a user from all topics
     * @param User $user
     */
    public function unsubscribeToAllTopics(User $user): array
    {
        $devices      = $user->devices;
        $res          = [];
        $deviceTokens = [];
        foreach ($devices as $device) {
            $deviceToken    = $device->fcm_token;
            if (empty($deviceToken)) {
                continue;
            }
            $deviceTokens[] = $deviceToken;
        }
        if (count($deviceTokens) > 0) {
            $res = $this->messaging->unsubscribeFromAllTopics($deviceTokens);

            //check if the tokens were recognized by FCM
            //if not we need to remove them from the database

            if (count($res) > 0) {
                $invalidTokens = [];
                foreach ($res as $token => $info) {
                    //check if the result for this token is an array
                    //if it is the token is valid
                    //otherwise we need to remove them frem the tokens database
                    if (!is_array($info)) {
                        $invalidTokens[] = $token;
                    }
                }

                //check for invalid Tokens
                if (count($invalidTokens) > 0) {
                    $this->userDataService->clearInvalidTokensFromDatabase($user, $invalidTokens);
                }
            }
        }



        return $res;
    }

    /**
     * Sends a message to a specific FCM topic
     * @param array $messageContents
     * 
     * @return ?string the message identifier on FCM or null
     */
    public function sendMessageToTopic(array $messageContents): ?string
    {
        $message = CloudMessage::fromArray($messageContents);
        $resSend = $this->messaging->send($message);
        return  isset($resSend['name']) ? $resSend['name'] : null;
    }


    /**
     * Gets the topics for a token
     * @param string $token
     * 
     * @return array
     */
    public function getTopicsFromFirebase(string $token): array
    {
        /**@var AppInstance $appInstance */
        $appInstance   = $this->messaging->getAppInstance($token);
        /**@var TopicSubscriptions $subscriptions */
        $subscriptions = $appInstance->topicSubscriptions();
        $localTopics   = [];
        $topics        = [];

        foreach ($subscriptions as $subscription) {
            /**@var  TopicSubscription $subscription */
            /**@var Topic $topic */
            $topic    = $subscription->topic();
            $topics[] = $topic->value();
        }

        //the topics stored in FCM are localized, we need to convert them to the general topic names
        $langCodes = getAvailableLanguageCodes();

        foreach ($langCodes as $langCode) {
            foreach ($topics as $topic) {

                if (str_ends_with($topic, '_' . $langCode)) {
                    $localTopics[] = rtrim($topic, '_' . $langCode);
                }
            }
        }

        return $localTopics;
    }


    /**
     * Gets the structure of a topic message to be sent
     * @param string $topic
     * @param string $title
     * @param string $body
     * @param array $data 
     * 
     * @return array
     */
    public function getTopicMessageStructure(string $topic, string $title, string $body, array $data = []): array
    {

        return [
            'topic'        => $topic,
            'notification' => [
                'title' => $title,
                'body'  => $body,
            ],
            'data'         => $data,
        ];
    }


    /**
     * Gets the structure of a token message to be sent
     * @param string $title
     * @param string $body
     * @param array $data 
     * 
     * @return array
     */
    public function getTokenMessageStructure(string $title, string $body, array $data = []): array
    {

        return [
            'notification' => [
                'title' => $title,
                'body'  => $body,
            ],
            'data'         => $data,
        ];
    }


    /**
     * Send a message to a group of tokens
     * @param array $messageContents
     * @param array $tokens the tokens to send the message To
     * 
     * @return array of MulticastSendReports with information for each token sent
     * 
     */
    public function sendMessageToTokens(array $messageContents, array $tokens): ?array
    {

        $result  = [];
        $message = CloudMessage::fromArray($messageContents);

        //sending messages to tokan has a limit of 500 tokens
        //we will need to batch the tokens groups of 500

        $tokenCollection = collect($tokens);

        foreach ($tokenCollection->chunk(500) as $tokensToSendCol) {
            /**@var SupportCollection $tokensToSendCol */
            $tokensToSend = $tokensToSendCol->toArray();
            $resSend      = $this->messaging->sendMulticast($message, $tokensToSend);
            $result[]     = $resSend;
        }

        return $result;
    }



    /**
     * Checks for database stored FCM topic subscriptions and resubscribes them
     */
    public function restoreFcmTopicSubscriptionsFromDatabase($user)
    {

        $devices = $user->devices;
        $topics  = [];
        foreach ($devices as $device) {
            if ($device->fcm_token && $user->fcmSubscriptions->count() > 0) {
                $topics = array_map(function ($item) {
                    return $item['topic'];
                }, $user->fcmSubscriptionTopics->toArray());
            }
        }
        if (count($topics) > 0) {
            try {
                $this->subscribeToTopicsInFCM($user, $topics);
            } catch (Throwable $t) {
                Log::error($t);
            }
        }
    }


    /**
     * Gets the config entry for the backoffice control field for sending push notifications
     */
    public static function getConfigForSendPushNotifications($name, $topicName): array
    {

        return [
            "name"          => "send_push_messages",
            "label"         => "Send Push Messages on create",
            "type"          => "checkBox",
            "saveField"     => FALSE,
            'createOnly'    => TRUE,
            "info"          => "When creating a new " . $name . " a push notification will be sent to the users that subscribed the " . $topicName . " topic",
        ];
    }


    /**
     * Checks if the FCM notifications sending is enabled
     */
    public static function areFcmNotificationsActive(): bool
    {
        $res = config('api.fcm_notifications_enabled');

        if (!$res) {
            Log::alert(__METHOD__ . ' - An atemp to send a FCM notification was made but the feature is disabled.');
        }

        return $res;
    }

    /**
     * Gets all the tipics from a user across devices
     * @param User $user
     * @return array
     */
    public function getTopicsDataForUser(User $user): array
    {

        $devices = $user->devices;
        $topics  = [];

        foreach ($devices as $device) {
            if (empty($device->fcm_token)) {
                continue;
            }
            try {
                $tokenTopics = $this->getTopicsFromFirebase($device->fcm_token);;
                $topics      = array_merge($topics, $tokenTopics);
            } catch (Throwable $t) {
                Log::error($t);
                continue;
            }
        }

        return $topics;
    }



    /**
     * Subscribes token to a specific topic in FCM 
     * @param string $token
     * @param string $topic
     * 
     * @return bool
     */
    public function subscribeTokenToTopic(string $token, string $topic): bool
    {

        $res = $this->messaging->subscribeToTopic($topic, $token);
        if (isset($res[$topic][$token]) && $res[$topic][$token] === 'OK') {
            return true;
        }
        return false;
    }


    /**
     * Unsubscribes a token from a topic
     * @param string $token
     * @param string $topic
     * 
     * @return bool
     */
    public function unsubscribeTokenFromTopic(string $token, string $topic): bool
    {

        $res = $this->messaging->unsubscribeFromTopic($topic, $token);
        if (isset($res[$topic][$token]) && $res[$topic][$token] === 'OK') {
            return true;
        }
        return false;
    }
}
