function withFilingEstimate(draft, value) {
  draft.estimateMinutes = Number(value) || null;
  return draft;
}

function unfiledDraftSummary(item) {
  const plan = item.filingPlan || {};
  const ready = Boolean(plan.repository);
  const startsToday = plan.completionIntent === 'create-and-start';
  const evidenceCount = Number(item.attachmentCount || (item.hasAttachment ? 1 : 0));
  const evidence = evidenceCount ? `${evidenceCount} screenshot${evidenceCount === 1 ? '' : 's'}` : 'No screenshots';
  return {
    label:ready ? 'Ready to file' : 'Needs filing',
    repository:plan.repository || '', ready,
    action:ready ? (startsToday ? 'Review & start' : 'Review & file') : 'Choose repository',
    details:[startsToday ? 'Create & start' : 'Create issue',
      plan.estimateMinutes ? `${plan.estimateMinutes} min` : '', evidence].filter(Boolean).join(' · '),
  };
}

function unfiledDraftDisplayTitle(item) {
  return String(item?.title || '').trim() || (item?.hasAttachment ? 'Untitled photo draft' : 'Untitled draft');
}

function unfiledShouldFocusTitle({mobile = false, continuing = false} = {}) {
  return !mobile || continuing;
}

function unfiledSavedMessage(item) {
  return item.repository ? 'Saved planned Draft. Resume on any signed-in device.' :
    'Saved to Drafts. Choose a repository when you’re ready to file it.';
}

function applyIssueFilingMode(qs, enabled) {
  qs('#create-issue-filing').hidden = !enabled;
  qs('.create-issue-capture-actions').hidden = false;
  qs('#file-new-issue').hidden = enabled;
  qs('#create-issue-heading').textContent = enabled ? 'File issue' : 'Capture work';
}

