// Version: 1.0.0 | Created: 2026-04-27
// Web Push subscription and Matrix pusher registration.
// Only active on web — no-ops on other platforms.
//
// Flow:
//  1. registerPushServiceWorker() — register push_sw.js (called at startup)
//  2. setupWebPush(client)        — called after login; requests permission,
//     subscribes to Web Push, registers the subscription as a Matrix pusher.

import 'dart:async';
import 'dart:convert';
import 'dart:js_interop';
import 'dart:js_interop_unsafe';

import 'package:flutter/foundation.dart';
import 'package:matrix/matrix.dart';
import 'package:web/web.dart' as web;

const _kVapidPublicKey =
    'BPP0AqC7qTxspQn1i0x2sPbPj3owJTTkhV256A67VUK9748NjaBfbMrQBJtI58foVSVpbk2n390NIq_w2glNjs8';

const _kAppId = 'au.m8chat.web.push';
const _kPushGatewayUrl = 'https://matrix.m8chat.au/_matrix/push/v1/notify';

/// Registers push_sw.js at startup so the browser knows the service worker
/// before the user logs in. Idempotent. No-op on non-web platforms.
Future<void> registerPushServiceWorker() async {
  if (!kIsWeb) return;
  try {
    await _registerServiceWorker();
  } catch (e) {
    debugPrint('[Push] SW pre-registration failed (non-fatal): $e');
  }
}

/// Called after login. Requests notification permission, subscribes to Web
/// Push using the VAPID public key, and registers the subscription as a
/// Matrix pusher so Synapse routes notifications through Sygnal → browser.
///
/// Idempotent — safe to call on every login. No-op on non-web platforms.
Future<void> setupWebPush(Client client) async {
  if (!kIsWeb) return;

  try {
    final sw = await _registerServiceWorker();
    if (sw == null) {
      debugPrint('[Push] SW not available — push skipped');
      return;
    }

    final permission = await _requestPermission();
    if (permission != 'granted') {
      debugPrint('[Push] Notification permission $permission — skipped');
      return;
    }

    final sub = await _subscribe(sw);
    if (sub == null) {
      debugPrint('[Push] Could not create push subscription');
      return;
    }

    await _registerPusher(client, sub);
  } catch (e) {
    debugPrint('[Push] Setup failed (non-fatal): $e');
  }
}

// ---------------------------------------------------------------------------

Future<web.ServiceWorkerRegistration?> _registerServiceWorker() async {
  if (!_pushSupported()) return null;

  try {
    final reg = await web.window.navigator.serviceWorker
        .register('/push_sw.js'.toJS, web.RegistrationOptions(scope: '/'))
        .toDart;
    debugPrint('[Push] Service worker registered: ${reg.scope}');
    return reg;
  } catch (e) {
    debugPrint('[Push] Service worker error: $e');
    return null;
  }
}

Future<String> _requestPermission() async {
  try {
    final result = await web.Notification.requestPermission().toDart;
    return result.toDart;
  } catch (_) {
    return 'denied';
  }
}

Future<web.PushSubscription?> _subscribe(
    web.ServiceWorkerRegistration reg) async {
  try {
    // Re-use existing subscription if the browser already has one.
    final existing = await reg.pushManager.getSubscription().toDart;
    if (existing != null) {
      debugPrint('[Push] Reusing existing push subscription');
      return existing;
    }

    final keyBytes = _vapidKeyToBytes(_kVapidPublicKey);
    final sub = await reg.pushManager
        .subscribe(web.PushSubscriptionOptionsInit(
          userVisibleOnly: true,
          applicationServerKey: keyBytes,
        ))
        .toDart;
    debugPrint('[Push] New subscription: ${sub.endpoint}');
    return sub;
  } catch (e) {
    debugPrint('[Push] Subscribe error: $e');
    return null;
  }
}

Future<void> _registerPusher(
    Client client, web.PushSubscription sub) async {
  final json = sub.toJSON();
  final endpoint = json.endpoint;
  if (endpoint.isEmpty) return;

  // Extract p256dh and auth keys from the subscription.
  final keysObj = json.keys;
  final p256dh = _jsObjectGet(keysObj, 'p256dh') ?? '';
  final auth = _jsObjectGet(keysObj, 'auth') ?? '';

  await client.postPusher(
    Pusher(
      pushkey: endpoint,
      kind: 'http',
      appId: _kAppId,
      appDisplayName: 'M8Chat Web',
      deviceDisplayName: 'Web browser',
      lang: 'en',
      data: PusherData(
        url: Uri.parse(_kPushGatewayUrl),
        format: 'event_id_only',
        additionalProperties: {
          'keys': {'p256dh': p256dh, 'auth': auth},
        },
      ),
    ),
  );
  debugPrint('[Push] Matrix pusher registered');
}

/// Converts a URL-safe base64 VAPID public key to a JS Uint8Array.
JSUint8Array _vapidKeyToBytes(String base64Url) {
  final padded = base64Url +
      '=' * ((4 - base64Url.length % 4) % 4);
  final standard = padded.replaceAll('-', '+').replaceAll('_', '/');
  final bytes = base64Decode(standard);
  return bytes.toJS;
}

/// Reads a string property from a JSObject via js_interop.
String? _jsObjectGet(JSObject obj, String key) {
  try {
    final val = obj.getProperty<JSAny?>(key.toJS);
    if (val == null || val.isUndefinedOrNull) return null;
    return (val as JSString).toDart;
  } catch (_) {
    return null;
  }
}

bool _pushSupported() {
  try {
    web.window.navigator.serviceWorker;
    return true;
  } catch (_) {
    return false;
  }
}
