/**
 * Popup UI: folder selection (File System Access API), options, Scan/Apply, report.
 */
(function () {
  'use strict';

  var selectInputBtn = document.getElementById('selectInputBtn');
  var selectOutputBtn = document.getElementById('selectOutputBtn');
  var inputLabel = document.getElementById('inputLabel');
  var outputLabel = document.getElementById('outputLabel');
  var optExtractZip = document.getElementById('optExtractZip');
  var optExtractOnly = document.getElementById('optExtractOnly');
  var optDeleteZip = document.getElementById('optDeleteZip');
  var optDeleteOriginals = document.getElementById('optDeleteOriginals');
  var optSingleLake = document.getElementById('optSingleLake');
  var optAlbumsDuplicateFiles = document.getElementById('optAlbumsDuplicateFiles');
  var optVerbose = document.getElementById('optVerbose');
  var optGenerateMap = document.getElementById('optGenerateMap');
  var optTransferUnmatched = document.getElementById('optTransferUnmatched');
  var mergeOutputSection = document.getElementById('mergeOutputSection');
  var mappingSection = document.getElementById('mappingSection');
  var scanBtn = document.getElementById('scanBtn');
  var applyBtn = document.getElementById('applyBtn');
  var aboutBtn = document.getElementById('aboutBtn');
  var supportBtn = document.getElementById('supportBtn');
  var remarksEl = document.getElementById('remarks');
  var outputEl = document.getElementById('output');

  /** Set to true to show Scan (dry run) button and allow scan. When false, button is hidden and scan no-ops. */
  var ENABLE_SCAN_DRY_RUN = false;

  var inputHandle = null;
  var outputHandle = null;
  var directZipFiles = [];
  var directZipBuffers = [];
  var inputZipHint = document.getElementById('inputZipHint');
  var inputZipListSection = document.getElementById('inputZipListSection');
  var inputZipList = document.getElementById('inputZipList');

  function openUrl(url) {
    if (chrome.tabs && typeof chrome.tabs.create === 'function') {
      chrome.tabs.create({ url: url });
    } else {
      window.open(url, '_blank');
    }
  }

  if (aboutBtn) aboutBtn.addEventListener('click', function () { openUrl(chrome.runtime.getURL('web/about.html')); });
  if (supportBtn) supportBtn.addEventListener('click', function () { openUrl('https://www.paypal.com/ncp/payment/HVLCMK7433C7N'); });

  /* Full-tab background (Gphoto_BG2.jpg) */
  try {
    var bgUrl = chrome.runtime.getURL('web/Gphoto_BG2.jpg');
    document.body.style.setProperty('--popup-bg-image', 'url(' + bgUrl + ')');
  } catch (e) {}
  /* Random image panel: show placeholder on load so no broken image icon */
  try {
    var panelPlaceUrl = chrome.runtime.getURL('web/Gphoto_panel_BG.jpg');
    var panelImg = document.getElementById('randomImageImg');
    if (panelImg && panelPlaceUrl) {
      panelImg.src = panelPlaceUrl;
      panelImg.alt = '';
    }
  } catch (e) {}

  var IMAGE_EXT = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'heic'];
  function ext(name) {
    if (!name || name.indexOf('.') < 0) return '';
    return name.split('.').pop().toLowerCase();
  }
  function isImageFileName(name) {
    return IMAGE_EXT.indexOf(ext(name)) >= 0;
  }

  /** List relative paths of image files in Processed (root and one level of subdirs for year folders). */
  function listProcessedImagePaths(processedHandle) {
    var paths = [];
    function collectFromIterator(handle, prefix) {
      return new Promise(function (resolve, reject) {
        var it;
        try {
          it = handle.entries();
        } catch (e) {
          reject(e);
          return;
        }
        function next() {
          var n = it.next();
          var prom = n && typeof n.then === 'function' ? n : Promise.resolve(n);
          prom.then(function (r) {
            if (r.done) {
              resolve();
              return;
            }
            var name = r.value[0];
            var entry = r.value[1];
            var fullPath = prefix ? prefix + '/' + name : name;
            if (entry.kind === 'file' && isImageFileName(name)) {
              paths.push(fullPath);
            }
            if (entry.kind === 'directory') {
              collectFromIterator(entry, fullPath).then(next).catch(reject);
              return;
            }
            next();
          }).catch(reject);
        }
        next();
      });
    }
    return collectFromIterator(processedHandle, '').then(function () { return paths; });
  }

  function getFileFromProcessedByPath(processedHandle, relativePath) {
    var parts = relativePath.replace(/\\/g, '/').split('/').filter(Boolean);
    if (parts.length === 0) return Promise.reject(new Error('empty path'));
    if (parts.length === 1) {
      return processedHandle.getFileHandle(parts[0]).then(function (h) { return h.getFile(); });
    }
    var dirPromise = processedHandle.getDirectoryHandle(parts[0]);
    for (var i = 1; i < parts.length - 1; i++) {
      (function (partName) {
        dirPromise = dirPromise.then(function (dir) { return dir.getDirectoryHandle(partName); });
      })(parts[i]);
    }
    var last = parts[parts.length - 1];
    return dirPromise.then(function (dir) { return dir.getFileHandle(last).then(function (h) { return h.getFile(); }); });
  }

  /** List top-level entries (files and directories) in the given directory handle. Returns [{ name, kind: 'file'|'directory' }]. */
  function listTopLevelEntries(dirHandle) {
    return new Promise(function (resolve, reject) {
      var entries = [];
      var it;
      try {
        it = dirHandle.entries();
      } catch (e) {
        reject(e);
        return;
      }
      if (typeof it.next !== 'function') {
        reject(new Error('entries() did not return an iterator'));
        return;
      }
      function next() {
        var n = it.next();
        var prom = n && typeof n.then === 'function' ? n : Promise.resolve(n);
        prom.then(function (r) {
          if (r.done) {
            resolve(entries);
            return;
          }
          var name = r.value[0];
          var entry = r.value[1];
          entries.push({ name: name, kind: entry.kind === 'directory' ? 'directory' : 'file' });
          return next();
        }).catch(reject);
      }
      next();
    });
  }

  /** List top-level .zip file names in the given directory handle (suggests zip location after user selects input folder). */
  function listTopLevelZipNames(dirHandle) {
    return new Promise(function (resolve, reject) {
      var zips = [];
      var it;
      try {
        it = dirHandle.entries();
      } catch (e) {
        reject(e);
        return;
      }
      if (typeof it.next !== 'function') {
        reject(new Error('entries() did not return an iterator'));
        return;
      }
      function next() {
        var n = it.next();
        var prom = n && typeof n.then === 'function' ? n : Promise.resolve(n);
        prom.then(function (r) {
          if (r.done) {
            resolve(zips);
            return;
          }
          var name = r.value[0];
          var entry = r.value[1];
          if (entry.kind === 'file' && name.toLowerCase().endsWith('.zip')) zips.push(name);
          return next();
        }).catch(reject);
      }
      next();
    });
  }

  function log(msg) {
    var t = new Date();
    var timeStr = (t.getHours() < 10 ? '0' : '') + t.getHours() + ':' +
      (t.getMinutes() < 10 ? '0' : '') + t.getMinutes() + ':' +
      (t.getSeconds() < 10 ? '0' : '') + t.getSeconds();
    remarksEl.textContent = (remarksEl.textContent || '') + '[' + timeStr + '] ' + msg + '\n';
    remarksEl.scrollTop = remarksEl.scrollHeight;
    remarksEl.classList.remove('empty');
    if (typeof requestAnimationFrame === 'function') {
      requestAnimationFrame(function () { remarksEl.scrollTop = remarksEl.scrollHeight; });
    }
  }

  function setOutput(text, append) {
    if (!outputEl) return;
    if (append) outputEl.textContent = (outputEl.textContent || '') + text + '\n';
    else outputEl.textContent = text || '';
    outputEl.classList.toggle('empty', !outputEl.textContent);
  }

  /** Build an elaborated summary string for the Summary panel from pipeline summaryReport. */
  function formatSummaryForPanel(summary) {
    if (!summary) return 'Processing complete.';
    var merged = Number(summary.merge_merged_count) || 0;
    var transferred = Number(summary.merge_transferred_count) || 0;
    var totalProcessed = Number(summary.total_media_processed) || 0;
    var images = Number(summary.extraction_zip_entries_images) || 0;
    var videos = Number(summary.extraction_zip_entries_videos) || 0;
    var orphanMedia = Number(summary.orphan_media) || Number(summary.missing_json) || 0;
    var totalJsonInLake = Number(summary.total_json_in_lake) || 0;
    var remerged = Number(summary.remerged_count) || 0;
    var orphanJson = Math.max(0, totalJsonInLake - merged - remerged);
    var albumsRebuilt = Number(summary.albums_rebuilt) || 0;
    var yearsCreated = summary.processed_by_year || [];
    var yearAlbum = yearsCreated.length ? yearsCreated.join(', ') : '(none)';
    var problematic = Number(summary.problematic_files_count) || 0;
    var lines = ['Processing complete.'];
    lines.push('Total files processed and transferred: ' + (merged + transferred) + (totalProcessed > 0 ? ' (total in Processed: ' + totalProcessed + ')' : ''));
    lines.push('Images: ' + images + ' | Videos: ' + videos);
    lines.push('Orphan media (no JSON): ' + orphanMedia);
    lines.push('Orphan JSON (no media): ' + orphanJson);
    lines.push('Albums: ' + albumsRebuilt + ' | Year albums: ' + yearAlbum);
    lines.push('Problematic / skipped: ' + problematic);
    return lines.join('\n');
  }

  function updateProgressBar(barId, percent) {
    var p = Math.round(Math.min(100, Math.max(0, percent)));
    var bar = document.getElementById(barId);
    var label = document.getElementById(barId + 'Label');
    if (bar) { bar.value = p; bar.setAttribute('aria-valuenow', p); }
    if (label) label.textContent = p + '%';
  }

  function getOptions() {
    return {
      extractZip: optExtractZip ? optExtractZip.checked : true,
      extractOnly: optExtractOnly ? optExtractOnly.checked : false,
      deleteZipAfterExtract: optDeleteZip.checked,
      deleteOriginalsAfterMerge: optDeleteOriginals.checked,
      singleLake: optSingleLake.checked,
      rebuildAlbums: !optSingleLake.checked,
      albumsDuplicateFiles: optAlbumsDuplicateFiles ? optAlbumsDuplicateFiles.checked : false,
      verbose: optVerbose.checked,
      detectDuplicates: true,
      generateMap: optGenerateMap ? optGenerateMap.checked : false,
      transferUnmatchedMedia: optTransferUnmatched ? optTransferUnmatched.checked : false,
      dryRun: false
    };
  }

  function loadStoredOptions() {
    try {
      chrome.storage.local.get(['options'], function (r) {
        if (r.options) {
          if (r.options.extractZip !== undefined && optExtractZip) optExtractZip.checked = r.options.extractZip;
          if (r.options.extractOnly !== undefined && optExtractOnly) optExtractOnly.checked = r.options.extractOnly;
          if (r.options.deleteZipAfterExtract !== undefined) optDeleteZip.checked = r.options.deleteZipAfterExtract;
          if (r.options.deleteOriginalsAfterMerge !== undefined) optDeleteOriginals.checked = r.options.deleteOriginalsAfterMerge;
          if (r.options.singleLake !== undefined) optSingleLake.checked = r.options.singleLake;
          if (r.options.albumsDuplicateFiles !== undefined && optAlbumsDuplicateFiles) optAlbumsDuplicateFiles.checked = r.options.albumsDuplicateFiles;
          if (r.options.verbose !== undefined) optVerbose.checked = r.options.verbose;
          if (r.options.generateMap !== undefined && optGenerateMap) optGenerateMap.checked = r.options.generateMap;
          if (r.options.transferUnmatchedMedia !== undefined && optTransferUnmatched) optTransferUnmatched.checked = r.options.transferUnmatchedMedia;
        }
        updateExtractOnlyDisabledState();
      });
    } catch (e) {}
  }

  function saveOptions() {
    try {
      chrome.storage.local.set({ options: getOptions() });
    } catch (e) {}
  }

  function updateExtractOnlyDisabledState() {
    var disabled = optExtractOnly && optExtractOnly.checked;
    [mergeOutputSection, mappingSection].forEach(function (section) {
      if (!section) return;
      if (disabled) {
        section.classList.add('disabled');
        var inputs = section.querySelectorAll('input[type="checkbox"]');
        for (var i = 0; i < inputs.length; i++) inputs[i].disabled = true;
      } else {
        section.classList.remove('disabled');
        var inputs = section.querySelectorAll('input[type="checkbox"]');
        for (var i = 0; i < inputs.length; i++) inputs[i].disabled = false;
      }
    });
  }

  if (selectInputBtn) {
    selectInputBtn.addEventListener('click', function () {
      if (typeof showDirectoryPicker !== 'function') {
        inputLabel.textContent = 'Not supported: use Chrome/Edge with File System Access API';
        return;
      }
      showDirectoryPicker({ mode: 'read' }).then(function (handle) {
        inputHandle = handle;
        inputLabel.textContent = handle.name;
        inputLabel.classList.remove('empty');
        /* Default output folder to the same as input (user can change it in Advance). */
        outputHandle = handle;
        outputLabel.textContent = 'Same as input: ' + handle.name;
        outputLabel.classList.remove('empty');
        if (inputZipListSection) inputZipListSection.hidden = true;
        if (inputZipList) inputZipList.innerHTML = '';
        listTopLevelZipNames(handle).then(function (zipNames) {
          if (inputZipList) {
            zipNames.forEach(function (name) {
              var li = document.createElement('li');
              li.textContent = name;
              inputZipList.appendChild(li);
            });
            if (inputZipListSection) inputZipListSection.hidden = zipNames.length === 0;
          }
        }).catch(function () {
          if (inputZipListSection) inputZipListSection.hidden = true;
        });
      }).catch(function (err) {
        inputLabel.textContent = 'Cancelled or error: ' + (err.message || err);
      });
    });
  }

  var advanceBtn = document.getElementById('advanceBtn');
  var advancePanel = document.getElementById('advancePanel');
  if (advanceBtn && advancePanel) {
    advanceBtn.addEventListener('click', function () {
      var isOpen = !advancePanel.hidden;
      advancePanel.hidden = isOpen;
      advanceBtn.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
      advanceBtn.textContent = isOpen ? 'Advanced options' : 'Advanced options (hide)';
    });
  }

  var selectZipFilesBtn = document.getElementById('selectZipFilesBtn');
  var zipFilesLabel = document.getElementById('zipFilesLabel');
  var zipFileInput = document.getElementById('zipFileInput');
  if (selectZipFilesBtn && zipFileInput) {
    selectZipFilesBtn.addEventListener('click', function () {
      zipFileInput.click();
    });
  }
  if (zipFileInput && zipFilesLabel) {
    zipFileInput.addEventListener('change', function () {
      var files = zipFileInput.files;
      if (!files || files.length === 0) {
        directZipBuffers = [];
        directZipFiles = [];
        zipFilesLabel.textContent = 'No zips selected';
        zipFilesLabel.classList.toggle('empty', true);
        return;
      }
      /* Store File references only; pipeline will stream each zip (no full buffer allocation).
       * Avoids "Array buffer allocation failed" for large zips. */
      directZipBuffers = [];
      directZipFiles = Array.prototype.slice.call(files);
      zipFilesLabel.textContent = directZipFiles.length + ' zip(s) selected (will stream on Start Processing)';
      zipFilesLabel.classList.remove('empty');
      zipFileInput.value = '';
    });
  }

  if (selectOutputBtn) {
    selectOutputBtn.addEventListener('click', function () {
      if (typeof showDirectoryPicker !== 'function') {
        outputLabel.textContent = 'Not supported: use Chrome/Edge with File System Access API';
        return;
      }
      showDirectoryPicker({ mode: 'readwrite' }).then(function (handle) {
        outputHandle = handle;
        outputLabel.textContent = handle.name;
        outputLabel.classList.remove('empty');
      }).catch(function (err) {
        outputLabel.textContent = 'Cancelled or error: ' + (err.message || err);
      });
    });
  }

  [optExtractZip, optDeleteZip, optDeleteOriginals, optSingleLake, optAlbumsDuplicateFiles, optVerbose, optGenerateMap, optTransferUnmatched].forEach(function (el) {
    if (el) el.addEventListener('change', saveOptions);
  });
  if (optExtractOnly) {
    optExtractOnly.addEventListener('change', function () {
      updateExtractOnlyDisabledState();
      saveOptions();
    });
  }
  updateExtractOnlyDisabledState();

  if (scanBtn) {
    if (!ENABLE_SCAN_DRY_RUN) scanBtn.style.display = 'none';
    scanBtn.addEventListener('click', function () {
      if (!ENABLE_SCAN_DRY_RUN) return;
      if (!inputHandle) {
        setOutput('Select an input folder first.', false);
        return;
      }
      remarksEl.textContent = '';
      setOutput('Scanning…', false);
      scanBtn.disabled = true;
      var opts = getOptions();
      opts.dryRun = true;
      if (typeof GphotoPipeline === 'undefined') {
        setOutput('Pipeline not loaded.', false);
        scanBtn.disabled = false;
        return;
      }
      GphotoPipeline.scan(inputHandle, opts, log).then(function (report) {
        setOutput(JSON.stringify({
          zip_count: report.zip_count,
          files_scanned: report.files_scanned,
          matched_images: report.matched_images,
          matched_videos: report.matched_videos,
          transfer_only: report.transfer_only
        }, null, 2), false);
        scanBtn.disabled = false;
      }).catch(function (err) {
        setOutput('Scan error: ' + (err.message || err), false);
        scanBtn.disabled = false;
      });
    });
  }

  if (applyBtn) {
    applyBtn.addEventListener('click', function () {
      var hasInput = !!inputHandle;
      var hasZips = (directZipBuffers && directZipBuffers.length > 0) || (directZipFiles && directZipFiles.length > 0);
      if (!outputHandle || (!hasInput && !hasZips)) {
        setOutput('Select an output folder and either an input folder or zip files (or both). Then click Start Processing.', false);
        return;
      }
      /* Show Gphoto_panel_BG placeholder immediately when Start Processing is pressed; it stays until first random image loads */
      var randomImageImgEl = document.getElementById('randomImageImg');
      var randomImageCaptionEl = document.getElementById('randomImageCaption');
      try {
        var placeUrl = chrome.runtime.getURL('web/Gphoto_panel_BG.jpg');
        if (randomImageImgEl && placeUrl) {
          randomImageImgEl.src = placeUrl;
          randomImageImgEl.alt = 'Loading…';
          if (randomImageCaptionEl) randomImageCaptionEl.textContent = 'Loading…';
        }
      } catch (e) {}
      remarksEl.textContent = '';
      setOutput('Applying…', false);
      var summaryLineElStart = document.getElementById('summaryLine');
      if (summaryLineElStart) summaryLineElStart.textContent = '';
      updateProgressBar('progressExtract', 0);
      updateProgressBar('progressMerge', 0);
      applyBtn.disabled = true;
      scanBtn.disabled = true;
      var opts = getOptions();
      opts.dryRun = false;
      opts.onExtractProgress = function (p) { updateProgressBar('progressExtract', p); };
      opts.onMergeProgress = function (p) { updateProgressBar('progressMerge', p); };
      opts.onExtractError = function (msg) {
        if (remarksEl) {
          remarksEl.textContent = (remarksEl.textContent || '') + '[extract error] ' + msg + '\n';
          remarksEl.scrollTop = remarksEl.scrollHeight;
          remarksEl.classList.remove('empty');
        }
      };
      opts.onExtractionSummary = function (summary) {
        if (remarksEl && summary) {
          var line = 'Extraction: ' + (summary.success_percent || 0) + '% succeeded (' + (summary.entries_ok || 0) + ' OK, ' + (summary.entries_failed || 0) + ' failed).';
          remarksEl.textContent = (remarksEl.textContent || '') + line + '\n';
          remarksEl.scrollTop = remarksEl.scrollHeight;
        }
      };
      try {
        var manifest = typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.getManifest ? chrome.runtime.getManifest() : {};
        var ver = (manifest.version || '').toLowerCase();
        opts.extensionVersion = (ver.indexOf('dev') !== -1 || ver.indexOf('-dev') !== -1) ? 'Dev' : ('V' + (manifest.version || '0.0.0'));
      } catch (e) {
        opts.extensionVersion = 'Dev';
      }
      if (typeof GphotoExif !== 'undefined' && typeof GphotoExif.injectExifFromGoogleJson === 'function') {
        opts.injectExif = GphotoExif.injectExifFromGoogleJson;
      }
      if (typeof GphotoPipeline === 'undefined') {
        setOutput('Pipeline not loaded.', false);
        applyBtn.disabled = false;
        scanBtn.disabled = false;
        return;
      }
      var randomImageIntervalId = null;
      var lastRandomImageUrl = null;
      var randomImageImg = document.getElementById('randomImageImg');
      var randomImageCaption = document.getElementById('randomImageCaption');

      function showRandomImageFromProcessed() {
        if (!outputHandle || !randomImageImg) return;
        outputHandle.getDirectoryHandle('Processed').then(function (processedHandle) {
          return listProcessedImagePaths(processedHandle).then(function (paths) {
            if (paths.length === 0) return;
            var path = paths[Math.floor(Math.random() * paths.length)];
            return getFileFromProcessedByPath(processedHandle, path).then(function (file) {
              if (lastRandomImageUrl) URL.revokeObjectURL(lastRandomImageUrl);
              lastRandomImageUrl = URL.createObjectURL(file);
              randomImageImg.src = lastRandomImageUrl;
              randomImageImg.alt = path;
              if (randomImageCaption) randomImageCaption.textContent = path;
            });
          });
        }).catch(function () {});
      }

      var placeholderImageUrl = null;
      try {
        placeholderImageUrl = chrome.runtime.getURL('web/Gphoto_panel_BG.jpg');
      } catch (e) {}

      function runApply(optsToUse) {
        /* Show placeholder (Gphoto_panel_BG.jpg) until first random image from Processed is loaded */
        if (randomImageImg && placeholderImageUrl) {
          randomImageImg.src = placeholderImageUrl;
          randomImageImg.alt = 'Loading…';
          if (randomImageCaption) randomImageCaption.textContent = 'Loading…';
        }
        randomImageIntervalId = setInterval(function () {
          showRandomImageFromProcessed();
        }, 5000);
        /* Show first random image after 2s; placeholder stays until then */
        setTimeout(showRandomImageFromProcessed, 2000);

        GphotoPipeline.apply(inputHandle, outputHandle, optsToUse, log).then(function (result) {
          var report = result.report || result;
          var summary = result.summaryReport || null;
          var out = {
            summary: {
              dry_run: report.dry_run,
              merged: (report.merged && report.merged.length) || 0,
              transferred: (report.transferred && report.transferred.length) || 0,
              errors: (report.errors && report.errors.length) || 0,
              problematic_files: (report.problematic_files && report.problematic_files.length) || 0
            },
            extraction_summary: report.extraction_summary || null,
            errors_list: report.errors || []
          };
          if (summary) {
            out.summary_report = {
              generated_at: summary.generated_at,
              total_media_processed: summary.total_media_processed,
              json_matched: summary.json_matched,
              missing_json: summary.missing_json,
              exif_restored: summary.exif_restored,
              albums_rebuilt: summary.albums_rebuilt,
              duplicates_detected: summary.duplicates_detected,
              problematic_files_count: summary.problematic_files_count,
              extraction_zip_entries_total: summary.extraction_zip_entries_total,
              extraction_zip_entries_images: summary.extraction_zip_entries_images,
              extraction_zip_entries_videos: summary.extraction_zip_entries_videos,
              extraction_zip_entries_other: summary.extraction_zip_entries_other,
              extraction_entries_ok: summary.extraction_entries_ok,
              extraction_entries_failed: summary.extraction_entries_failed,
              merge_merged_count: summary.merge_merged_count,
              merge_transferred_count: summary.merge_transferred_count,
              merge_skipped_count: summary.merge_skipped_count,
              not_merged_count: summary.not_merged_count,
              remerged_count: summary.remerged_count
            };
          }
          setOutput(JSON.stringify(out, null, 2), false);
          var summaryLineEl = document.getElementById('summaryLine');
          if (summaryLineEl) {
            summaryLineEl.textContent = formatSummaryForPanel(summary);
          }
          if (randomImageIntervalId) {
            clearInterval(randomImageIntervalId);
            randomImageIntervalId = null;
          }
          if (lastRandomImageUrl) {
            URL.revokeObjectURL(lastRandomImageUrl);
            lastRandomImageUrl = null;
          }
          if (randomImageImg) {
            var fallbackUrl = chrome.runtime.getURL('web/Gphoto_panel_BG.jpg');
            randomImageImg.onerror = function () { randomImageImg.src = fallbackUrl; randomImageImg.onerror = null; };
            randomImageImg.src = chrome.runtime.getURL('web/Gphoto_BG1.png');
            randomImageImg.alt = 'Processing complete';
            if (randomImageCaption) randomImageCaption.textContent = 'Processing complete';
          }
          applyBtn.disabled = false;
          scanBtn.disabled = false;
        }).catch(function (err) {
          setOutput('Processing error: ' + (err.message || err), false);
          if (remarksEl) {
            var t = new Date();
            var timeStr = (t.getHours() < 10 ? '0' : '') + t.getHours() + ':' + (t.getMinutes() < 10 ? '0' : '') + t.getMinutes() + ':' + (t.getSeconds() < 10 ? '0' : '') + t.getSeconds();
            remarksEl.textContent = (remarksEl.textContent || '') + '[' + timeStr + '] Start Processing failed.\n';
            remarksEl.scrollTop = remarksEl.scrollHeight;
          }
          applyBtn.disabled = false;
          scanBtn.disabled = false;
        }).finally(function () {
          if (randomImageIntervalId) {
            clearInterval(randomImageIntervalId);
            randomImageIntervalId = null;
          }
          if (lastRandomImageUrl) {
            URL.revokeObjectURL(lastRandomImageUrl);
            lastRandomImageUrl = null;
          }
          /* Always show completion image when processing ends (success or error) */
          if (randomImageImg) {
            var completionUrl = chrome.runtime.getURL('web/Gphoto_BG1.png');
            var fallbackUrl = chrome.runtime.getURL('web/Gphoto_panel_BG.jpg');
            randomImageImg.onerror = function () { randomImageImg.src = fallbackUrl; randomImageImg.onerror = null; };
            randomImageImg.src = completionUrl;
            randomImageImg.alt = 'Processing complete';
            if (randomImageCaption) randomImageCaption.textContent = 'Processing complete';
          }
          applyBtn.disabled = false;
          scanBtn.disabled = false;
          updateProgressBar('progressExtract', 0);
          updateProgressBar('progressMerge', 0);
        });
      }
      /* Use zip buffers read at selection time (same user gesture); do not read File objects on Start Processing. */
      if (directZipBuffers && directZipBuffers.length > 0) {
        opts.directZipBuffers = directZipBuffers;
      }
      if (directZipFiles && directZipFiles.length > 0) {
        opts.directZipFiles = directZipFiles;
      }
      /* Pass EXIF injector from this context so merge always uses it (avoids undefined in some loads). */
      opts.injectExif = (typeof GphotoExif !== 'undefined' && typeof GphotoExif.injectExifFromGoogleJson === 'function')
        ? GphotoExif.injectExifFromGoogleJson
        : null;
      runApply(opts);
    });
  }

  loadStoredOptions();
})();
