{#
/**
 * OTS Signage — Modern Upload Media Modal
 * Replaces the core Xibo include-file-upload.twig with a redesigned,
 * drag-and-drop, multi-file upload experience.
 *
 * Reuses the existing openUploadForm(options) API so every page
 * (library, layout, fonts, player software, dataset, etc.) keeps working
 * without any caller changes.
 *
 * Dependencies already present in Xibo: jQuery, jQuery UI, jQuery File Upload,
 * Bootstrap 4 modal, moment.js.
 */
#}

{# ── Upload Modal Markup ────────────────────────────────────────────────── #}
<div class="modal fade ots-upload-modal" id="ots-upload-modal" tabindex="-1"
     role="dialog" aria-labelledby="ots-upload-modal-title" aria-modal="true">
  <div class="modal-dialog modal-lg modal-dialog-centered" role="document">
    <div class="modal-content ots-upload-content">

      {# Header #}
      <div class="modal-header ots-upload-header">
        <h5 class="modal-title ots-upload-title" id="ots-upload-modal-title"></h5>
        <button type="button" class="ots-upload-close" data-dismiss="modal" aria-label="Close">
          <svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M15 5L5 15M5 5l10 10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
        </button>
      </div>

      {# Body #}
      <div class="modal-body ots-upload-body">

        {# Tab switcher: File / URL #}
        <div class="ots-upload-tabs" id="ots-upload-tabs">
          <button type="button" class="ots-upload-tab active" data-tab="file" id="ots-tab-file">
            <i class="fas fa-file-upload"></i> File
          </button>
          <button type="button" class="ots-upload-tab" data-tab="url" id="ots-tab-url">
            <i class="fas fa-link"></i> URL
          </button>
        </div>

        {# ── FILE TAB ──────────────────────────────────────────────── #}
        <div class="ots-upload-tab-content" id="ots-upload-tab-file">

        {# Folder selector row – shown only when options.folderSelector is true #}
        <div class="ots-upload-folder-row d-none" id="ots-upload-folder-row">
          <span class="ots-upload-folder-label" id="ots-upload-folder-label"></span>
          <button type="button" class="btn btn-sm ots-upload-folder-btn" id="ots-upload-folder-btn" title="">
            <i class="fas fa-folder-open"></i> <span id="ots-upload-folder-text"></span>
          </button>
        </div>

        {# Max file size notice #}
        <div class="ots-upload-notice d-none" id="ots-upload-size-notice"></div>

        {# Drop-zone #}
        <form id="ots-upload-form" enctype="multipart/form-data" method="POST">
          <div class="ots-upload-dropzone" id="ots-upload-dropzone" role="button" tabindex="0"
               aria-label="Drag and drop files here or click to browse">
            <div class="ots-upload-dropzone-inner">
              <div class="ots-upload-dropzone-icon">
                <svg width="48" height="48" viewBox="0 0 48 48" fill="none">
                  <rect x="4" y="8" width="40" height="32" rx="6" stroke="currentColor" stroke-width="2" fill="none"/>
                  <path d="M24 30V18m0 0l-6 6m6-6l6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
                </svg>
              </div>
              <p class="ots-upload-dropzone-text">
                <strong id="ots-upload-drop-label">Drop files here</strong><br>
                <span class="ots-upload-dropzone-sub">or <span class="ots-upload-browse-link">browse your computer</span></span>
              </p>
            </div>
          </div>
          {# File input lives outside the dropzone to avoid click-event loops #}
          <input type="file" id="ots-upload-input" name="files[]" multiple class="ots-upload-input-hidden" />
        </form>

        {# Valid extensions badge #}
        <div class="ots-upload-ext-info d-none" id="ots-upload-ext-info"></div>

        {# Options row (update in layouts / delete old revisions) #}
        <div class="ots-upload-options d-none" id="ots-upload-options"></div>

        {# File list / queue #}
        <div class="ots-upload-queue d-none" id="ots-upload-queue">
          <div class="ots-upload-queue-header">
            <span class="ots-upload-queue-title">Files</span>
            <span class="ots-upload-queue-count" id="ots-upload-queue-count"></span>
          </div>
          <ul class="ots-upload-file-list" id="ots-upload-file-list"></ul>
        </div>

        </div>{# /ots-upload-tab-file #}

        {# ── URL TAB ───────────────────────────────────────────────── #}
        <div class="ots-upload-tab-content d-none" id="ots-upload-tab-url">
          <div class="ots-upload-url-section">
            <div class="ots-upload-url-icon">
              <svg width="40" height="40" viewBox="0 0 40 40" fill="none">
                <path d="M17 23l6-6m-3.5.5a5 5 0 017.07 0l1.42 1.42a5 5 0 010 7.07l-2.83 2.83a5 5 0 01-7.07 0" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
                <path d="M23 17l-6 6m3.5-.5a5 5 0 00-7.07 0l-1.42-1.42a5 5 0 010-7.07l2.83-2.83a5 5 0 017.07 0" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
              </svg>
            </div>
            <p class="ots-upload-url-desc">Add media from an external URL</p>
            <div class="ots-upload-url-fields">
              <div class="ots-upload-url-field">
                <label for="ots-upload-url-input">URL</label>
                <input type="url" id="ots-upload-url-input" class="form-control ots-upload-url-input"
                       placeholder="https://example.com/image.jpg" autocomplete="off" />
              </div>
              <button type="button" class="btn ots-upload-btn-start ots-upload-url-add" id="ots-upload-url-add">
                <i class="fas fa-plus"></i> Add to queue
              </button>
            </div>
            {# URL queue list #}
            <div class="ots-upload-queue d-none" id="ots-upload-url-queue">
              <div class="ots-upload-queue-header">
                <span class="ots-upload-queue-title">URLs</span>
                <span class="ots-upload-queue-count" id="ots-upload-url-queue-count"></span>
              </div>
              <ul class="ots-upload-file-list" id="ots-upload-url-list"></ul>
            </div>
          </div>
        </div>{# /ots-upload-tab-url #}

      </div>

      {# Footer #}
      <div class="modal-footer ots-upload-footer">
        <button type="button" class="btn ots-upload-btn-cancel" data-dismiss="modal" id="ots-upload-btn-cancel">Cancel</button>
        <button type="button" class="btn ots-upload-btn-start d-none" id="ots-upload-btn-start">
          <i class="fas fa-cloud-upload-alt"></i> <span id="ots-upload-btn-start-label">Start upload</span>
        </button>
        <button type="button" class="btn ots-upload-btn-done d-none" id="ots-upload-btn-done">Done</button>
      </div>

    </div>
  </div>
</div>

{# ── Upload JavaScript ──────────────────────────────────────────────────── #}
<script type="text/javascript" nonce="{{ cspNonce }}">
/**
 * openUploadForm(options)
 * Drop-in replacement for the core Xibo openUploadForm.
 * Keeps the same options API so existing page callers (library, layout, fonts,
 * player-software, dataset) work without modification.
 *
 * Options shape (all optional except url):
 * {
 *   url:              String   – POST endpoint
 *   title:            String   – modal title
 *   initialisedBy:    String   – an identifier for the caller
 *   buttons: {
 *     main: { label, className, callback }
 *   },
 *   templateOptions: {
 *     multi:           Boolean  – allow multiple files (default true)
 *     trans: { addFiles, startUpload, cancelUpload, selectFolder, ... },
 *     upload: { maxSize, maxSizeMessage, validExt, validExtensionsMessage },
 *     folderSelector:  Boolean,
 *     currentWorkingFolderId: Number,
 *     oldMediaId:      Number   – when replacing a media item
 *     oldFolderId:     Number,
 *     updateInAllChecked:        Boolean,
 *     deleteOldRevisionsChecked: Boolean,
 *   },
 *   uploadDoneEvent:   Function – called when all uploads finish
 * }
 */
window.openUploadForm = function openUploadForm(options) {
  'use strict';

  options = options || {};
  var tOpts = options.templateOptions || {};
  var trans = tOpts.trans || {};
  var upload = tOpts.upload || {};
  var multi = tOpts.multi !== false;

  // ── References ──
  var $modal     = $('#ots-upload-modal');
  var $title     = $('#ots-upload-modal-title');
  var $dropzone  = $('#ots-upload-dropzone');
  var $form      = $('#ots-upload-form');
  var $input     = $('#ots-upload-input');
  var $queue     = $('#ots-upload-queue');
  var $fileList  = $('#ots-upload-file-list');
  var $queueCount= $('#ots-upload-queue-count');
  var $btnStart  = $('#ots-upload-btn-start');
  var $btnDone   = $('#ots-upload-btn-done');
  var $btnCancel = $('#ots-upload-btn-cancel');
  var $startLabel= $('#ots-upload-btn-start-label');
  var $folderRow = $('#ots-upload-folder-row');
  var $sizeNotice= $('#ots-upload-size-notice');
  var $extInfo   = $('#ots-upload-ext-info');
  var $optionsRow= $('#ots-upload-options');
  var $dropLabel = $('#ots-upload-drop-label');

  // ── Extra references for URL tab ──
  var $tabFile   = $('#ots-tab-file');
  var $tabUrl    = $('#ots-tab-url');
  var $panelFile = $('#ots-upload-tab-file');
  var $panelUrl  = $('#ots-upload-tab-url');
  var $urlInput  = $('#ots-upload-url-input');
  var $urlAddBtn = $('#ots-upload-url-add');
  var $urlQueue  = $('#ots-upload-url-queue');
  var $urlList   = $('#ots-upload-url-list');
  var $urlCount  = $('#ots-upload-url-queue-count');
  var urlQueue   = [];   // { url, id, status, $el, xhr }

  // ── Reset state ──
  $fileList.empty();
  $urlList.empty();
  $queue.addClass('d-none');
  $urlQueue.addClass('d-none');
  $btnStart.addClass('d-none');
  $btnDone.addClass('d-none');
  $folderRow.addClass('d-none');
  $sizeNotice.addClass('d-none');
  $extInfo.addClass('d-none');
  $optionsRow.addClass('d-none').empty();
  $input.val('');
  $urlInput.val('');
  $dropzone.removeClass('ots-upload-dropzone--over ots-upload-dropzone--has-files');

  // Reset to file tab
  $tabFile.addClass('active');
  $tabUrl.removeClass('active');
  $panelFile.removeClass('d-none');
  $panelUrl.addClass('d-none');

  // ── Populate UI from options ──
  $title.text(options.title || 'Upload');
  $startLabel.text(trans.startUpload || 'Start upload');
  $btnCancel.text(trans.cancelUpload || 'Cancel');
  $dropLabel.text(trans.addFiles || 'Drop files here');

  if (!multi) {
    $input.removeAttr('multiple');
  } else {
    $input.attr('multiple', 'multiple');
  }

  // Max file size notice
  if (upload.maxSizeMessage) {
    $sizeNotice.text(upload.maxSizeMessage).removeClass('d-none');
  }

  // Valid extensions
  if (upload.validExt) {
    var extList = upload.validExt.replace(/\|/g, ', ');
    var extMsg  = upload.validExtensionsMessage || ('Allowed: ' + extList);
    $extInfo.text(extMsg).removeClass('d-none');
  }

  // Folder selector
  if (tOpts.folderSelector) {
    $folderRow.removeClass('d-none');
    $('#ots-upload-folder-label').text((trans.selectedFolder || 'Current Folder:'));
    $('#ots-upload-folder-text').text(trans.selectFolder || 'Select Folder');
    $('#ots-upload-folder-btn').attr('title', trans.selectFolderTitle || 'Change folder');

    // Wire folder-selector button using the CMS's built-in folder-tree modal
    // (templates['folder-tree'], initJsTreeAjax — provided by the Xibo core)
    $('#ots-upload-folder-btn').off('click').on('click', function() {
      var modalId = 'ots-upload-folder-tree-modal';
      var containerId = 'ots-upload-folder-form-tree';
      var $ftModal = $('#' + modalId);

      // ── First open: build the modal from the Handlebars template ──
      if ($ftModal.length === 0 && typeof templates !== 'undefined' && templates['folder-tree']) {
        var folderTreeTpl = templates['folder-tree'];
        var treeConfig = {
          container: containerId,
          modal: modalId
        };
        if (typeof translations !== 'undefined' && translations.folderTree) {
          treeConfig.trans = translations.folderTree;
        }
        $('body').append(folderTreeTpl(treeConfig));
        $ftModal = $('#' + modalId);

        // Inject OK / Cancel footer
        var $footer = $ftModal.find('.modal-footer');
        if ($footer.length === 0) {
          $footer = $('<div class="modal-footer"></div>');
          $ftModal.find('.modal-content').append($footer);
        }
        $footer.empty().append(
          '<button type="button" class="btn btn-sm ots-upload-btn-cancel" data-dismiss="modal">Cancel</button>' +
          '<button type="button" class="btn btn-sm ots-upload-btn-start" id="ots-folder-confirm-btn">' +
            '<i class="fas fa-check"></i> OK' +
          '</button>'
        );

        // Configure as static backdrop once
        $ftModal.modal({ backdrop: 'static', keyboard: true, show: false });

        // Fix stacked-modal body class when this modal closes
        $ftModal.on('hidden.bs.modal', function() {
          if ($('.modal:visible').length) {
            $(document.body).addClass('modal-open');
          }
        });
      }

      if ($ftModal.length === 0) {
        console.warn('Folder tree template not available');
        return;
      }

      // ── Every open: reset pending selection and re-init jstree ──
      var pendingFolderId = tOpts.currentWorkingFolderId || null;
      var pendingFolderName = null;

      // Destroy previous jstree instance so it re-initialises cleanly
      var $treeContainer = $ftModal.find('#' + containerId);
      if ($treeContainer.jstree && $treeContainer.jstree(true)) {
        try { $treeContainer.jstree('destroy'); } catch(e) {}
      }

      // Initialise jstree
      if (typeof initJsTreeAjax === 'function') {
        initJsTreeAjax($treeContainer, 'ots-upload-form', true, 600);
      }

      // Show the modal (works on first and subsequent opens)
      $ftModal.modal('show');

      // Bind selection handler after the modal is visible + jstree auto-select settles
      $ftModal.off('shown.bs.modal.otsUpload').on('shown.bs.modal.otsUpload', function() {
        setTimeout(function() {
          $treeContainer.off('select_node.jstree.otsUpload')
            .on('select_node.jstree.otsUpload', function(e, data) {
              if (data && data.node) {
                pendingFolderId = data.node.id;
                pendingFolderName = data.node.text || data.node.id;
              }
            });
        }, 500);
      });

      // OK button — apply selection and close
      $ftModal.find('#ots-folder-confirm-btn').off('click').on('click', function() {
        if (pendingFolderId) {
          tOpts.currentWorkingFolderId = pendingFolderId;
          $('#ots-upload-folder-text').text(pendingFolderName || pendingFolderId);
        }
        $ftModal.modal('hide');
      });
    });
  }

  // Done button
  var mainBtn = (options.buttons && options.buttons.main) || {};
  $btnDone.text(mainBtn.label || 'Done');
  if (mainBtn.className) {
    $btnDone.attr('class', 'btn ots-upload-btn-done d-none ' + mainBtn.className);
  }

  // ── Internal state ──
  var fileQueue   = [];   // { file, id, status, $el, xhr }
  var nextId      = 0;
  var uploading   = false;
  var uploadCount = 0;
  var successCount= 0;

  // ── Helper: human-readable size ──
  function humanSize(bytes) {
    if (bytes < 1024) return bytes + ' B';
    if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
    return (bytes / 1048576).toFixed(1) + ' MB';
  }

  // ── Helper: valid extension check ──
  function isExtAllowed(filename) {
    if (!upload.validExt) return true;
    var ext = filename.split('.').pop().toLowerCase();
    var allowed = upload.validExt.toLowerCase().split('|');
    return allowed.indexOf(ext) !== -1;
  }

  // ── Helper: generate preview (images only) ──
  function generatePreview(file, $thumb) {
    if (file.type && file.type.indexOf('image/') === 0 && file.size < 10 * 1048576) {
      var reader = new FileReader();
      reader.onload = function(e) {
        $thumb.css('background-image', 'url(' + e.target.result + ')').addClass('has-preview');
      };
      reader.readAsDataURL(file);
    } else {
      // Icon based on type
      var icon = 'fa-file';
      if (file.type && file.type.indexOf('video/') === 0) icon = 'fa-file-video';
      else if (file.type && file.type.indexOf('audio/') === 0) icon = 'fa-file-audio';
      else if (file.type && file.type.indexOf('application/pdf') === 0) icon = 'fa-file-pdf';
      else if (file.name && /\.(xlsx?|csv)$/i.test(file.name)) icon = 'fa-file-excel';
      $thumb.html('<i class="fas ' + icon + '"></i>');
    }
  }

  // ── Add files to queue ──
  function addFiles(files) {
    for (var i = 0; i < files.length; i++) {
      var file = files[i];

      // Multi check
      if (!multi && fileQueue.length >= 1) {
        // Replace existing file
        fileQueue = [];
        $fileList.empty();
      }

      var id = nextId++;
      var extOk = isExtAllowed(file.name);
      var sizeOk = !upload.maxSize || file.size <= upload.maxSize;

      var statusClass = '';
      var statusText = humanSize(file.size);
      if (!extOk) { statusClass = 'ots-upload-file--error'; statusText = 'Invalid file type'; }
      else if (!sizeOk) { statusClass = 'ots-upload-file--error'; statusText = 'File too large'; }

      var $el = $(
        '<li class="ots-upload-file-item ' + statusClass + '" data-id="' + id + '">' +
          '<div class="ots-upload-file-thumb"></div>' +
          '<div class="ots-upload-file-info">' +
            '<span class="ots-upload-file-name">' + $('<span>').text(file.name).html() + '</span>' +
            '<span class="ots-upload-file-meta">' + statusText + '</span>' +
            '<div class="ots-upload-file-progress"><div class="ots-upload-file-progress-bar"></div></div>' +
          '</div>' +
          '<button type="button" class="ots-upload-file-remove" aria-label="Remove" title="Remove">' +
            '<i class="fas fa-times"></i>' +
          '</button>' +
        '</li>'
      );

      generatePreview(file, $el.find('.ots-upload-file-thumb'));

      $el.find('.ots-upload-file-remove').on('click', (function(fileId) {
        return function() { removeFile(fileId); };
      })(id));

      $fileList.append($el);

      fileQueue.push({
        file: file,
        id: id,
        status: (extOk && sizeOk) ? 'pending' : 'error',
        $el: $el,
        xhr: null
      });
    }

    updateQueueUI();
  }

  // ── Remove file ──
  function removeFile(id) {
    fileQueue = fileQueue.filter(function(f) {
      if (f.id === id) {
        f.$el.slideUp(200, function() { $(this).remove(); });
        if (f.xhr) { try { f.xhr.abort(); } catch(e){} }
        return false;
      }
      return true;
    });
    updateQueueUI();
  }

  // ── Queue UI update ──
  function updateQueueUI() {
    var validFiles = fileQueue.filter(function(f) { return f.status === 'pending'; });
    var total = fileQueue.length;
    $queueCount.text(total + ' file' + (total !== 1 ? 's' : ''));
    if (total > 0) {
      $queue.removeClass('d-none');
      $dropzone.addClass('ots-upload-dropzone--has-files');
    } else {
      $queue.addClass('d-none');
      $dropzone.removeClass('ots-upload-dropzone--has-files');
    }
    // Show start button only when there are valid pending files and not already uploading
    if (validFiles.length > 0 && !uploading) {
      $btnStart.removeClass('d-none');
    } else if (!uploading) {
      $btnStart.addClass('d-none');
    }
  }

  // ── Upload all pending items (files + URLs) ──
  function startUpload() {
    var filePending = fileQueue.filter(function(f) { return f.status === 'pending'; });
    var urlPending  = urlQueue.filter(function(f)  { return f.status === 'pending'; });
    var allPending  = filePending.concat(urlPending);
    if (allPending.length === 0) return;
    uploading = true;
    uploadCount = allPending.length;
    successCount = 0;

    $btnStart.addClass('d-none');
    $btnCancel.text(trans.cancelUpload || 'Cancel');

    // Upload sequentially
    var idx = 0;
    function uploadNext() {
      if (idx >= allPending.length) {
        uploading = false;
        onAllDone();
        return;
      }
      var item = allPending[idx++];
      if (item.file) {
        uploadSingle(item, uploadNext);
      } else if (item.url) {
        uploadUrlItem(item, uploadNext);
      } else {
        uploadNext();
      }
    }
    uploadNext();
  }

  // ── Upload a single file ──
  function uploadSingle(item, callback) {
    item.status = 'uploading';
    item.$el.addClass('ots-upload-file--uploading');

    var formData = new FormData();
    formData.append('files[]', item.file, item.file.name);

    // Standard Xibo hidden fields
    if (tOpts.currentWorkingFolderId) formData.append('folderId', tOpts.currentWorkingFolderId);
    if (tOpts.oldMediaId) formData.append('oldMediaId', tOpts.oldMediaId);
    if (tOpts.oldFolderId) formData.append('oldFolderId', tOpts.oldFolderId);

    // Checkboxes
    $optionsRow.find('input[type="checkbox"]').each(function() {
      formData.append($(this).attr('name'), $(this).is(':checked') ? '1' : '0');
    });

    var $bar = item.$el.find('.ots-upload-file-progress-bar');
    var $meta = item.$el.find('.ots-upload-file-meta');

    item.xhr = $.ajax({
      url: options.url,
      type: 'POST',
      data: formData,
      processData: false,
      contentType: false,
      timeout: 120000,
      xhr: function() {
        var xhr = new XMLHttpRequest();
        xhr.upload.addEventListener('progress', function(e) {
          if (e.lengthComputable) {
            var pct = Math.round((e.loaded / e.total) * 100);
            $bar.css('width', pct + '%');
            $meta.text(pct + '%');
          }
        });
        return xhr;
      },
      success: function(response) {
        item.status = 'done';
        item.$el.removeClass('ots-upload-file--uploading').addClass('ots-upload-file--done');
        $bar.css('width', '100%');
        $meta.text('Complete');
        successCount++;
        if (typeof options.uploadDoneEvent === 'function') {
          options.uploadDoneEvent(item.file, response);
        }
        callback();
      },
      error: function(xhr) {
        item.status = 'error';
        item.$el.removeClass('ots-upload-file--uploading').addClass('ots-upload-file--error');
        var msg = 'Upload failed';
        try { msg = JSON.parse(xhr.responseText).message || msg; } catch(e){}
        $meta.text(msg);
        $bar.css('width', '0%');
        callback();
      }
    });
  }

  // ── All uploads finished ──
  function onAllDone() {
    $btnDone.removeClass('d-none');
    $btnStart.addClass('d-none');
    $queueCount.text(successCount + '/' + uploadCount + ' uploaded');
  }

  // ── Drag & drop ──
  $dropzone.off('.otsUpload').on({
    'dragenter.otsUpload dragover.otsUpload': function(e) {
      e.preventDefault();
      e.stopPropagation();
      $dropzone.addClass('ots-upload-dropzone--over');
    },
    'dragleave.otsUpload': function(e) {
      e.preventDefault();
      e.stopPropagation();
      $dropzone.removeClass('ots-upload-dropzone--over');
    },
    'drop.otsUpload': function(e) {
      e.preventDefault();
      e.stopPropagation();
      $dropzone.removeClass('ots-upload-dropzone--over');
      var dt = e.originalEvent.dataTransfer;
      if (dt && dt.files && dt.files.length) {
        addFiles(dt.files);
      }
    }
  });

  // Click to browse — use native .click() on the raw DOM element;
  // jQuery's .trigger('click') does NOT open the file picker in most browsers.
  $dropzone.off('click.otsUpload').on('click.otsUpload', function(e) {
    // Don't trigger if clicking on the remove button inside the queue
    if ($(e.target).closest('.ots-upload-file-remove').length) return;
    $input[0].click();
  });

  // Keyboard accessibility on dropzone
  $dropzone.off('keydown.otsUpload').on('keydown.otsUpload', function(e) {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      $input[0].click();
    }
  });

  // File input change
  $input.off('change.otsUpload').on('change.otsUpload', function() {
    if (this.files && this.files.length) {
      addFiles(this.files);
      // Reset so the same file can be re-selected
      this.value = '';
    }
  });

  // Start upload button
  $btnStart.off('click.otsUpload').on('click.otsUpload', function() {
    startUpload();
  });

  // Done button
  $btnDone.off('click.otsUpload').on('click.otsUpload', function() {
    if (mainBtn.callback) {
      mainBtn.callback();
    }
    $modal.modal('hide');
  });

  // Clean up on modal close
  $modal.off('hidden.bs.modal.otsUpload').on('hidden.bs.modal.otsUpload', function() {
    // Abort any in-progress uploads
    fileQueue.forEach(function(f) {
      if (f.xhr) { try { f.xhr.abort(); } catch(e){} }
    });
    fileQueue = [];
    $fileList.empty();
    uploading = false;
  });

  // ── Tab switching ──
  $tabFile.off('click.otsUpload').on('click.otsUpload', function() {
    $tabFile.addClass('active');
    $tabUrl.removeClass('active');
    $panelFile.removeClass('d-none');
    $panelUrl.addClass('d-none');
  });
  $tabUrl.off('click.otsUpload').on('click.otsUpload', function() {
    $tabUrl.addClass('active');
    $tabFile.removeClass('active');
    $panelUrl.removeClass('d-none');
    $panelFile.addClass('d-none');
  });

  // ── URL: add to queue ──
  function addUrlToQueue(url) {
    if (!url || !url.trim()) return;
    url = url.trim();
    var id = nextId++;
    var displayName = url.length > 60 ? url.substring(0, 57) + '...' : url;

    var $el = $(
      '<li class="ots-upload-file-item" data-id="' + id + '">' +
        '<div class="ots-upload-file-thumb"><i class="fas fa-link"></i></div>' +
        '<div class="ots-upload-file-info">' +
          '<span class="ots-upload-file-name">' + $('<span>').text(displayName).html() + '</span>' +
          '<span class="ots-upload-file-meta">Ready</span>' +
          '<div class="ots-upload-file-progress"><div class="ots-upload-file-progress-bar"></div></div>' +
        '</div>' +
        '<button type="button" class="ots-upload-file-remove" aria-label="Remove" title="Remove">' +
          '<i class="fas fa-times"></i>' +
        '</button>' +
      '</li>'
    );

    $el.find('.ots-upload-file-remove').on('click', (function(fileId) {
      return function() { removeUrlItem(fileId); };
    })(id));

    $urlList.append($el);
    urlQueue.push({ url: url, id: id, status: 'pending', $el: $el, xhr: null });
    updateUrlQueueUI();
  }

  function removeUrlItem(id) {
    urlQueue = urlQueue.filter(function(f) {
      if (f.id === id) {
        f.$el.slideUp(200, function() { $(this).remove(); });
        if (f.xhr) { try { f.xhr.abort(); } catch(e){} }
        return false;
      }
      return true;
    });
    updateUrlQueueUI();
  }

  function updateUrlQueueUI() {
    var pending = urlQueue.filter(function(f) { return f.status === 'pending'; });
    var total = urlQueue.length;
    $urlCount.text(total + ' URL' + (total !== 1 ? 's' : ''));
    if (total > 0) {
      $urlQueue.removeClass('d-none');
    } else {
      $urlQueue.addClass('d-none');
    }
    if (pending.length > 0 && !uploading) {
      $btnStart.removeClass('d-none');
    } else if (!uploading && fileQueue.filter(function(f) { return f.status === 'pending'; }).length === 0) {
      $btnStart.addClass('d-none');
    }
  }

  $urlAddBtn.off('click.otsUpload').on('click.otsUpload', function() {
    addUrlToQueue($urlInput.val());
    $urlInput.val('').focus();
  });

  // Allow Enter key in URL input to add
  $urlInput.off('keydown.otsUpload').on('keydown.otsUpload', function(e) {
    if (e.key === 'Enter') {
      e.preventDefault();
      addUrlToQueue($urlInput.val());
      $urlInput.val('').focus();
    }
  });

  // ── Upload a single URL item ──
  function uploadUrlItem(item, callback) {
    item.status = 'uploading';
    item.$el.addClass('ots-upload-file--uploading');
    var $bar = item.$el.find('.ots-upload-file-progress-bar');
    var $meta = item.$el.find('.ots-upload-file-meta');
    $bar.css('width', '50%');
    $meta.text('Downloading...');

    var postData = { url: item.url };
    if (tOpts.currentWorkingFolderId) postData.folderId = tOpts.currentWorkingFolderId;

    item.xhr = $.ajax({
      url: options.url,
      type: 'POST',
      data: postData,
      timeout: 120000,
      success: function(response) {
        item.status = 'done';
        item.$el.removeClass('ots-upload-file--uploading').addClass('ots-upload-file--done');
        $bar.css('width', '100%');
        $meta.text('Complete');
        successCount++;
        if (typeof options.uploadDoneEvent === 'function') {
          options.uploadDoneEvent(null, response);
        }
        callback();
      },
      error: function(xhr) {
        item.status = 'error';
        item.$el.removeClass('ots-upload-file--uploading').addClass('ots-upload-file--error');
        var msg = 'Upload failed';
        try { msg = JSON.parse(xhr.responseText).message || msg; } catch(e){}
        $meta.text(msg);
        $bar.css('width', '0%');
        callback();
      }
    });
  }

  // ── Clean up URL queue on modal close ──
  $modal.off('hidden.bs.modal.otsUploadUrl').on('hidden.bs.modal.otsUploadUrl', function() {
    urlQueue.forEach(function(f) {
      if (f.xhr) { try { f.xhr.abort(); } catch(e){} }
    });
    urlQueue = [];
    $urlList.empty();
  });

  // ── Show modal ──
  $modal.modal({ backdrop: 'static', keyboard: true });
};
</script>
