// Version: 1.2.0 | Created: 2026-04-05 | Updated: 2026-07-04
// Web implementation: uses JitsiMeetExternalAPI via dart:js_interop.
// This file is web-only — guarded by the Flutter web build.
// The external_api.js script is lazy-loaded on first use — not in index.html.
// This prevents privacy browsers (Brave, Mullvad) from blocking the app
// at startup due to a cross-origin script from conf.m8chat.au.

import 'dart:async';
import 'dart:js_interop';
import 'dart:js_interop_unsafe';
import 'dart:ui_web' as ui_web;

import 'package:web/web.dart' as web;

import '../../../core/config/app_config.dart';

/// Manages a single Jitsi iframe session via the JitsiMeetExternalAPI.
class JitsiWebService {
  JitsiWebService._();
  static final instance = JitsiWebService._();

  JSObject? _api;
  bool _viewRegistered = false;
  bool _scriptLoaded = false;
  bool _scriptLoading = false;

  /// Unique view type for HtmlElementView.
  static const String viewType = 'jitsi-meet-container';

  /// Lazy-loads the JitsiMeetExternalAPI script from conf.m8chat.au.
  /// Returns true if the script loaded successfully, false otherwise.
  Future<bool> _ensureScriptLoaded() async {
    if (_scriptLoaded) return true;
    if (_scriptLoading) {
      // Wait for in-flight load to finish.
      for (var i = 0; i < 50; i++) {
        await Future<void>.delayed(const Duration(milliseconds: 100));
        if (_scriptLoaded) return true;
      }
      return false;
    }
    _scriptLoading = true;

    final completer = Completer<bool>();
    final script =
        web.document.createElement('script') as web.HTMLScriptElement;
    script.src = 'https://${AppConfig.jitsiDomain}/external_api.js';
    script.async = true;
    script.onload = (web.Event event) {
      _scriptLoaded = true;
      _scriptLoading = false;
      completer.complete(true);
    }.toJS;
    script.onerror = (web.Event event) {
      _scriptLoading = false;
      completer.complete(false);
    }.toJS;
    web.document.head?.append(script);

    return completer.future;
  }

  /// Registers the platform view factory (once).
  void ensureViewRegistered() {
    if (_viewRegistered) return;
    ui_web.platformViewRegistry.registerViewFactory(
      viewType,
      (int viewId, {Object? params}) {
        final div = web.document.createElement('div') as web.HTMLDivElement;
        div.id = 'jitsi-container-$viewId';
        div.style
          ..width = '100%'
          ..height = '100%';
        return div;
      },
    );
    _viewRegistered = true;
  }

  /// Starts a Jitsi meeting inside the container div created by the platform view.
  /// Call this AFTER the HtmlElementView has been scheduled for mounting
  /// (e.g. in a post-frame callback). Lazy-loads the Jitsi script on first
  /// call and waits for the platform-view container div to appear — the
  /// post-frame callback can run before the HtmlElementView factory has
  /// inserted the div, which previously made this method bail out silently
  /// and leave the meeting screen blank.
  /// Returns true if the meeting iframe was created.
  Future<bool> joinMeeting({
    required String roomName,
    String? jwt,
    String? displayName,
    String? avatarUrl,
    void Function()? onReadyToClose,
  }) async {
    dispose(); // tear down any previous meeting

    // Lazy-load the external_api.js script if not already present.
    final loaded = await _ensureScriptLoaded();
    if (!loaded) {
      web.console.warn('[Jitsi] external_api.js failed to load'.toJS);
      return false;
    }

    // Wait for the container div — the platform view may not be in the DOM
    // yet when the post-frame callback fires.
    web.Node? parentNode;
    for (var i = 0; i < 50; i++) {
      final containers =
          web.document.querySelectorAll('[id^="jitsi-container-"]');
      if (containers.length > 0) {
        parentNode = containers.item(containers.length - 1);
        break;
      }
      await Future<void>.delayed(const Duration(milliseconds: 100));
    }
    if (parentNode == null) {
      web.console.warn('[Jitsi] container div never appeared'.toJS);
      return false;
    }

    // Build the options as a real JS object. Passing a DOM node through
    // Map.jsify() is not supported and threw at runtime in the dart2js build.
    final configOverwrite = JSObject()
      ..setProperty('startAudioMuted'.toJS, 0.toJS)
      ..setProperty('startVideoMuted'.toJS, 0.toJS)
      ..setProperty('disableDeepLinking'.toJS, true.toJS)
      ..setProperty('prejoinPageEnabled'.toJS, true.toJS);

    final interfaceConfigOverwrite = JSObject()
      ..setProperty('SHOW_CHROME_EXTENSION_BANNER'.toJS, false.toJS);

    final options = JSObject()
      ..setProperty('roomName'.toJS, roomName.toJS)
      ..setProperty('parentNode'.toJS, parentNode as JSAny)
      ..setProperty('width'.toJS, '100%'.toJS)
      ..setProperty('height'.toJS, '100%'.toJS)
      ..setProperty('configOverwrite'.toJS, configOverwrite)
      ..setProperty('interfaceConfigOverwrite'.toJS, interfaceConfigOverwrite);

    if (jwt != null && jwt.isNotEmpty) {
      options.setProperty('jwt'.toJS, jwt.toJS);
    }

    if (displayName != null && displayName.isNotEmpty) {
      final userInfo = JSObject()
        ..setProperty('displayName'.toJS, displayName.toJS);
      if (avatarUrl != null) {
        userInfo.setProperty('avatarUrl'.toJS, avatarUrl.toJS);
      }
      options.setProperty('userInfo'.toJS, userInfo);
    }

    try {
      _api = _createJitsiApi(AppConfig.jitsiDomain.toJS, options);
    } catch (e) {
      web.console.error('[Jitsi] JitsiMeetExternalAPI threw: $e'.toJS);
      return false;
    }

    if (onReadyToClose != null && _api != null) {
      _addEventListener(_api!, 'readyToClose'.toJS, () {
        onReadyToClose();
      }.toJS);
    }
    return true;
  }

  /// Cleans up the Jitsi iframe.
  void dispose() {
    if (_api != null) {
      _disposeApi(_api!);
      _api = null;
    }
  }
}

/// Binding for the JitsiMeetExternalAPI ES6 class.
/// The external constructor compiles to `new JitsiMeetExternalAPI(...)`.
/// (A plain external function binding calls it WITHOUT `new`, which throws
/// "Class constructor cannot be invoked without 'new'" — the original cause
/// of the blank meeting screen.)
@JS('JitsiMeetExternalAPI')
extension type _JitsiApi._(JSObject _) implements JSObject {
  external _JitsiApi(JSString domain, JSObject options);
  external void addEventListener(JSString event, JSFunction callback);
  external void dispose();
}

JSObject _createJitsiApi(JSString domain, JSObject options) =>
    _JitsiApi(domain, options);

void _addEventListener(JSObject api, JSString event, JSFunction callback) {
  (api as _JitsiApi).addEventListener(event, callback);
}

void _disposeApi(JSObject api) {
  (api as _JitsiApi).dispose();
}
