function mergeEligibility(detail, reviewState) {
  if (!detail || detail.state !== 'open' || detail.merged) {
    return { allowed: false, reason: 'Pull request is not open' };
  }
  if (detail.draft) return { allowed: false, reason: 'Draft pull requests cannot be merged' };
  if (!detail.mergeable) return { allowed: false, reason: 'Resolve conflicts before merging' };
  if (detail.ci_state !== 'success') {
    const blockers = (detail.checks || []).filter(check =>
      ['failure', 'error', 'pending'].includes(check.state)
    ).map(check => check.name).filter(Boolean).slice(0, 3);
    return { allowed: false, reason: blockers.length ?
      'CI blocked by ' + blockers.join(', ') : 'CI must succeed before merging' };
  }
  if (!detail.head_sha) return { allowed: false, reason: 'Current head is unavailable' };
  if (reviewState && !reviewState.complete) {
    return { allowed: false, reason: 'Review every changed file before merging' };
  }
  return { allowed: true, reason: 'Ready to merge' };
}

function renderFile(file, index, reviewed, escapeHtml) {
  const filename = escapeHtml(file.filename || 'Unknown file');
  const panelId = 'pull-diff-' + index;
  const lines = (file.diff_lines || []).map(line => {
    const text = String(line);
    const kind = text.startsWith('@@') ? 'hunk' : text.startsWith('+') ? 'added' :
      text.startsWith('-') ? 'removed' : 'context';
    return '<span class="pull-diff-line ' + kind + '">' + escapeHtml(text) + '</span>';
  }).join('');
  const preview = file.diff_available
    ? '<pre class="pull-diff" id="' + panelId + '" hidden>' + lines +
      (file.diff_truncated ? '<span class="pull-diff-note">Preview truncated · open in Gitea for the full diff.</span>' : '') + '</pre>'
    : '<div class="pull-diff-empty" id="' + panelId + '" hidden>' +
      (file.diff_binary ? 'Binary file · preview unavailable.' : 'Diff preview unavailable.') +
      (file.diff_truncated ? ' The response was truncated.' : '') + '</div>';
  return '<article class="pull-file" data-pull-filename="' + filename + '">' +
    '<button type="button" class="pull-file-toggle" aria-expanded="false" aria-controls="' + panelId + '">' +
    '<strong>' + filename + '</strong><span class="small">' + escapeHtml(file.status || 'changed') +
    ' · +' + Number(file.additions || 0) + ' / −' + Number(file.deletions || 0) + '</span></button>' + preview +
    '<button type="button" class="pull-review-file" data-pull-review-file="' + filename +
    '" aria-pressed="' + String(reviewed) + '">' + (reviewed ? 'Reviewed' : 'Mark reviewed') + '</button></article>';
}

function focusNextUnreviewed(doc, detail, state, controller) {
  if (!detail || !state) return;
  const filename = controller.nextUnreviewed(detail, state);
  const article = Array.from(doc.querySelectorAll('#pull-files .pull-file'))
    .find(file => file.dataset.pullFilename === filename);
  const toggle = article?.querySelector('.pull-file-toggle');
  const panel = toggle && doc.getElementById(toggle.getAttribute('aria-controls'));
  if (!toggle || !panel) return;
  toggle.setAttribute('aria-expanded', 'true');
  panel.hidden = false;
  toggle.scrollIntoView({ block: 'center', behavior: 'smooth' });
  toggle.focus();
}

function removeFromSnapshot(data, item) {
  return { ...data, pulls:(data.pulls || []).filter(candidate =>
    candidate.repository !== item.repository || candidate.number !== item.number) };
}

function sameTarget(left, right) {
  return Boolean(left && right && left.repository === right.repository && Number(left.number) === Number(right.number));
}

function ownershipSelectors() {
  return ['#release-pull', '#load-pull-handoff', '#pull-handoff-recipient', '#confirm-pull-handoff'];
}

function renderHandoffCandidates(select, candidates, doc) {
  select.textContent = '';
  const placeholder = doc.createElement('option');
  placeholder.value = '';
  placeholder.textContent = candidates.length ? 'Select a teammate' : 'No eligible teammates';
  select.appendChild(placeholder);
  candidates.forEach(candidate => {
    const option = doc.createElement('option');
    option.value = candidate.login;
    option.textContent = candidate.name + (candidate.name === candidate.login ? '' : ' (@' + candidate.login + ')');
    select.appendChild(option);
  });
  return candidates.length;
}

function ownershipExitMessage(item, action, transitionResult) {
  return item.key + ' ' + action + (transitionResult === 'opened' ?
    '. Next work item opened.' : '. Choose the next ready Today item.');
}

function resetOwnershipControls(doc, item, checkpointed) {
  const qs = selector => doc.querySelector(selector);
  qs('#pull-ownership').open = false;
  qs('#pull-handoff-recipient').innerHTML = '<option value="">Select a teammate</option>';
  qs('#pull-handoff-recipient').disabled = true;
  qs('#confirm-pull-handoff').disabled = true;
  qs('#confirm-pull-handoff').textContent = checkpointed(item) ? 'Hand off & next' : 'Confirm handoff';
  qs('#load-pull-handoff').disabled = false;
  qs('#release-pull').disabled = false;
  qs('#release-pull').textContent = checkpointed(item) ? 'Release & next' : 'Release assignment';
  qs('#pull-handoff-status').textContent = 'Load teammates to transfer ownership.';
}

