// Job lifecycle, resume storage, and share-link ownership for the no-build UI.
// Loaded before sortof-shared.jsx and sortof-app.jsx.
(function installSortofJobClient(global) {
  const RESUME_STORAGE_KEY = 'sortof.resume.v2';
  const RESUME_VERSION = 2;
  const RESUME_RETENTION_MS = 90 * 24 * 60 * 60 * 1000;
  const POLL_INTERVAL_MS = 2500;
  const MAX_TRANSIENT_RETRIES = 2;

  let configuredFetch = null;

  function getFetch() {
    if (configuredFetch) return configuredFetch;
    if (typeof global.__SORTOF_FAKE_FETCH__ === 'function') {
      return global.__SORTOF_FAKE_FETCH__;
    }
    return global.fetch.bind(global);
  }

  function setFetch(fetchImpl) {
    configuredFetch = typeof fetchImpl === 'function' ? fetchImpl : null;
  }

  function classifyHttpFailure(status) {
    if (status === 404) {
      return { kind: 'missing', status, retryable: false };
    }
    if (status === 0 || status === 429 || status >= 500) {
      return { kind: 'error', status, retryable: true };
    }
    return { kind: 'error', status, retryable: false };
  }

  function createResultOwner() {
    let current = { generation: 0, jobId: null };
    return {
      establish(jobId = null) {
        current = { generation: current.generation + 1, jobId };
        return { ...current };
      },
      claimJob(ownership, jobId) {
        if (
          ownership.generation !== current.generation
          || ownership.jobId !== current.jobId
        ) {
          return null;
        }
        current = { generation: current.generation, jobId: jobId || null };
        return { ...current };
      },
      isCurrent(ownership) {
        return Boolean(
          ownership
          && ownership.generation === current.generation
          && ownership.jobId === current.jobId,
        );
      },
      snapshot() {
        return { ...current };
      },
    };
  }

  function resultKey(ownership) {
    if (!ownership) return '';
    return `${ownership.generation}:${ownership.jobId || ''}`;
  }

  function filterBranchSelections(selections, modDb) {
    const available = new Map();
    for (const mod of (modDb || [])) {
      const wsid = String(mod.wsid || '');
      const modId = String(mod.modId || mod.mod_id || mod.id || '');
      if (!wsid || !modId) continue;
      if (!available.has(wsid)) available.set(wsid, new Set());
      available.get(wsid).add(modId);
    }

    const filtered = {};
    for (const [rawWsid, rawModIds] of Object.entries(selections || {})) {
      const wsid = String(rawWsid);
      const validForWsid = available.get(wsid);
      if (!validForWsid || !Array.isArray(rawModIds)) continue;
      const modIds = [...new Set(rawModIds.map(String).filter(id => validForWsid.has(id)))];
      if (modIds.length) filtered[wsid] = modIds;
    }
    return filtered;
  }

  async function pollJobOnce(jobId, options = {}) {
    const fetchImpl = options.fetchImpl || getFetch();
    const source = options.interactionSource;
    const query = source
      ? `?interaction_source=${encodeURIComponent(source)}`
      : '';
    try {
      const response = await fetchImpl(`/api/jobs/${jobId}${query}`, {
        headers: { 'X-Job-Token': options.jobToken },
      });
      if (!response.ok) return classifyHttpFailure(response.status);
      return { kind: 'ok', status: response.status, body: await response.json() };
    } catch (error) {
      console.warn('pollJobOnce: network error', error);
      return { kind: 'error', status: 0, retryable: true, error };
    }
  }

  function pollJobLoop(jobId, signal, onTick, options = {}) {
    const intervalMs = options.intervalMs ?? POLL_INTERVAL_MS;
    const maxTransientRetries = options.maxTransientRetries ?? MAX_TRANSIENT_RETRIES;
    const setTimer = options.setTimer || global.setTimeout.bind(global);
    const clearTimer = options.clearTimer || global.clearTimeout.bind(global);
    const fetchImpl = options.fetchImpl;
    const interactionSource = options.interactionSource;
    const jobToken = options.jobToken;

    return new Promise((resolve) => {
      let timer = null;
      let transientFailures = 0;
      let firstRequest = true;

      async function tick() {
        if (signal.aborted) {
          if (timer) clearTimer(timer);
          resolve({ kind: 'aborted' });
          return;
        }

        const result = await pollJobOnce(jobId, {
          fetchImpl,
          interactionSource: firstRequest ? interactionSource : null,
          jobToken,
        });
        firstRequest = false;
        if (signal.aborted) {
          resolve({ kind: 'aborted' });
          return;
        }

        onTick(result);
        if (result.kind === 'missing') {
          resolve(result);
          return;
        }
        if (result.kind === 'error') {
          if (result.retryable && transientFailures < maxTransientRetries) {
            transientFailures += 1;
            timer = setTimer(tick, intervalMs * transientFailures);
            return;
          }
          resolve(result);
          return;
        }

        transientFailures = 0;
        if (result.body.phase === 'done' || result.body.phase === 'failed') {
          resolve(result);
          return;
        }
        timer = setTimer(tick, intervalMs);
      }

      tick();
    });
  }

  function getStorage(storage) {
    if (storage) return storage;
    try {
      return global.localStorage;
    } catch {
      return null;
    }
  }

  function clearResumeRecord(options = {}) {
    const storage = getStorage(options.storage);
    if (!storage) return;
    try {
      storage.removeItem(RESUME_STORAGE_KEY);
    } catch {}
  }

  function normalizeResumeRecord(value) {
    if (!value || value.version !== RESUME_VERSION) return null;
    if (typeof value.jobId !== 'string' || !value.jobId) return null;
    if (typeof value.jobToken !== 'string' || !value.jobToken) return null;
    if (typeof value.input !== 'string' || typeof value.pzBuild !== 'string') return null;
    if (value.state !== 'active' && value.state !== 'complete') return null;
    const submittedAtMs = Date.parse(value.submittedAt);
    if (!Number.isFinite(submittedAtMs)) return null;
    return {
      version: RESUME_VERSION,
      jobId: value.jobId,
      jobToken: value.jobToken,
      input: value.input,
      pzBuild: value.pzBuild,
      submittedAt: new Date(submittedAtMs).toISOString(),
      state: value.state,
    };
  }

  function readResumeRecord(options = {}) {
    const storage = getStorage(options.storage);
    if (!storage) return null;
    try {
      const record = normalizeResumeRecord(JSON.parse(storage.getItem(RESUME_STORAGE_KEY)));
      const nowMs = options.nowMs ?? Date.now();
      const retentionMs = options.retentionMs ?? RESUME_RETENTION_MS;
      if (!record || nowMs - Date.parse(record.submittedAt) > retentionMs) {
        clearResumeRecord({ storage });
        return null;
      }
      return record;
    } catch {
      clearResumeRecord({ storage });
      return null;
    }
  }

  function writeResumeRecord(record, options = {}) {
    const storage = getStorage(options.storage);
    const normalized = normalizeResumeRecord({
      version: RESUME_VERSION,
      state: 'active',
      submittedAt: new Date().toISOString(),
      ...record,
    });
    if (!storage || !normalized) return null;
    try {
      storage.setItem(RESUME_STORAGE_KEY, JSON.stringify(normalized));
      return normalized;
    } catch {
      return null;
    }
  }

  function markResumeComplete(jobId, options = {}) {
    const record = readResumeRecord(options);
    if (!record || record.jobId !== jobId) return null;
    return writeResumeRecord({ ...record, state: 'complete' }, options);
  }

  async function createShareLink(jobId, options = {}) {
    const fetchImpl = options.fetchImpl || getFetch();
    try {
      const response = await fetchImpl(`/api/jobs/${jobId}/share`, {
        method: 'POST',
        headers: { 'X-Job-Token': options.jobToken },
      });
      if (!response.ok) return classifyHttpFailure(response.status);
      const body = await response.json();
      if (!body || typeof body.url !== 'string' || !body.url) {
        return { kind: 'error', status: response.status, retryable: false };
      }
      return { kind: 'ok', status: response.status, body };
    } catch (error) {
      return { kind: 'error', status: 0, retryable: true, error };
    }
  }

  async function copyText(text) {
    if (!global.navigator?.clipboard?.writeText) {
      throw new Error('clipboard unavailable');
    }
    await global.navigator.clipboard.writeText(text);
  }

  function ShareIcon({ copied }) {
    if (copied) {
      return (
        <svg className="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
          <polyline points="20 6 9 17 4 12"></polyline>
        </svg>
      );
    }
    return (
      <svg className="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
      </svg>
    );
  }

  function ShareBtn({ jobId, jobToken }) {
    const [state, setState] = React.useState('idle');
    const [message, setMessage] = React.useState('');
    const urlRef = React.useRef(null);
    const timerRef = React.useRef(null);

    React.useEffect(() => () => {
      if (timerRef.current) global.clearTimeout(timerRef.current);
    }, []);

    React.useEffect(() => {
      urlRef.current = null;
      setState('idle');
      setMessage('');
  }, [jobId, jobToken]);

    if (!jobId || !jobToken) return null;

    const resetLater = () => {
      if (timerRef.current) global.clearTimeout(timerRef.current);
      timerRef.current = global.setTimeout(() => {
        setState('idle');
        setMessage('');
      }, 2200);
    };

    const onClick = async () => {
      if (state === 'loading') return;
      setState('loading');
      setMessage('');

      let url = urlRef.current;
      if (!url) {
        const share = await createShareLink(jobId, { jobToken });
        if (share.kind !== 'ok') {
          setState(share.kind === 'missing' ? 'expired' : 'share-error');
          setMessage(
            share.kind === 'missing'
              ? 'sort expired'
              : share.retryable
                ? 'share unavailable. retry'
                : 'sort cannot be shared',
          );
          return;
        }
        url = share.body.url;
        urlRef.current = url;
      }

      try {
        await copyText(url);
        setState('copied');
        setMessage('link copied');
        resetLater();
      } catch {
        setState('copy-error');
        setMessage('copy failed. retry');
      }
    };

    const label = state === 'loading'
      ? 'creating link…'
      : message || 'copy share link';
    return (
      <button
        type="button"
        className={'copy-btn share-btn' + (state === 'copied' ? ' copied' : '')}
        onClick={onClick}
        disabled={state === 'loading' || state === 'expired'}
        data-share-state={state}
      >
        <ShareIcon copied={state === 'copied'} />
        {label}
      </button>
    );
  }

  global.SortofJobClient = {
    MAX_TRANSIENT_RETRIES,
    POLL_INTERVAL_MS,
    RESUME_RETENTION_MS,
    RESUME_STORAGE_KEY,
    RESUME_VERSION,
    ShareBtn,
    classifyHttpFailure,
    clearResumeRecord,
    createResultOwner,
    createShareLink,
    filterBranchSelections,
    markResumeComplete,
    pollJobLoop,
    pollJobOnce,
    readResumeRecord,
    resultKey,
    setFetch,
    writeResumeRecord,
  };
  global.ShareBtn = ShareBtn;
})(window);
