(function(root, factory) {
  const review = typeof module === 'object' && module.exports ? require('./issue-evidence-review.js') : root.issueEvidenceReview;
  const editor = typeof module === 'object' && module.exports ? require('./issue-evidence-editor.js') : root.issueEvidenceEditor;
  const api = factory(review, editor);
  if (typeof module === 'object' && module.exports) module.exports = api;
  else root.issueAttachment = api;
})(typeof self !== 'undefined' ? self : this, function(issueEvidenceReview, issueEvidenceEditor) {
  'use strict';

  const MAX_BYTES = 2 * 1024 * 1024;
  const MAX_PIXELS = issueEvidenceReview.MAX_PIXELS;
  const MAX_FILES = 5;
  const MAX_FILES_MESSAGE = 'Up to 5 screenshots. Remove one before adding another.';
  const IMAGE_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp']);

  function normalizeNote(value) {
    return String(value || '').replace(/\s+/g, ' ').trim().slice(0, 240);
  }

  function escapeMarkdown(value) {
    return value.replace(/([\\`*_[\]{}()<>#+\-.!|])/g, '\\$1');
  }

  const optimizeImage = issueEvidenceReview.optimizeImage;

  function multipart(attachment) {
    let blob = attachment?.blob;
    if (!blob && attachment?.data) {
      const binary = atob(String(attachment.data));
      const bytes = Uint8Array.from(binary, character => character.charCodeAt(0));
      blob = new Blob([bytes], { type: String(attachment.contentType || '') });
    }
    if (!blob) throw new Error('The saved screenshot is unavailable. Retry before sending.');
    const form = new FormData();
    form.append('file', blob, String(attachment.filename || 'screenshot'));
    return form;
  }

  function create(options) {
    const upload = options.upload;
    const maxFiles = options.maxFiles === MAX_FILES ? MAX_FILES : 1;
    const inspectPixels = options.inspectPixels === true;
    const optimizeSelectedImage = options.optimizeImage || optimizeImage;
    const onCheckpoint = options.onCheckpoint;
    const createOperationId = options.createOperationId || (() => {
      if (typeof globalThis !== 'undefined' && globalThis.crypto?.randomUUID) {
        return globalThis.crypto.randomUUID();
      }
      return Date.now().toString(36) + '-' + Math.random().toString(36).slice(2);
    });
    let selected = [];
    let selectionGeneration = 0;
    let restoring = false;

    function changed() {
      if (!restoring && typeof onCheckpoint === 'function') {
        Promise.resolve().then(onCheckpoint).catch(() => { /* Caller surfaces durable-storage failures. */ });
      }
    }

    function commitSelection(file) {
      if (!file || !IMAGE_TYPES.has(file.type) || !Number.isFinite(file.size) ||
          file.size <= 0 || file.size > MAX_BYTES) {
        throw new Error('The screenshot could not be optimized below 2 MB. Try cropping it and choose it again.');
      }
      if (selected.length >= maxFiles) {
        if (maxFiles === 1) selected = [];
        else throw new Error(MAX_FILES_MESSAGE);
      }
      selected.push({file, note:'', confirmed:null, serialized:null, operationId:createOperationId()});
      changed();
      return state();
    }

    function select(file) {
      if (selected.length >= maxFiles && maxFiles > 1) {
        throw new Error(MAX_FILES_MESSAGE);
      }
      const generation = ++selectionGeneration;
      if (!file || !IMAGE_TYPES.has(file.type)) {
        throw new Error('Choose a PNG, JPEG, or WebP screenshot.');
      }
      if (!Number.isFinite(file.size) || file.size <= 0) {
        throw new Error('Choose a screenshot that is 2 MB or smaller.');
      }
      if (file.size > MAX_BYTES || inspectPixels) {
        return Promise.resolve(optimizeSelectedImage(file)).then(optimized => {
          if (generation !== selectionGeneration) return state();
          return commitSelection(optimized);
        });
      }
      return commitSelection(file);
    }

    function clear() {
      selectionGeneration += 1;
      selected = [];
    }

    function remove(index) {
      const position = Number(index);
      if (!Number.isInteger(position) || position < 0 || position >= selected.length) return state();
      selectionGeneration += 1;
      selected.splice(position, 1);
      changed();
      return state();
    }

    function move(fromIndex, toIndex) {
      const from = Number(fromIndex);
      const to = Number(toIndex);
      if (!Number.isInteger(from) || !Number.isInteger(to) || from < 0 ||
          from >= selected.length || to < 0 || to >= selected.length || from === to) return state();
      const [item] = selected.splice(from, 1);
      selected.splice(to, 0, item);
      changed();
      return state();
    }

    function replace(index, file) {
      const position = Number(index);
      if (!Number.isInteger(position) || position < 0 || position >= selected.length) return state();
      if (!file || !IMAGE_TYPES.has(file.type) || !Number.isFinite(file.size) ||
          file.size <= 0 || file.size > MAX_BYTES) {
        throw new Error('The edited screenshot must be a PNG, JPEG, or WebP image no larger than 2 MB.');
      }
      selectionGeneration += 1;
      selected[position] = {
        ...selected[position], file, confirmed:null, serialized:null, operationId:createOperationId(),
      };
      changed();
      return state();
    }

    function setNote(index, value) {
      const position = Number(index);
      if (!Number.isInteger(position) || position < 0 || position >= selected.length) return state();
      selected[position].note = normalizeNote(value);
      selected[position].serialized = null;
      changed();
      return state();
    }

    function note(index) {
      const position = Number(index);
      return Number.isInteger(position) && selected[position] ? selected[position].note : '';
    }

    function restore(value) {
      restoring = true;
      try {
        if (Array.isArray(value)) {
          clear();
          if (value.length > maxFiles) throw new Error(MAX_FILES_MESSAGE);
          value.forEach(attachment => restoreOne(attachment));
          return state();
        }
        clear();
        restoreOne(value);
        return state();
      } finally { restoring = false; }
    }

    function restoreOne(value) {
      const contentType = String(value?.contentType || '');
      const filename = String(value?.filename || '');
      const blob = value?.blob;
      const data = String(value?.data || '');
      if (!blob && !data) {
        throw new Error('The saved screenshot is unavailable. Retry before editing this issue.');
      }
      const padding = (data.match(/=*$/) || [''])[0].length;
      const size = blob ? Number(blob.size) : Math.max(1, Math.floor(data.length * 3 / 4) - padding);
      commitSelection({ name: filename, type: contentType, size, ...(blob ? { blob } : {}) });
      const item = selected[selected.length - 1];
      item.note = normalizeNote(value?.note);
      item.serialized = {
        ...(blob ? { filename, contentType, blob } : { filename, contentType, data }),
        ...(item.note ? {note:item.note} : {}),
      };
      const operationId = String(value?.operationId || '').slice(0, 128);
      if (operationId) item.operationId = operationId;
      const markdown = String(value?.confirmed?.markdown || '');
      if (markdown) item.confirmed = { markdown };
    }

    function state() {
      const values = selected.map(item => ({
        name: item.file.name,
        size: item.file.size,
        uploaded: Boolean(item.confirmed),
      }));
      return values.length > 1 ? values : (values[0] || null);
    }

    function serializeItem(item) {
      if (!item.serialized) item.serialized = {
        filename:item.file.name, contentType:item.file.type, blob:item.file.blob || item.file,
        ...(item.note ? {note:item.note} : {}),
      };
      return {
        ...item.serialized,
        operationId:item.operationId,
        ...(item.confirmed?.markdown ? { confirmed:{ markdown:item.confirmed.markdown } } : {}),
      };
    }

    async function serialize() {
      if (!selected.length) return null;
      const values = selected.map(serializeItem);
      return values.length > 1 ? values : values[0];
    }

    async function prepareComment(target, body) {
      const text = String(body || '').trim();
      if (!selected.length) return text;
      const markdown = [];
      for (const evidence of selected) {
        if (!evidence.confirmed) {
          const attachment = serializeItem(evidence);
          evidence.confirmed = await upload({
            repository: target.repository,
            number: target.number,
            filename: attachment.filename,
            content_type: attachment.contentType,
            ...(attachment.blob ? { blob: attachment.blob } : { data: attachment.data }),
            operation_id: evidence.operationId,
          });
          if (!evidence.confirmed || typeof evidence.confirmed.markdown !== 'string' || !evidence.confirmed.markdown) {
            evidence.confirmed = null;
            throw new Error('The server did not confirm the screenshot upload.');
          }
          await onCheckpoint?.();
        }
        if (evidence.note) {
          markdown.push('**Screenshot ' + (markdown.length + 1) + ' — ' + escapeMarkdown(evidence.note) + '**\n\n' +
            evidence.confirmed.markdown);
        } else markdown.push(evidence.confirmed.markdown);
      }
      const evidence = markdown.join('\n\n');
      return text ? text + '\n\n' + evidence : evidence;
    }

    return { select, restore, remove, move, replace, setNote, note, clear, state, serialize, prepareComment };
  }

  function mount(options) {
    const inputs = Array.from(new Set((options.inputs || [options.input]).filter(Boolean)));
    if (!inputs.includes(options.input)) inputs.push(options.input);
    const controller = create({
      ...options, inspectPixels:true,
      maxFiles:options.maxFiles || (options.input?.multiple ? MAX_FILES : 1),
    });
    const clearSelection = controller.clear;
    const restoreSelection = controller.restore;
    const reviewEnabled = Boolean(options.tray && options.earlier && options.later);
    let previewUrl = '';
    let selectionSequence = 0;
    const review = reviewEnabled ? issueEvidenceReview.create({
      ...options, edit:options.editor?.edit, controller,
    }) : null;

    function setBusy(value) {
      inputs.forEach(input => { input.disabled = Boolean(value); });
      options.remove.disabled = Boolean(value);
      if (options.editor?.edit) options.editor.edit.disabled = Boolean(value) || !controller.state();
      review?.setBusy(value);
    }

    function clearPreview() {
      selectionSequence += 1;
      if (previewUrl) options.revokeObjectURL(previewUrl);
      previewUrl = '';
      review?.clear();
      if (options.editor?.edit) options.editor.edit.disabled = true;
      options.image.src = '';
      options.preview.hidden = true;
      inputs.forEach(input => {
        input.value = '';
        input.disabled = false;
      });
      clearSelection();
      options.onChange?.(controller.state());
    }

    function showPreview(file, optimized) {
      if (previewUrl) options.revokeObjectURL(previewUrl);
      previewUrl = options.createObjectURL(file);
      options.image.src = previewUrl;
      options.meta.textContent = file.name + ' · ' + Math.ceil(file.size / 1024) + ' KB';
      options.preview.hidden = false;
      if (options.editor?.edit) options.editor.edit.disabled = false;
      options.status.textContent = optimized ? 'Screenshot optimized and ready to upload.' :
        (options.readyMessage || 'Screenshot ready to upload with this comment.');
    }

    function selectFromInput(event) {
      const sourceInput = event.target;
      const files = Array.from(sourceInput.files || []);
      const sequence = ++selectionSequence;
      if (!files.length) return;
      let first;
      try { first = controller.select(files[0]); }
      catch (error) {
        options.status.textContent = error.message;
        sourceInput.value = '';
        return;
      }
      if (!reviewEnabled && files.length === 1 && (!first || typeof first.then !== 'function')) {
        showPreview(files[0], false);
        return;
      }
      let optimized = Boolean(first && typeof first.then === 'function');
      if (optimized) {
        options.status.textContent = 'Optimizing screenshot…';
        inputs.forEach(input => { input.disabled = true; });
      }
      const selectAll = files.slice(1).reduce((pending, file) => pending.then(async () => {
        const result = controller.select(file);
        if (result && typeof result.then === 'function') {
          optimized = true;
          options.status.textContent = 'Optimizing screenshot…';
          inputs.forEach(input => { input.disabled = true; });
          await result;
        }
      }), Promise.resolve(first));
      return selectAll.then(() => controller.serialize()).then(value => {
        if (sequence !== selectionSequence) return;
        const values = Array.isArray(value) ? value : [value];
        const latest = values[values.length - 1];
        if (reviewEnabled) {
          review.render(values, values.length - 1, optimized);
        } else {
          showPreview(latest.blob, optimized);
          if (values.length > 1) {
            options.meta.textContent = values.length + ' screenshots ready · latest: ' + latest.filename;
            options.status.textContent = values.length + ' screenshots ready to file in this order.';
          }
        }
        options.onChange?.(controller.state());
      }).catch(error => {
        if (sequence === selectionSequence) {
          options.status.textContent = error.message;
          sourceInput.value = '';
        }
      }).finally(() => {
        if (sequence === selectionSequence) inputs.forEach(input => { input.disabled = false; });
      });
    }
    inputs.forEach(input => input.addEventListener('change', selectFromInput));
    options.remove.addEventListener('click', () => {
      const current = controller.state();
      const count = Array.isArray(current) ? current.length : (current ? 1 : 0);
      if (count <= 1) {
        clearPreview();
        options.status.textContent = options.removedMessage || 'Screenshot removed. Your comment is unchanged.';
        return Promise.resolve();
      }
      const removedIndex = reviewEnabled ? review.activeIndex() : count - 1;
      controller.remove(removedIndex);
      selectionSequence += 1;
      review?.afterRemoval(removedIndex, count);
      return controller.serialize().then(value => {
        const values = Array.isArray(value) ? value : [value];
        if (reviewEnabled) {
          review.render(values, review.activeIndex());
        } else {
          const latest = values[values.length - 1];
          showPreview(latest.blob, false);
          options.meta.textContent = values.length + ' screenshots ready · latest: ' + latest.filename;
        }
        options.status.textContent = options.removedMessage || 'Latest screenshot removed. Your text is unchanged.';
        options.onChange?.(controller.state());
      });
    });

    function restorePreview(value) {
      clearPreview();
      const restored = restoreSelection(value);
      const values = Array.isArray(value) ? value : [value];
      if (reviewEnabled) {
        review.render(values, values.length - 1);
        return restored;
      }
      const latest = values[values.length - 1];
      previewUrl = latest.blob ? options.createObjectURL(latest.blob) :
        'data:' + latest.contentType + ';base64,' + latest.data;
      options.image.src = previewUrl;
      options.meta.textContent = values.length > 1 ?
        values.length + ' screenshots ready · latest: ' + latest.filename :
        restored.name + ' · ' + Math.ceil(restored.size / 1024) + ' KB';
      options.preview.hidden = false;
      options.status.textContent = values.length > 1 ?
        values.length + ' screenshots ready to file in this order.' :
        (options.readyMessage || 'Screenshot ready to upload with this comment.');
      return restored;
    }

    if (review && options.editor && issueEvidenceEditor) {
      issueEvidenceEditor.mount({
        ...options.editor,
        controller,
        getActiveIndex:review.activeIndex,
        optimizeImage,
        onApplied: async index => {
          const value = await controller.serialize();
          review.render(value, index);
          options.status.textContent = options.editor.appliedMessage ||
            'Edited screenshot flattened and ready to file.';
        },
      });
      options.editor.edit.disabled = !controller.state();
    }
    return Object.assign(controller, { clear: clearPreview, restore: restorePreview, setBusy });
  }

  return { create, mount, multipart, optimizeImage, MAX_BYTES, MAX_PIXELS };
});