function bindOwnershipControls(doc, controller, getSelected, finish) {
  const qs = selector => doc.querySelector(selector);
  const load = qs('#load-pull-handoff');
  if (load.dataset.ownershipBound === 'true') return;
  load.dataset.ownershipBound = 'true';
  load.addEventListener('click', async () => {
    const selected = getSelected();
    if (!selected) return;
    const select = qs('#pull-handoff-recipient');
    load.disabled = true;
    qs('#pull-handoff-status').textContent = 'Loading eligible teammates…';
    try {
      const count = renderHandoffCandidates(select, await controller.loadHandoffCandidates(selected), doc);
      select.disabled = !count;
      qs('#confirm-pull-handoff').disabled = true;
      qs('#pull-handoff-status').textContent = count ?
        'Choose who should own this pull request next.' : 'No other eligible assignees were found.';
      if (count) select.focus();
    } catch (error) {
      qs('#pull-handoff-status').textContent = error.message + ' Retry loading teammates.';
      load.disabled = false;
      load.focus();
    }
  });
  qs('#pull-handoff-recipient').addEventListener('change', event => {
    qs('#confirm-pull-handoff').disabled = !event.target.value;
  });
  qs('#confirm-pull-handoff').addEventListener('click', async () => {
    const selected = getSelected();
    const recipient = qs('#pull-handoff-recipient').value;
    if (!selected || !recipient || !globalThis.confirm('Hand off ' + selected.key + ' to @' + recipient + '?')) return;
    const button = qs('#confirm-pull-handoff');
    button.disabled = true;
    qs('#pull-handoff-status').textContent = 'Confirming handoff…';
    try {
      await controller.handoff(selected, recipient);
      const transition = await finish(selected);
      qs('#my-work-action-status').textContent = transition === false ?
        selected.key + ' handed off to @' + recipient + '.' :
        ownershipExitMessage(selected, 'handed off to @' + recipient, transition);
    } catch (error) {
      qs('#pull-handoff-status').textContent = error.message + ' The pull request remains in My Work; retry.';
      button.disabled = false;
      button.focus();
    }
  });
  qs('#release-pull').addEventListener('click', async () => {
    const selected = getSelected();
    if (!selected || !globalThis.confirm('Release ' + selected.key + ' from your My Work?')) return;
    const button = qs('#release-pull');
    button.disabled = true;
    qs('#pull-sheet-status').textContent = 'Releasing assignment…';
    try {
      await controller.release(selected);
      const transition = await finish(selected);
      qs('#my-work-action-status').textContent = transition === false ?
        selected.key + ' released.' : ownershipExitMessage(selected, 'released', transition);
    } catch (error) {
      qs('#pull-sheet-status').textContent = error.message + ' The pull request remains in My Work; retry.';
      button.disabled = false;
      button.focus();
    }
  });
}

