/**
 * PWA Module
 * 
 * Handles Progressive Web App functionality:
 * - Service Worker registration
 * - Install prompt handling
 * - Offline detection
 * - Update notifications
 * 
 * Note: The Service Worker (sw.js) handles caching.
 *       The SharedWorker (shared-db-worker.js) handles database.
 *       They are separate workers with different purposes.
 */

// ============================================================================
// Service Worker Registration
// ============================================================================

let swRegistration = null;

export async function registerServiceWorker() {
  if (!('serviceWorker' in navigator)) {
    console.warn('[PWA] Service Workers not supported');
    return null;
  }
  
  try {
    swRegistration = await navigator.serviceWorker.register('/sw.js', {
      scope: '/'
    });
    
    console.log('[PWA] Service Worker registered:', swRegistration.scope);
    
    // Handle updates
    swRegistration.addEventListener('updatefound', () => {
      const newWorker = swRegistration.installing;
      
      newWorker.addEventListener('statechange', () => {
        if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
          // New version available
          console.log('[PWA] New version available');
          showUpdateNotification();
        }
      });
    });
    
    return swRegistration;
    
  } catch (error) {
    console.error('[PWA] Service Worker registration failed:', error);
    return null;
  }
}

// ============================================================================
// Install Prompt
// ============================================================================

let deferredPrompt = null;
let installButton = null;

/**
 * Initialize install prompt handling
 * @param {string|HTMLElement} buttonSelector - Button element or selector
 */
export function initInstallPrompt(buttonSelector = '#install-btn') {
  installButton = typeof buttonSelector === 'string' 
    ? document.querySelector(buttonSelector)
    : buttonSelector;
  
  if (!installButton) {
    console.warn('[PWA] Install button not found:', buttonSelector);
    return;
  }
  
  // Initially hide the button
  installButton.style.display = 'none';
  
  // Listen for the beforeinstallprompt event
  window.addEventListener('beforeinstallprompt', (e) => {
    e.preventDefault();
    deferredPrompt = e;
    
    // Show the install button
    installButton.style.display = 'block';
    console.log('[PWA] Install prompt ready');
  });
  
  // Handle install button click
  installButton.addEventListener('click', async () => {
    if (!deferredPrompt) {
      // Show manual instructions for Safari
      showManualInstallInstructions();
      return;
    }
    
    deferredPrompt.prompt();
    const { outcome } = await deferredPrompt.userChoice;
    
    console.log('[PWA] Install prompt outcome:', outcome);
    
    deferredPrompt = null;
    installButton.style.display = 'none';
  });
  
  // Hide button if app is already installed
  window.addEventListener('appinstalled', () => {
    console.log('[PWA] App installed');
    deferredPrompt = null;
    installButton.style.display = 'none';
  });
  
  // Check if running as installed PWA
  if (window.matchMedia('(display-mode: standalone)').matches) {
    installButton.style.display = 'none';
  }
}

function showManualInstallInstructions() {
  const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
  const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
  
  let message = 'To install this app:\n\n';
  
  if (isIOS) {
    message += '1. Tap the Share button (square with arrow)\n';
    message += '2. Scroll down and tap "Add to Home Screen"';
  } else if (isSafari) {
    message += '1. Click File menu\n';
    message += '2. Click "Add to Dock"';
  } else {
    message += '1. Click the menu button (three dots)\n';
    message += '2. Click "Install" or "Add to Home Screen"';
  }
  
  alert(message);
}

// ============================================================================
// Offline Detection
// ============================================================================

let offlineIndicator = null;
const offlineListeners = new Set();

/**
 * Initialize offline detection
 * @param {string|HTMLElement} indicatorSelector - Element to show when offline
 */
export function initOfflineDetection(indicatorSelector = '#offline-indicator') {
  offlineIndicator = typeof indicatorSelector === 'string'
    ? document.querySelector(indicatorSelector)
    : indicatorSelector;
  
  // Set initial state
  updateOfflineUI(!navigator.onLine);
  
  // Listen for online/offline events
  window.addEventListener('online', () => {
    console.log('[PWA] Back online');
    updateOfflineUI(false);
    notifyOfflineListeners(false);
  });
  
  window.addEventListener('offline', () => {
    console.log('[PWA] Gone offline');
    updateOfflineUI(true);
    notifyOfflineListeners(true);
  });
}

function updateOfflineUI(isOffline) {
  if (offlineIndicator) {
    offlineIndicator.style.display = isOffline ? 'block' : 'none';
  }
  
  // Also toggle a class on body for CSS styling
  document.body.classList.toggle('is-offline', isOffline);
}

