<?php
defined('BASEPATH') or exit('No direct script access allowed');

use Minishlink\WebPush\Subscription;
use Minishlink\WebPush\WebPush;

class Push_service
{
    private $CI;
    private $enabled = false;
    private $config = array();

    public function __construct()
    {
        $this->CI =& get_instance();
        $this->CI->config->load('push_notifications');
        $this->CI->load->model('m_push_notification');

        $this->config = array(
            'enabled' => (bool) $this->CI->config->item('push_enabled'),
            'subject' => (string) $this->CI->config->item('push_vapid_subject'),
            'public_key' => (string) $this->CI->config->item('push_vapid_public_key'),
            'private_key' => (string) $this->CI->config->item('push_vapid_private_key'),
            'title' => (string) $this->CI->config->item('push_default_title'),
            'icon' => (string) $this->CI->config->item('push_default_icon'),
            'badge' => (string) $this->CI->config->item('push_default_badge'),
            'url' => (string) $this->CI->config->item('push_default_url'),
            'ttl' => (int) $this->CI->config->item('push_ttl'),
            'urgency' => (string) $this->CI->config->item('push_urgency'),
            'timeout' => (int) $this->CI->config->item('push_timeout'),
            'disable_after_failures' => (int) $this->CI->config->item('push_disable_after_failures')
        );

        $this->enabled = $this->config['enabled']
            && $this->config['subject'] !== ''
            && $this->config['public_key'] !== ''
            && $this->config['private_key'] !== '';
    }

    public function getPublicKey()
    {
        return $this->config['public_key'];
    }

    public function isEnabled()
    {
        return $this->enabled;
    }

    public function sendToUser($userId, $title, $body, array $data = array())
    {
        $result = array('sent' => 0, 'failed' => 0, 'skipped' => 0);

        if (!$this->enabled || !$this->runtimeIsAvailable()) {
            $result['skipped'] = 1;
            return $result;
        }

        $subscriptions = $this->CI->m_push_notification->getActiveSubscriptions((int) $userId);
        if (empty($subscriptions)) {
            $result['skipped'] = 1;
            return $result;
        }

        $autoload = APPPATH . 'third_party/webpush/vendor/autoload.php';
        if (!is_file($autoload)) {
            log_message('error', 'Browser push skipped because the Web Push autoloader is missing.');
            $result['skipped'] = 1;
            return $result;
        }

        require_once $autoload;

        $targetUrl = isset($data['url']) ? (string) $data['url'] : $this->config['url'];
        $event = isset($data['event']) ? (string) $data['event'] : 'notification';
        $tag = isset($data['tag'])
            ? (string) $data['tag']
            : 'pluscash-' . $event . '-' . substr(hash('sha256', $body . microtime(true)), 0, 16);

        $payload = json_encode(array(
            'title' => $title !== '' ? (string) $title : $this->config['title'],
            'body' => (string) $body,
            'icon' => $this->config['icon'],
            'badge' => $this->config['badge'],
            'tag' => $tag,
            'renotify' => true,
            'url' => $targetUrl,
            'data' => array_merge($data, array('url' => $targetUrl, 'event' => $event))
        ), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);

        if ($payload === false) {
            log_message('error', 'Browser push skipped because its payload could not be encoded.');
            $result['skipped'] = 1;
            return $result;
        }

        $auth = array(
            'VAPID' => array(
                'subject' => $this->config['subject'],
                'publicKey' => $this->config['public_key'],
                'privateKey' => $this->config['private_key']
            )
        );
        $defaultOptions = array(
            'TTL' => max(0, $this->config['ttl']),
            'urgency' => in_array($this->config['urgency'], array('very-low', 'low', 'normal', 'high'), true)
                ? $this->config['urgency']
                : 'high',
            'batchSize' => 20
        );

        try {
            $webPush = new WebPush(
                $auth,
                $defaultOptions,
                max(1, $this->config['timeout']),
                array('connect_timeout' => min(3, max(1, $this->config['timeout'])))
            );
            $webPush->setReuseVAPIDHeaders(true);

            $subscriptionMap = array();
            foreach ($subscriptions as $row) {
                try {
                    $subscription = Subscription::create(array(
                        'endpoint' => $row['endpoint'],
                        'keys' => array(
                            'p256dh' => $row['public_key'],
                            'auth' => $row['auth_token']
                        ),
                        'contentEncoding' => $row['content_encoding'] ?: 'aes128gcm'
                    ));
                    $webPush->queueNotification($subscription, $payload);
                    $subscriptionMap[hash('sha256', $row['endpoint'])] = (int) $row['id'];
                } catch (Throwable $exception) {
                    $this->CI->m_push_notification->markFailure(
                        (int) $row['id'],
                        max(1, $this->config['disable_after_failures'])
                    );
                    $result['failed']++;
                    log_message('error', 'Invalid browser push subscription: ' . $exception->getMessage());
                }
            }

            foreach ($webPush->flush() as $report) {
                $endpoint = method_exists($report, 'getEndpoint')
                    ? (string) $report->getEndpoint()
                    : (string) $report->getRequest()->getUri();
                $subscriptionId = isset($subscriptionMap[hash('sha256', $endpoint)])
                    ? $subscriptionMap[hash('sha256', $endpoint)]
                    : 0;

                if ($report->isSuccess()) {
                    if ($subscriptionId > 0) {
                        $this->CI->m_push_notification->markSuccess($subscriptionId);
                    }
                    $result['sent']++;
                    continue;
                }

                if ($subscriptionId > 0) {
                    if ($report->isSubscriptionExpired()) {
                        $this->CI->m_push_notification->deleteSubscription($subscriptionId);
                    } else {
                        $this->CI->m_push_notification->markFailure(
                            $subscriptionId,
                            max(1, $this->config['disable_after_failures'])
                        );
                    }
                }
                $result['failed']++;
                log_message('error', 'Browser push delivery failed: ' . $report->getReason());
            }
        } catch (Throwable $exception) {
            log_message('error', 'Browser push could not be sent: ' . $exception->getMessage());
            $result['failed']++;
        }

        return $result;
    }

    private function runtimeIsAvailable()
    {
        foreach (array('curl', 'json', 'mbstring', 'openssl') as $extension) {
            if (!extension_loaded($extension)) {
                log_message('error', 'Browser push skipped because PHP extension is missing: ' . $extension);
                return false;
            }
        }

        return true;
    }
}
