// @ts-nocheck — Module entry point with FoundryVTT globals, no exports needed
/* global Handlebars */
/**
 * module.js — Entry point and wiring diagram for Scrying Pool.
 *
 * This file is the wiring diagram ONLY. It imports all modules, constructs them
 * with injected dependencies, and holds NO business logic.
 *
 * Initialisation order:
 *   Hooks.once('init')  → register world settings → construct FoundryAdapter
 *                       → StateStore → SocketHandler (queue+drain)
 *   Hooks.once('ready') → hydrate StateStore → probe WebRTC
 *                       → VisibilityManager → SocketHandler.setReady()
 *                       → ScryingPoolController
 *                       → AVTileAdapter → RoleRenderer → openStrip (GM only)
 *                       → VisibilityBadge (player only) — Story 1.6
 *                       → NotificationBus (all clients) — Story 2.1
 *                       → Story 2.2: DirectorsBoard (lazy, GM only)
 */

import { FoundryAdapter } from './src/foundry/FoundryAdapter.js';
import { StateStore } from './src/core/StateStore.js';
import { SocketHandler } from './src/core/SocketHandler.js';
import { VisibilityManager } from './src/core/VisibilityManager.js';
import { ScryingPoolController } from './src/core/ScryingPoolController.js';
import { ScenePresetManager } from './src/core/ScenePresetManager.js';
import { PlayerPrivacyManager } from './src/core/PlayerPrivacyManager.js';
import { PortraitFallbackHandler } from './src/core/PortraitFallbackHandler.js';
import { AVTileAdapter } from './src/ui/shared/AVTileAdapter.js';
import { RoleRenderer } from './src/ui/RoleRenderer.js';
import { VisibilityBadge } from './src/ui/player/VisibilityBadge.js';
import { NotificationBus } from './src/notifications/NotificationBus.js';
import { DirectorsBoard } from './src/ui/gm/DirectorsBoard.js';
import { ConfirmationBar } from './src/ui/gm/ConfirmationBar.js';
import { PlayerPrivacyPanelMenu, initPlayerPrivacyPanelMenu } from './src/ui/player/PlayerPrivacyPanelMenu.js';
import { initGMPlayerPrivacySelector } from './src/ui/gm/GMPlayerPrivacySelector.js';
import { GMActorMappingPanel, initGMActorMappingPanel } from './src/ui/gm/GMActorMappingPanel.js';
import { ScryingPoolCameraViews, initScryingPoolCameraViews } from './src/ui/shared/ScryingPoolCameraViews.js';
import { TokenVideoOverlay } from './src/ui/shared/TokenVideoOverlay.js';
import { FreeformLayoutManager } from './src/ui/gm/FreeformLayoutManager.js';
import { ScryingPoolSettings } from './src/ui/gm/ScryingPoolSettings.js';
import { SOCKET_EVENTS } from './src/contracts/socket-message.js';

// Factory function to create ScryingPoolSettings with roleRenderer and adapter dependencies
// Returns a class constructor (not a function) that Foundry can use for registerMenu
function initScryingPoolSettings(roleRendererRef, adapterRef) {
  return class extends ScryingPoolSettings {
    constructor(options = {}) {
      super(roleRendererRef, adapterRef, options);
    }
  };
}



// Module-level references — constructed in init hook, used across hooks
let adapter;
let stateStore;
let socketHandler;
let visibilityManager;
let scryingPoolController;
let scenePresetManager;
let playerPrivacyManager;
let portraitFallbackHandler;
let avTileAdapter;
let roleRenderer;
let visibilityBadge;
let notificationBus;
let directorsBoard;
let confirmationBar;
let tokenVideoOverlay;
let freeformLayoutManager;
/** @type {boolean} Flag to prevent duplicate scene control button addition */
let directorsBoardButtonAdded = false;

