(function (root, factory) {
  const api = factory();
  if (typeof module === 'object' && module.exports) module.exports = api;
  else root.createWorkRoute = api;
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
  'use strict';

  const repositoryPart = /^[A-Za-z0-9_.-]+$/;
  const queueFilters = ['today', 'agenda', 'attention', 'filed', 'update', 'later', 'draft'];
  const sections = {
    issue: ['overview', 'conversation', 'reply', 'actions'],
    filed: ['overview', 'conversation', 'reply', 'actions'],
    pull: ['overview', 'conversation', 'reply', 'review'],
    review: ['overview', 'files', 'feedback', 'history'],
    update: ['activity', 'conversation', 'context', 'reply'],
  };

  function positiveInteger(value) {
    const number = Number(value);
    return Number.isSafeInteger(number) && number > 0 ? number : null;
  }

  function parse(fragment) {
    const parts = String(fragment || '').split('/');
    if (parts[0] !== '#' || parts[1] !== 'my-work') return null;
    const queue = parts[2]?.replace(/s$/, '');
    if (queueFilters.includes(queue) && parts.length === 3) {
      return { kind: 'queue', filter: queue };
    }
    if (queue === 'agenda' && parts[3] === 'protect-today' && parts.length === 4) {
      return { kind: 'queue', filter: 'agenda', action: 'protect-today' };
    }
    if (parts[2] === 'update' && [4, 5].includes(parts.length)) {
      const notificationId = positiveInteger(parts[3]);
      const section = parts[4];
      if (!notificationId || (section && !sections.update.includes(section))) return null;
      return { kind: 'update', notification_id: notificationId, ...(section ? { section } : {}) };
    }
    if (!['issue', 'filed', 'pull', 'review'].includes(parts[2]) || ![6, 7].includes(parts.length)) return null;
    if (!repositoryPart.test(parts[3]) || !repositoryPart.test(parts[4])) return null;
    const number = positiveInteger(parts[5]);
    const section = parts[6];
    if (!number || (section && !sections[parts[2]].includes(section))) return null;
    return {
      kind: parts[2], repository: parts[3] + '/' + parts[4], number,
      ...(section ? { section } : {}),
    };
  }

  function serialize(item) {
    if (item?.kind === 'update') {
      const notificationId = positiveInteger(item.notification_id);
      if (!notificationId || (item.section && !sections.update.includes(item.section))) return '';
      return '#/my-work/update/' + notificationId + (item.section ? '/' + item.section : '');
    }
    const kind = item?.is_filed && !item?.is_assigned ? 'filed' : item?.kind;
    if (!['issue', 'filed', 'pull', 'review'].includes(kind)) return '';
    const repository = String(item.repository || '').split('/');
    const number = positiveInteger(item.number);
    if (repository.length !== 2 || !repository.every(part => repositoryPart.test(part)) || !number) return '';
    if (item.section && !sections[kind].includes(item.section)) return '';
    return '#/my-work/' + kind + '/' + repository.join('/') + '/' + number +
      (item.section ? '/' + item.section : '');
  }

  function sameRoute(item, route) {
    if (route.kind === 'queue') return false;
    if (route.kind === 'update') {
      return Number(item.notification_id) === route.notification_id;
    }
    const itemKind = item.is_review ? 'review' :
      (item.is_filed && !item.is_assigned ? 'filed' : item.kind);
    return itemKind === route.kind && item.repository === route.repository &&
      Number(item.number) === route.number;
  }

  function createController({
    location, history, eventTarget, onOpen, onClose, onInvalid,
    onQueue = function () {}, onSection = function () {},
    resolve, onResolving = function () {}, onError = function () {},
  }) {
    let items = [];
    let started = false;
    let active = '';
    let ready = false;
    let resolving = '';
    let resolution = 0;

    function resolveMissing(fragment, route) {
      if (typeof resolve !== 'function') {
        onInvalid();
        return;
      }
      if (resolving === fragment) return;
      resolving = fragment;
      const request = ++resolution;
      onResolving(route);
      Promise.resolve(resolve(route)).then(item => {
        if (request !== resolution || String(location.hash || '') !== fragment) return;
        resolving = '';
        if (!item || !sameRoute(item, route)) {
          onInvalid();
          return;
        }
        active = fragment;
        onOpen({ ...item, kind: route.kind, ...(route.section ? { section: route.section } : {}) });
      }).catch(error => {
        if (request !== resolution || String(location.hash || '') !== fragment) return;
        resolving = '';
        if (error?.unavailable) onInvalid();
        else onError(error, route);
      });
    }

    function sync() {
      const fragment = String(location.hash || '');
      if (!fragment) {
        resolution += 1;
        resolving = '';
        if (active) onClose();
        active = '';
        return;
      }
      const route = parse(fragment);
      if (!route) {
        resolution += 1;
        resolving = '';
        if (fragment.startsWith('#/my-work/')) onInvalid();
        active = '';
        return;
      }
      if (route.kind === 'queue') {
        if (!ready) return;
        resolution += 1;
        resolving = '';
        if (active && active !== fragment) onClose();
        if (active === fragment) return;
        active = fragment;
        onQueue(route.filter, route.action || null);
        return;
      }
      const item = items.find(candidate => sameRoute(candidate, route));
      if (!item) {
        if (ready) resolveMissing(fragment, route);
        return;
      }
      if (active === fragment) return;
      const activeRoute = parse(active);
      if (activeRoute && sameRoute(item, activeRoute)) {
        active = fragment;
        onSection(route.section || sections[route.kind][0], { restore: true });
        return;
      }
      resolution += 1;
      resolving = '';
      active = fragment;
      onOpen({ ...item, kind: route.kind, ...(route.section ? { section: route.section } : {}) });
    }

    return {
      start() {
        if (started) return;
        started = true;
        eventTarget.addEventListener('popstate', sync);
        eventTarget.addEventListener('hashchange', sync);
        sync();
      },
      setItems(nextItems) {
        items = Array.isArray(nextItems) ? nextItems.slice() : [];
        ready = true;
        sync();
      },
      open(item, options = {}) {
        const fragment = serialize(item);
        if (!fragment) return false;
        const method = options.replace ? 'replaceState' : 'pushState';
        history[method]({ workRoute: fragment }, '', fragment);
        active = fragment;
        onOpen(item);
        return true;
      },
      section(name, options = {}) {
        const route = parse(location.hash);
        if (!route || route.kind === 'queue' || !sections[route.kind].includes(name)) return false;
        if ((route.section || sections[route.kind][0]) === name) return true;
        const fragment = serialize({ ...route, section: name });
        const method = options.replace ? 'replaceState' : 'pushState';
        history[method]({ workRoute: fragment }, '', fragment);
        active = fragment;
        onSection(name, { restore: false });
        return true;
      },
      queue(filter) {
        if (!queueFilters.includes(filter)) return false;
        const name = filter + (['update', 'draft'].includes(filter) ? 's' : '');
        const fragment = '#/my-work/' + name;
        if (location.hash !== fragment) history.pushState(null, '', fragment);
        active = fragment;
        onQueue(filter);
        return true;
      },
      close() {
        if (parse(location.hash)) history.back();
        else {
          active = '';
          onClose();
        }
      },
      sync,
    };
  }

  async function share(url, navigatorObject, clipboard) {
    if (typeof navigatorObject?.share === 'function') {
      await navigatorObject.share({ url });
      return 'shared';
    }
    if (typeof clipboard?.writeText === 'function') {
      await clipboard.writeText(url);
      return 'copied';
    }
    throw new Error('Sharing is unavailable.');
  }

  return { parse, serialize, createController, share };
});