/**
 * Subscribe to offline state changes
 * @param {Function} listener - Callback(isOffline: boolean)
 * @returns {Function} Unsubscribe function
 */
export function onOfflineChange(listener) {
  offlineListeners.add(listener);
  // Immediately call with current state
  listener(!navigator.onLine);
  return () => offlineListeners.delete(listener);
}

function notifyOfflineListeners(isOffline) {
  for (const listener of offlineListeners) {
    try {
      listener(isOffline);
    } catch (e) {
      console.error('[PWA] Offline listener error:', e);
    }
  }
}

/**
 * Check if currently online
 */
export function isOnline() {
  return navigator.onLine;
}

// ============================================================================
// Update Handling
// ============================================================================

let updateCallback = null;

/**
 * Set callback for when updates are available
 * @param {Function} callback - Called when new version is ready
 */
export function onUpdateAvailable(callback) {
  updateCallback = callback;
}

function showUpdateNotification() {
  if (updateCallback) {
    updateCallback();
    return;
  }
  
  // Default behavior
  if (confirm('A new version is available. Reload now?')) {
    applyUpdate();
  }
}

/**
 * Apply pending update (reload with new version)
 */
export function applyUpdate() {
  if (swRegistration?.waiting) {
    swRegistration.waiting.postMessage({ type: 'SKIP_WAITING' });
  }
  window.location.reload();
}

// ============================================================================
// Communication with Service Worker
// ============================================================================

/**
 * Send a message to the service worker
 * @param {Object} message - Message to send
 */
export function postToServiceWorker(message) {
  navigator.serviceWorker.controller?.postMessage(message);
}

/**
 * Request the service worker to cache specific modules
 * @param {string[]} moduleNames - Array of module names to cache
 */
export function cacheModules(moduleNames) {
  postToServiceWorker({
    type: 'CACHE_MODULES',
    payload: { modules: moduleNames }
  });
}

/**
 * Request the service worker to clear user-specific caches
 * (Call this on logout)
 */
export function clearUserCaches() {
  postToServiceWorker({
    type: 'CLEAR_USER_CACHE'
  });
}

/**
 * Get the Service Worker we can postMessage to. Resolves with:
 *   • `navigator.serviceWorker.controller` if it's already in control of the
 *     page (fastest path), or
 *   • `registration.active` once `navigator.serviceWorker.ready` resolves
 *     (covers the first-load case before the SW has claimed the page).
 *
 * Rejects after `timeoutMs` if no SW becomes available — which would only
 * happen in a private/incognito context, an unsupported browser, or when
 * registration genuinely failed.
 *
 * @param {{ timeoutMs?: number }} [opts]
 * @returns {Promise<ServiceWorker>}
 */
export async function getActiveServiceWorker({ timeoutMs = 10000 } = {}) {
  if (!('serviceWorker' in navigator)) {
    throw new Error('Service Workers not supported in this browser');
  }

  // Fastest path — page is already SW-controlled
  if (navigator.serviceWorker.controller) {
    return navigator.serviceWorker.controller;
  }

  // Otherwise wait for the registration to become ready (active SW exists
  // for this scope, even if it hasn't claimed THIS page yet)
  const ready = navigator.serviceWorker.ready;
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Service-worker readiness timeout')), timeoutMs)
  );

  const registration = await Promise.race([ready, timeout]);

  // The controller may have appeared while we were waiting; otherwise use
  // the registration's active worker (we can still postMessage to it — caches
  // are shared across the origin)
  const sw = navigator.serviceWorker.controller || registration.active;
  if (!sw) {
    throw new Error('No active service worker available');
  }
  return sw;
}

/**
 * Subscribe to controller-change events. The callback fires whenever a new
 * Service Worker takes control of the page (e.g. after an SW update or first
 * activation on initial load). Useful for re-querying SW-backed state once
 * the SW has actually taken over.
 *
 * @param {() => void} callback
 * @returns {() => void} unsubscribe function
 */
export function onServiceWorkerControllerChange(callback) {
  if (!('serviceWorker' in navigator)) return () => {};
  const handler = () => {
    try { callback(); } catch (e) { console.error('[PWA] controllerchange handler error:', e); }
  };
  navigator.serviceWorker.addEventListener('controllerchange', handler);
  return () => navigator.serviceWorker.removeEventListener('controllerchange', handler);
}

/**
 * Send a message to the service worker and wait for a single reply with the
 * given response type. Waits for the SW to become available if it isn't yet.
 * Resolves with the reply payload, or rejects after a timeout.
 *
 * @template T
 * @param {string} requestType   - Message type to send (e.g. 'GET_TILE_STATS')
 * @param {string} responseType  - Message type expected back (e.g. 'TILE_STATS')
 * @param {Object} [extra={}]    - Extra fields merged into the outgoing message
 * @param {number} [timeoutMs=5000]   Reply timeout (after the SW is available)
 * @param {number} [readyTimeoutMs=10000] Timeout for the SW to be available
 * @returns {Promise<T>}
 */
