(function (root, factory) {
  if (typeof module === 'object' && module.exports) module.exports = factory;
  else root.createLoginController = factory;
}(typeof self !== 'undefined' ? self : this, function createLoginController(options) {
  function validShareContinuation(value) {
    if (typeof value !== 'string' || !value.startsWith('./?') || value.includes('#')) return './';
    const limits = { title: 200, text: 8000, url: 2048, launch: 8, shared: 6, search: 200,
      preview: 200, search_kind: 5, search_state: 6, search_repository: 161 };
    const params = new URLSearchParams(value.slice(3));
    const entries = Array.from(params.entries());
    if (!entries.length) return './';
    const invalid = entries.some(([name, content]) => (
      !Object.prototype.hasOwnProperty.call(limits, name)
      || !content
      || content.length > limits[name]
    ));
    if (invalid) return './';
    if (Object.keys(limits).some(name => params.getAll(name).length > 1)) return './';
    const search = params.get('search');
    const preview = params.get('preview');
    if (search !== null || preview !== null) {
      if (entries.some(([name]) => ![
        'search', 'preview', 'search_kind', 'search_state', 'search_repository'
      ].includes(name))) return './';
      if (search === null || preview === null) return './';
      if (!/^(issue|pull):[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+:[1-9]\d*$/.test(preview)) return './';
      const kind = params.get('search_kind');
      const state = params.get('search_state');
      const repository = params.get('search_repository');
      if (kind !== null && !['all', 'issue', 'pull'].includes(kind)) return './';
      if (state !== null && !['all', 'open', 'closed'].includes(state)) return './';
      if (repository !== null && !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) return './';
      return value;
    }
    const launch = params.get('launch');
    const shared = params.get('shared');
    if (launch && !['continue', 'new', 'agenda'].includes(launch)) return './';
    if (shared === 'bundle') {
      if (launch !== 'new' || entries.some(([name]) => !['launch', 'shared'].includes(name))) return './';
    } else if (shared !== null && (shared !== 'image' || launch !== 'new')) return './';
    return value;
  }

  const form = options.form;
  const status = options.status;
  const button = options.button;
  const passkeyButton = options.passkeyButton;
  const credentials = options.credentials;
  const fetchImpl = options.fetchImpl;
  const location = options.location;
  const clearPrivateDeviceData = options.clearPrivateDeviceData;
  const continuation = validShareContinuation(options.continuation);
  const setIntervalImpl = options.setIntervalImpl || setInterval;
  const clearIntervalImpl = options.clearIntervalImpl || clearInterval;
  const setTimeoutImpl = options.setTimeoutImpl || setTimeout;
  const clearTimeoutImpl = options.clearTimeoutImpl || clearTimeout;
  const requestTimeoutMs = options.requestTimeoutMs || 15000;
  let timer = null;
  let activeAttempt = false;

  function setAttemptActive(active) {
    activeAttempt = active;
    button.disabled = active;
    if (passkeyButton) passkeyButton.disabled = active || !credentials?.get;
  }

  async function requestWithDeadline(url, init) {
    const controller = new AbortController();
    const timeout = setTimeoutImpl(() => {
      const error = new Error('Sign-in request timed out');
      error.name = 'TimeoutError';
      controller.abort(error);
    }, requestTimeoutMs);
    try {
      return await fetchImpl(url, { ...init, signal: controller.signal });
    } finally {
      clearTimeoutImpl(timeout);
    }
  }

  function decodeBase64Url(value) {
    const padded = String(value).replaceAll('-', '+').replaceAll('_', '/')
      + '='.repeat((4 - String(value).length % 4) % 4);
    return Uint8Array.from(atob(padded), character => character.charCodeAt(0));
  }

  function encodeBase64Url(value) {
    if (value === null || value === undefined) return null;
    const bytes = new Uint8Array(value);
    let binary = '';
    bytes.forEach(byte => { binary += String.fromCharCode(byte); });
    return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');
  }

  function credentialJSON(credential) {
    return {
      id: credential.id,
      type: credential.type,
      rawId: encodeBase64Url(credential.rawId),
      response: {
        authenticatorData: encodeBase64Url(credential.response.authenticatorData),
        clientDataJSON: encodeBase64Url(credential.response.clientDataJSON),
        signature: encodeBase64Url(credential.response.signature),
        userHandle: encodeBase64Url(credential.response.userHandle),
      },
    };
  }

  if (continuation !== './') {
    const continuationParams = new URLSearchParams(continuation.slice(3));
    status.textContent = continuationParams.has('preview')
      ? 'Sign in to open the shared Search result.'
      : 'Sign in to continue your shared capture.';
  }

  async function showReason(reason) {
    if (reason === 'session-expired') {
      status.textContent = 'Your session expired. Private drafts remain on this device. Sign in to continue.';
      return;
    }
    if (reason === 'session-idle') {
      status.textContent = 'Stackchain locked after inactivity. Your drafts and queued work are still on this device. Sign in to resume.';
      return;
    }
    if (reason !== 'session-revoked') return;
    button.disabled = true;
    status.textContent = 'This device was remotely signed out. Clearing Stackchain private data…';
    try {
      if (typeof clearPrivateDeviceData !== 'function') throw new Error('Private data purger unavailable.');
      await clearPrivateDeviceData();
      status.textContent = 'This device was remotely signed out. Stackchain private data was cleared. Sign in to use it again.';
      button.disabled = false;
    } catch (_error) {
      status.textContent = 'This device was remotely signed out, but private data could not be cleared. Close other Stackchain tabs and clear this site’s data before signing in.';
    }
  }

  function showRetryCountdown(seconds) {
    let remaining = Math.max(1, Number.parseInt(seconds, 10) || 1);
    button.disabled = true;
    status.textContent = `Too many attempts. Try again in ${remaining} seconds.`;
    timer = setIntervalImpl(() => {
      remaining -= 1;
      if (remaining <= 0) {
        clearIntervalImpl(timer);
        timer = null;
        button.disabled = false;
        status.textContent = 'You can try signing in again.';
        return;
      }
      status.textContent = `Too many attempts. Try again in ${remaining} seconds.`;
    }, 1000);
  }

  async function submit(accessToken, deviceLabel = 'This device') {
    if (activeAttempt) return false;
    setAttemptActive(true);
    status.textContent = 'Signing in…';
    let response;
    try {
      response = await requestWithDeadline('api/v1/session', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ access_token: accessToken, device_label: deviceLabel }),
      });
    } catch (error) {
      form.reset();
      status.textContent = error?.name === 'TimeoutError'
        ? 'Sign-in timed out. Check your connection and try again.'
        : 'Sign-in failed. Check your connection and try again.';
      return false;
    } finally {
      setAttemptActive(false);
    }
    form.reset();
    if (response.ok) {
      location.replace(continuation);
      return;
    }
    if (response.status === 429) {
      showRetryCountdown(response.headers.get('Retry-After'));
      return;
    }
    status.textContent = 'Sign-in failed. Check the token and try again.';
  }

  async function signInWithPasskey(deviceLabel = 'This device') {
    if (!credentials?.get) {
      status.textContent = 'Passkeys are not supported in this browser. Use the access token.';
      return false;
    }
    if (activeAttempt) return false;
    setAttemptActive(true);
    status.textContent = 'Waiting for your passkey…';
    try {
      const optionsResponse = await requestWithDeadline('api/v1/passkeys/authentication/options', {
        method: 'POST', headers: { Accept: 'application/json' },
      });
      if (!optionsResponse.ok) throw new Error('No enrolled passkey');
      const publicKey = await optionsResponse.json();
      const challenge = publicKey.challenge;
      publicKey.challenge = decodeBase64Url(publicKey.challenge);
      publicKey.allowCredentials = (publicKey.allowCredentials || []).map(item => ({
        ...item, id: decodeBase64Url(item.id),
      }));
      const credential = await credentials.get({ publicKey });
      const response = await requestWithDeadline('api/v1/passkeys/authentication/verify', {
        method: 'POST',
        headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
        body: JSON.stringify({
          challenge,
          credential: credentialJSON(credential),
          device_label: deviceLabel,
          action: 'sign_in',
          target: 'dashboard',
        }),
      });
      if (!response.ok) throw new Error('Passkey verification failed');
      location.replace(continuation);
      return true;
    } catch (error) {
      status.textContent = error?.name === 'TimeoutError'
        ? 'Passkey sign-in timed out. Check your connection and try again.'
        : 'Passkey sign-in was not completed. Try again or use the access token.';
      return false;
    } finally {
      setAttemptActive(false);
    }
  }

  if (passkeyButton) passkeyButton.disabled = !credentials?.get;
  return { submit, signInWithPasskey, showReason };
}));

if (typeof document !== 'undefined') {
  const form = document.getElementById('sign-in');
  const status = document.getElementById('status');
  const button = document.getElementById('submit-sign-in');
  const passkeyButton = document.getElementById('passkey-sign-in');
  const loginParams = new URLSearchParams(window.location.search);
  const controller = createLoginController({
    form,
    status,
    button,
    passkeyButton,
    credentials: window.navigator?.credentials,
    fetchImpl: fetch.bind(window),
    location: window.location,
    continuation: loginParams.get('continue'),
    clearPrivateDeviceData: window.stackchainPrivateDeviceData,
  });
  controller.showReason(loginParams.get('reason'));
  form.addEventListener('submit', event => {
    event.preventDefault();
    const data = new FormData(form);
    const accessToken = data.get('access_token');
    const deviceLabel = data.get('device_label');
    controller.submit(accessToken, deviceLabel);
  });
  passkeyButton?.addEventListener('click', () => {
    const data = new FormData(form);
    controller.signInWithPasskey(data.get('device_label'));
  });
}