Hooks.once("init", () => {
  console.log("[ScryingPool] init — module loading");

  // Foundry sets body class from URL route. If the URL has a query string
  // (e.g. /game?presetName=ALL), the class becomes "game?presetName=ALL"
  // instead of "game", breaking body.game { display:flex } and collapsing the UI.
  const badClass = Array.from(document.body.classList).find(c => c.startsWith("game") && c !== "game");
  if (badClass) {
    document.body.classList.remove(badClass);
    document.body.classList.add("game");
  }

  // Take over Foundry's camera dock — must be set before Foundry instantiates ui.webrtc
  CONFIG.ui.webrtc = ScryingPoolCameraViews;

  // WebRTC mode setting — determines how the module handles AV integration
  // Updated for FULL REPLACEMENT architecture (hiding Foundry's dock, showing our own)
  game.settings.register("scrying-pool", "webrtcMode", {
    scope: "world",
    config: false,
    type: String,
    default: "stream-access",
    choices: {
      "stream-access": "Stream Access (full replacement - hide Foundry dock, show VVM dock with actual video)",
      "track-disable": "Track Disable (bandwidth-saving) - DEPRECATED",
      "css-fallback": "CSS Fallback (cosmetic hiding) - DEPRECATED",
      "unsupported": "Unsupported (AV not available)",
    },
  });

  // Construct adapter first — needed for settings registration
  adapter = new FoundryAdapter(game);

  // Story 1.3: register remaining world settings via adapter
  adapter.settings.register("visibilityMatrix", {
    scope: "world",
    config: false,
    type: Object,
    default: { _version: 1, matrix: {} },
    onChange: () => { stateStore?.init(); roleRenderer?.rerenderStrip(); },
  });

  adapter.settings.register("showGMSelfFeed", {
    scope: "world",
    config: true,
    type: Boolean,
    default: true,
    name: "Show GM Self Feed",
    hint: "When enabled, the GM's own camera feed is shown in the Scrying Pool strip.",
  });

  // Story 5.X: Token Video Overlay — completely optional
  adapter.settings.register("showVideoOnTokens", {
    scope: "world",
    config: true,
    type: Boolean,
    default: false,
    name: "Show Webcam Video on Tokens",
    hint: "When enabled, each player's webcam feed replaces their character token image on the active scene, masked to a circle. The video strip is automatically hidden to free screen space.",
    onChange: (val) => {
      tokenVideoOverlay?.[val ? 'enable' : 'disable']();
      if (val) {
        roleRenderer?.closeStrip();
      } else {
        roleRenderer?.openStrip();
      }
    },
  });

  // Story 5.X: GM actor-to-user mapping for token video overlay
  adapter.settings.register("userActorMapping", {
    scope: "world",
    config: false,
    type: Object,
    default: {},
  });

  // Story 5.3: Freeform layout — floating camera window positions
  adapter.settings.register("freeformLayout", {
    scope: "world",
    config: false,
    type: Object,
    default: { windows: {} },
  });

  // Story 2.1: per-user notification verbosity preference (client-scoped)
  adapter.settings.register("notificationVerbosity", {
    scope: "client",
    config: true,
    type: String,
    name: "Notification Verbosity",
    hint: "Controls which camera-state notifications you see. 'All' shows every change; 'GM Only' shows changes only to the GM and affected participant; 'Silent' suppresses all notifications except your own camera changes.",
    choices: {
      all:       "All",
      "gm-only": "GM Only",
      silent:    "Silent",
    },
    default: "all",
  });

  // Story 3.2: Global auto-apply enable/disable setting
  adapter.settings.register("autoApplyEnabled", {
    scope: "world",
    config: true,
    type: Boolean,
    default: true,
    name: "Enable Camera Layout Auto-Apply",
    hint: "When enabled, scenes with a configured camera layout will automatically apply it on activation",
  });

  // Dock layout — world-scoped so the GM controls the layout direction for all players
  adapter.settings.register("dockLayout", {
    scope: "world",
    config: false,
    type: String,
    default: "vertical-sm",
    onChange: (val) => {
      if (val === 'freeform') {
        roleRenderer?.closeStrip();
        freeformLayoutManager?.init();
        freeformLayoutManager?.sync();
      } else {
        freeformLayoutManager?.destroy();
        roleRenderer?.rerenderStrip();
      }
    },
  });

  // Per-user size toggle — client-scoped so each user can expand/collapse independently.
  // '' = no preference (follow world dockLayout), 'sm' = force small, 'md' = force large.
  adapter.settings.register("dockLayoutExpanded", {
    scope: "client",
    config: false,
    type: String,
    default: "",
    onChange: () => roleRenderer?.rerenderStrip(),
  });

  // Tile shape for participant avatars in the Director's Board
  adapter.settings.register("tileShape", {
    scope: "world",
    config: false,
    type: String,
    default: "circle",
    onChange: () => roleRenderer?.rerenderStrip(),
  });

  // Tile border width around video shapes (px). 0 = no border.
  adapter.settings.register("tileBorderWidth", {
    scope: "world",
    config: false,
    type: Number,
    default: 0,
    onChange: () => roleRenderer?.rerenderStrip(),
  });

  // Tile border color around video shapes.
  adapter.settings.register("tileBorderColor", {
    scope: "world",
    config: false,
    type: String,
    default: "#ffffff",
    onChange: () => roleRenderer?.rerenderStrip(),
  });

  // Story 5.2: Video widget width customization — world-scoped settings for small and large tile widths
  adapter.settings.register("widgetWidthSm", {
    scope: "world",
    config: false,
    type: String,
    default: "83",
    onChange: () => roleRenderer?.rerenderStrip(),
  });

  adapter.settings.register("widgetWidthMd", {
    scope: "world",
    config: false,
    type: String,
    default: "150",
    onChange: () => roleRenderer?.rerenderStrip(),
  });

  // Construct data layer — constructors are side-effect-free
  // Note: ScenePresetManager is constructed in 'ready' hook after visibilityManager is available
  stateStore = new StateStore(adapter.settings);
  socketHandler = new SocketHandler(adapter.socket, adapter.hooks);

  // Story 2.2: GM-only keyboard shortcut to open/close Director's Board
  game.keybindings.register('scrying-pool', 'openDirectorsBoard', {
    name: "Open/Close Director's Board",
    hint: "Toggles the Director's Board window",
    editable: [{ key: 'KeyV', modifiers: ['Control', 'Shift'] }],
    restricted: true, // GM only
    onDown: () => directorsBoard?.toggle(),
  });

  // Story 2.3: Bulk-action keybindings (GM only, migrated to scrying-pool namespace)
  game.keybindings.register('scrying-pool', 'showAll', {
    name: game.i18n.localize('scrying-pool.keybindings.showAll.name'),
    hint: game.i18n.localize('scrying-pool.keybindings.showAll.hint'),
    editable: [{ key: 'KeyS', modifiers: ['Control', 'Shift'] }],
    restricted: true,
    onDown: () => directorsBoard?.showAll(),
  });
  game.keybindings.register('scrying-pool', 'hideAll', {
    name: game.i18n.localize('scrying-pool.keybindings.hideAll.name'),
    hint: game.i18n.localize('scrying-pool.keybindings.hideAll.hint'),
    editable: [{ key: 'KeyH', modifiers: ['Control', 'Shift'] }],
    restricted: true,
    onDown: () => directorsBoard?.hideAll(),
  });
  game.keybindings.register('scrying-pool', 'spotlightParticipant', {
    name: game.i18n.localize('scrying-pool.keybindings.spotlightParticipant.name'),
    hint: game.i18n.localize('scrying-pool.keybindings.spotlightParticipant.hint'),
    editable: [{ key: 'KeyP', modifiers: ['Control', 'Shift'] }],
    restricted: true,
    onDown: () => directorsBoard?.spotlightFocused(),
  });

  // Story 2.2: Inject GM-only sidebar button via scene controls hook.
  // Uses the strip footer CTA pattern if getSceneControlButtons API is unavailable.
  Hooks.on('getSceneControlButtons', (controls) => {
    // Prevent duplicate button addition
    if (directorsBoardButtonAdded) return;
    if (!game.user?.isGM) return;
    const tokenGroup = controls.find?.(c => c.name === 'token');
    if (!tokenGroup?.tools) return;
    // Check if button already exists
    if (tokenGroup.tools.some(t => t.name === 'directors-board')) return;
    tokenGroup.tools.push({
      name: 'directors-board',
      title: "Director's Board",
      icon: 'fas fa-border-all',
      onClick: () => directorsBoard?.toggle(),
      button: true,
    });
    directorsBoardButtonAdded = true;
  });
});

