import 'leaflet-defaulticon-compatibility';
import 'leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css';
import 'leaflet/dist/leaflet.css';
import L from 'leaflet';
import { fetchDEMData, setSelectedDemType } from './dem-service.js';
import { generateSTL, downloadSTL } from './stl-generator.js';
import { createHeightmapVisualization, getColorForElevation } from './terrain-utils.js';
import { debounce, throttle, rafThrottle, performanceMark, performanceMeasure } from './utils.js';
import { setupGlobalErrorHandling, withErrorHandling, showUserFriendlyError } from './error-handler.js';
import { initPerformanceMonitoring, monitorResourceLoading, generatePerformanceReport } from './performance-monitor.js';
import { saveApiKey, getApiKey, hasApiKey, clearApiKey, validateApiKeyFormat } from './api-key-manager.js';
import { initAuthGuard, createAppUserBar } from './auth-guard.js';
import { initWelcomeModal } from './welcome-modal.js';
import {
  getProcessingMode,
  setProcessingMode,
  isCloudProcessingEnabled,
  getProcessingModeLabel,
  ProcessingMode
} from './processing-mode-manager.js';
import {
  getCloudProfile,
  saveApiKeyToUserProfile,
  clearApiKeyInUserProfile,
  getApiKeyFromUserProfile,
  calculateCloudCreditsForArea,
  getAvailableCreditPacks,
  createPaypalOrder,
  capturePaypalOrder,
  createCloudJob,
  subscribeToCloudJobs,
  recoverSubmittedCloudJobs
} from './cloud-service.js';
import { generateWithWorkerOrFallback } from './worker-manager.js';
import { initPinManager, getPinsInBbox, toggleAllPinsVisibility, isPinPlacementActive } from './pin-manager.js';
import 'leaflet-control-geocoder/dist/Control.Geocoder.css';
import * as LControlGeocoder from 'leaflet-control-geocoder';

// 🎉 NEW FEATURES: Import validation, caching, and enhanced progress
import { validateAllInputs, setupRealtimeValidation, showValidationSummary, VALIDATION_RULES } from './input-validator.js';
import { createProgressIndicator, cancelCurrentOperation, showCancelledMessage, showSuccessMessage, createAbortController, getCurrentAbortController } from './progress-manager.js';
import { getCacheStats, clearExpiredCache, clearAllCache, deleteCacheForBbox } from './dem-cache.js';

// Setup global error handling
setupGlobalErrorHandling();

// Initialize authentication guard
let authenticatedUser = null;
initAuthGuard()
  .then((user) => {
    authenticatedUser = user;
    // Create user bar at the top of the app
    createAppUserBar(user);
    // Initialize the app
    initializeApp();
  })
  .catch((error) => {
    console.error('Authentication failed:', error.message);
    // Auth guard will show appropriate UI
  });

// Initialize performance monitoring (only in development or if needed)
if (import.meta.env.DEV) {
  initPerformanceMonitoring();
  window.addEventListener('load', () => {
    monitorResourceLoading();
    // Generate report after 5 seconds
    setTimeout(() => {
      generatePerformanceReport();
    }, 5000);
  });
}