async function requestFromServiceWorker(requestType, responseType, extra = {}, timeoutMs = 5000, readyTimeoutMs = 10000) {
  const sw = await getActiveServiceWorker({ timeoutMs: readyTimeoutMs });

  return new Promise((resolve, reject) => {
    const channel = new MessageChannel();
    const timer = setTimeout(() => {
      channel.port1.close();
      reject(new Error(`Service-worker reply "${responseType}" timed out`));
    }, timeoutMs);

    channel.port1.onmessage = (event) => {
      if (event.data?.type === responseType) {
        clearTimeout(timer);
        channel.port1.close();
        const { type, ...rest } = event.data;
        resolve(rest);
      }
    };

    sw.postMessage({ type: requestType, ...extra }, [channel.port2]);
  });
}

/**
 * Get statistics about tiles cached locally on this device, broken down by
 * provider. Waits up to `readyTimeoutMs` for the service worker to become
 * available. Returns null only if the SW genuinely cannot be reached
 * (private mode, registration failure, or timeout).
 *
 * @returns {Promise<{
 *   totals: { count: number, estBytes: number },
 *   byProvider: Array<{ key: string, label: string, count: number, limit: number, estBytes: number }>
 * } | null>}
 */
export async function getTileCacheStats() {
  try {
    const reply = await requestFromServiceWorker('GET_TILE_STATS', 'TILE_STATS');
    return reply.stats;
  } catch (err) {
    console.warn('[PWA] getTileCacheStats failed:', err);
    return null;
  }
}

/**
 * Delete every cached tile from this device. Doesn't touch the app shell,
 * modules, or API caches — only the per-provider tile buckets.
 * Waits for the SW to be available before sending the request.
 *
 * @returns {Promise<boolean>} true if the request was acknowledged
 */
export async function clearTileCaches() {
  try {
    await requestFromServiceWorker('CLEAR_TILE_CACHES', 'TILE_CACHES_CLEARED');
    return true;
  } catch (err) {
    console.warn('[PWA] clearTileCaches failed:', err);
    return false;
  }
}

/**
 * Delete cached tiles for a single provider. `cacheName` must match one of
 * the per-provider caches reported by `getTileCacheStats()` (e.g.
 * `tiles-osm-v4`, `tiles-topo-v4`). Unknown names are rejected by the SW.
 *
 * @param {string} cacheName
 * @returns {Promise<boolean>} true if the cache was actually deleted
 */
export async function clearTileCacheForProvider(cacheName) {
  if (!cacheName) return false;
  try {
    const reply = await requestFromServiceWorker(
      'CLEAR_TILE_CACHE', 'TILE_CACHE_CLEARED',
      { cacheName },
    );
    return !!reply.deleted;
  } catch (err) {
    console.warn(`[PWA] clearTileCacheForProvider(${cacheName}) failed:`, err);
    return false;
  }
}

/**
 * Get total disk used by this origin (Cache API + IndexedDB + OPFS).
 * Returns null if the Storage API is not available.
 *
 * @returns {Promise<{ usage: number, quota: number } | null>}
 */
export async function getStorageEstimate() {
  if (!navigator.storage?.estimate) return null;
  try {
    const { usage, quota } = await navigator.storage.estimate();
    return { usage: usage || 0, quota: quota || 0 };
  } catch (err) {
    console.warn('[PWA] getStorageEstimate failed:', err);
    return null;
  }
}

// ============================================================================
// Auto-initialization
// ============================================================================

/**
 * Initialize all PWA features
 * @param {Object} options
 */
export async function initPWA(options = {}) {
  const {
    installButton = '#install-btn',
    offlineIndicator = '#offline-indicator',
    autoRegisterSW = true
  } = options;
  
  if (autoRegisterSW) {
    await registerServiceWorker();
  }
  
  initInstallPrompt(installButton);
  initOfflineDetection(offlineIndicator);
  
  console.log('[PWA] Initialized');
}

// Export for direct use
export default {
  registerServiceWorker,
  initInstallPrompt,
  initOfflineDetection,
  initPWA,
  isOnline,
  onOfflineChange,
  onUpdateAvailable,
  applyUpdate,
  postToServiceWorker,
  cacheModules,
  clearUserCaches,
  getTileCacheStats,
  clearTileCaches,
  clearTileCacheForProvider,
  getStorageEstimate,
  getActiveServiceWorker,
  onServiceWorkerControllerChange,
};