Hooks.once("ready", () => {
  console.log("[ScryingPool] ready — module active");

  // Migration: reset stale boolean dockLayoutExpanded to '' (empty string)
  // Old registration used type:Boolean, saved value `false` persists in client storage
  try {
    const legacyVal = adapter.settings.get('dockLayoutExpanded');
    if (typeof legacyVal === 'boolean') {
      adapter.settings.set('dockLayoutExpanded', '').catch(() => {});
    }
  } catch (_) {}

  // Hydrate StateStore from persisted world setting (AC-6, AC-7)
  stateStore.init();

  // Probe WebRTC capability and set adapter.webrtc
  // For FULL REPLACEMENT: we need stream access to create our own video tiles
  
  // Migration: if webrtcMode is an old deprecated value, probe fresh capability
  // This ensures existing installations get the new stream-access mode if available
  const currentWebRtcMode = adapter.settings.get(FoundryAdapter.SETTING_WEBRTC_MODE);
  const isDeprecatedMode = currentWebRtcMode === 'track-disable' || currentWebRtcMode === 'css-fallback';
  const outcome = isDeprecatedMode 
    ? FoundryAdapter.probeCapability(game.webrtc)
    : currentWebRtcMode || FoundryAdapter.probeCapability(game.webrtc);
  adapter.settings?.set(FoundryAdapter.SETTING_WEBRTC_MODE, outcome).catch(err => {
    console.error('[ScryingPool] Failed to set webrtcMode setting:', err);
  });
  
  // Build WebRTC surface for stream access (full replacement mode)
  try {
    if (outcome === 'stream-access') {
      adapter.webrtc = FoundryAdapter.buildWebRTCSurface(game.webrtc);
      console.log('[ScryingPool] WebRTC stream access available - full replacement mode enabled');
    } else if (outcome === 'unsupported') {
      adapter.webrtc = null;
      console.log('[ScryingPool] WebRTC not available - AV features disabled');
      // Show info message to GM about AV being disabled
      if (game.user?.isGM) {
        ui.notifications?.info(game.i18n.localize('SCRYING_POOL.Notifications.AVDisabled'));
      }
    } else {
      // Legacy: track-disable or css-fallback (deprecated)
      adapter.webrtc = FoundryAdapter.buildWebRTCSurface(game.webrtc);
      console.warn('[ScryingPool] WebRTC mode is deprecated:', outcome, '- consider using stream-access');
    }
  } catch (err) {
    console.error('[ScryingPool] Failed to build WebRTC surface:', err);
    adapter.webrtc = null;
  }

  // Wire core managers — construct both before setReady so handler can reference both
  visibilityManager = new VisibilityManager(stateStore, adapter);
  scryingPoolController = new ScryingPoolController(stateStore, socketHandler, adapter);
  
  // Story 3.2: Re-construct ScenePresetManager with visibilityManager for auto-apply
  scenePresetManager = new ScenePresetManager(adapter, stateStore, socketHandler, visibilityManager);
  
  // Story 4.1: Create PlayerPrivacyManager for automation opt-ins
  playerPrivacyManager = new PlayerPrivacyManager(adapter);
  
  // Story 4.2: Create PortraitFallbackHandler for custom portrait fallback
  portraitFallbackHandler = new PortraitFallbackHandler(adapter, playerPrivacyManager);
  portraitFallbackHandler.init();
  
  // Story 3.2: Register updateScene hook for auto-apply
  adapter.hooks.on('updateScene', (scene) => {
    if (adapter.users.isGM()) {
      scenePresetManager.onSceneActivate(scene);
    }
  });
  
  // Story 3.1: Initialize ScenePresetManager to load presets from current scene
  scenePresetManager.init();

  // Set up composite handler for SocketHandler timeout callbacks
  // This allows cleanup of ScryingPoolController._pendingOps when onRevert fires
  socketHandler.setReady({
    onRevert: (pendingOp) => {
      visibilityManager.onRevert(pendingOp);
      scryingPoolController.cleanupPendingOp(pendingOp.userId);
    }
  });

  // Initialize both managers — must come after setReady so queue is drained
  // before echo listener is registered (prevents early echo loss)
  try {
    visibilityManager.init();
    scryingPoolController.init();

    // Story 1.5: AV tile integration + GM control UI
    avTileAdapter = new AVTileAdapter(adapter);
    // Story 4.2: Pass portraitFallbackHandler for custom portrait display
    roleRenderer = new RoleRenderer(stateStore, scryingPoolController, avTileAdapter, adapter, portraitFallbackHandler);
    roleRenderer.init();
    roleRenderer.openStrip();

    if (adapter.users.isGM()) {
      // Story 3.2: Create ConfirmationBar for preset apply feedback (GM only)
      // Pass roleRenderer to access ScryingPoolStrip.stripOverlayLayer (created lazily)
      confirmationBar = new ConfirmationBar(adapter, visibilityManager, socketHandler, roleRenderer);
      confirmationBar.init();
    }
    
    if (!adapter.users.isGM()) {
      visibilityBadge = new VisibilityBadge(stateStore, scryingPoolController, avTileAdapter, adapter);
      visibilityBadge.init();
    }
    // Story 2.1: NotificationBus — runs for all clients (GM and players)
    notificationBus = new NotificationBus(adapter);
    notificationBus.init();

    // Story 5.X: Token Video Overlay — optional, all clients (GM sees all players)
    if (adapter.webrtc) {
      tokenVideoOverlay = new TokenVideoOverlay(adapter);
      tokenVideoOverlay.init();
    }

    // Story 3.1: Register socket listener for preset apply echo (all clients receive)
    // Note: In Foundry, socket messages are automatically broadcast to all clients.
    // The GM emits PRESET_APPLIED, and all clients (including GM) receive it.
    // We skip processing on the GM since they already applied it locally.
    adapter.socket.on(SOCKET_EVENTS.PRESET_APPLIED, async (payload) => {
      try {
        // Validate payload
        if (!payload || typeof payload !== 'object' || typeof payload.presetName !== 'string') {
          console.warn('[ScryingPool] Invalid PRESET_APPLIED payload:', payload);
          return;
        }
        
        // Skip on GM — they already applied the preset locally
        if (adapter.users.isGM()) {
          return;
        }
        
        // Load the preset on this client (emitSocket: false to prevent loop)
        await scenePresetManager.load(payload.presetName, { emitSocket: false });
      } catch (err) {
        console.error('[ScryingPool] Failed to handle PRESET_APPLIED:', err);
      }
    });

    // Story 2.2: DirectorsBoard (lazy, GM only)
    // Story 3.1: Pass scenePresetManager for preset save/load functionality
    // Story 4.1: Pass playerPrivacyManager for Reaction Cam badge display
    if (adapter.users.isGM()) {
      directorsBoard = new DirectorsBoard(stateStore, scryingPoolController, adapter, scenePresetManager, playerPrivacyManager);
      directorsBoard.init();
      window.directorsBoard = directorsBoard;

      // Story 5.3: Freeform Layout Manager
      const SettingsCls = initScryingPoolSettings(roleRenderer, adapter);
      let settingsPanel = null;
      freeformLayoutManager = new FreeformLayoutManager(adapter, scryingPoolController, stateStore, {
        onOpenSettings: () => {
          if (settingsPanel && settingsPanel.rendered) {
            settingsPanel.close();
            settingsPanel = null;
          } else {
            settingsPanel = new SettingsCls();
            settingsPanel.render(true);
          }
        },
      });

      // If initial layout is freeform, switch immediately
      const initialLayout = adapter.settings?.get?.('dockLayout');
      if (initialLayout === 'freeform') {
        roleRenderer.closeStrip();
        freeformLayoutManager.init();
        freeformLayoutManager.sync();
      }
    }

    // Inject Scrying Pool deps into our camera views replacement (all clients)
    // Directors Board reference is GM-only — players get null so _onConfigure is a no-op
    initScryingPoolCameraViews(
      adapter.users.isGM() ? directorsBoard : null,
      stateStore,
      adapter
    );

    // Pre-load participant-card as a Handlebars partial for directors-board
    // ApplicationV2 requires partials to be registered explicitly
    (async () => {
      try {
        const resp = await fetch('modules/scrying-pool/templates/participant-card.hbs');
        const source = await resp.text();
        Handlebars.registerPartial('modules/scrying-pool/templates/participant-card.hbs', source);
      } catch (err) {
        console.warn('[ScryingPool] Failed to register participant-card partial:', err);
      }
    })();
    
    // Story 5.X: Initialize GMActorMappingPanel with DI dependencies
    initGMActorMappingPanel(adapter);

    // Story 4.1: Initialize PlayerPrivacyPanelMenu with DI dependencies
    // Story 4.2: Pass portraitFallbackHandler for portrait selection
    initPlayerPrivacyPanelMenu(adapter, playerPrivacyManager, portraitFallbackHandler);
    
    // Story 4.1: Register GM-only Player Privacy Selector (Task 5.2)
    // Allows GM to select any player and view their privacy settings in read-only mode
    // Story 4.2: Pass portraitFallbackHandler for portrait display
    initGMPlayerPrivacySelector(adapter, playerPrivacyManager, portraitFallbackHandler);
    
    // Story 4.1: Register PlayerPrivacyPanel in module settings (Task 5.1)
    // Note: Must be registered AFTER init calls to avoid race conditions
    game.settings.registerMenu('scrying-pool', 'playerPrivacyPanel', {
      name: 'SCRYING_POOL.Settings.PlayerPrivacyPanel',
      label: 'SCRYING_POOL.Settings.PlayerPrivacyPanelLabel',
      hint: 'SCRYING_POOL.Settings.PlayerPrivacyPanelHint',
      icon: 'fa-solid fa-user-shield',
      type: PlayerPrivacyPanelMenu,
      restricted: false,
    });

    // Story 5.X: Register Actor-Webcam Mapping settings menu (GM only)
    game.settings.registerMenu('scrying-pool', 'actorMapping', {
      name: 'SCRYING_POOL.ActorMapping.title',
      label: 'SCRYING_POOL.ActorMapping.label',
      hint: 'SCRYING_POOL.ActorMapping.hint',
      icon: 'fa-solid fa-address-card',
      type: GMActorMappingPanel,
      restricted: true,
    });

    // Register ScryingPoolSettings in module settings
    // Provides button to reopen the strip when user closes it
    game.settings.registerMenu('scrying-pool', 'stripSettings', {
      name: 'SCRYING_POOL.Settings.Title',
      label: 'SCRYING_POOL.Settings.Title',
      hint: 'SCRYING_POOL.Settings.Hint',
      icon: 'fa-solid fa-cog',
      type: initScryingPoolSettings(roleRenderer, adapter),
      restricted: true, // GM only
    });

  } catch (err) {
    console.error('[ScryingPool] Module initialization failed:', err);
    throw err; // Re-throw to prevent module from loading in broken state
  }
});