// Main app initialization function
function initializeApp() {
  const isFiniteNumber = (value) => Number.isFinite(Number(value));

  function loadBoundingBoxesFromStorage() {
    const normalizeBbox = (bbox, index) => {
      if (!bbox || typeof bbox !== 'object') return null;

      let swLat = Number(bbox.southwestLat);
      let swLng = Number(bbox.southwestLng);
      let neLat = Number(bbox.northeastLat);
      let neLng = Number(bbox.northeastLng);

      if (![swLat, swLng, neLat, neLng].every(isFiniteNumber)) return null;
      if (swLat < -90 || swLat > 90 || neLat < -90 || neLat > 90) return null;
      if (swLng < -180 || swLng > 180 || neLng < -180 || neLng > 180) return null;

      // Recover from swapped coordinates instead of dropping user data.
      if (swLat > neLat) [swLat, neLat] = [neLat, swLat];
      if (swLng > neLng) [swLng, neLng] = [neLng, swLng];

      // Degenerate boxes break geometry calculations and map layer creation.
      if (Math.abs(neLat - swLat) < 1e-9 || Math.abs(neLng - swLng) < 1e-9) return null;

      return {
        ...bbox,
        southwestLat: swLat,
        southwestLng: swLng,
        northeastLat: neLat,
        northeastLng: neLng,
        name: typeof bbox.name === 'string' && bbox.name.trim() ? bbox.name.trim() : `Box ${index + 1}`,
        visible: bbox.visible !== false
      };
    };

    try {
      const raw = localStorage.getItem('boundingBoxes');
      if (!raw) return [];

      const parsed = JSON.parse(raw);
      if (!Array.isArray(parsed)) {
        console.warn('boundingBoxes storage is not an array. Resetting to empty.');
        localStorage.removeItem('boundingBoxes');
        return [];
      }

      const sanitizedBoxes = parsed
        .map((bbox, index) => normalizeBbox(bbox, index))
        .filter(Boolean);

      if (sanitizedBoxes.length !== parsed.length) {
        console.warn(`Dropped ${parsed.length - sanitizedBoxes.length} invalid bounding box(es) from storage.`);
        localStorage.setItem('boundingBoxes', JSON.stringify(sanitizedBoxes));
      }

      return sanitizedBoxes;
    } catch (error) {
      console.warn('Failed to parse boundingBoxes from storage. Resetting to empty.', error);
      localStorage.removeItem('boundingBoxes');
      return [];
    }
  }

  // Register service worker for PWA support (only in production)
  if ('serviceWorker' in navigator && !import.meta.env.DEV) {
    window.addEventListener('load', () => {
      navigator.serviceWorker.register('/sw.js')
        .then(registration => {
          // ServiceWorker registered successfully
        })
        .catch(error => {
          console.warn('ServiceWorker registration failed:', error);
        });
    });
  }

  const map = L.map('map').setView([0, 0], 2);

  // OpenStreetMap layer
  const osmLayer = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
    attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
  });

  // Digital Terrain Model (DEM) layer - Open Topo Map
  const openTopoMapLayer = L.tileLayer('https://tile.opentopomap.org/{z}/{x}/{y}.png', {
    maxZoom: 17,
    attribution: 'Map data: &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, <a href="http://viewfinderpanoramas.org">SRTM</a> | Map style: &copy; <a href="https://opentopomap.org">OpenTopoMap</a> (<a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>)'
  }).addTo(map); // Set Open Topo Map as default

  // Add a scale control
  L.control.scale().addTo(map);

  // Layer control
  const baseMaps = {
    "OpenStreetMap": osmLayer,
    "Open Topo Map": openTopoMapLayer,
  };

  L.control.layers(baseMaps).addTo(map);

  // After map initialization and layer controls
  if (typeof L.Control.Geocoder === 'undefined') {
    console.error('Leaflet Control Geocoder not loaded');
  } else {
    const geocoder = L.Control.geocoder({
      defaultMarkGeocode: true,
      position: 'topleft',
      placeholder: 'Search for a location...'
    }).addTo(map);
    // Move the geocoder control below the zoom controls
    function moveGeocoderBelowZoom() {
      const zoomControl = document.querySelector('.leaflet-control-zoom');
      const geocoderControl = document.querySelector('.leaflet-control-geocoder');
      if (zoomControl && geocoderControl && zoomControl.parentNode) {
        zoomControl.parentNode.insertBefore(geocoderControl, zoomControl.nextSibling);
      }
    }
    map.on('load', moveGeocoderBelowZoom);
    setTimeout(moveGeocoderBelowZoom, 100);
    // Also move on fullscreen change
    document.addEventListener('fullscreenchange', moveGeocoderBelowZoom);
  }

  // Set max bounds to prevent map from repeating
  const southWest = L.latLng(-90, -180);
  const northEast = L.latLng(90, 180);
  const bounds = L.latLngBounds(southWest, northEast);

  map.setMaxBounds(bounds);
  map.on('drag', function () {
    map.panInsideBounds(bounds, { animate: false });
  });

  map.setMinZoom(2);

  // Initialize Pin Manager unconditionally so it works even if auth/network fails later
  try {
    initPinManager(map, {
      pinListElement: document.getElementById('pin-list'),
      placePinButton: document.getElementById('place-pin-button'),
      mobilePlacePinButton: document.getElementById('mobile-place-pin-button'),
      onPinChange: () => {
        // Optional callback when pins change
      }
    });
  } catch (error) {
    // Keep core map interactions available even if pin data/module has issues.
    console.error('Pin manager initialization failed. Continuing without pins.', error);
  }

  // Toggle all pins visibility
  let allPinsVisible = true;
  const toggleAllPinsBtn = document.getElementById('toggle-all-pins-btn');
  if (toggleAllPinsBtn) {
    toggleAllPinsBtn.addEventListener('click', () => {
      allPinsVisible = !allPinsVisible;
      toggleAllPinsVisibility(allPinsVisible);
      toggleAllPinsBtn.textContent = allPinsVisible ? '👁️ All' : '👁️‍🗨️ All';
    });
  }

  // Bounding Box Drawing Functionality
  let drawingMode = false;
  let clickedPoints = []; // Array to store the 4 clicked points
  let temporaryMarkers = []; // Array to store temporary markers for visual feedback
  let previewPolygon = null; // Polygon to show preview of the area
  let boundingBoxes = loadBoundingBoxesFromStorage();
  let bboxLayers = [];
  let bboxHandles = [];
  const handleOrder = ['sw', 'nw', 'ne', 'se'];
  const bboxHandleIcon = L.divIcon({
    className: 'bbox-handle-icon',
    iconSize: [16, 16],
    iconAnchor: [8, 8]
  });

  const drawBboxButton = document.getElementById('draw-bbox-button');
  const mobileDrawButton = document.getElementById('mobile-draw-button');
  const mobilePlacePinButton = document.getElementById('mobile-place-pin-button');
  const mobileModeHint = document.getElementById('mobile-mode-hint');
  const bboxList = document.getElementById('bbox-list');
  const bboxPanel = document.getElementById('bbox-panel');
  const mobileToggleButton = document.getElementById('mobile-toggle-button');

  if (!drawBboxButton) {
    console.error('Draw bounding box button not found!');
    return;
  }

  // Mobile toggle functionality - panel hidden by default on mobile
  let isPanelVisible = false;
  let pinPlacementModeActive = false;

  let toggleDebounce = false;

  function toggleMobileView() {
    const panelElement = document.getElementById('bbox-panel');

    // Prevent rapid toggling
    if (toggleDebounce) return;
    toggleDebounce = true;
    setTimeout(() => { toggleDebounce = false; }, 300);

    if (window.innerWidth <= 768) {
      isPanelVisible = !isPanelVisible;
      panelElement.style.display = isPanelVisible ? 'flex' : 'none';
      panelElement.classList.toggle('mobile-open', isPanelVisible);
      document.body.classList.toggle('mobile-panel-open', isPanelVisible);
      if (mobileToggleButton) {
        mobileToggleButton.textContent = isPanelVisible ? '✕ Close Panel' : '☰ Panel';
        mobileToggleButton.setAttribute('aria-expanded', isPanelVisible ? 'true' : 'false');
      }
      if (isPanelVisible) attachPanelEventListeners();

      // Trigger map resize after layout change
      setTimeout(() => {
        map.invalidateSize();
      }, 100);
    }
  }

  // Initialize mobile state on page load
  function initializeMobileState() {
    if (window.innerWidth <= 768) {
      const panelElement = document.getElementById('bbox-panel');

      // Set default state: panel hidden, map full screen
      isPanelVisible = false;
      panelElement.style.display = 'none';
      panelElement.classList.remove('mobile-open');
      document.body.classList.remove('mobile-panel-open');
      if (mobileToggleButton) {
        mobileToggleButton.textContent = '☰ Panel';
        mobileToggleButton.setAttribute('aria-expanded', 'false');
      }
      updateMobileQuickActionState();

      // Trigger map resize
      setTimeout(() => {
        map.invalidateSize();
      }, 100);
    }
  }

  // Function to attach event listeners to panel elements
  function attachPanelEventListeners() {
    if (!bboxPanel || window.innerWidth > 768) return;

    const formElements = bboxPanel.querySelectorAll('input, select, button, textarea');
    formElements.forEach(element => {
      // Remove existing listeners to prevent duplicates
      element.removeEventListener('click', stopPropagationHandler);
      element.removeEventListener('touchstart', stopPropagationHandler);
      element.removeEventListener('touchend', stopPropagationHandler);
      element.removeEventListener('focus', stopPropagationHandler);
      element.removeEventListener('change', stopPropagationHandler);

      // Add new listeners
      element.addEventListener('click', stopPropagationHandler);
      element.addEventListener('touchstart', stopPropagationHandler);
      element.addEventListener('touchend', stopPropagationHandler);
      element.addEventListener('focus', stopPropagationHandler);
      element.addEventListener('change', stopPropagationHandler);
    });
  }

  // Handler function for stopping event propagation
  function stopPropagationHandler(e) {
    if (window.innerWidth <= 768) {
      e.stopPropagation();
    }
  }

  // Add click event listener for mobile toggle
  if (mobileToggleButton) {
    mobileToggleButton.addEventListener('click', toggleMobileView);
  }

  // Initial setup of form element event listeners
  if (bboxPanel) {
    attachPanelEventListeners();
  }

  // Handle window resize to reset mobile view (debounced for performance)
  const handleResize = debounce(() => {
    if (window.innerWidth > 768) {
      // Reset to desktop view
      const panelElement = document.getElementById('bbox-panel');
      panelElement.style.display = 'flex';
      panelElement.classList.remove('mobile-open');
      document.body.classList.remove('mobile-panel-open');
      isPanelVisible = true;

      setTimeout(() => {
        map.invalidateSize();
      }, 100);
    } else {
      // Initialize mobile state when resizing to mobile
      initializeMobileState();
    }
  }, 250);

  window.addEventListener('resize', handleResize);

  // Initialize mobile state on page load
  initializeMobileState();

  // Settings Modal Functionality
  const settingsModal = document.getElementById('settings-modal');
  const closeSettingsModal = document.getElementById('close-settings-modal');
  const apiKeyInput = document.getElementById('api-key-input');
  const saveApiKeyButton = document.getElementById('save-api-key-button');
  const clearApiKeyButton = document.getElementById('clear-api-key-button');
  const apiKeyStatus = document.getElementById('api-key-status');
  const statusIndicator = document.getElementById('status-indicator');
  const statusText = document.getElementById('status-text');
  let cloudProfile = { enabled: false, creditBalance: 0, status: 'inactive' };

  // Function to update API key status display
  function updateApiKeyStatus() {
    if (hasApiKey()) {
      const key = getApiKey();
      const maskedKey = key.substring(0, 4) + '...' + key.substring(key.length - 4);
      statusIndicator.textContent = '✅';
      statusText.textContent = `API Key configured: ${maskedKey}`;
      apiKeyStatus.classList.add('configured');
    } else {
      statusIndicator.textContent = '❌';
      statusText.textContent = 'No API key configured';
      apiKeyStatus.classList.remove('configured');
    }
  }

  // Function to open settings modal
  function openSettingsModal() {
    settingsModal.classList.add('active');
    updateApiKeyStatus();

    // Load current API key into input if it exists
    if (hasApiKey()) {
      apiKeyInput.value = getApiKey();
    } else {
      apiKeyInput.value = '';
    }
  }

  // Function to close settings modal
  function closeSettingsModalFunction() {
    settingsModal.classList.remove('active');
  }

  // Function to save API key
  async function saveApiKeyFunction() {
    const apiKey = apiKeyInput.value.trim();

    if (!apiKey) {
      showUserFriendlyError('Please enter an API key.');
      return;
    }

    if (!validateApiKeyFormat(apiKey)) {
      showUserFriendlyError('Invalid API key format. OpenTopography API keys are typically 32-character hexadecimal strings.');
      return;
    }

    try {
      saveApiKey(apiKey);
      await saveApiKeyToUserProfile(apiKey);
      updateApiKeyStatus();
      showSuccessMessage('API key saved locally and synced to your account.');

      // Close modal after a short delay
      setTimeout(() => {
        closeSettingsModalFunction();
      }, 1500);
    } catch (error) {
      showUserFriendlyError(`Error saving API key: ${error.message}`);
    }
  }

  // Function to clear API key
  async function clearApiKeyFunction() {
    if (confirm('Are you sure you want to clear your API key? You will need to enter it again to generate models.')) {
      clearApiKey();
      await clearApiKeyInUserProfile();
      apiKeyInput.value = '';
      updateApiKeyStatus();
      showSuccessMessage('API key cleared locally and from your cloud profile.');
    }
  }

  // Event listeners for settings modal
  // Note: Settings modal is now opened via user menu (see auth-guard.js)

  if (closeSettingsModal) {
    closeSettingsModal.addEventListener('click', closeSettingsModalFunction);
  }

  if (saveApiKeyButton) {
    saveApiKeyButton.addEventListener('click', () => {
      saveApiKeyFunction().catch((error) => {
        showUserFriendlyError(`Error saving API key: ${error.message}`);
      });
    });
  }

  if (clearApiKeyButton) {
    clearApiKeyButton.addEventListener('click', () => {
      clearApiKeyFunction().catch((error) => {
        showUserFriendlyError(`Error clearing API key: ${error.message}`);
      });
    });
  }

  // Allow Enter key to save API key
  if (apiKeyInput) {
    apiKeyInput.addEventListener('keypress', (e) => {
      if (e.key === 'Enter') {
        saveApiKeyFunction().catch((error) => {
          showUserFriendlyError(`Error saving API key: ${error.message}`);
        });
      }
    });
  }

  // Sync API key from profile for existing sessions.
  (async () => {
    try {
      const profileKey = await getApiKeyFromUserProfile();
      if (profileKey && !hasApiKey()) {
        saveApiKey(profileKey);
      }
      updateApiKeyStatus();
    } catch (error) {
      console.warn('Unable to sync API key from profile:', error.message);
    }
  })();

  // Close modal when clicking outside
  if (settingsModal) {
    settingsModal.addEventListener('click', (e) => {
      if (e.target === settingsModal) {
        closeSettingsModalFunction();
      }
    });
  }

  // Processing Mode Modal
  const processingModeModal = document.getElementById('processing-mode-modal');
  const closeProcessingModeModal = document.getElementById('close-processing-mode-modal');

  // Processing Mode Settings - Card-based selection
  const saveProcessingModeButton = document.getElementById('save-processing-mode');
  const cloudModeStatus = document.getElementById('cloud-mode-status');
  const browserCard = document.querySelector('.processing-mode-card[data-mode="browser"]');
  const cloudCard = document.querySelector('.processing-mode-card[data-mode="cloud"]');
  const cloudStatusTitle = document.getElementById('cloud-status-title');
  const cloudStatusMessage = document.getElementById('cloud-status-message');
  const cloudCreditSummary = document.getElementById('cloud-credit-summary');
  const cloudCardBadge = document.getElementById('cloud-card-badge');
  const cloudPackButtons = document.querySelectorAll('.cloud-pack-button');
  const cloudDevHint = document.getElementById('cloud-dev-hint');
  const CLOUD_PROCESSING_DEV_LOCK = false;

  let selectedMode = getProcessingMode(); // Track selected mode

  async function refreshCloudProfileUI() {
    try {
      cloudProfile = await getCloudProfile();
    } catch (error) {
      console.warn('Unable to load cloud profile:', error.message);
      cloudProfile = { enabled: false, creditBalance: 0, status: 'inactive' };
    }

    const credits = Number(cloudProfile.creditBalance || 0);
    if (cloudProfile.enabled) {
      if (cloudStatusTitle) cloudStatusTitle.textContent = 'Cloud Processing Active';
      if (cloudStatusMessage) cloudStatusMessage.textContent = `Your account has ${credits} credits available.`;
      if (cloudCreditSummary) cloudCreditSummary.textContent = `${credits} credits available`;
      if (cloudCardBadge) cloudCardBadge.textContent = 'ACTIVE';
      ensureCloudJobsSubscription();
    } else {
      if (cloudStatusTitle) cloudStatusTitle.textContent = 'Activate Cloud Processing';
      if (cloudStatusMessage) cloudStatusMessage.textContent = 'Buy your first credit pack to activate cloud processing automatically.';
      if (cloudCreditSummary) cloudCreditSummary.textContent = 'Inactive account (0 credits)';
      if (cloudCardBadge) cloudCardBadge.textContent = 'INACTIVE';
    }
  }

  function applyCloudDevLockUI() {
    if (cloudDevHint) {
      cloudDevHint.style.display = CLOUD_PROCESSING_DEV_LOCK ? 'block' : 'none';
    }

    cloudPackButtons.forEach((button) => {
      button.disabled = CLOUD_PROCESSING_DEV_LOCK;
      button.classList.toggle('is-disabled', CLOUD_PROCESSING_DEV_LOCK);
      if (CLOUD_PROCESSING_DEV_LOCK) {
        button.setAttribute('aria-disabled', 'true');
        button.title = 'Cloud Processing is temporarily disabled during development.';
      } else {
        button.removeAttribute('aria-disabled');
        button.removeAttribute('title');
      }
    });
  }

  async function handleCloudPackPurchase(packId) {
    if (CLOUD_PROCESSING_DEV_LOCK) {
      showUserFriendlyError('Cloud Processing is temporarily disabled while this feature is in development.');
      return;
    }

    try {
      const appUrl = `${window.location.origin}/app.html`;
      const returnUrl = `${appUrl}?cloud_payment=success`;
      const cancelUrl = `${appUrl}?cloud_payment=cancel`;

      const order = await createPaypalOrder(packId, returnUrl, cancelUrl);
      if (!order?.approveUrl) {
        throw new Error('Payment link was not returned by the server.');
      }

      window.location.href = order.approveUrl;
    } catch (error) {
      showUserFriendlyError(`Unable to start payment: ${error.message}`);
    }
  }

  async function handlePaypalReturnIfNeeded() {
    const params = new URLSearchParams(window.location.search);
    const token = params.get('token');
    const cloudPaymentStatus = params.get('cloud_payment');
    if (!token) {
      if (cloudPaymentStatus === 'cancel') {
        showUserFriendlyError('Payment was cancelled. No credits were charged.');
        history.replaceState(null, '', window.location.pathname);
      }
      return;
    }

    try {
      const capture = await capturePaypalOrder(token);
      await refreshCloudProfileUI();
      showSuccessMessage(`Payment completed. Added ${capture.creditsAdded || 0} credits.`);
    } catch (error) {
      showUserFriendlyError(`Payment capture failed: ${error.message}`);
    } finally {
      history.replaceState(null, '', window.location.pathname);
    }
  }

  // Cloud Jobs Modal
  const cloudJobsModal = document.getElementById('cloud-jobs-modal');
  const closeCloudJobsModal = document.getElementById('close-cloud-jobs-modal');
  const cloudJobsList = document.getElementById('cloud-jobs-list');
  const cloudJobsFilterChips = document.getElementById('cloud-jobs-filter-chips');
  const cloudJobsPagination = document.getElementById('cloud-jobs-pagination');
  const cloudJobsPrevButton = document.getElementById('cloud-jobs-prev');
  const cloudJobsNextButton = document.getElementById('cloud-jobs-next');
  const cloudJobsPageInfo = document.getElementById('cloud-jobs-page-info');
  let unsubscribeCloudJobs = null;
  const knownCloudJobStatuses = new Map();
  let cloudJobsAll = [];
  let cloudJobsFilter = 'all';
  let cloudJobsPage = 1;
  const CLOUD_JOBS_PAGE_SIZE = 8;
  let recoveringSubmittedJobs = false;

  function timestampToMillis(value) {
    if (!value) return 0;
    if (typeof value.toDate === 'function') return value.toDate().getTime();
    if (typeof value.seconds === 'number') return value.seconds * 1000;
    const parsed = Date.parse(String(value));
    return Number.isFinite(parsed) ? parsed : 0;
  }

  function formatDateTime(value) {
    const millis = timestampToMillis(value);
    if (!millis) return '-';
    return new Date(millis).toLocaleString();
  }

  function normalizeJobStatus(status) {
    const normalized = String(status || '').toLowerCase();
    if (normalized === 'succeeded') return 'completed';
    if (normalized === 'queued') return 'submitted';
    return normalized || 'submitted';
  }

  function notifyJobCompletion(job) {
    const bboxName = job?.bbox?.name || `Job ${job.id}`;
    showSuccessMessage(`Cloud job completed: ${bboxName}. Download is now available.`);

    if ('Notification' in window && Notification.permission === 'granted') {
      try {
        new Notification('Cloud Job Completed', {
          body: `${bboxName} is ready for download.`
        });
      } catch (error) {
        console.warn('Browser notification failed:', error.message);
      }
    }
  }

  function getFilteredCloudJobs(jobs = []) {
    if (cloudJobsFilter === 'completed') {
      return jobs.filter((job) => normalizeJobStatus(job.status) === 'completed');
    }
    if (cloudJobsFilter === 'failed') {
      return jobs.filter((job) => normalizeJobStatus(job.status) === 'failed');
    }
    if (cloudJobsFilter === 'processing') {
      return jobs.filter((job) => {
        const status = normalizeJobStatus(job.status);
        return status === 'submitted' || status === 'processing';
      });
    }
    return jobs;
  }

  function updateCloudJobsPagination(totalJobs) {
    const totalPages = Math.max(1, Math.ceil(totalJobs / CLOUD_JOBS_PAGE_SIZE));
    if (cloudJobsPage > totalPages) {
      cloudJobsPage = totalPages;
    }
    if (cloudJobsPageInfo) {
      cloudJobsPageInfo.textContent = `Page ${cloudJobsPage} of ${totalPages}`;
    }
    if (cloudJobsPrevButton) {
      cloudJobsPrevButton.disabled = cloudJobsPage <= 1;
    }
    if (cloudJobsNextButton) {
      cloudJobsNextButton.disabled = cloudJobsPage >= totalPages;
    }
    if (cloudJobsPagination) {
      cloudJobsPagination.style.display = totalJobs > CLOUD_JOBS_PAGE_SIZE ? 'flex' : 'none';
    }
  }

  function renderCloudJobsList(jobs = []) {
    if (!cloudJobsList) return;

    const ordered = [...jobs].sort((a, b) => timestampToMillis(b.submittedAt) - timestampToMillis(a.submittedAt));

    ordered.forEach((job) => {
      const status = normalizeJobStatus(job.status);
      const previousStatus = knownCloudJobStatuses.get(job.id);
      if (previousStatus && previousStatus !== status && status === 'completed') {
        notifyJobCompletion(job);
      }
      knownCloudJobStatuses.set(job.id, status);
    });

    const filtered = getFilteredCloudJobs(ordered);
    updateCloudJobsPagination(filtered.length);

    if (filtered.length === 0) {
      const noItemsMessage = ordered.length === 0
        ? 'No cloud jobs submitted yet.'
        : 'No jobs match the selected filter.';
      cloudJobsList.innerHTML = `<div class="cloud-jobs-empty">${noItemsMessage}</div>`;
      return;
    }

    const start = (cloudJobsPage - 1) * CLOUD_JOBS_PAGE_SIZE;
    const pagedJobs = filtered.slice(start, start + CLOUD_JOBS_PAGE_SIZE);

    cloudJobsList.innerHTML = pagedJobs.map((job) => {
      const status = normalizeJobStatus(job.status);
      const bbox = job.bbox || {};
      const settings = job.settings || {};
      const result = job.result || {};
      const downloadUrl = result.downloadUrl || '';
      const credits = Number(job.creditsCharged || 0);
      const areaKm2 = Number(job.areaKm2 || 0);
      const expiresAt = result.expiresAt ? formatDateTime(result.expiresAt) : '-';
      const submittedAt = formatDateTime(job.submittedAt || job.requestedAt);
      const finishedAt = formatDateTime(job.finishedAt);

      return `
        <article class="cloud-job-item">
          <div class="cloud-job-top">
            <div>
              <strong>${bbox.name || `Job ${job.id}`}</strong><br>
              <small>${job.id}</small>
            </div>
            <span class="cloud-job-status ${status}">${status}</span>
          </div>
          <div class="cloud-job-grid">
            <div><strong>Submitted</strong><br>${submittedAt}</div>
            <div><strong>Completed</strong><br>${finishedAt}</div>
            <div><strong>Area</strong><br>${areaKm2 > 0 ? `${areaKm2.toFixed(2)} km²` : '-'}</div>
            <div><strong>Credits</strong><br>${credits}</div>
            <div><strong>DEM</strong><br>${settings.demType || '-'}</div>
            <div><strong>Resolution</strong><br>${settings.resolutionReductionFactor || '-'}</div>
            <div><strong>Smoothness</strong><br>${settings.smoothness || '-'}</div>
            <div><strong>Expires</strong><br>${expiresAt}</div>
          </div>
          ${job.error ? `<div style="margin-top:8px;color:#c62828;"><strong>Error:</strong> ${job.error}</div>` : ''}
          <div class="cloud-job-actions">
            <button class="btn btn-secondary download-cloud-job-btn" data-download-url="${downloadUrl}" ${downloadUrl ? '' : 'disabled'}>Download STL</button>
          </div>
        </article>
      `;
    }).join('');

    cloudJobsList.querySelectorAll('.download-cloud-job-btn').forEach((button) => {
      button.addEventListener('click', () => {
        const downloadUrl = button.getAttribute('data-download-url');
        if (!downloadUrl) return;
        const link = document.createElement('a');
        link.href = downloadUrl;
        link.target = '_blank';
        link.rel = 'noopener noreferrer';
        link.click();
      });
    });
  }

  if (cloudJobsFilterChips) {
    cloudJobsFilterChips.querySelectorAll('.cloud-chip').forEach((chip) => {
      chip.addEventListener('click', () => {
        const nextFilter = chip.getAttribute('data-filter') || 'all';
        cloudJobsFilter = nextFilter;
        cloudJobsPage = 1;

        cloudJobsFilterChips.querySelectorAll('.cloud-chip').forEach((item) => {
          item.classList.toggle('active', item === chip);
        });

        renderCloudJobsList(cloudJobsAll);
      });
    });
  }

  if (cloudJobsPrevButton) {
    cloudJobsPrevButton.addEventListener('click', () => {
      if (cloudJobsPage <= 1) return;
      cloudJobsPage -= 1;
      renderCloudJobsList(cloudJobsAll);
    });
  }

  if (cloudJobsNextButton) {
    cloudJobsNextButton.addEventListener('click', () => {
      cloudJobsPage += 1;
      renderCloudJobsList(cloudJobsAll);
    });
  }

  function ensureCloudJobsSubscription() {
    if (unsubscribeCloudJobs) return;
    try {
      unsubscribeCloudJobs = subscribeToCloudJobs(
        (jobs) => {
          cloudJobsAll = jobs;
          renderCloudJobsList(cloudJobsAll);
        },
        (error) => {
          console.warn('Cloud jobs subscription failed:', error.message);
          if (cloudJobsList) {
            cloudJobsList.innerHTML = '<div class="cloud-jobs-empty">Unable to load cloud jobs right now.</div>';
          }
        }
      );
    } catch (error) {
      console.warn('Unable to subscribe to cloud jobs:', error.message);
    }
  }

  async function openCloudJobsModal() {
    await refreshCloudProfileUI();
    if (!cloudProfile.enabled) {
      showUserFriendlyError('Cloud Jobs is available after cloud processing is activated.');
      return;
    }

    ensureCloudJobsSubscription();
    if (!recoveringSubmittedJobs) {
      recoveringSubmittedJobs = true;
      recoverSubmittedCloudJobs(20)
        .catch((error) => {
          console.warn('Unable to recover submitted cloud jobs:', error.message);
        })
        .finally(() => {
          recoveringSubmittedJobs = false;
        });
    }
    if ('Notification' in window && Notification.permission === 'default') {
      Notification.requestPermission().catch(() => {});
    }
    if (cloudJobsModal) {
      cloudJobsModal.classList.add('active');
    }
  }

  // Function to update processing mode UI
  function updateProcessingModeUI() {
    const currentMode = getProcessingMode();
    selectedMode = currentMode;

    // Update card active states
    if (browserCard && cloudCard) {
      if (currentMode === ProcessingMode.BROWSER) {
        browserCard.classList.add('active');
        cloudCard.classList.remove('active');
      } else {
        browserCard.classList.remove('active');
        cloudCard.classList.add('active');
      }
    }

    // Show cloud status panel only when cloud card is selected.
    if (cloudModeStatus) {
      cloudModeStatus.style.display = currentMode === ProcessingMode.CLOUD ? 'block' : 'none';
    }

    if (currentMode === ProcessingMode.CLOUD) {
      refreshCloudProfileUI().catch((error) => {
        console.warn('Cloud profile refresh failed:', error.message);
      });
    }
  }

  // Function to save processing mode
  async function saveProcessingModeFunction() {
    if (selectedMode === ProcessingMode.CLOUD) {
      if (CLOUD_PROCESSING_DEV_LOCK) {
        showUserFriendlyError('Cloud Processing is temporarily disabled while this feature is in development.');
        return;
      }

      await refreshCloudProfileUI();
      if (!cloudProfile.enabled) {
        showUserFriendlyError('Cloud mode is not active yet. Buy a credit pack first.');
        return;
      }
    }

    try {
      setProcessingMode(selectedMode);
      updateProcessingModeUI();
      updateGenerateButtonIndicators();
      updateBboxList();
      showSuccessMessage(
        `Processing mode set to ${selectedMode === ProcessingMode.BROWSER ? 'Browser' : 'Cloud'}`
      );
    } catch (error) {
      showUserFriendlyError(`Error saving processing mode: ${error.message}`);
    }
  }

  // Card click handlers
  if (browserCard) {
    browserCard.addEventListener('click', () => {
      selectedMode = ProcessingMode.BROWSER;
      browserCard.classList.add('active');
      if (cloudCard) cloudCard.classList.remove('active');
      if (cloudModeStatus) cloudModeStatus.style.display = 'none';
    });
  }

  if (cloudCard) {
    cloudCard.addEventListener('click', () => {
      selectedMode = ProcessingMode.CLOUD;
      cloudCard.classList.add('active');
      if (browserCard) browserCard.classList.remove('active');
      if (cloudModeStatus) cloudModeStatus.style.display = 'block';
      refreshCloudProfileUI().catch((error) => {
        console.warn('Cloud profile refresh failed:', error.message);
      });
    });
  }

  // Event listener for save button
  if (saveProcessingModeButton) {
    saveProcessingModeButton.addEventListener('click', () => {
      saveProcessingModeFunction().catch((error) => {
        showUserFriendlyError(`Error saving processing mode: ${error.message}`);
      });
    });
  }

  getAvailableCreditPacks().forEach((pack) => {
    const selector = `.cloud-pack-button[data-pack-id="${pack.id}"]`;
    const button = document.querySelector(selector);
    if (button) {
      button.addEventListener('click', () => {
        handleCloudPackPurchase(pack.id);
      });
    }
  });

  applyCloudDevLockUI();

  // Close processing mode modal
  if (closeProcessingModeModal) {
    closeProcessingModeModal.addEventListener('click', () => {
      if (processingModeModal) {
        processingModeModal.classList.remove('active');
      }
    });
  }

  // Close modal when clicking outside
  if (processingModeModal) {
    processingModeModal.addEventListener('click', (e) => {
      if (e.target === processingModeModal) {
        processingModeModal.classList.remove('active');
      }
    });
  }

  if (closeCloudJobsModal) {
    closeCloudJobsModal.addEventListener('click', () => {
      if (cloudJobsModal) {
        cloudJobsModal.classList.remove('active');
      }
    });
  }

  if (cloudJobsModal) {
    cloudJobsModal.addEventListener('click', (e) => {
      if (e.target === cloudJobsModal) {
        cloudJobsModal.classList.remove('active');
      }
    });
  }

  window.addEventListener('open-cloud-jobs-modal', () => {
    openCloudJobsModal().catch((error) => {
      showUserFriendlyError(`Unable to open Cloud Jobs: ${error.message}`);
    });
  });

  // Function to update all generate button indicators
  function updateGenerateButtonIndicators() {
    const indicators = document.querySelectorAll('[data-mode-indicator]');
    const modeLabel = getProcessingModeLabel();
    const isCloud = isCloudProcessingEnabled();

    indicators.forEach(indicator => {
      // Update text (just the label, no "Mode:" prefix)
      indicator.textContent = modeLabel;

      // Update styling
      indicator.classList.remove('browser-mode', 'cloud-mode');
      indicator.classList.add(isCloud ? 'cloud-mode' : 'browser-mode');
    });
  }

  // Update settings modal when opened
  const originalOpenSettingsModal = openSettingsModal;
  function enhancedOpenSettingsModal() {
    originalOpenSettingsModal();
    updateProcessingModeUI();
  }
  // Replace the function
  openSettingsModal = enhancedOpenSettingsModal;

  // Initial cloud profile refresh + return capture handling.
  refreshCloudProfileUI().catch((error) => {
    console.warn('Cloud profile initial refresh failed:', error.message);
  });
  handlePaypalReturnIfNeeded().catch((error) => {
    console.warn('PayPal return handling failed:', error.message);
  });

  // Initialize welcome modal
  initWelcomeModal();

  // Check if user came from landing page with settings hash
  if (window.location.hash === '#settings') {
    // Remove hash and open settings modal
    history.replaceState(null, null, ' ');
    // Wait a bit for modal to initialize
    setTimeout(() => {
      openSettingsModal();
    }, 500);
  }

  // Check if user came from landing page with processing-mode hash
  if (window.location.hash === '#processing-mode') {
    // Remove hash and open processing mode modal
    history.replaceState(null, null, ' ');
    // Wait a bit for modal to initialize
    setTimeout(() => {
      const processingModeModal = document.getElementById('processing-mode-modal');
      if (processingModeModal) {
        processingModeModal.classList.add('active');
        refreshCloudProfileUI().catch((error) => {
          console.warn('Cloud profile refresh failed:', error.message);
        });
      }
    }, 500);
  }

  if (window.location.hash === '#cloud-jobs') {
    history.replaceState(null, null, ' ');
    setTimeout(() => {
      openCloudJobsModal().catch((error) => {
        showUserFriendlyError(`Unable to open Cloud Jobs: ${error.message}`);
      });
    }, 500);
  }

  // API key check is now handled by the welcome modal
  // The welcome modal will guide users to set up their API key

  // Function to calculate distance between two points using Haversine formula
  function calculateDistance(lat1, lon1, lat2, lon2) {
    const R = 6371; // Radius of Earth in km
    const dLat = (lat2 - lat1) * Math.PI / 180;
    const dLon = (lon2 - lon1) * Math.PI / 180;
    const a =
      Math.sin(dLat / 2) * Math.sin(dLat / 2) +
      Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
      Math.sin(dLon / 2) * Math.sin(dLon / 2);
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    const distance = R * c;
    return distance;
  }

  // Function to calculate area of bounding box
  function calculateArea(bbox) {
    const width = calculateDistance(bbox.southwestLat, bbox.southwestLng, bbox.southwestLat, bbox.northeastLng);
    const height = calculateDistance(bbox.southwestLat, bbox.southwestLng, bbox.northeastLat, bbox.southwestLng);
    return width * height;
  }

  // Maximum recommended area in km²
  const MAX_RECOMMENDED_AREA = 100;

  // Function to check if area exceeds recommended maximum
  function isAreaTooLarge(area) {
    return area > MAX_RECOMMENDED_AREA;
  }

  // Function to show area warning
  function showAreaWarning(area, bboxName) {
    const usageGuideDocsUrl = './docs.html#usage';
    const warningDiv = document.createElement('div');
    warningDiv.className = 'area-warning-notification';
    warningDiv.innerHTML = `
      <div class="warning-content">
        <span class="warning-icon">⚠️</span>
        <div class="warning-text">
          <strong>Large Area Warning (browser computation)</strong>
          <p>The box exceeds the recommended maximum of ${MAX_RECOMMENDED_AREA} km² and the process could fail.</p>
          <p style="font-size: 13px; margin-top: 8px;">
            Processing may be slow. Consider reducing area or increasing the resolution reduction factor.
            For large areas, you can also switch to Cloud Processing mode.
            <a href="${usageGuideDocsUrl}" target="_blank" rel="noopener noreferrer" style="color: #fff; text-decoration: underline;">See Usage Guide</a>.
          </p>
        </div>
      </div>
    `;

    warningDiv.style.cssText = `
      position: fixed;
      top: 20px;
      right: 20px;
      background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
      color: white;
      padding: 16px 24px;
      border-radius: 8px;
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
      z-index: 10001;
      max-width: 400px;
      animation: slideInRight 0.3s ease;
    `;

    document.body.appendChild(warningDiv);

    setTimeout(() => {
      warningDiv.style.animation = 'fadeOut 0.5s ease';
      setTimeout(() => warningDiv.remove(), 500);
    }, 8000);
  }

  // Function to clear temporary drawing elements
  function clearTemporaryDrawing() {
    // Remove temporary markers
    temporaryMarkers.forEach(marker => {
      map.removeLayer(marker);
    });
    temporaryMarkers = [];

    // Remove preview polygon
    if (previewPolygon) {
      map.removeLayer(previewPolygon);
      previewPolygon = null;
    }
  }

  // Function to update the drawing preview
  function updateDrawingPreview() {
    // Remove existing preview
    if (previewPolygon) {
      map.removeLayer(previewPolygon);
      previewPolygon = null;
    }

    if (clickedPoints.length < 2) return;

    // Create preview based on number of points
    if (clickedPoints.length === 2) {
      // Show a line between the two points
      previewPolygon = L.polyline(clickedPoints, {
        color: 'red',
        weight: 2,
        opacity: 0.8,
        dashArray: '5, 10'
      }).addTo(map);
    } else if (clickedPoints.length === 3) {
      // Show a triangle (incomplete polygon)
      previewPolygon = L.polygon(clickedPoints, {
        color: 'red',
        weight: 2,
        opacity: 0.8,
        fillOpacity: 0.1,
        dashArray: '5, 10'
      }).addTo(map);
    } else if (clickedPoints.length === 4) {
      // Show the bounding box preview based on min/max coordinates
      const bbox = createBoundingBoxFromPoints(clickedPoints);
      previewPolygon = L.rectangle([
        [bbox.southwestLat, bbox.southwestLng],
        [bbox.northeastLat, bbox.northeastLng]
      ], {
        color: 'red',
        weight: 2,
        opacity: 0.8,
        fillOpacity: 0.2
      }).addTo(map);
    }
  }

  // Function to create bounding box from 4 points
  function createBoundingBoxFromPoints(points) {
    // Find the min/max coordinates to create a proper bounding box
    const lats = points.map(p => p.lat);
    const lngs = points.map(p => p.lng);

    const southwestLat = Math.min(...lats);
    const northeastLat = Math.max(...lats);
    const southwestLng = Math.min(...lngs);
    const northeastLng = Math.max(...lngs);

    return {
      southwestLat,
      southwestLng,
      northeastLat,
      northeastLng,
      visible: true,
      name: `Box ${boundingBoxes.length + 1}`,
      demType: 'SRTMGL1',
      resolutionReductionFactor: 2,
      dimensionsLinked: true  // Default: dimensions are linked
    };
  }

  // Helpers for interactive bbox editing
  function getCornerLatLngs(bbox) {
    return {
      sw: L.latLng(bbox.southwestLat, bbox.southwestLng),
      se: L.latLng(bbox.southwestLat, bbox.northeastLng),
      nw: L.latLng(bbox.northeastLat, bbox.southwestLng),
      ne: L.latLng(bbox.northeastLat, bbox.northeastLng)
    };
  }

  function removeHandles(index) {
    if (!bboxHandles[index]) return;
    bboxHandles[index].forEach(handle => {
      map.removeLayer(handle);
    });
    bboxHandles[index] = null;
  }

  function updateRectangleLayer(index) {
    const bbox = boundingBoxes[index];
    if (!bbox || !bboxLayers[index]) return;
    bboxLayers[index].setBounds([[bbox.southwestLat, bbox.southwestLng], [bbox.northeastLat, bbox.northeastLng]]);
  }

  function updateHandlePositions(index) {
    if (!bboxHandles[index]) return;
    const corners = getCornerLatLngs(boundingBoxes[index]);
    bboxHandles[index].forEach((handle, handleIndex) => {
      handle.setLatLng(corners[handleOrder[handleIndex]]);
    });
  }

  function updateBboxFromHandleDrag(index, cornerKey, latlng, finalize = false) {
    const bbox = boundingBoxes[index];
    if (!bbox) return;

    const updated = { ...bbox };
    switch (cornerKey) {
      case 'sw':
        updated.southwestLat = latlng.lat;
        updated.southwestLng = latlng.lng;
        break;
      case 'nw':
        updated.northeastLat = latlng.lat;
        updated.southwestLng = latlng.lng;
        break;
      case 'ne':
        updated.northeastLat = latlng.lat;
        updated.northeastLng = latlng.lng;
        break;
      case 'se':
        updated.southwestLat = latlng.lat;
        updated.northeastLng = latlng.lng;
        break;
      default:
        break;
    }

    // Normalize coordinates so southwest/northeast stay consistent
    const minLat = Math.min(updated.southwestLat, updated.northeastLat);
    const maxLat = Math.max(updated.southwestLat, updated.northeastLat);
    const minLng = Math.min(updated.southwestLng, updated.northeastLng);
    const maxLng = Math.max(updated.southwestLng, updated.northeastLng);

    updated.southwestLat = minLat;
    updated.northeastLat = maxLat;
    updated.southwestLng = minLng;
    updated.northeastLng = maxLng;

    // Keep dimensions aligned with the new aspect ratio
    updated.width = bbox.width || calculateDefaultWidth(updated);
    updated.length = calculateLinkedLength(updated);

    boundingBoxes[index] = updated;
    updateRectangleLayer(index);
    updateHandlePositions(index);

    if (finalize) {
      localStorage.setItem('boundingBoxes', JSON.stringify(boundingBoxes));
      updateBboxList();
    }
  }

  function createHandlesForBbox(index) {
    const bbox = boundingBoxes[index];
    if (!bbox || !bbox.visible) return;

    removeHandles(index);
    const corners = getCornerLatLngs(bbox);
    const handles = handleOrder.map(key => {
      const marker = L.marker(corners[key], {
        icon: bboxHandleIcon,
        draggable: true,
        autoPan: true,
        zIndexOffset: 1000
      });
      marker.on('drag', (event) => updateBboxFromHandleDrag(index, key, event.target.getLatLng(), false));
      marker.on('dragend', (event) => updateBboxFromHandleDrag(index, key, event.target.getLatLng(), true));
      marker.addTo(map);
      return marker;
    });

    bboxHandles[index] = handles;
  }

  function ensureHandlesForBbox(index) {
    const bbox = boundingBoxes[index];
    if (!bbox) return;

    if (!bbox.editing || !bbox.visible) {
      removeHandles(index);
      return;
    }

    if (!bboxHandles[index]) {
      createHandlesForBbox(index);
    } else {
      updateHandlePositions(index);
    }
  }

  // Function to update the bounding box list in the panel
  function updateBboxList() {
    if (!bboxList) {
      console.error('bboxList is null, cannot update bounding box list.');
      return;
    }
    bboxList.innerHTML = '';
    boundingBoxes.forEach((bbox, index) => {
      const listItem = document.createElement('li');
      if (!listItem) {
        console.error('listItem is null, cannot create list item for bounding box index:', index);
        return;
      }

      // Calculate default width and length if not already defined
      if (bbox.width === undefined || bbox.width === null || isNaN(bbox.width)) {
        bbox.width = calculateDefaultWidth(bbox) || 100;
      }
      if (bbox.length === undefined || bbox.length === null || isNaN(bbox.length)) {
        bbox.length = 100;
      }

      // Initialize dimensionsLinked property if not set (default: true)
      if (bbox.dimensionsLinked === undefined) {
        bbox.dimensionsLinked = true;
      }

      // Calculate dimensions
      const width = calculateDistance(bbox.southwestLat, bbox.southwestLng, bbox.southwestLat, bbox.northeastLng);
      const height = calculateDistance(bbox.southwestLat, bbox.southwestLng, bbox.northeastLat, bbox.southwestLng);
      const area = calculateArea(bbox);
      const isLargeArea = isAreaTooLarge(area);
      const showLargeAreaWarning = isLargeArea && !isCloudProcessingEnabled();
      const showCloudCreditEstimate = isCloudProcessingEnabled();
      const requiredCloudCredits = calculateCloudCreditsForArea(area);
      const availableCloudCredits = Number(cloudProfile?.creditBalance || 0);
      const hasEnoughCloudCredits = availableCloudCredits >= requiredCloudCredits;

      // Initialize collapsed state if not set
      if (bbox.collapsed === undefined) {
        bbox.collapsed = false;
      }

      listItem.innerHTML = `
            <div class="bbox-title-container">
              <button class="collapse-button" data-index="${index}" title="${bbox.collapsed ? 'Expand' : 'Collapse'}" style="
                background: none;
                border: none;
                color: #1976d2;
                cursor: pointer;
                font-size: 18px;
                padding: 0 8px 0 0;
                transition: transform 0.3s ease;
                transform: rotate(${bbox.collapsed ? '-90deg' : '0deg'});
              ">${bbox.collapsed ? '▶' : '▼'}</button>
              <span style="font-weight: bold; color: red;">${bbox.name || `Box ${index + 1}`}</span>
              <div class="bbox-title-actions">
                <button class="zoom-to-bbox-button" data-index="${index}" title="Zoom to box">🔍</button>
                <button class="edit-button" data-index="${index}">${bbox.editing ? 'Finish Edit' : 'Edit'}</button>
                <button class="rename-button" data-index="${index}">Rename</button>
                <button class="toggle-button" data-index="${index}">${bbox.visible ? 'Hide' : 'Show'}</button>
                <button class="delete-button" data-index="${index}">Delete</button>
              </div>
            </div>
            <div class="bbox-collapsible-content" style="display: ${bbox.collapsed ? 'none' : 'block'};">
            <div class="coordinates">
              <span>North: ${bbox.northeastLat.toFixed(4)}</span>
              <span>East: ${bbox.northeastLng.toFixed(4)}</span>
              <span>South: ${bbox.southwestLat.toFixed(4)}</span>
              <span>West: ${bbox.southwestLng.toFixed(4)}</span>
            </div>
            <div class="dimensions">
              <p>Width: ${width.toFixed(2)} km</p>
              <p>Height: ${height.toFixed(2)} km</p>
              <p style="${showLargeAreaWarning ? 'color: #f44336; font-weight: bold;' : ''}">
                Area: ${area.toFixed(2)} km²
                ${showLargeAreaWarning ? '<span style="margin-left: 8px;" title="Area exceeds recommended maximum">⚠️</span>' : ''}
              </p>
              ${showCloudCreditEstimate ? `
                <div class="cloud-credits-badge" style="
                  background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
                  border: 2px solid #1e88e5;
                  border-radius: 8px;
                  padding: 10px 12px;
                  margin-top: 10px;
                  margin-bottom: 12px;
                  color: #0d47a1;
                  box-shadow: 0 2px 6px rgba(30, 136, 229, 0.2);
                ">
                  <div style="font-size: 11px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;">☁️ Cloud Processing Cost</div>
                  <div style="font-size: 18px; font-weight: 800; line-height: 1.2; margin-top: 3px;">${requiredCloudCredits} credits required</div>
                  <div style="font-size: 12px; margin-top: 4px; color: ${hasEnoughCloudCredits ? '#1b5e20' : '#b71c1c'};">
                    ${hasEnoughCloudCredits ? `Balance: ${availableCloudCredits} credits` : `Balance: ${availableCloudCredits} credits (insufficient)`}
                  </div>
                </div>
              ` : ''}
              ${showLargeAreaWarning ? `
                <div class="area-warning-badge" style="
                  background: #fff3e0;
                  border: 1px solid #ff9800;
                  border-radius: 4px;
                  padding: 8px;
                  margin-top: 12px;
                  margin-bottom: 16px;
                  font-size: 12px;
                  color: #e65100;
                ">
                  <strong>⚠️ Large Area</strong><br>
                  Processing may be slow. Consider reducing area or increasing resolution reduction factor.
                  You can also switch to Cloud Processing mode.
                  <a href="./docs.html#usage" target="_blank" rel="noopener noreferrer" style="color: #bf360c; text-decoration: underline; font-weight: 600;">See Usage Guide</a>.
                </div>
              ` : ''}
            </div>
          <div class="generation-settings">
            <h4>3D Model Settings</h4>
            
            <div class="dimensions-container">
              <div class="dimensions-wrapper">
                <div class="dimensions-inputs">
                  <div class="dimension-row">
                    <label for="width-${index}">Width (mm):</label>
                    <input type="number" id="width-${index}" data-index="${index}" value="${bbox.width ? bbox.width.toFixed(2) : '100'}" class="dimension-input">
                  </div>
                  <div class="dimension-row">
                    <label for="length-${index}">Length (mm):</label>
                    <input type="number" id="length-${index}" data-index="${index}" value="${bbox.length ? bbox.length.toFixed(2) : '100'}" class="dimension-input">
                  </div>
                </div>
                <button class="dimension-link-button ${bbox.dimensionsLinked ? 'linked' : 'unlinked'}" data-index="${index}" aria-pressed="${bbox.dimensionsLinked ? 'true' : 'false'}" title="${bbox.dimensionsLinked ? 'Dimensions linked (Maintain Aspect Ratio)' : 'Dimensions unlinked (Independent Width/Length)'}">
                  <svg class="link-icon" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                    ${bbox.dimensionsLinked ?
          /* Linked Icon - Tied */
          `<path d="M10 7H7a4 4 0 0 0 0 8h3"/><path d="M14 17h3a4 4 0 0 0 0-8h-3"/><line x1="9" y1="12" x2="15" y2="12"/>` :
          /* Unlinked Icon - Open */
          `<path d="M10 7H7a4 4 0 0 0 0 8h3"/><path d="M14 17h3a4 4 0 0 0 0-8h-3"/><line x1="10" y1="10.5" x2="8.5" y2="12"/><line x1="14" y1="13.5" x2="15.5" y2="12"/>`
        }
                  </svg>
                </button>
              </div>
            </div>

            <label for="vertical-exaggeration-${index}">Vertical Exaggeration:</label>
            <input type="number" id="vertical-exaggeration-${index}" data-index="${index}" value="${bbox.verticalExaggeration || 1}">

            <label for="smoothness-${index}">Smoothness:</label>
            <input type="number" id="smoothness-${index}" data-index="${index}" value="${bbox.smoothness || 0}">

            <label for="dem-type-${index}">DEM Source:</label>
            <select id="dem-type-${index}" data-index="${index}">
              <option value="SRTMGL3" ${bbox.demType === 'SRTMGL3' ? 'selected' : ''}>SRTMGL3 (SRTM GL3 90m)</option>
              <option value="SRTMGL1" ${bbox.demType === 'SRTMGL1' || !bbox.demType ? 'selected' : ''}>SRTMGL1 (SRTM GL1 30m)</option>
              <option value="SRTMGL1_E" ${bbox.demType === 'SRTMGL1_E' ? 'selected' : ''}>SRTMGL1_E (SRTM GL1 Ellipsoidal 30m)</option>
              <option value="AW3D30" ${bbox.demType === 'AW3D30' ? 'selected' : ''}>AW3D30 (ALOS World 3D 30m)</option>
              <option value="AW3D30_E" ${bbox.demType === 'AW3D30_E' ? 'selected' : ''}>AW3D30_E (ALOS World 3D Ellipsoidal, 30m)</option>
              <option value="SRTM15Plus" ${bbox.demType === 'SRTM15Plus' ? 'selected' : ''}>SRTM15Plus (Global Bathymetry SRTM15+ V2.1 500m)</option>
              <option value="NASADEM" ${bbox.demType === 'NASADEM' ? 'selected' : ''}>NASADEM (NASADEM Global DEM)</option>
              <option value="COP30" ${bbox.demType === 'COP30' ? 'selected' : ''}>COP30 (Copernicus Global DSM 30m)</option>
              <option value="COP90" ${bbox.demType === 'COP90' ? 'selected' : ''}>COP90 (Copernicus Global DSM 90m)</option>
              <option value="EU_DTM" ${bbox.demType === 'EU_DTM' ? 'selected' : ''}>EU_DTM (DTM 30m)</option>
              <option value="GEDI_L3" ${bbox.demType === 'GEDI_L3' ? 'selected' : ''}>GEDI_L3 (DTM 1000m)</option>
              <option value="GEBCOIceTopo" ${bbox.demType === 'GEBCOIceTopo' ? 'selected' : ''}>GEBCOIceTopo (Global Bathymetry 500m)</option>
              <option value="GEBCOSubIceTopo" ${bbox.demType === 'GEBCOSubIceTopo' ? 'selected' : ''}>GEBCOSubIceTopo (Global Bathymetry 500m)</option>
            </select>

            <label for="resolution-reduction-${index}">Resolution Reduction Factor:</label>
            <input type="number" id="resolution-reduction-${index}" data-index="${index}" value="${bbox.resolutionReductionFactor || 2}" min="1" step="1">
            <p class="resolution-description">
              <b>1</b> - Original DEM resolution.<br>
              <b>2</b> - Process every other cell.<br>
              <b>3</b> - Process every third cell.<br>
              ... and so on ...
              <div>Higher values reduce detail but also processing time, a default value of 2 could be a good compromise between computational time and quality.</div>
            </p>

            <h4 style="margin-top: 20px; border-top: 1px solid #ddd; padding-top: 15px;">Generation settings</h4>
            
            <label for="color-mode-${index}">Composition mode:</label>
            <select id="color-mode-${index}" data-index="${index}">
              <option value="solid" ${!bbox.colorMode || bbox.colorMode === 'solid' ? 'selected' : ''}>Full-Solid</option>
              <option value="topographic" ${bbox.colorMode === 'topographic' ? 'selected' : ''}>Topographic (Elevation-based)</option>
            </select>

            <div id="band-controls-${index}" style="margin-top: 12px; padding: 10px; border: 1px solid #e0e0e0; border-radius: 8px; display: ${bbox.colorMode === 'topographic' ? 'block' : 'none'};">
              <label for="band-mode-${index}">Band Mode:</label>
              <select id="band-mode-${index}" data-index="${index}">
                <option value="adaptive" ${!bbox.bandMode || bbox.bandMode === 'adaptive' ? 'selected' : ''}>Adaptive</option>
                <option value="equal_step" ${bbox.bandMode === 'equal_step' ? 'selected' : ''}>Equal Step</option>
                <option value="coastal_preserve" ${bbox.bandMode === 'coastal_preserve' ? 'selected' : ''}>Coastal Preserve</option>
              </select>
              <span id="band-mode-help-${index}" style="font-size: 11px; color: #666; display: block; margin-top: 6px;"></span>

              <div style="margin-top: 10px;">
                <label for="num-bands-${index}">Topography Bands:</label>
                <input type="number" id="num-bands-${index}" data-index="${index}" min="1" max="8" step="1" value="${bbox.numBands || 3}">
              </div>
              <div id="coastal-preserve-controls-${index}" style="margin-top: 10px; display: ${bbox.bandMode === 'coastal_preserve' ? 'block' : 'none'};">
                <label for="coastal-preserve-percent-${index}">Coastal Preserve (%):</label>
                <input type="number" id="coastal-preserve-percent-${index}" data-index="${index}" min="0" max="100" step="1" value="${Number.isFinite(Number(bbox.coastalPreservePercent)) ? bbox.coastalPreservePercent : 12}">
                <span id="coastal-preserve-hint-${index}" style="font-size: 11px; color: #666; display: block; margin-top: 4px;"></span>
                <span style="font-size: 11px; color: #666; display: block; margin-top: 4px;">Higher values preserve more low-elevation/coastal terrain with the base (less low-level band splitting). Lower values start topographic banding earlier.</span>
              </div>
              <span style="font-size: 11px; color: #666; display: block; margin-top: 6px;">Default: 3 (base + 3 topo parts = 4 objects)</span>
            </div>

            <div id="solid-color-controls-${index}" style="display: ${!bbox.colorMode || bbox.colorMode === 'solid' ? 'block' : 'none'};">
              <label for="model-color-${index}">Terrain Color:</label>
              <input type="color" id="model-color-${index}" data-index="${index}" value="${bbox.modelColor || '#1976d2'}">
              <span style="font-size: 11px; color: #666; margin-left: 8px; display: block; margin-top: 4px;">
                💡 Base layer uses contrasting color automatically
              </span>
            </div>

            <div id="topo-color-controls-${index}" style="display: ${bbox.colorMode === 'topographic' ? 'block' : 'none'};">
              <span style="font-size: 12px; color: #666; display: block;">Topographic mode now focuses on geometry banding. Preview is shown as a solid model.</span>
            </div>

            <div class="generate-button-container" style="margin-top: 30px;">
              <button class="generate-button" data-index="${index}" style="padding: 12px 16px; width: fit-content;">Generate 3D Model</button>
              <span class="processing-mode-badge ${isCloudProcessingEnabled() ? 'cloud-mode' : 'browser-mode'}" data-mode-indicator="${index}" title="Current processing mode">
                ${getProcessingModeLabel()}
              </span>
            </div>
          </div>
          </div>
        `;
      listItem.dataset.index = index; // Store the index in the list item
      bboxList.appendChild(listItem);

      // Collapse button handler
      const collapseButton = listItem.querySelector('.collapse-button');
      if (collapseButton) {
        collapseButton.addEventListener('click', () => {
          bbox.collapsed = !bbox.collapsed;
          updateBboxList();
          localStorage.setItem('boundingBoxes', JSON.stringify(boundingBoxes));
        });
      }

      // Zoom to bbox handler
      const zoomToBboxButton = listItem.querySelector('.zoom-to-bbox-button');
      if (zoomToBboxButton) {
        zoomToBboxButton.addEventListener('click', () => {
          const bounds = L.latLngBounds(
            [bbox.southwestLat, bbox.southwestLng],
            [bbox.northeastLat, bbox.northeastLng]
          );
          map.fitBounds(bounds, { padding: [30, 30], animate: true });
        });
      }

      const renameButton = listItem.querySelector('.rename-button');
      if (!renameButton) {
        console.error('renameButton is null for bounding box index:', index);
      } else {
        renameButton.addEventListener('click', () => {
          const newName = prompt('Enter new name for the bounding box:', bbox.name || `Box ${index + 1}`);
          if (newName) {
            bbox.name = newName;
            updateBboxList();
            localStorage.setItem('boundingBoxes', JSON.stringify(boundingBoxes));
          }
        });
      }

      const editButton = listItem.querySelector('.edit-button');
      if (editButton) {
        editButton.addEventListener('click', () => {
          bbox.editing = !bbox.editing;
          if (bbox.editing) {
            createHandlesForBbox(index);
          } else {
            removeHandles(index);
          }
          localStorage.setItem('boundingBoxes', JSON.stringify(boundingBoxes));
          updateBboxList();
        });
      }

      const deleteButton = listItem.querySelector('.delete-button');
      if (deleteButton) {
        deleteButton.addEventListener('click', async () => {
          // Get the bbox before removing it
          const bboxToDelete = boundingBoxes[index];

          if (bboxLayers[index]) {
            map.removeLayer(bboxLayers[index]);
          }
          removeHandles(index);
          bboxLayers.splice(index, 1); // Remove the layer from the array
          bboxHandles.splice(index, 1); // Remove handles
          boundingBoxes.splice(index, 1); // Remove the bounding box from the array
          updateBboxList();
          localStorage.setItem('boundingBoxes', JSON.stringify(boundingBoxes));

          // Delete cached data for this bbox from IndexedDB to free memory
          // This ensures all data stored for this box is cleaned up
          if (bboxToDelete && bboxToDelete.demType) {
            try {
              await deleteCacheForBbox(bboxToDelete, bboxToDelete.demType);
            } catch (error) {
              console.warn('Failed to clean up cache for deleted box:', error);
            }
          }
        });
      }

      const toggleButton = listItem.querySelector('.toggle-button');
      if (toggleButton) {
        toggleButton.addEventListener('click', () => {
          bbox.visible = !bbox.visible;
          localStorage.setItem('boundingBoxes', JSON.stringify(boundingBoxes));

          if (bboxLayers[index]) {
            bboxLayers[index].setStyle({ opacity: bbox.visible ? 1 : 0, fillOpacity: bbox.visible ? 0.2 : 0 });
          }
          if (!bbox.visible && bbox.editing) {
            bbox.editing = false;
            removeHandles(index);
          }
          updateBboxList();
        });
      }

      const demTypeSelect = listItem.querySelector(`#dem-type-${index}`);
      if (demTypeSelect) {
        demTypeSelect.addEventListener('change', (event) => {
          const demType = event.target.value;
          bbox.demType = demType;
          localStorage.setItem('boundingBoxes', JSON.stringify(boundingBoxes));
        });
      }

      const resolutionReductionInput = listItem.querySelector(`#resolution-reduction-${index}`);
      if (resolutionReductionInput) {
        resolutionReductionInput.addEventListener('change', (event) => {
          const resolutionReductionFactor = parseInt(event.target.value, 10);
          bbox.resolutionReductionFactor = resolutionReductionFactor;
          localStorage.setItem('boundingBoxes', JSON.stringify(boundingBoxes));
        });
      }

      // Color mode selection
      const colorModeSelect = listItem.querySelector(`#color-mode-${index}`);
      if (colorModeSelect) {
        colorModeSelect.addEventListener('change', (event) => {
          const colorMode = event.target.value;
          bbox.colorMode = colorMode;

          // Toggle visibility of color controls
          const solidControls = listItem.querySelector(`#solid-color-controls-${index}`);
          const topoControls = listItem.querySelector(`#topo-color-controls-${index}`);
          const bandControls = listItem.querySelector(`#band-controls-${index}`);

          if (colorMode === 'solid') {
            solidControls.style.display = 'block';
            topoControls.style.display = 'none';
            if (bandControls) bandControls.style.display = 'none';
          } else {
            solidControls.style.display = 'none';
            topoControls.style.display = 'block';
            if (bandControls) bandControls.style.display = 'block';
            // Update color preview when switching to topographic mode
            updateColorPreview(index, bbox.colorScheme || 'terrain');
          }

          const bandModeEl = listItem.querySelector(`#band-mode-${index}`);
          const numBandsEl = listItem.querySelector(`#num-bands-${index}`);
          const coastalPreserveEl = listItem.querySelector(`#coastal-preserve-percent-${index}`);
          const topoSelected = colorMode === 'topographic';
          if (bandModeEl) bandModeEl.disabled = !topoSelected;
          if (numBandsEl) numBandsEl.disabled = !topoSelected;
          if (coastalPreserveEl) coastalPreserveEl.disabled = !topoSelected;
          updateCoastalPreserveControls(index, bbox.bandMode || 'adaptive', topoSelected);

          localStorage.setItem('boundingBoxes', JSON.stringify(boundingBoxes));
        });
      }

      // Solid color input
      const modelColorInput = listItem.querySelector(`#model-color-${index}`);
      if (modelColorInput) {
        modelColorInput.addEventListener('change', (event) => {
          bbox.modelColor = event.target.value;
          localStorage.setItem('boundingBoxes', JSON.stringify(boundingBoxes));
        });
      }

      // Base color input
      const baseColorInput = listItem.querySelector(`#base-color-${index}`);
      if (baseColorInput) {
        baseColorInput.addEventListener('change', (event) => {
          bbox.baseColor = event.target.value;
          localStorage.setItem('boundingBoxes', JSON.stringify(boundingBoxes));
        });
      }

      // Color scheme selection
      const colorSchemeSelect = listItem.querySelector(`#color-scheme-${index}`);
      if (colorSchemeSelect) {
        colorSchemeSelect.addEventListener('change', (event) => {
          bbox.colorScheme = event.target.value;
          updateColorPreview(index, event.target.value);
          localStorage.setItem('boundingBoxes', JSON.stringify(boundingBoxes));
        });
        // Initial preview
        updateColorPreview(index, bbox.colorScheme || 'terrain');
      }

      const bandModeSelect = listItem.querySelector(`#band-mode-${index}`);
      if (bandModeSelect) {
        bandModeSelect.disabled = (bbox.colorMode || 'solid') !== 'topographic';
        updateBandModeHelp(index, bandModeSelect.value);
        updateCoastalPreserveControls(index, bandModeSelect.value, (bbox.colorMode || 'solid') === 'topographic');
        bandModeSelect.addEventListener('change', (event) => {
          const selectedBandMode = event.target.value;
          if (selectedBandMode === 'equal_step') {
            bbox.bandMode = 'equal_step';
          } else if (selectedBandMode === 'coastal_preserve') {
            bbox.bandMode = 'coastal_preserve';
          } else {
            bbox.bandMode = 'adaptive';
          }
          updateBandModeHelp(index, bbox.bandMode);
          updateCoastalPreserveControls(index, bbox.bandMode, (bbox.colorMode || 'solid') === 'topographic');
          localStorage.setItem('boundingBoxes', JSON.stringify(boundingBoxes));
        });
      }

      const coastalPreserveInput = listItem.querySelector(`#coastal-preserve-percent-${index}`);
      if (coastalPreserveInput) {
        coastalPreserveInput.disabled = (bbox.colorMode || 'solid') !== 'topographic' || (bbox.bandMode || 'adaptive') !== 'coastal_preserve';
        updateCoastalPreserveHint(index, coastalPreserveInput.value);
        coastalPreserveInput.addEventListener('input', (event) => {
          updateCoastalPreserveHint(index, event.target.value);
        });
        coastalPreserveInput.addEventListener('change', (event) => {
          const parsed = parseFloat(event.target.value);
          const normalized = Number.isFinite(parsed) ? Math.max(0, Math.min(100, parsed)) : 12;
          bbox.coastalPreservePercent = normalized;
          event.target.value = String(normalized);
          updateCoastalPreserveHint(index, normalized);
          localStorage.setItem('boundingBoxes', JSON.stringify(boundingBoxes));
        });
      }

      const numBandsInput = listItem.querySelector(`#num-bands-${index}`);
      if (numBandsInput) {
        numBandsInput.disabled = (bbox.colorMode || 'solid') !== 'topographic';
        numBandsInput.addEventListener('change', (event) => {
          const parsed = parseInt(event.target.value, 10);
          bbox.numBands = Number.isFinite(parsed) ? Math.max(1, Math.min(8, parsed)) : 3;
          event.target.value = String(bbox.numBands);
          localStorage.setItem('boundingBoxes', JSON.stringify(boundingBoxes));
        });
      }

      const generateButton = listItem.querySelector('.generate-button');
      if (generateButton) {
        generateButton.addEventListener('click', async () => {
          const index = parseInt(listItem.dataset.index, 10);

          // 🛡️ Prevent double-clicks by disabling button
          if (generateButton.disabled) {
            return;
          }

          // Retrieve the settings from form inputs
          const settings = {
            width: document.getElementById(`width-${index}`).value,
            length: document.getElementById(`length-${index}`).value,
            verticalExaggeration: document.getElementById(`vertical-exaggeration-${index}`).value,
            smoothness: document.getElementById(`smoothness-${index}`).value,
            demType: document.getElementById(`dem-type-${index}`).value,
            resolutionReductionFactor: document.getElementById(`resolution-reduction-${index}`).value,
            colorMode: document.getElementById(`color-mode-${index}`).value,
            modelColor: document.getElementById(`model-color-${index}`).value,
            colorScheme: document.getElementById(`color-scheme-${index}`)?.value || 'terrain',
            baseColor: document.getElementById(`base-color-${index}`)?.value || '#FFFFFF',
            bandMode: document.getElementById(`band-mode-${index}`)?.value || 'adaptive',
            numBands: document.getElementById(`num-bands-${index}`)?.value || 3,
            coastalPreservePercent: document.getElementById(`coastal-preserve-percent-${index}`)?.value || 12
          };

          // 🎯 NEW: Validate all inputs before proceeding
          const validation = validateAllInputs(settings);
          if (!validation.valid) {
            // Show validation errors to user
            showValidationSummary(validation.errors);
            console.error('❌ Validation failed:', validation.errors);
            return; // Stop here - don't generate with invalid inputs
          }

          // All inputs are valid! Use the validated settings

          // Disable button during generation
          generateButton.disabled = true;
          const originalButtonText = generateButton.textContent;
          generateButton.textContent = 'Generating...';
          generateButton.style.opacity = '0.6';
          generateButton.style.cursor = 'not-allowed';

          // Store the VALIDATED settings in the boundingBoxes array
          bbox.width = validation.settings.width;
          bbox.length = validation.settings.length;
          bbox.verticalExaggeration = validation.settings.verticalExaggeration;
          bbox.smoothness = validation.settings.smoothness;
          bbox.demType = validation.settings.demType;
          bbox.resolutionReductionFactor = validation.settings.resolutionReductionFactor;
          bbox.colorMode = validation.settings.colorMode;
          bbox.modelColor = validation.settings.modelColor;
          bbox.colorScheme = validation.settings.colorScheme;
          bbox.baseColor = validation.settings.baseColor;
          bbox.bandMode = validation.settings.bandMode;
          bbox.numBands = validation.settings.numBands;
          bbox.coastalPreservePercent = validation.settings.coastalPreservePercent;

          localStorage.setItem('boundingBoxes', JSON.stringify(boundingBoxes));
          setSelectedDemType(validation.settings.demType);

          // 🎯 NEW: Create enhanced progress indicator with cancel button
          const progressIndicator = createProgressIndicator(
            'Generating 3D Model',
            () => {
              // This function is called when user clicks "Cancel"
              // Cancel operations immediately
              cancelCurrentOperation();

              // Show cancelled message after a brief delay to let cancel propagate
              setTimeout(() => {
                showCancelledMessage();
                // Re-enable button
                generateButton.disabled = false;
                generateButton.textContent = originalButtonText;
                generateButton.style.opacity = '1';
                generateButton.style.cursor = 'pointer';
              }, 600); // Wait for progress indicator to auto-remove
            }
          );

          try {
            let generationResult = null;
            // Generate 3D model with selected processing mode
            if (isCloudProcessingEnabled()) {
              generationResult = await generate3DModelWithCloudProgress(bbox, progressIndicator);
            } else {
              generationResult = await generate3DModelWithProgress(bbox, progressIndicator);
            }

            // 🎉 Success!
            if (generationResult?.mode === 'cloud' && generationResult?.submitted) {
              showSuccessMessage('Cloud job submitted successfully. Track progress in Cloud Jobs.');
            } else {
              showSuccessMessage('Model generated successfully!');
            }
          } catch (error) {
            // Handle errors
            if (error.name === 'AbortError') {
              // User cancelled the operation
              // Don't show message here - already shown in cancel callback
            } else {
              // Real error occurred
              console.error('❌ Error generating model:', error);
              showUserFriendlyError(error.message);
            }
          } finally {
            // Always remove progress indicator when done (if not already removed)
            // Note: Cancel callback already removes it, so this is mainly for success/error cases
            try {
              progressIndicator.remove();
            } catch (e) {
              // Already removed, ignore
            }

            // Re-enable button (if not already re-enabled by cancel callback)
            generateButton.disabled = false;
            generateButton.textContent = originalButtonText;
            generateButton.style.opacity = '1';
            generateButton.style.cursor = 'pointer';
          }
        });
      }

      // 🎯 NEW: Setup real-time validation for all numeric inputs
      setupRealtimeValidation(`width-${index}`, 'width');
      setupRealtimeValidation(`length-${index}`, 'length');
      setupRealtimeValidation(`vertical-exaggeration-${index}`, 'verticalExaggeration');
      setupRealtimeValidation(`smoothness-${index}`, 'smoothness');
      setupRealtimeValidation(`resolution-reduction-${index}`, 'resolutionReductionFactor');

      // Event listeners for input changes with real-time updates
      const widthInput = listItem.querySelector(`#width-${index}`);
      if (widthInput) {
        widthInput.addEventListener('input', (event) => {
          const index = parseInt(event.target.dataset.index, 10);
          const value = parseFloat(event.target.value);

          if (!isNaN(value) && value > 0) {
            boundingBoxes[index].width = value;

            // Only update length if dimensions are linked
            if (boundingBoxes[index].dimensionsLinked) {
              boundingBoxes[index].length = calculateLinkedLength(boundingBoxes[index]);
              lengthInput.value = boundingBoxes[index].length ? boundingBoxes[index].length.toFixed(2) : '100';
            }

            localStorage.setItem('boundingBoxes', JSON.stringify(boundingBoxes));
          }
        });
      }

      const lengthInput = listItem.querySelector(`#length-${index}`);
      if (lengthInput) {
        lengthInput.addEventListener('input', (event) => {
          const index = parseInt(event.target.dataset.index, 10);
          const value = parseFloat(event.target.value);

          if (!isNaN(value) && value > 0) {
            boundingBoxes[index].length = value;

            // Only update width if dimensions are linked
            if (boundingBoxes[index].dimensionsLinked) {
              boundingBoxes[index].width = calculateLinkedWidth(boundingBoxes[index]);
              widthInput.value = boundingBoxes[index].width ? boundingBoxes[index].width.toFixed(2) : '100';
            }

            localStorage.setItem('boundingBoxes', JSON.stringify(boundingBoxes));
          }
        });
      }

      // Dimension link/unlink button handler
      const linkButton = listItem.querySelector('.dimension-link-button');
      if (linkButton) {
        linkButton.addEventListener('click', () => {
          const index = parseInt(linkButton.dataset.index, 10);

          // Toggle the link state
          boundingBoxes[index].dimensionsLinked = !boundingBoxes[index].dimensionsLinked;

          // If linking, recalculate length based on current width to maintain aspect ratio
          if (boundingBoxes[index].dimensionsLinked) {
            boundingBoxes[index].length = calculateLinkedLength(boundingBoxes[index]);
            lengthInput.value = boundingBoxes[index].length ? boundingBoxes[index].length.toFixed(2) : '100';
          }

          // Save to localStorage and update UI
          localStorage.setItem('boundingBoxes', JSON.stringify(boundingBoxes));
          updateBboxList();
        });
      }

      // Event listeners for other inputs
      listItem.querySelectorAll('input[type="number"]').forEach(input => {
        if (input.id.includes('width') || input.id.includes('length') || input.id.includes('resolution-reduction')) {
          return; // Skip width, length and resolution-reduction inputs as they're handled above
        }

        input.addEventListener('change', (event) => {
          const index = parseInt(event.target.dataset.index, 10);
          const setting = event.target.id.replace(`-${index}`, '');
          const value = parseFloat(event.target.value);

          boundingBoxes[index][setting] = value;
          localStorage.setItem('boundingBoxes', JSON.stringify(boundingBoxes));
        });
      });

      ensureHandlesForBbox(index);
    });

    // Re-attach event listeners for mobile panel stability
    if (window.innerWidth <= 768) {
      attachPanelEventListeners();
    }
  }

  // Function to calculate the default width based on the aspect ratio and a fixed length
  function calculateDefaultWidth(bbox) {
    const defaultLength = 100; // mm
    const aspectRatio = calculateAspectRatio(bbox);
    return defaultLength * aspectRatio;
  }

  // Function to calculate the aspect ratio of a bounding box
  function calculateAspectRatio(bbox) {
    // Calculate the width/length ratio based on the geographic coordinates
    const latDiff = bbox.northeastLat - bbox.southwestLat;
    const lngDiff = bbox.northeastLng - bbox.southwestLng;

    // Convert to approximate distances
    const latDistanceKm = latDiff * 111.32; // 1 degree of latitude is approximately 111.32 km
    const lngDistanceKm = lngDiff * 111.32 * Math.cos(((bbox.northeastLat + bbox.southwestLat) / 2) * Math.PI / 180);

    if (!Number.isFinite(latDistanceKm) || Math.abs(latDistanceKm) < 1e-9) {
      return 1;
    }
    const ratio = lngDistanceKm / latDistanceKm;
    return Number.isFinite(ratio) && ratio > 0 ? ratio : 1;
  }

  // Function to calculate linked length based on width and aspect ratio
  function calculateLinkedLength(bbox) {
    const aspectRatio = calculateAspectRatio(bbox);
    return bbox.width / aspectRatio;
  }

  // Function to calculate linked width based on length and aspect ratio
  function calculateLinkedWidth(bbox) {
    const aspectRatio = calculateAspectRatio(bbox);
    return bbox.length * aspectRatio;
  }

  // Function to update color preview
  function updateColorPreview(index, colorScheme) {
    const previewElement = document.getElementById(`color-preview-${index}`);
    if (!previewElement) return;

    // Create a 4-band discrete color preview (matching the 3D printing system)
    // Each band represents 25% of the terrain height
    const bands = [
      { normalized: 0.125 },   // Middle of first band (low elevation)
      { normalized: 0.375 },   // Middle of second band
      { normalized: 0.625 },   // Middle of third band
      { normalized: 0.875 }    // Middle of fourth band (high elevation)
    ];

    let previewHTML = '<div style="margin-top: 8px;">';
    previewHTML += '<div style="display: flex; height: 40px; border-radius: 4px; overflow: hidden; border: 1px solid #ddd; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">';

    // Create 4 equal-width color blocks
    for (let i = 0; i < bands.length; i++) {
      const color = getColorForElevation(bands[i].normalized, colorScheme);
      previewHTML += `<div style="flex: 1; background-color: rgb(${color.r}, ${color.g}, ${color.b}); border-right: ${i < bands.length - 1 ? '1px solid rgba(0,0,0,0.1)' : 'none'};"></div>`;
    }

    previewHTML += '</div>';

    // Add simple explanation
    previewHTML += '<div style="font-size: 11px; color: #666; margin-top: 6px; text-align: center;">💡 4 colors from low to high elevation</div>';
    previewHTML += '</div>';

    previewElement.innerHTML = previewHTML;
  }

  function updateBandModeHelp(index, bandMode) {
    const helpEl = document.getElementById(`band-mode-help-${index}`);
    if (!helpEl) return;

    if (bandMode === 'equal_step') {
      helpEl.textContent = 'Equal vertical thickness per band. Best for clean contour-style layering.';
      return;
    }
    if (bandMode === 'coastal_preserve') {
      helpEl.textContent = 'Keeps low coastal terrain merged into the base area, then bands the remaining relief.';
      return;
    }
    helpEl.textContent = 'Balances terrain points across bands. Good for skewed elevation distributions.';
  }

  function updateCoastalPreserveControls(index, bandMode, topoEnabled) {
    const controlEl = document.getElementById(`coastal-preserve-controls-${index}`);
    const inputEl = document.getElementById(`coastal-preserve-percent-${index}`);
    if (!controlEl || !inputEl) return;

    const show = topoEnabled && bandMode === 'coastal_preserve';
    controlEl.style.display = show ? 'block' : 'none';
    inputEl.disabled = !show;
    if (show) {
      updateCoastalPreserveHint(index, inputEl.value);
    }
  }

  function updateCoastalPreserveHint(index, value) {
    const hintEl = document.getElementById(`coastal-preserve-hint-${index}`);
    if (!hintEl) return;
    const parsed = Number(value);
    const normalized = Number.isFinite(parsed) ? Math.max(0, Math.min(100, parsed)) : 0;
    if (normalized < 34) {
      hintEl.textContent = 'Hint: Low preserve';
      return;
    }
    if (normalized < 67) {
      hintEl.textContent = 'Hint: Medium preserve';
      return;
    }
    hintEl.textContent = 'Hint: High preserve';
  }

  // Function to initialize bounding boxes from local storage on page load
  function initializeBBoxes() {
    updateBboxList(); // Call updateBboxList here to ensure list is populated on load
    const sanitizedOnInit = [];
    boundingBoxes.forEach((bbox, index) => {
      try {
        // Ensure width and length are defined, calculate if not
        if (bbox.width === undefined || bbox.length === undefined) {
          bbox.length = 100;
          bbox.width = calculateDefaultWidth(bbox);
        }
        bbox.editing = false;

        const bounds = [[bbox.southwestLat, bbox.southwestLng], [bbox.northeastLat, bbox.northeastLng]];
        const rect = L.rectangle(bounds, { color: 'blue', weight: 1, opacity: bbox.visible ? 1 : 0, fillOpacity: bbox.visible ? 0.2 : 0 });
        bboxLayers.push(rect);
        bboxHandles.push(null);
        rect.addTo(map);
        ensureHandlesForBbox(index);
        sanitizedOnInit.push(bbox);
      } catch (error) {
        console.warn(`Skipping invalid bounding box at index ${index}.`, error);
      }
    });

    if (sanitizedOnInit.length !== boundingBoxes.length) {
      boundingBoxes = sanitizedOnInit;
      localStorage.setItem('boundingBoxes', JSON.stringify(boundingBoxes));
      updateBboxList();
    }
  }

  initializeBBoxes();

  // Expose mobile toggle and panel state for pin manager
  window.__toggleMobileView = toggleMobileView;
  window.__isPanelVisible = () => isPanelVisible;

  function updateMobileQuickActionState() {
    const drawActive = drawingMode === true;
    const pinActive = pinPlacementModeActive === true;

    if (!mobileModeHint) return;
    if (drawActive) {
      mobileModeHint.textContent = 'Bounding box mode active: tap 4 points on map';
      mobileModeHint.classList.add('is-visible');
    } else if (pinActive) {
      mobileModeHint.textContent = 'Pin mode active: tap map to place a pin';
      mobileModeHint.classList.add('is-visible');
    } else {
      mobileModeHint.classList.remove('is-visible');
      mobileModeHint.textContent = '';
    }
  }

  window.addEventListener('pin-placement-mode-changed', (event) => {
    pinPlacementModeActive = !!event?.detail?.active;
    if (pinPlacementModeActive && drawingMode) {
      toggleDrawingMode(false);
    }
    updateMobileQuickActionState();
  });

  // Function to handle drawing mode toggle
  function toggleDrawingMode(forceState = null) {
    drawingMode = typeof forceState === 'boolean' ? forceState : !drawingMode;
    if (drawingMode) {
      if (isPinPlacementActive() && mobilePlacePinButton) {
        mobilePlacePinButton.click();
      }
      // Close the panel on mobile when starting to draw
      if (window.innerWidth <= 768 && isPanelVisible) {
        toggleMobileView();
      }
      drawBboxButton.textContent = 'Cancel Drawing (Click 4 points)';
      if (mobileDrawButton) mobileDrawButton.textContent = '✕ Cancel Drawing (Click 4 points)';
      map.getContainer().style.cursor = 'crosshair';
      map.dragging.disable();
    } else {
      drawBboxButton.textContent = 'Draw Bounding Box';
      if (mobileDrawButton) mobileDrawButton.textContent = '+ Draw New Bounding Box';
      map.getContainer().style.cursor = '';
      map.dragging.enable();
      clearTemporaryDrawing();
      clickedPoints = [];
    }
    updateMobileQuickActionState();
  }

  // Button click handler for desktop
  drawBboxButton.addEventListener('click', (e) => {
    e.preventDefault();
    e.stopPropagation();
    toggleDrawingMode();
  });

  // Button click handler for mobile
  if (mobileDrawButton) {
    mobileDrawButton.addEventListener('click', (e) => {
      e.preventDefault();
      e.stopPropagation();
      toggleDrawingMode();
    });
  }

  // Map click handler
  map.on('click', (e) => {
    // Skip if pin placement mode is active (pin manager handles its own clicks)
    if (isPinPlacementActive()) return;
    if (!drawingMode) return;

    // Prevent panel from closing on mobile when drawing
    if (window.innerWidth <= 768 && isPanelVisible) {
      e.originalEvent.stopPropagation();
    }

    // Add the clicked point
    clickedPoints.push(e.latlng);

    // Add a temporary marker for visual feedback
    const marker = L.circleMarker(e.latlng, {
      color: 'red',
      fillColor: 'red',
      fillOpacity: 0.8,
      radius: 6,
      weight: 2
    }).addTo(map);
    temporaryMarkers.push(marker);

    // Update button text to show progress
    drawBboxButton.textContent = `Cancel Drawing (${clickedPoints.length}/4 points)`;
    if (mobileDrawButton) mobileDrawButton.textContent = `✕ Cancel Drawing (${clickedPoints.length}/4 points)`;
    if (mobileModeHint && window.innerWidth <= 768) {
      mobileModeHint.textContent = `Bounding box mode active: point ${clickedPoints.length}/4`;
      mobileModeHint.classList.add('is-visible');
    }

    updateDrawingPreview();

    // If we have 4 points, complete the bounding box
    if (clickedPoints.length === 4) {
      completeBoundingBox();
    }
  });

  // Function to complete the bounding box creation
  function completeBoundingBox() {
    drawingMode = false;
    drawBboxButton.textContent = 'Draw Bounding Box';
    if (mobileDrawButton) mobileDrawButton.textContent = '+ Draw New Bounding Box';
    map.getContainer().style.cursor = '';
    map.dragging.enable(); // Enable map dragging
    updateMobileQuickActionState();

    // Clear temporary drawing elements
    clearTemporaryDrawing();

    // Create the bounding box from the 4 points
    const newBbox = createBoundingBoxFromPoints(clickedPoints);

    // Calculate initial width and length
    newBbox.length = 100;
    newBbox.width = calculateDefaultWidth(newBbox);
    newBbox.editing = false;

    // Check if the area exceeds recommended maximum
    const area = calculateArea(newBbox);
    if (isAreaTooLarge(area) && !isCloudProcessingEnabled()) {
      showAreaWarning(area, newBbox.name);
    }

    // Add new box at the beginning (top of the list)
    boundingBoxes.unshift(newBbox);
    const rect = L.rectangle([[newBbox.southwestLat, newBbox.southwestLng], [newBbox.northeastLat, newBbox.northeastLng]], {
      color: 'blue',
      weight: 1,
      opacity: 1,
      fillOpacity: 0.2
    });
    // Add layer at the beginning too to maintain index correspondence
    bboxLayers.unshift(rect);
    bboxHandles.unshift(null);
    rect.addTo(map);

    localStorage.setItem('boundingBoxes', JSON.stringify(boundingBoxes));
    updateBboxList();

    clickedPoints = [];
  }

  // Define processing stages with detailed descriptions
  const processingStages = [
    { progress: 0, message: "Initializing 3D model generation..." },
    { progress: 5, message: "Configuring terrain parameters..." },
    { progress: 10, message: "Connecting to elevation data service..." },
    { progress: 15, message: "Requesting terrain data from server..." },
    { progress: 25, message: "Downloading elevation data..." },
    { progress: 35, message: "Processing raw elevation data..." },
    { progress: 45, message: "Filling gaps in elevation model..." },
    { progress: 50, message: "Preparing terrain mesh generation..." },
    { progress: 55, message: "Applying terrain smoothing algorithms..." },
    { progress: 60, message: "Generating base mesh triangulation..." },
    { progress: 65, message: "Calculating terrain surface normals..." },
    { progress: 70, message: "Building 3D terrain model structure..." },
    { progress: 75, message: "Adding model base and side walls..." },
    { progress: 80, message: "Optimizing 3D model for printing..." },
    { progress: 85, message: "Finalizing STL binary data..." },
    { progress: 90, message: "Creating terrain preview image..." },
    { progress: 95, message: "Preparing model for download..." },
    { progress: 100, message: "3D model generation complete!" }
  ];

  // Function to update progress bar with smooth animation
  function updateProgress(loadingIndicator, progressBar, stage, callback) {
    const currentProgress = parseFloat(progressBar.style.width) || 0;
    const targetProgress = processingStages[stage].progress;
    const message = processingStages[stage].message;

    // Update text immediately
    updateLoadingText(loadingIndicator, message);

    // Add detailed substage if provided
    const detailElement = loadingIndicator.querySelector('.loading-detail');
    if (detailElement) {
      // Reset any animation
      detailElement.style.animation = 'none';
      detailElement.offsetHeight; // Trigger reflow
      detailElement.style.animation = null;

      // Add fade in animation
      detailElement.style.opacity = 0;
      detailElement.textContent = `Step ${stage + 1}/${processingStages.length}: ${message}`;

      setTimeout(() => {
        detailElement.style.opacity = 1;
      }, 50);
    }

    // Update percentage indicator
    const progressContainer = loadingIndicator.querySelector('.progress-container');
    if (progressContainer) {
      progressContainer.setAttribute('data-progress', Math.round(targetProgress));
    }

    // Only animate if there's a significant change
    if (Math.abs(targetProgress - currentProgress) < 0.5) {
      progressBar.style.width = `${targetProgress}%`;
      if (callback) setTimeout(callback, 50);
      return;
    }

    // Animate the progress bar smoothly
    const duration = 500; // ms
    const startTime = performance.now();

    function animate(time) {
      const elapsed = time - startTime;
      const progress = Math.min(elapsed / duration, 1);
      const easedProgress = progress < 0.5
        ? 2 * progress * progress
        : -1 + (4 - 2 * progress) * progress; // easeInOutQuad

      const newProgress = currentProgress + (targetProgress - currentProgress) * easedProgress;
      progressBar.style.width = `${newProgress}%`;

      // Update percentage indicator during animation
      if (progressContainer) {
        progressContainer.setAttribute('data-progress', Math.round(newProgress));
      }

      if (progress < 1) {
        requestAnimationFrame(animate);
      } else {
        if (callback) setTimeout(callback, 100); // Small delay after animation completes
      }
    }

    requestAnimationFrame(animate);
  }

  // Function to generate 3D model from DEM data with improved progress indicators
  async function generate3DModel(bbox) {
    try {
      // Create loading indicator with enhanced styling
      const loadingIndicator = document.createElement('div');
      loadingIndicator.className = 'loading-indicator';
      loadingIndicator.innerHTML = `
      <div class="loading-spinner"></div>
      <h3>Generating 3D Model</h3>
      <p class="loading-title">Starting process for ${bbox.name || 'Selected Area'}</p>
      <div class="loading-detail"></div>
      <div class="progress-container" data-progress="0">
        <div class="progress-bar" id="progress-bar"></div>
      </div>
    `;
      document.body.appendChild(loadingIndicator);

      // Get progress bar and initialize
      const progressBar = loadingIndicator.querySelector('#progress-bar');
      progressBar.style.width = '0%';

      // Process in sequence with chained promises and progress updates
      let demData = null;
      let stlResult = null;

      // Retrieve settings
      const settings = {
        width: bbox.width,
        length: bbox.length,
        verticalExaggeration: bbox.verticalExaggeration,
        smoothness: bbox.smoothness,
        demType: bbox.demType,
        resolutionReductionFactor: bbox.resolutionReductionFactor,
        colorMode: bbox.colorMode || 'solid',
        modelColor: bbox.modelColor || '#1976d2',
        colorScheme: bbox.colorScheme || 'terrain',
        baseColor: bbox.baseColor || '#FFFFFF',
        bandMode: bbox.bandMode || 'adaptive',
        numBands: bbox.numBands || 3,
        coastalPreservePercent: Number.isFinite(Number(bbox.coastalPreservePercent)) ? Number(bbox.coastalPreservePercent) : 12
      };

      // Set DEM type
      setSelectedDemType(settings.demType);

      // Use async/await for cleaner flow
      for (let stage = 0; stage < processingStages.length; stage++) {
        await new Promise((resolve) => {
          updateProgress(loadingIndicator, progressBar, stage, async () => {
            try {
              switch (stage) {
                case 4: // Download elevation data
                  try {
                    demData = await fetchDEMData(bbox);
                    demData.bboxName = bbox.name;
                  } catch (error) {
                    console.error('Failed to download elevation data:', error);
                    // If the error message contains API key information, preserve it
                    if (error.message && error.message.toLowerCase().includes('api key')) {
                      showUserFriendlyError(error.message);
                    } else {
                      showUserFriendlyError('Failed to download elevation data. Please check your internet connection and try again.');
                    }
                    throw error;
                  }
                  break;
                case 14: // Finalize STL binary data (after all processing)
                  stlResult = generateSTL(demData, settings);
                  break;
                case 16: // Create terrain preview image
                  const smoothedData = stlResult.smoothedData;
                  const processedWidth = stlResult.width;
                  const processedHeight = stlResult.height;
                  const previewColorScheme = stlResult.colorMode === 'topographic' ? stlResult.colorScheme : null;
                  const previewModelInfo = previewColorScheme && stlResult.stats ? {
                    maxModelHeight: stlResult.stats.maxModelHeight - (stlResult.modelDimensions?.baseThickness || 4),
                    baseThickness: stlResult.modelDimensions?.baseThickness || 4,
                    verticalScale: stlResult.stats.verticalScaleFactor * stlResult.stats.baseScalingFactor
                  } : null;
                  stlResult.previewImage = createHeightmapVisualization(smoothedData, processedWidth, processedHeight, previewColorScheme, previewModelInfo);
                  break;
                case 17: // Prepare model for download (final step)
                  // Success animation and completion effects
                  const titleElement = loadingIndicator.querySelector('.loading-title');
                  if (titleElement) {
                    titleElement.textContent = 'Success!';
                    titleElement.style.color = '#4CAF50';
                    titleElement.style.fontWeight = 'bold';
                  }

                  const detailElement = loadingIndicator.querySelector('.loading-detail');
                  if (detailElement) {
                    detailElement.textContent = 'Your 3D model is ready to download';
                    detailElement.style.backgroundColor = 'rgba(76, 175, 80, 0.2)';
                  }

                  // Final completion with delay for UX
                  setTimeout(() => {
                    try {
                      // Remove loading indicator
                      if (loadingIndicator && loadingIndicator.parentNode) {
                        document.body.removeChild(loadingIndicator);
                      }

                      // Download the STL file and show preview
                      const filename = `${stlResult.bboxName}.stl`;
                      downloadSTL(stlResult, filename, stlResult.previewImage, bbox.demType, bbox.smoothness, bbox.resolutionReductionFactor);

                      // Show success message
                      showSuccessMessage(`3D model for ${stlResult.bboxName} has been generated successfully!`);
                    } catch (error) {
                      console.error('Error in final completion:', error);
                      showUserFriendlyError(`Error showing preview: ${error.message}`);
                    }
                  }, 1000); // Slight delay for better user experience
                  break;
              }
              resolve(); // Resolve the promise to proceed to the next stage
            } catch (error) {
              console.error(`Error during stage ${stage}:`, error);
              handleModelGenerationError(loadingIndicator, error);
              resolve(); // Resolve even on error to prevent infinite loop
            }
          });
        });
      }
    } catch (error) {
      console.error('Error setting up model generation:', error);
      // Remove loading indicator if it exists
      const loadingIndicator = document.querySelector('.loading-indicator');
      if (loadingIndicator && loadingIndicator.parentNode) {
        try {
          document.body.removeChild(loadingIndicator);
        } catch (removeError) {
          console.error('Error removing loading indicator:', removeError);
        }
      }
      // Show error message
      showUserFriendlyError(`Error generating 3D model: ${error.message}`);
    }
  }

  async function generate3DModelWithCloudProgress(bbox, progressIndicator) {
    await refreshCloudProfileUI();

    if (!cloudProfile.enabled) {
      throw new Error('Cloud processing is not active. Buy a credit pack first.');
    }

    const areaKm2 = calculateArea(bbox);
    const requiredCredits = calculateCloudCreditsForArea(areaKm2);
    if (cloudProfile.creditBalance < requiredCredits) {
      throw new Error(
        `Not enough credits. Required: ${requiredCredits}, available: ${cloudProfile.creditBalance}.`
      );
    }

    progressIndicator.update(10, 'Preparing cloud job', `Area: ${areaKm2.toFixed(2)} km²`);
    progressIndicator.update(25, 'Estimating credits', `This job will use ${requiredCredits} credits`);

    const payload = {
      areaKm2,
      bbox: {
        southwestLat: bbox.southwestLat,
        southwestLng: bbox.southwestLng,
        northeastLat: bbox.northeastLat,
        northeastLng: bbox.northeastLng,
        name: bbox.name
      },
      settings: {
        width: bbox.width,
        length: bbox.length,
        verticalExaggeration: bbox.verticalExaggeration,
        smoothness: bbox.smoothness,
        demType: bbox.demType,
        resolutionReductionFactor: bbox.resolutionReductionFactor,
        colorMode: bbox.colorMode || 'solid',
        modelColor: bbox.modelColor || '#1976d2',
        colorScheme: bbox.colorScheme || 'terrain',
        baseColor: bbox.baseColor || '#FFFFFF',
        bandMode: bbox.bandMode || 'adaptive',
        numBands: bbox.numBands || 3,
        coastalPreservePercent: Number.isFinite(Number(bbox.coastalPreservePercent))
          ? Number(bbox.coastalPreservePercent)
          : 12
      }
    };

    progressIndicator.update(45, 'Submitting job', 'Sending request to cloud backend...');
    const cloudResult = await createCloudJob(payload);

    await refreshCloudProfileUI();

    if (cloudResult?.status !== 'submitted') {
      throw new Error(cloudResult?.message || 'Cloud job submission failed.');
    }

    progressIndicator.update(100, 'Submitted', 'Cloud job submitted. Track progress in Cloud Jobs.');
    return {
      mode: 'cloud',
      submitted: true,
      jobId: cloudResult.jobId || null
    };
  }

  // 🎯 NEW: Enhanced generate function with progress tracking and cancellation support
  async function generate3DModelWithProgress(bbox, progressIndicator) {
    // Create abort controller for cancellation support
    const abortController = createAbortController();
    const signal = abortController.signal;

    // Declare variables at function scope so they're accessible in catch block
    let demData = null;
    let stlResult = null;
    let settings = null;

    try {
      // Retrieve settings from bbox
      settings = {
        width: bbox.width,
        length: bbox.length,
        verticalExaggeration: bbox.verticalExaggeration,
        smoothness: bbox.smoothness,
        demType: bbox.demType,
        resolutionReductionFactor: bbox.resolutionReductionFactor,
        colorMode: bbox.colorMode || 'solid',
        modelColor: bbox.modelColor || '#1976d2',
        colorScheme: bbox.colorScheme || 'terrain',
        baseColor: bbox.baseColor || '#FFFFFF',
        bandMode: bbox.bandMode || 'adaptive',
        numBands: bbox.numBands || 3,
        coastalPreservePercent: Number.isFinite(Number(bbox.coastalPreservePercent)) ? Number(bbox.coastalPreservePercent) : 12
      };

      // Set DEM type
      setSelectedDemType(settings.demType);

      // STAGE 1: Fetch DEM data with progress tracking and cache support
      progressIndicator.update(5, 'Checking cache', 'Looking for cached elevation data...');
      progressIndicator.setStage('cache-check');

      // Check if cancelled
      if (signal.aborted) throw new DOMException('Operation cancelled', 'AbortError');

      // Fetch DEM data with progress callback
      let isCacheHit = false;

      try {
        demData = await fetchDEMData(bbox, (progressInfo) => {
          // Update progress based on DEM fetch stage
          const stageMessages = {
            'cache-check': 'Checking cache...',
            'cache-hit': 'Using cached data',
            'api-fetch': 'Connecting to API...',
            'downloading': 'Downloading elevation data...',
            'processing': 'Processing elevation data...',
            'caching': 'Caching for future use...',
            'complete': 'DEM data ready!'
          };

          const message = stageMessages[progressInfo.stage] || progressInfo.stage;

          // Detect cache hit
          if (progressInfo.stage === 'cache-hit') {
            isCacheHit = true;
          }

          // Remap progress for smooth 0-100% experience
          // DEM fetch takes 5-70% of total progress
          // This leaves room for STL generation (70-90%) and preview (90-100%)
          let mappedProgress;

          // Clamp progressInfo.progress to max 85 to ensure we don't exceed 70%
          const clampedProgress = Math.min(progressInfo.progress, 85);

          if (isCacheHit && progressInfo.progress === 100) {
            // Cache hit completed: Jump to 70%
            mappedProgress = 70;
          } else {
            // Map 5-85% to 5-70% (cache miss or cache hit in progress)
            mappedProgress = 5 + ((clampedProgress - 5) / 80) * 65;
          }

          // Ensure we never go below previous progress or above 70
          mappedProgress = Math.max(5, Math.min(70, mappedProgress));

          progressIndicator.update(mappedProgress, progressInfo.stage, message);
          progressIndicator.setStage(progressInfo.stage);
        }, signal);  // Pass abort signal for cancellation

        demData.bboxName = bbox.name;
      } catch (error) {
        if (signal.aborted || error.name === 'AbortError') {
          throw new DOMException('Operation cancelled during DEM fetch', 'AbortError');
        }
        console.error('❌ Failed to download elevation data:', error);

        // If the error message contains API key information, preserve it
        if (error.message && error.message.toLowerCase().includes('api key')) {
          throw error; // Preserve the original API key error message
        }

        // Otherwise, throw a generic network error
        throw new Error('Failed to download elevation data. Please check your connection and try again.');
      }

      // Check if cancelled after DEM fetch
      if (signal.aborted) throw new DOMException('Operation cancelled', 'AbortError');

      // STAGE 2: Generate STL (70-90% of progress)
      progressIndicator.update(70, 'Generating model', 'Creating 3D geometry...');
      progressIndicator.setStage('generating');

      // Yield to browser to allow UI update
      await new Promise(resolve => setTimeout(resolve, 100));

      // Check if cancelled before STL generation
      if (signal.aborted) throw new DOMException('Operation cancelled', 'AbortError');

      try {
        // Update progress before heavy computation
        progressIndicator.update(75, 'Generating model', 'Processing mesh data...');

        // Longer yield before heavy computation to allow cancel to register
        await new Promise(resolve => setTimeout(resolve, 150));

        // Final check before starting heavy computation
        if (signal.aborted) throw new DOMException('Operation cancelled', 'AbortError');

        // Use Web Worker for browser mode to keep UI responsive
        // Define progress callback for worker
        const workerProgressCallback = (progress, stage, message) => {
          progressIndicator.update(progress, 'Generating model', message);
        };

        // Define main thread fallback
        const mainThreadFallback = async (demData, settings, progressCb) => {
          if (progressCb) {
            progressCb(75, 'generating', 'Processing on main thread...');
          }
          return generateSTL(demData, settings);
        };

        // Use worker with fallback
        stlResult = await generateWithWorkerOrFallback(
          demData,
          settings,
          workerProgressCallback,
          mainThreadFallback
        );

        // Check immediately after STL generation
        if (signal.aborted) {
          throw new DOMException('Operation cancelled', 'AbortError');
        }

        progressIndicator.update(90, 'Generating model', '3D geometry created successfully');
      } catch (error) {
        if (signal.aborted || error.name === 'AbortError') {
          throw new DOMException('Operation cancelled during STL generation', 'AbortError');
        }
        console.error('❌ Failed to generate STL:', error);
        throw new Error('Failed to generate 3D model. Please try again.');
      }

      // Yield to browser
      await new Promise(resolve => setTimeout(resolve, 100));

      // Check if cancelled after STL generation
      if (signal.aborted) throw new DOMException('Operation cancelled', 'AbortError');

      // STAGE 3: Create preview (90-95% of progress)
      progressIndicator.update(92, 'Creating preview', 'Generating terrain visualization...');

      // Yield to browser
      await new Promise(resolve => setTimeout(resolve, 50));

      // Check if cancelled
      if (signal.aborted) throw new DOMException('Operation cancelled', 'AbortError');

      try {
        const smoothedData = stlResult.smoothedData;
        const processedWidth = stlResult.width;
        const processedHeight = stlResult.height;
        const previewColorScheme = stlResult.colorMode === 'topographic' ? stlResult.colorScheme : null;
        const previewModelInfo = previewColorScheme && stlResult.stats ? {
          maxModelHeight: stlResult.stats.maxModelHeight - (stlResult.modelDimensions?.baseThickness || 4),
          baseThickness: stlResult.modelDimensions?.baseThickness || 4,
          verticalScale: stlResult.stats.verticalScaleFactor * stlResult.stats.baseScalingFactor
        } : null;

        stlResult.previewImage = createHeightmapVisualization(
          smoothedData,
          processedWidth,
          processedHeight,
          previewColorScheme,
          previewModelInfo
        );
        progressIndicator.update(95, 'Creating preview', 'Preview generated successfully');
      } catch (error) {
        // Preview is optional, don't fail the whole process
        console.warn('⚠️ Failed to create preview:', error);
        progressIndicator.update(95, 'Creating preview', 'Preview skipped');
      }

      // Yield to browser
      await new Promise(resolve => setTimeout(resolve, 50));

      // Check if cancelled before download
      if (signal.aborted) throw new DOMException('Operation cancelled', 'AbortError');

      // STAGE 4: Prepare download (95-100% of progress)
      progressIndicator.update(98, 'Preparing download', 'Finalizing your model...');
      progressIndicator.setStage('complete');

      // Yield to browser to show 98%
      await new Promise(resolve => setTimeout(resolve, 100));

      try {
        const filename = `${stlResult.bboxName}.stl`;

        // Attach pins within this bbox to the stlResult for the download modal
        const bboxPins = getPinsInBbox({
          southwestLat: bbox.southwestLat,
          northeastLat: bbox.northeastLat,
          southwestLng: bbox.southwestLng,
          northeastLng: bbox.northeastLng
        });
        stlResult.pins = bboxPins;
        stlResult.bbox = {
          southwestLat: bbox.southwestLat,
          northeastLat: bbox.northeastLat,
          southwestLng: bbox.southwestLng,
          northeastLng: bbox.northeastLng
        };

        downloadSTL(
          stlResult,
          filename,
          stlResult.previewImage,
          bbox.demType,
          bbox.smoothness,
          bbox.resolutionReductionFactor
        );
      } catch (error) {
        console.error('❌ Failed to download STL:', error);
        throw new Error('Failed to download model file. Please try again.');
      }

      // Complete!
      progressIndicator.update(100, 'Complete', 'Model downloaded successfully!');

    } catch (error) {
      // If operation was cancelled, perform complete cleanup
      if (error.name === 'AbortError' || signal.aborted) {
        // Clear DEM data from memory to free up RAM
        if (demData) {
          if (demData.data) demData.data = null;
          demData = null;
        }

        // Clear STL result from memory to free up RAM
        if (stlResult) {
          if (stlResult.buffer) stlResult.buffer = null;
          if (stlResult.previewImage) stlResult.previewImage = null;
          stlResult = null;
        }

        // NOTE: We do NOT delete cached data on cancellation because:
        // 1. If download completed, the cache is valid and should be kept for future use
        // 2. If download was cancelled mid-way, the cache won't exist yet (caching happens after download)
        // 3. The OT-API key is stored in sessionStorage and is NEVER touched during cleanup

      }

      // Re-throw to be handled by caller
      throw error;
    }
  }

  // Helper function to handle errors during model generation
  function handleModelGenerationError(loadingIndicator, error) {
    // Update loading indicator to show error
    const titleElement = loadingIndicator.querySelector('.loading-title');
    if (titleElement) {
      titleElement.textContent = 'Error Generating Model';
      titleElement.style.color = '#f44336';
    }

    const detailElement = loadingIndicator.querySelector('.loading-detail');
    if (detailElement) {
      detailElement.textContent = `${error.message}`;
      detailElement.style.color = '#f44336';
      detailElement.style.backgroundColor = 'rgba(244, 67, 54, 0.1)';
    }

    // Add error styling to progress bar
    const progressBar = loadingIndicator.querySelector('#progress-bar');
    if (progressBar) {
      progressBar.style.backgroundColor = '#f44336';
      progressBar.style.boxShadow = '0 0 8px rgba(244, 67, 54, 0.8)';
    }

    // Remove loading indicator after error display
    setTimeout(() => {
      try {
        if (loadingIndicator && loadingIndicator.parentNode && document.body.contains(loadingIndicator)) {
          document.body.removeChild(loadingIndicator);
        }
      } catch (error) {
        console.error('Error removing loading indicator after error:', error);
      }
    }, 3000);
  }

  // Helper function to update loading text
  function updateLoadingText(loadingIndicator, text) {
    const titleElement = loadingIndicator.querySelector('.loading-title');
    if (titleElement) {
      titleElement.textContent = text;
    }
  }

  // Helper function to show success message
  function showSuccessMessage(message) {
    const successMessage = document.createElement('div');
    successMessage.className = 'success-message';
    successMessage.textContent = message;
    document.body.appendChild(successMessage);

    // Remove after 4 seconds
    setTimeout(() => {
      if (document.body.contains(successMessage)) {
        document.body.removeChild(successMessage);
      }
    }, 4000);
  }
}