function createUnfiledCaptures({
  storage,
  attachmentStore = null,
  getCaptureLogin = () => '',
  getCurrentLogin = () => '',
  createId = () => globalThis.crypto?.randomUUID?.() || String(Date.now()) + '-' + Math.random().toString(16).slice(2),
  now = () => Date.now(),
  maxItems = 20,
}) {
  const storageKey = 'stackchain.unfiled-issues.v1';
  const listeners = new Set();
  const onChange = (id, removed) => listeners.forEach(listener => listener(id, removed));

  function read() {
    try {
      const record = JSON.parse(storage?.getItem(storageKey) || 'null');
      if (record?.version !== 1 || !Array.isArray(record.items)) return [];
      return record.items.filter(item =>
        item && typeof item.id === 'string' && typeof item.ownerLogin === 'string' &&
        typeof item.title === 'string' && (item.title.trim() || item.hasAttachment === true) &&
        typeof item.body === 'string'
      );
    } catch (_error) { return []; }
  }

  function write(items) {
    storage?.setItem(storageKey, JSON.stringify({version:1, items}));
  }

  function list() {
    const currentLogin = String(getCurrentLogin() || '').trim();
    return read().slice().sort((left, right) => Number(right.savedAt) - Number(left.savedAt))
      .map(item => ({...item, quarantined: !currentLogin || currentLogin !== item.ownerLogin}));
  }

  function capacity() {
    const items = read().slice().sort((left, right) => Number(right.savedAt) - Number(left.savedAt));
    return {full:items.length >= maxItems, count:items.length, maxItems, oldest:items.at(-1) || null};
  }

  function filingPlan(value = {}, remote = false) {
    const get = (local, wire) => value?.[remote ? wire : local];
    const repository = String(get('repository', 'repository') || '').trim();
    if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) return null;
    const labelIds = Array.from(new Set((Array.isArray(get('labelIds', 'label_ids')) ?
      get('labelIds', 'label_ids') : []).filter(id => Number.isInteger(id) && id > 0))).slice(0, 20);
    const plan = {repository, labelIds};
    const milestoneId = Number(get('milestoneId', 'milestone_id'));
    if (Number.isInteger(milestoneId) && milestoneId > 0) plan.milestoneId = milestoneId;
    const dueDate = String(get('dueDate', 'due_date') || '');
    if (/^\d{4}-\d{2}-\d{2}$/.test(dueDate)) plan.dueDate = dueDate;
    const bounded = (local, wire, limit) => {
      const text = String(get(local, wire) || '').trim();
      if (text) plan[local] = text.slice(0, limit);
    };
    bounded('templateName', 'template_name', 80);
    bounded('templateId', 'template_id', 80);
    bounded('capturedBody', 'captured_body', 10000);
    if (get('unassigned', 'unassigned') === true) plan.unassigned = true;
    const assignee = String(get('assignee', 'assignee') || '').trim();
    if (!plan.unassigned && /^[A-Za-z0-9_.-]+$/.test(assignee)) {
      plan.assignee = assignee;
      bounded('assigneeName', 'assignee_name', 255);
    }
    const estimateMinutes = Number(get('estimateMinutes', 'estimate_minutes'));
    if (Number.isInteger(estimateMinutes) && estimateMinutes >= 5 && estimateMinutes <= 1440) {
      plan.estimateMinutes = estimateMinutes;
    }
    const completionIntent = String(get('completionIntent', 'completion_intent') || '');
    if (['create', 'create-and-start'].includes(completionIntent)) plan.completionIntent = completionIntent;
    return plan;
  }

  function exportFilingPlan(plan) {
    if (!plan) return null;
    return {
      repository:plan.repository, label_ids:plan.labelIds,
      ...(plan.milestoneId ? {milestone_id:plan.milestoneId} : {}),
      ...(plan.dueDate ? {due_date:plan.dueDate} : {}),
      ...(plan.templateName ? {template_name:plan.templateName} : {}),
      ...(plan.templateId ? {template_id:plan.templateId} : {}),
      ...(plan.capturedBody ? {captured_body:plan.capturedBody} : {}),
      ...(plan.unassigned ? {unassigned:true} : {}),
      ...(plan.assignee ? {assignee:plan.assignee} : {}),
      ...(plan.assigneeName ? {assignee_name:plan.assigneeName} : {}),
      ...(plan.estimateMinutes ? {estimate_minutes:plan.estimateMinutes} : {}),
      ...(plan.completionIntent ? {completion_intent:plan.completionIntent} : {}),
    };
  }

  function prepare(note) {
    const title = String(note?.title || '').trim().slice(0, 255);
    const body = String(note?.body || '').trim().slice(0, 10000);
    const attachment = note?.attachment;
    const attachments = note?.attachments;
    if (Array.isArray(attachments) && attachments.length > 5) {
      throw new Error('You can attach up to 5 screenshots.');
    }
    const validAttachments = Array.isArray(attachments) && attachments.length > 0 &&
      attachments.every(value => value?.blob && value?.filename &&
        ['image/png', 'image/jpeg', 'image/webp'].includes(String(value?.contentType || '')));
    if (attachments && !validAttachments) {
      throw new Error('One or more screenshots are unavailable. Choose them again before saving.');
    }
    const hasAttachment = Boolean(attachment?.blob && attachment?.filename &&
      ['image/png', 'image/jpeg', 'image/webp'].includes(String(attachment?.contentType || '')));
    if (attachment && !hasAttachment) throw new Error('The screenshot is unavailable. Choose it again before saving.');
    if (!title && !hasAttachment && !validAttachments) throw new Error('Add a title or photo before saving.');
    const ownerLogin = String(getCaptureLogin() || '').trim();
    if (!ownerLogin) throw new Error('Offline identity is unavailable. Reconnect once before saving private work.');
    if ((hasAttachment || validAttachments) && !attachmentStore) {
      throw new Error('Screenshot storage is unavailable. Your capture is still open; retry after reloading.');
    }
    const seenBlockers = new Set();
    const blockers = (Array.isArray(note?.blockers) ? note.blockers : []).reduce((items, blocker) => {
      const repository = String(blocker?.repository || '').trim();
      const number = Number(blocker?.number);
      const key = repository + '#' + number;
      if (items.length >= 5 || seenBlockers.has(key) ||
          !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository) ||
          !Number.isInteger(number) || number < 1) return items;
      seenBlockers.add(key);
      items.push({repository, number,
        title:String(blocker?.title || '').replace(/\s+/g, ' ').trim().slice(0, 255)});
      return items;
    }, []);
    return {
      title, body, ownerLogin, attachment, attachments, blockers,
      filingPlan:filingPlan(note),
      hasAttachment:hasAttachment || validAttachments,
      attachmentCount:validAttachments ? attachments.length : (hasAttachment ? 1 : 0),
    };
  }

  function persist(prepared, existing, removed = null) {
    const item = {
      id:String(createId()), ownerLogin:prepared.ownerLogin, title:prepared.title, body:prepared.body,
      savedAt:Number(now()), ...(prepared.hasAttachment ? {
        hasAttachment:true, attachmentCount:prepared.attachmentCount,
      } : {}), ...(prepared.blockers.length ? {
        blockers:prepared.blockers, blockerCount:prepared.blockers.length,
      } : {}), ...(prepared.filingPlan ? {filingPlan:prepared.filingPlan} : {}),
    };
    const items = [item, ...existing.filter(candidate => candidate.id !== item.id)];
    const writeItems = () => {
      try { write(items); }
      catch (error) {
        if (prepared.hasAttachment) Promise.resolve(attachmentStore.delete(item.id)).catch(() => {});
        throw error;
      }
      onChange(item.id, false);
      return item;
    };
    const stage = prepared.hasAttachment
      ? Promise.resolve(attachmentStore.put(item.id, prepared.attachments ? {
        attachments:prepared.attachments.map(value => ({
          filename:String(value.filename).slice(0, 255),
          contentType:String(value.contentType), blob:value.blob,
          ...(String(value.note || '').trim() ? {note:String(value.note).trim().slice(0, 240)} : {}),
        })),
      } : {
        filename:String(prepared.attachment.filename).slice(0, 255),
        contentType:String(prepared.attachment.contentType), blob:prepared.attachment.blob,
      }))
      : null;
    if (!stage && !removed?.hasAttachment) return writeItems();
    return Promise.resolve(stage).then(async () => {
      if (!removed?.hasAttachment) return writeItems();
      const removedAttachment = await attachmentStore.get(removed.id);
      try {
        await attachmentStore.delete(removed.id);
      } catch (error) {
        if (prepared.hasAttachment) await Promise.resolve(attachmentStore.delete(item.id)).catch(() => {});
        throw error;
      }
      try { return writeItems(); }
      catch (error) {
        if (removedAttachment) await Promise.resolve(attachmentStore.put(removed.id, removedAttachment)).catch(() => {});
        throw error;
      }
    });
  }

  function save(note) {
    const prepared = prepare(note);
    const existing = read();
    if (existing.length >= maxItems) throw new Error('Drafts full — nothing was deleted.');
    return persist(prepared, existing);
  }

  function replaceOldest(note, expectedOldestId) {
    const prepared = prepare(note);
    const existing = read().slice().sort((left, right) => Number(right.savedAt) - Number(left.savedAt));
    const oldest = existing.at(-1);
    if (existing.length < maxItems || !oldest || oldest.id !== expectedOldestId) {
      throw new Error('Drafts changed. Review them before replacing anything.');
    }
    return persist(prepared, existing.filter(item => item.id !== oldest.id), oldest);
  }

  function discard(id) {
    const items = read();
    const remaining = items.filter(item => item.id !== id);
    if (remaining.length === items.length) return false;
    const removed = items.find(item => item.id === id);
    if (!removed?.hasAttachment) {
      write(remaining);
      onChange(id, true);
      return true;
    }
    return Promise.resolve(attachmentStore?.get(id)).then(removedAttachment =>
      Promise.resolve(attachmentStore?.delete(id)).then(async () => {
        try { write(remaining); }
        catch (error) {
          if (removedAttachment) await Promise.resolve(attachmentStore?.put(id, removedAttachment)).catch(() => {});
          throw error;
        }
        onChange(id, true);
        return true;
      })
    );
  }

  function resume(id, confirmedLogin) {
    const item = read().find(candidate => candidate.id === id);
    if (!item) throw new Error('This capture is no longer available.');
    if (!confirmedLogin || String(confirmedLogin).trim() !== item.ownerLogin) {
      throw new Error('Reconnect with the account that saved this capture.');
    }
    const draft = {...(item.filingPlan || {repository:'', labelIds:[]}), title:item.title, body:item.body,
      ...(Array.isArray(item.blockers) && item.blockers.length ? {blockers:item.blockers} : {})};
    if (!item.hasAttachment) return draft;
    if (!attachmentStore) throw new Error('The saved screenshot is unavailable. Retry after reloading.');
    return Promise.resolve(attachmentStore.get(id)).then(attachment => {
      if (Array.isArray(attachment?.attachments) && attachment.attachments.length) {
        if (attachment.attachments.length > 5 || attachment.attachments.some(value =>
          !value?.blob || !value?.filename || !value?.contentType)) {
          throw new Error('The saved screenshots are unavailable. Keep this Draft and retry.');
        }
        return {...draft, attachments:attachment.attachments};
      }
      if (!attachment?.blob || !attachment?.filename || !attachment?.contentType) {
        throw new Error('The saved screenshot is unavailable. Keep this Draft and retry.');
      }
      return {...draft, attachment};
    });
  }

  function completeResume(id) { return discard(id); }

  function encodeBytes(buffer) {
    const bytes = new Uint8Array(buffer);
    let binary = '';
    for (let offset = 0; offset < bytes.length; offset += 0x8000) {
      binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000));
    }
    return btoa(binary);
  }

  function decodeBytes(value, contentType) {
    const binary = atob(value);
    const bytes = new Uint8Array(binary.length);
    for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
    return new Blob([bytes], {type:contentType});
  }

  async function exportOwned(login) {
    const owner = String(login || '').trim();
    const exported = [];
    for (const item of read().filter(candidate => candidate.ownerLogin === owner)) {
      let evidence = [];
      if (item.hasAttachment) {
        const stored = await attachmentStore?.get(item.id);
        const attachments = Array.isArray(stored?.attachments) ? stored.attachments :
          (stored?.blob ? [stored] : []);
        if (attachments.length !== Number(item.attachmentCount || 1)) {
          throw new Error('The saved screenshots are unavailable. Keep this Draft and retry.');
        }
        evidence = await Promise.all(attachments.map(async attachment => ({
          filename:attachment.filename,
          content_type:attachment.contentType,
          ...(attachment.note ? {note:attachment.note} : {}),
          data:encodeBytes(await attachment.blob.arrayBuffer()),
        })));
      }
      exported.push({
        id:item.id, title:item.title, body:item.body, saved_at:Number(item.savedAt),
        ...(item.filingPlan ? {filing_plan:exportFilingPlan(item.filingPlan)} : {}),
        ...(Array.isArray(item.blockers) && item.blockers.length ? {blockers:item.blockers} : {}),
        ...(evidence.length ? {evidence} : {}),
      });
    }
    return exported;
  }

  async function mergeRemote(drafts, login) {
    const ownerLogin = String(login || '').trim();
    if (!ownerLogin || !Array.isArray(drafts)) return 0;
    const existing = read();
    const known = new Set(existing.map(item => item.id));
    const imported = [];
    for (const remote of drafts) {
      if (!remote || known.has(remote.id) || imported.length + existing.length >= maxItems) continue;
      const evidence = Array.isArray(remote.evidence) ? remote.evidence : [];
      if (evidence.length && !attachmentStore) continue;
      const plan = filingPlan(remote.filing_plan, true);
      const item = {
        id:String(remote.id), ownerLogin, title:String(remote.title || ''), body:String(remote.body || ''),
        savedAt:Number(remote.saved_at),
        ...(plan ? {filingPlan:plan} : {}),
        ...(Array.isArray(remote.blockers) && remote.blockers.length ? {
          blockers:remote.blockers, blockerCount:remote.blockers.length,
        } : {}),
        ...(evidence.length ? {hasAttachment:true, attachmentCount:evidence.length} : {}),
      };
      if (!item.id || (!item.title.trim() && !evidence.length) || !Number.isFinite(item.savedAt)) continue;
      if (evidence.length) {
        await attachmentStore.put(item.id, {attachments:evidence.map(entry => ({
          filename:String(entry.filename), contentType:String(entry.content_type),
          blob:decodeBytes(String(entry.data), String(entry.content_type)),
          ...(entry.note ? {note:String(entry.note)} : {}),
        }))});
      }
      known.add(item.id);
      imported.push(item);
    }
    if (imported.length) write(imported.concat(existing));
    return imported.length;
  }

  async function reconcileRemote(drafts, login) {
    const ownerLogin = String(login || '').trim();
    if (!ownerLogin || !Array.isArray(drafts)) return 0;
    const existing = read();
    const retained = existing.filter(item => item.ownerLogin !== ownerLogin);
    const reconciled = [];
    const stagedEvidence = new Map();
    for (const remote of drafts) {
      if (!remote || reconciled.length + retained.length >= maxItems) continue;
      const evidence = Array.isArray(remote.evidence) ? remote.evidence : [];
      if (evidence.length && !attachmentStore) {
        throw new Error('The synchronized screenshots cannot be stored on this device.');
      }
      const plan = filingPlan(remote.filing_plan, true);
      const item = {
        id:String(remote.id || ''), ownerLogin, title:String(remote.title || ''), body:String(remote.body || ''),
        savedAt:Number(remote.saved_at),
        ...(plan ? {filingPlan:plan} : {}),
        ...(Array.isArray(remote.blockers) && remote.blockers.length ? {
          blockers:remote.blockers, blockerCount:remote.blockers.length,
        } : {}),
        ...(evidence.length ? {hasAttachment:true, attachmentCount:evidence.length} : {}),
      };
      if (!item.id || (!item.title.trim() && !evidence.length) || !Number.isFinite(item.savedAt)) continue;
      if (evidence.length) {
        stagedEvidence.set(item.id, {attachments:evidence.map(entry => ({
          filename:String(entry.filename), contentType:String(entry.content_type),
          blob:decodeBytes(String(entry.data), String(entry.content_type)),
          ...(entry.note ? {note:String(entry.note)} : {}),
        }))});
      }
      reconciled.push(item);
    }
    const evidenceIds = new Set(reconciled.filter(item => item.hasAttachment).map(item => item.id));
    const affectedIds = new Set(stagedEvidence.keys());
    for (const item of existing) if (item.ownerLogin === ownerLogin && item.hasAttachment) affectedIds.add(item.id);
    const previousEvidence = new Map();
    for (const id of affectedIds) previousEvidence.set(id, await attachmentStore?.get(id));
    try {
      for (const [id, value] of stagedEvidence) await attachmentStore.put(id, value);
      for (const item of existing) {
        if (item.ownerLogin === ownerLogin && item.hasAttachment && !evidenceIds.has(item.id)) {
          await attachmentStore?.delete(item.id);
        }
      }
      write(reconciled.concat(retained));
    } catch (error) {
      for (const [id, value] of previousEvidence) {
        await Promise.resolve(value ? attachmentStore?.put(id, value) : attachmentStore?.delete(id)).catch(() => {});
      }
      throw error;
    }
    return reconciled.length;
  }

  return {list, capacity, save, replaceOldest, discard, resume, completeResume,
    exportOwned, mergeRemote, reconcileRemote, currentLogin:getCurrentLogin,
    subscribe:listener => (listeners.add(listener), () => listeners.delete(listener))};
}

if (typeof module !== 'undefined' && module.exports) {
  module.exports = createUnfiledCaptures;
  module.exports.summary = unfiledDraftSummary;
  module.exports.displayTitle = unfiledDraftDisplayTitle;
  module.exports.shouldFocusTitle = unfiledShouldFocusTitle;
}
