<?php
namespace EasyChat\Controller;

use EasyChat\Library\CentralGatewayClient;
use EasyChat\Library\CustomerWebPushNotifier;
use EasyChat\Model\Session;
use EasyChat\Model\Setting;

class WidgetPush extends Base {

    protected $layouts = '';

    public function index() {
        $this->jsonError('endpoint_not_found', 404);
    }

    public function manifest() {
        $model_setting = new Setting($this->registry);
        if (!$this->sendWidgetCorsHeaders($model_setting, 'GET, OPTIONS', 'Content-Type')) {
            $this->jsonError('unauthorized_widget_domain', 403);
        }

        header('Content-Type: application/manifest+json; charset=utf-8');
        echo json_encode([
            'name' => 'EasyChat',
            'short_name' => 'EasyChat',
            'id' => '/?easychat=widget',
            'description' => 'EasyChat website chat notifications',
            'start_url' => '/#easychat-open',
            'scope' => '/',
            'display' => 'standalone',
            'background_color' => '#ffffff',
            'theme_color' => '#6366f1',
            'icons' => [
                [
                    'src' => 'images/icon-192.png',
                    'sizes' => '192x192',
                    'type' => 'image/png',
                    'purpose' => 'any',
                ],
                [
                    'src' => 'images/icon-512.png',
                    'sizes' => '512x512',
                    'type' => 'image/png',
                    'purpose' => 'any maskable',
                ],
            ],
        ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        exit;
    }

    public function serviceWorker() {
        $model_setting = new Setting($this->registry);
        if (!$this->sendWidgetCorsHeaders($model_setting, 'GET, OPTIONS', 'Content-Type')) {
            $this->jsonError('unauthorized_widget_domain', 403);
        }

        header('Content-Type: application/javascript; charset=utf-8');
        header('Service-Worker-Allowed: ./');
        header('Cache-Control: no-cache, no-store, must-revalidate');
        echo <<<'JS'
function easyChatSafeOpenUrl(value) {
    try {
        var target = new URL(value || '/#easychat-open', self.location.origin);
        if (target.origin !== self.location.origin) {
            return new URL('/#easychat-open', self.location.origin).href;
        }
        target.hash = target.hash || '#easychat-open';
        return target.href;
    } catch (e) {
        return new URL('/#easychat-open', self.location.origin).href;
    }
}

function easyChatPostResume(client, reason) {
    try {
        if (client && 'postMessage' in client) {
            client.postMessage({
                type: 'EASYCHAT_WIDGET_RESUME',
                reason: reason || 'notificationclick'
            });
        }
    } catch (e) {}
}

function easyChatSamePage(clientUrl, targetUrl) {
    return clientUrl.origin === targetUrl.origin
        && clientUrl.pathname === targetUrl.pathname
        && clientUrl.search === targetUrl.search;
}

function easyChatFocusClient(client) {
    try {
        if (client && 'focus' in client) {
            return client.focus().catch(function() {
                return client;
            });
        }
    } catch (e) {}
    return Promise.resolve(client || null);
}

function easyChatNavigateFocusAndWake(client, url) {
    easyChatPostResume(client, 'notificationclick');
    var targetClientPromise = Promise.resolve(client);
    try {
        if (client && 'navigate' in client) {
            targetClientPromise = client.navigate(url).then(function(navigated) {
                return navigated || client;
            }).catch(function() {
                return client;
            });
        }
    } catch (e) {
        targetClientPromise = Promise.resolve(client);
    }
    return targetClientPromise.then(function(targetClient) {
        easyChatPostResume(targetClient, 'notificationclick');
        return easyChatFocusClient(targetClient);
    });
}

self.addEventListener('push', function(event) {
    var payload = {};
    if (event.data) {
        try {
            payload = event.data.json();
        } catch (e) {
            payload = { body: event.data.text() };
        }
    }
    var domain = payload.domain || self.location.host;
    var title = payload.title || 'Новое сообщение в чате';
    var body = payload.body || payload.preview || ('Для вас новое сообщение на ' + domain);
    var options = {
        body: body,
        icon: 'images/icon-192.png',
        badge: 'images/icon-192.png',
        tag: payload.chat_ref_hash ? 'easychat-customer-' + payload.chat_ref_hash : 'easychat-customer-push',
        renotify: true,
        data: {
            url: easyChatSafeOpenUrl(payload.url),
            chat_ref_hash: payload.chat_ref_hash || ''
        }
    };
    event.waitUntil(self.registration.showNotification(title, options));
});

self.addEventListener('notificationclick', function(event) {
    event.notification.close();
    var url = easyChatSafeOpenUrl(event.notification.data && event.notification.data.url);
    event.waitUntil(clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function(clientList) {
        var targetUrl = new URL(url);
        for (var exactIndex = 0; exactIndex < clientList.length; exactIndex++) {
            try {
                if (easyChatSamePage(new URL(clientList[exactIndex].url), targetUrl)) {
                    return easyChatNavigateFocusAndWake(clientList[exactIndex], url);
                }
            } catch (e) {}
        }
        for (var i = 0; i < clientList.length; i++) {
            var client = clientList[i];
            try {
                var clientUrl = new URL(client.url);
                if (clientUrl.origin === targetUrl.origin) {
                    return easyChatNavigateFocusAndWake(client, url);
                }
            } catch (e) {}
        }
        if (clients.openWindow) {
            return clients.openWindow(url).then(function(openedClient) {
                easyChatPostResume(openedClient, 'notificationclick');
                return easyChatFocusClient(openedClient);
            });
        }
    }));
});
JS;
        exit;
    }

    public function config() {
        $model_setting = new Setting($this->registry);
        if ($this->handleWidgetCorsPreflight($model_setting)) {
            return;
        }
        if (!$this->sendWidgetCorsHeaders($model_setting)) {
            $this->jsonError('unauthorized_widget_domain', 403);
        }

        header('Content-Type: application/json; charset=utf-8');

        $nonce = $this->issueNonce($model_setting);
        $central_api = CentralGatewayClient::normalizeBaseUrl($model_setting->get('chat_central_api'));
        if ($central_api === '') {
            $this->jsonOut([
                'success' => 1,
                'enabled' => false,
                'code' => 'central_api_empty',
                'nonce' => $nonce,
            ]);
        }

        if (!empty(\EasyChat\Library\RequestContext::session()['manager_id'])) {
            $this->jsonOut([
                'success' => 1,
                'enabled' => false,
                'code' => 'operator_session',
                'nonce' => $nonce,
            ]);
        }

        $body = CentralGatewayClient::buildSignedInstallBody($model_setting, $this->domain());
        $result = CentralGatewayClient::postJson($central_api, '/api/v1/auth/domain-status', $body, 5, $this->registry);
        $json = is_array($result['json'] ?? null) ? $result['json'] : [];
        $feature = [];
        if (isset($json['features']) && is_array($json['features']) && isset($json['features']['webpush']) && is_array($json['features']['webpush'])) {
            $feature = $json['features']['webpush'];
        }

        $service_worker_version = defined('EASYCHAT_VERSION') ? (string)EASYCHAT_VERSION : '1.0.0';

        $this->jsonOut([
            'success' => 1,
            'enabled' => !empty($feature['enabled']),
            'paid_required' => !empty($feature['paid_required']),
            'code' => !empty($feature['enabled']) ? '' : (string)($json['code'] ?? 'webpush_unavailable'),
            'vapid_key_id' => (string)($feature['vapid_key_id'] ?? ''),
            'vapid_public_key' => (string)($feature['vapid_public_key'] ?? ''),
            'service_worker_url' => 'index.php?route=widget_push/serviceWorker&ver=' . rawurlencode($service_worker_version),
            'service_worker_scope' => 'widget-push/',
            'manifest_url' => 'index.php?route=widget_push/manifest',
            'nonce' => $nonce,
        ]);
    }

    public function subscribe() {
        $model_setting = new Setting($this->registry);
        if ($this->handleWidgetCorsPreflight($model_setting)) {
            return;
        }
        if (!$this->sendWidgetCorsHeaders($model_setting)) {
            $this->jsonError('unauthorized_widget_domain', 403);
        }

        header('Content-Type: application/json; charset=utf-8');
        $input = $this->readJsonInput();
        $this->verifyNonce((string)($input['nonce'] ?? ''));

        if (!empty(\EasyChat\Library\RequestContext::session()['manager_id'])) {
            $this->jsonError('operator_session', 403);
        }

        $session_id = trim((string)($input['session_id'] ?? ''));
        if ($session_id === '') {
            $this->jsonError('missing_session_id', 422);
        }

        $subscription = isset($input['subscription']) && is_array($input['subscription']) ? $input['subscription'] : [];
        if (empty($subscription['endpoint'])) {
            $this->jsonError('missing_webpush_subscription', 422);
        }

        $session_model = new Session($this->registry);
        $chat = $session_model->getSessionBySid($session_id);
        if (!$chat || (int)($chat['chat_id'] ?? 0) <= 0) {
            $this->jsonError('chat_session_not_ready', 409);
        }

        $chat_id = (int)$chat['chat_id'];
        $input_chat_id = (int)($input['chat_id'] ?? 0);
        if ($input_chat_id > 0 && $input_chat_id !== $chat_id) {
            $this->jsonError('chat_id_mismatch', 403);
        }

        $browser_session = (string)(\EasyChat\Library\RequestContext::session()['chat_session_id'] ?? '');
        if ($browser_session === '' || !hash_equals($browser_session, $session_id)) {
             
             
             
            \EasyChat\Library\RequestContext::session()['chat_session_id'] = $session_id;
        }

        $central_api = CentralGatewayClient::normalizeBaseUrl($model_setting->get('chat_central_api'));
        if ($central_api === '') {
            $this->jsonError('central_api_empty', 422);
        }

        $install_id = CentralGatewayClient::ensureInstallId($model_setting);
        $payload = [
            'domain' => $this->domain(),
            'channel' => 'webpush',
            'action' => 'upsert',
            'audience' => 'customer',
            'chat_ref_hash' => CustomerWebPushNotifier::chatRefHash($install_id, $chat_id, $session_id),
            'vapid_key_id' => (string)($input['vapid_key_id'] ?? ''),
            'subscription' => $subscription,
            'open_path' => $this->safeOpenPath((string)($input['page_path'] ?? '/')),
            'client' => [
                'app' => 'widget_customer',
                'platform' => $this->platform(),
                'browser' => $this->browser(),
            ],
        ];
        $body = $this->signedBody($model_setting, $payload);
        $result = CentralGatewayClient::postJson($central_api, '/api/v1/push/tokens', $body, 5, $this->registry);
        if ((int)$result['status'] !== 200) {
            $this->jsonError((string)($result['error_code'] ?: 'webpush_registration_failed'), (int)$result['status'] ?: 502);
        }

        $this->jsonOut(['success' => 1]);
    }

    public function unsubscribe() {
        $model_setting = new Setting($this->registry);
        if ($this->handleWidgetCorsPreflight($model_setting)) {
            return;
        }
        if (!$this->sendWidgetCorsHeaders($model_setting)) {
            $this->jsonError('unauthorized_widget_domain', 403);
        }

        header('Content-Type: application/json; charset=utf-8');
        $input = $this->readJsonInput();
        $this->verifyNonce((string)($input['nonce'] ?? ''));

        $endpoint = trim((string)($input['endpoint'] ?? ''));
        if ($endpoint === '') {
            $this->jsonError('missing_webpush_endpoint', 422);
        }

        $central_api = CentralGatewayClient::normalizeBaseUrl($model_setting->get('chat_central_api'));
        if ($central_api === '') {
            $this->jsonOut(['success' => 1]);
        }

        $payload = [
            'domain' => $this->domain(),
            'channel' => 'webpush',
            'action' => 'deactivate',
            'audience' => 'customer',
            'endpoint' => $endpoint,
        ];
        $body = $this->signedBody($model_setting, $payload);
        CentralGatewayClient::postJson($central_api, '/api/v1/push/tokens', $body, 5, $this->registry);

        $this->jsonOut(['success' => 1]);
    }

    private function verifyNonce($nonce) {
        $nonce = (string)$nonce;
        if ($nonce === '') {
            $this->jsonError('invalid_nonce', 403);
        }

        if (!empty(\EasyChat\Library\RequestContext::session()['easychat_widget_push_nonce']) && hash_equals((string)\EasyChat\Library\RequestContext::session()['easychat_widget_push_nonce'], $nonce)) {
            return;
        }

        $model_setting = new Setting($this->registry);
        if ($this->verifySignedNonce($model_setting, $nonce)) {
            return;
        }

        $this->jsonError('invalid_nonce', 403);
    }

    private function issueNonce($model_setting) {
         
         
         
         
        $issued_at = time();
        $random = bin2hex(random_bytes(16));
        $payload = 'v1:' . $issued_at . ':' . $random;
        $signature = hash_hmac('sha256', $this->nonceSigningPayload($payload), $this->nonceSecret($model_setting));
        $nonce = $payload . ':' . $signature;

        if (session_status() === PHP_SESSION_ACTIVE) {
            \EasyChat\Library\RequestContext::session()['easychat_widget_push_nonce'] = $nonce;
        }

        return $nonce;
    }

    private function verifySignedNonce($model_setting, $nonce) {
        $parts = explode(':', (string)$nonce);
        if (count($parts) !== 4 || $parts[0] !== 'v1') {
            return false;
        }

        $issued_at_raw = (string)$parts[1];
        $random = (string)$parts[2];
        $signature = (string)$parts[3];
        if (!ctype_digit($issued_at_raw) || strlen($random) !== 32 || !ctype_xdigit($random) || strlen($signature) !== 64 || !ctype_xdigit($signature)) {
            return false;
        }

        $issued_at = (int)$issued_at_raw;
        $now = time();
        if ($issued_at < $now - 7200 || $issued_at > $now + 300) {
            return false;
        }

        $payload = 'v1:' . $issued_at_raw . ':' . $random;
        $expected = hash_hmac('sha256', $this->nonceSigningPayload($payload), $this->nonceSecret($model_setting));
        return hash_equals($expected, $signature);
    }

    private function nonceSigningPayload($payload) {
        return $this->domain() . ':widget_push:' . (string)$payload;
    }

    private function nonceSecret($model_setting) {
        $license = trim((string)$model_setting->get('chat_license'));
        if ($license !== '') {
            return hash('sha256', 'license:' . $license);
        }

        return CentralGatewayClient::ensureInstallSecret($model_setting);
    }

    private function signedBody($model_setting, array $payload) {
        $payload = CentralGatewayClient::withBaseDomain($model_setting, $payload);
        $license = trim((string)$model_setting->get('chat_license'));
        $install_id = CentralGatewayClient::ensureInstallId($model_setting);
        if ($license !== '') {
            return CentralGatewayClient::signedBody($license, $install_id, $payload);
        }

        return CentralGatewayClient::signedUnlicensedBody(
            CentralGatewayClient::ensureInstallSecret($model_setting),
            $install_id,
            $payload
        );
    }

    private function readJsonInput() {
        $raw = file_get_contents('php://input');
        $decoded = json_decode(is_string($raw) ? $raw : '', true);
        return is_array($decoded) ? $decoded : [];
    }

    private function safeOpenPath($path) {
        $path = trim((string)$path);
        if ($path === '') {
            return '/';
        }
        if (preg_match('/^https?:\/\//i', $path)) {
            $parts = parse_url($path);
            $host = strtolower((string)($parts['host'] ?? ''));
            $current_host = strtolower((string)(\EasyChat\Library\RequestContext::server()['HTTP_HOST'] ?? ''));
            if ($host === '' || $current_host === '' || $host !== $current_host) {
                return '/';
            }
            $path = (string)($parts['path'] ?? '/');
            if (!empty($parts['query'])) {
                $path .= '?' . (string)$parts['query'];
            }
        }
        $hash_pos = strpos($path, '#');
        if ($hash_pos !== false) {
            $path = substr($path, 0, $hash_pos);
        }
        if ($path === '' || $path[0] !== '/' || strpos($path, '//') === 0 || strlen($path) > 512 || preg_match('/[\x00-\x1F\x7F]/', $path)) {
            return '/';
        }

        return $path;
    }

    private function domain() {
        $domain = \EasyChat\Library\RequestContext::server()['HTTP_HOST'] ?? '';
        if ($domain !== '' && function_exists('easychat_public_host')) {
            $domain = easychat_public_host($domain);
        }
        return $domain !== '' ? $domain : 'localhost';
    }

    private function platform() {
        $ua = strtolower((string)(\EasyChat\Library\RequestContext::server()['HTTP_USER_AGENT'] ?? ''));
        if (strpos($ua, 'iphone') !== false || strpos($ua, 'ipad') !== false) return 'ios';
        if (strpos($ua, 'android') !== false) return 'android';
        if (strpos($ua, 'windows') !== false) return 'windows';
        if (strpos($ua, 'mac os') !== false) return 'macos';
        return 'desktop';
    }

    private function browser() {
        $ua = strtolower((string)(\EasyChat\Library\RequestContext::server()['HTTP_USER_AGENT'] ?? ''));
        if (strpos($ua, 'edg/') !== false) return 'edge';
        if (strpos($ua, 'firefox/') !== false) return 'firefox';
        if (strpos($ua, 'chrome/') !== false || strpos($ua, 'crios/') !== false) return 'chrome';
        if (strpos($ua, 'safari/') !== false) return 'safari';
        return 'other';
    }

    private function jsonOut(array $payload, $status = 200) {
        http_response_code((int)$status);
        header('Content-Type: application/json; charset=utf-8');
        echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        exit;
    }

    private function jsonError($code, $status = 400) {
        $this->jsonOut(['success' => 0, 'error' => (string)$code, 'code' => (string)$code], $status);
    }
}