function createPullSheet({ fetchJson, storage, createConversationPager = globalThis.createConversationPager, createOperationId = () => globalThis.crypto?.randomUUID?.() || String(Date.now()) + '-' + Math.random() }) {
  let commentRequest = null;
  let mergeRequest = null;
  let checkRequest = null;
  let ownershipRequest = null;
  let candidateRequest = null;
  const reviewRequests = new Map();
  const reviewCache = new Map();
  const pathFor = item => 'api/v1/repos/' + String(item.repository || '').split('/')
    .map(encodeURIComponent).join('/') + '/pulls/' + encodeURIComponent(item.number);
  const draftKey = item => 'stackchain.pull-comment.v1:' + item.repository + '#' + item.number;
  const operationKey = item => draftKey(item) + ':operation';
  const reviewKey = (item, detail) => 'stackchain.pull-review.v1:' + item.repository + '#' + item.number + ':' + detail.head_sha;
  const fileNames = detail => (detail?.files || []).map(file => file.filename).filter(Boolean);

  function reviewState(item, detail) {
    const files = fileNames(detail);
    let saved = [];
    try {
      const value = JSON.parse(storage?.getItem(reviewKey(item, detail)) || '[]');
      if (Array.isArray(value)) saved = value;
    } catch (_error) { /* Corrupt progress safely starts over. */ }
    const reviewed = files.filter(filename => saved.includes(filename));
    return { reviewed, total: files.length, complete: reviewed.length === files.length };
  }

  return {
    load(item) {
      return fetchJson(pathFor(item) + '/detail', { headers: { Accept: 'application/json' } });
    },
    loadReview(item, headSha, { refresh = false } = {}) {
      const key = item.repository + '#' + item.number + ':' + headSha;
      if (!refresh && reviewCache.has(key)) return Promise.resolve(reviewCache.get(key));
      if (reviewRequests.has(key)) return reviewRequests.get(key);
      const request = fetchJson(pathFor(item) + '/review-data', {
        headers: { Accept: 'application/json' },
      }).then(detail => {
        const confirmedKey = item.repository + '#' + item.number + ':' + detail.head_sha;
        reviewCache.set(confirmedKey, detail);
        return detail;
      }).finally(() => reviewRequests.delete(key));
      reviewRequests.set(key, request);
      return request;
    },
    loadChecks(item) {
      if (checkRequest) return checkRequest;
      checkRequest = fetchJson(pathFor(item) + '/checks', {
        headers: { Accept: 'application/json' },
      }).finally(() => { checkRequest = null; });
      return checkRequest;
    },
    loadHandoffCandidates(item) {
      if (candidateRequest) return candidateRequest;
      candidateRequest = fetchJson(pathFor(item) + '/handoff-candidates', {
        headers: { Accept: 'application/json' },
      }).finally(() => { candidateRequest = null; });
      return candidateRequest;
    },
    handoff(item, recipient) {
      if (ownershipRequest) return ownershipRequest;
      ownershipRequest = fetchJson(pathFor(item) + '/handoff', {
        method: 'PATCH',
        headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
        body: JSON.stringify({ recipient }),
      }).finally(() => { ownershipRequest = null; });
      return ownershipRequest;
    },
    release(item) {
      if (ownershipRequest) return ownershipRequest;
      ownershipRequest = fetchJson(pathFor(item) + '/release', {
        method: 'PATCH', headers: { Accept: 'application/json' },
      }).finally(() => { ownershipRequest = null; });
      return ownershipRequest;
    },
    conversation(item, initialPage) {
      const pager = createConversationPager({
        loadPage: page => fetchJson(pathFor(item) + '/comments?page=' + encodeURIComponent(page) + '&limit=20', {
          headers: { Accept: 'application/json' },
        }),
      });
      pager.reset(initialPage || { comments: [], page: 1, older_page: null, total: 0 });
      return pager;
    },
    reviewState,
    toggleReviewed(item, detail, filename) {
      const state = reviewState(item, detail);
      const reviewed = new Set(state.reviewed);
      if (reviewed.has(filename)) reviewed.delete(filename);
      else if (fileNames(detail).includes(filename)) reviewed.add(filename);
      try { storage?.setItem(reviewKey(item, detail), JSON.stringify(Array.from(reviewed))); }
      catch (_error) { /* In-memory controls still work for this render. */ }
      return reviewState(item, detail);
    },
    nextUnreviewed(detail, state) {
      return fileNames(detail).find(filename => !state.reviewed.includes(filename)) || null;
    },
    loadDraft(item) {
      try { return storage?.getItem(draftKey(item)) || ''; }
      catch (_error) { return ''; }
    },
    saveDraft(item, body) {
      try {
        if ((storage?.getItem(draftKey(item)) || '') !== body) storage?.removeItem(operationKey(item));
        storage?.setItem(draftKey(item), body);
      }
      catch (_error) { /* The textarea remains the fallback. */ }
    },
    comment(item, body) {
      if (commentRequest) return commentRequest;
      this.saveDraft(item, body);
      let operationId;
      try {
        operationId = storage?.getItem(operationKey(item)) || String(createOperationId()).slice(0, 128);
        storage?.setItem(operationKey(item), operationId);
      } catch (_error) { operationId = String(createOperationId()).slice(0, 128); }
      commentRequest = fetchJson(pathFor(item) + '/comments', {
        method: 'POST',
        headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'Idempotency-Key': operationId },
        body: JSON.stringify({ body }),
      }).then(result => {
        try { storage?.removeItem(draftKey(item)); }
        catch (_error) { /* The upstream comment is authoritative. */ }
        try { storage?.removeItem(operationKey(item)); }
        catch (_error) { /* A confirmed result no longer needs replay identity. */ }
        return result;
      }).finally(() => { commentRequest = null; });
      return commentRequest;
    },
    merge(item, expectedHeadSha) {
      if (mergeRequest) return mergeRequest;
      mergeRequest = fetchJson(pathFor(item) + '/merge', {
        method: 'POST',
        headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
        body: JSON.stringify({ expected_head_sha: expectedHeadSha }),
      }).then(result => {
        if (!result?.merged) {
          throw new Error(result?.confirmation_pending && result?.error
            ? result.error
            : 'Pull request merge was not confirmed.');
        }
        return result;
      }).finally(() => { mergeRequest = null; });
      return mergeRequest;
    },
  };
}

createPullSheet.mergeEligibility = mergeEligibility;
createPullSheet.renderFile = renderFile;
createPullSheet.focusNextUnreviewed = focusNextUnreviewed;
createPullSheet.removeFromSnapshot = removeFromSnapshot;
createPullSheet.sameTarget = sameTarget;
createPullSheet.ownershipSelectors = ownershipSelectors;
createPullSheet.renderHandoffCandidates = renderHandoffCandidates;
createPullSheet.ownershipExitMessage = ownershipExitMessage;
createPullSheet.resetOwnershipControls = resetOwnershipControls;
createPullSheet.bindOwnershipControls = bindOwnershipControls;
if (typeof module !== 'undefined' && module.exports) module.exports = createPullSheet;
