/**
 * Scan the current bookmark folder for hard 404/410 links and remove them after confirm.
 * Suspected broken links are re-checked 3 times before being marked.
 */

import { collectFolderBookmarks } from './ai-bookmark-organize.js';

const SCAN_ORIGINS = ['http://*/*', 'https://*/*'];
const SCAN_AUTH_PERMISSIONS = ['webRequest', 'webRequestAuthProvider'];
/** Per-request timeout (HEAD/GET attempt). */
const CHECK_TIMEOUT_MS = 4000;
const VERIFY_TIMEOUT_MS = 2500;
/** Hard wall-clock budget for one bookmark (all attempts combined). */
const BOOKMARK_MAX_MS = 7000;
/** Baseline concurrency for plan scan-speed ×1 (Free). Pro ×2 / Premium ×4. */
const BASE_CONCURRENCY = 12;
const BASE_PER_HOST_CONCURRENCY = 2;
const VERIFY_ATTEMPTS = 3;
const VERIFY_GAP_MS = 80;
const BROKEN_STATUSES = new Set([404, 410]);
/** Statuses that mean "blocked / challenged / auth" — site is likely alive. */
const ALIVE_BUT_BLOCKED = new Set([401, 403, 405, 429, 430, 503, 520, 521, 522, 523, 524]);
const SKIP_SCHEMES = /^(chrome|chrome-extension|edge|about|javascript|data|file|blob):/i;

function i18n(key, fallback, substitutions) {
  const msg = chrome.i18n.getMessage(key, substitutions);
  return msg || fallback || key;
}

function sleep(ms, signal) {
  return new Promise((resolve) => {
    if (signal?.aborted) {
      resolve();
      return;
    }
    const timer = setTimeout(resolve, ms);
    if (!signal) return;
    const onAbort = () => {
      clearTimeout(timer);
      resolve();
    };
    signal.addEventListener('abort', onAbort, { once: true });
  });
}

function permissionsApiCall(method, options) {
  return new Promise((resolve, reject) => {
    const api = chrome.permissions?.[method];
    if (!api) {
      resolve(true);
      return;
    }

    let settled = false;
    const finish = (fn, value) => {
      if (settled) return;
      settled = true;
      fn(value);
    };

    try {
      const result = api.call(chrome.permissions, options);
      if (result && typeof result.then === 'function') {
        result
          .then((value) => finish(resolve, Boolean(value)))
          .catch((error) => finish(reject, error));
        return;
      }

      api.call(chrome.permissions, options, (value) => {
        if (chrome.runtime.lastError) {
          finish(reject, new Error(chrome.runtime.lastError.message));
          return;
        }
        finish(resolve, Boolean(value));
      });
    } catch (error) {
      finish(reject, error);
    }
  });
}

export async function ensureScanHostPermission() {
  if (!chrome.permissions?.request) {
    return true;
  }
  try {
    const hasHosts = await permissionsApiCall('contains', { origins: SCAN_ORIGINS });
    if (!hasHosts) {
      const grantedHosts = await permissionsApiCall('request', { origins: SCAN_ORIGINS });
      if (!grantedHosts) return false;
    }

    // Optional: suppress HTTP Basic Auth login dialogs during scan (1Panel, etc.)
    const hasAuthApi = await permissionsApiCall('contains', {
      permissions: SCAN_AUTH_PERMISSIONS
    });
    if (!hasAuthApi) {
      try {
        await permissionsApiCall('request', { permissions: SCAN_AUTH_PERMISSIONS });
      } catch (error) {
        console.warn('Auth-suppression permission not granted:', error);
      }
    }
    return true;
  } catch (error) {
    console.error('Failed to request scan host permission:', error);
    throw error;
  }
}

async function setAuthSuppression(enabled) {
  try {
    const response = await chrome.runtime.sendMessage({
      action: 'clear404SetAuthSuppression',
      enabled: Boolean(enabled)
    });
    return Boolean(response?.ok);
  } catch (error) {
    console.warn('Failed to toggle auth suppression:', error);
    return false;
  }
}

function isCheckableUrl(url) {
  if (!url || typeof url !== 'string') return false;
  if (SKIP_SCHEMES.test(url)) return false;
  try {
    const parsed = new URL(url);
    return parsed.protocol === 'http:' || parsed.protocol === 'https:';
  } catch {
    return false;
  }
}

function looksLikeWafChallenge(response) {
  if (!response) return false;
  if (response.status === 202) return true;
  const wafAction = response.headers?.get?.('x-amzn-waf-action');
  if (wafAction) return true;
  const server = (response.headers?.get?.('server') || '').toLowerCase();
  const cf = response.headers?.get?.('cf-mitigated');
  if (cf) return true;
  // CloudFront/WAF challenge pages often expose this combo
  if (server.includes('cloudfront') && response.status >= 400 && response.status !== 404 && response.status !== 410) {
    return true;
  }
  return false;
}

function cancelBody(response) {
  try {
    if (response?.body && typeof response.body.cancel === 'function') {
      response.body.cancel();
    }
  } catch {
    // ignore
  }
}

/**
 * Resolve a reliable HTTP status for link checking.
 * Never trust HEAD 404 alone — many CDNs/SPA/WAF stacks lie on HEAD/Range.
 * Use credentials:omit so HTTP Basic Auth sites (e.g. 1Panel) do not prompt.
 */
async function fetchStatus(url, signal, { preferGet = false } = {}) {
  const common = {
    redirect: 'follow',
    cache: 'no-store',
    credentials: 'omit',
    signal,
    headers: {
      Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
    }
  };

  // Fast path: HEAD only when it clearly says "reachable"
  if (!preferGet) {
    try {
      const head = await fetch(url, { ...common, method: 'HEAD' });
      if (looksLikeWafChallenge(head) || ALIVE_BUT_BLOCKED.has(head.status)) {
        return head.status;
      }
      if (
        head.status >= 200
        && head.status < 400
        && head.status !== 202
        && !BROKEN_STATUSES.has(head.status)
      ) {
        return head.status;
      }
      // HEAD 404/410/odd codes → confirm with GET below
    } catch {
      if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');
      // Fall through to GET
    }
  }

  if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');

  const get = await fetch(url, { ...common, method: 'GET' });
  cancelBody(get);

  if (looksLikeWafChallenge(get) || ALIVE_BUT_BLOCKED.has(get.status)) {
    // Never treat bot-challenge / auth walls as deleted 404s
    return BROKEN_STATUSES.has(get.status) ? 403 : get.status;
  }

  return get.status;
}

/**
 * One network attempt with a hard Promise.race timeout so a hung TCP
 * cannot block the worker after abort().
 */
async function checkOnce(url, { preferGet = false, timeoutMs = CHECK_TIMEOUT_MS, signal } = {}) {
  if (signal?.aborted) return null;

  const controller = new AbortController();
  const onParentAbort = () => controller.abort();
  if (signal) {
    signal.addEventListener('abort', onParentAbort, { once: true });
  }

  let timer = null;
  try {
    const result = await Promise.race([
      fetchStatus(url, controller.signal, { preferGet }).catch(() => null),
      new Promise((resolve) => {
        timer = setTimeout(() => {
          try {
            controller.abort();
          } catch {
            // ignore
          }
          resolve(null);
        }, Math.max(400, timeoutMs));
      })
    ]);
    return result;
  } finally {
    if (timer != null) clearTimeout(timer);
    signal?.removeEventListener('abort', onParentAbort);
  }
}

/**
 * First check; if broken (404/410), re-check up to VERIFY_ATTEMPTS total times.
 * Only mark broken when every attempt reports 404/410.
 * Enforces a per-bookmark wall-clock budget so one slow URL cannot stall the scan.
 */
async function checkBookmarkUrl(bookmark, onVerify, { signal } = {}) {
  if (signal?.aborted) {
    return {
      ...bookmark,
      status: null,
      broken: false,
      skipped: true,
      cancelled: true,
      attempts: 0,
      statuses: []
    };
  }

  if (!isCheckableUrl(bookmark.url)) {
    return {
      ...bookmark,
      status: null,
      broken: false,
      skipped: true,
      attempts: 0,
      statuses: []
    };
  }

  const deadline = Date.now() + BOOKMARK_MAX_MS;
  const timeLeft = () => Math.max(0, deadline - Date.now());

  const statuses = [];
  let status = await checkOnce(bookmark.url, {
    signal,
    timeoutMs: Math.min(CHECK_TIMEOUT_MS, Math.max(400, timeLeft()))
  });
  statuses.push(status);

  if (signal?.aborted) {
    return {
      ...bookmark,
      status,
      broken: false,
      skipped: false,
      cancelled: true,
      attempts: 1,
      statuses
    };
  }

  // Timed out / unreachable — do not block on retries; treat as not confirmed broken
  if (status == null || timeLeft() < 600) {
    return {
      ...bookmark,
      status,
      broken: false,
      skipped: false,
      timedOut: status == null,
      attempts: 1,
      statuses
    };
  }

  if (!BROKEN_STATUSES.has(status)) {
    return {
      ...bookmark,
      status,
      broken: false,
      skipped: false,
      attempts: 1,
      statuses
    };
  }

  // Suspected broken — verify with GET-only retries while budget remains
  for (let attempt = 2; attempt <= VERIFY_ATTEMPTS; attempt += 1) {
    if (signal?.aborted || timeLeft() < 600) {
      // Incomplete verification → safer to keep the bookmark
      return {
        ...bookmark,
        status,
        broken: false,
        skipped: false,
        timedOut: true,
        attempts: attempt - 1,
        statuses
      };
    }

    onVerify?.(bookmark, attempt, VERIFY_ATTEMPTS);
    await sleep(Math.min(VERIFY_GAP_MS, timeLeft()), signal);

    if (signal?.aborted || timeLeft() < 400) {
      return {
        ...bookmark,
        status,
        broken: false,
        skipped: false,
        timedOut: true,
        attempts: attempt - 1,
        statuses
      };
    }

    status = await checkOnce(bookmark.url, {
      preferGet: true,
      timeoutMs: Math.min(VERIFY_TIMEOUT_MS, Math.max(400, timeLeft())),
      signal
    });
    statuses.push(status);

    if (status == null || !BROKEN_STATUSES.has(status)) {
      return {
        ...bookmark,
        status,
        broken: false,
        skipped: false,
        timedOut: status == null,
        attempts: attempt,
        statuses
      };
    }
  }

  return {
    ...bookmark,
    status: statuses[statuses.length - 1],
    broken: true,
    skipped: false,
    attempts: VERIFY_ATTEMPTS,
    statuses
  };
}

function hostnameOf(url) {
  try {
    return new URL(url).hostname || '_';
  } catch {
    return '_';
  }
}

function abortError() {
  const error = new Error('CLEAR_404_CANCELLED');
  error.name = 'Clear404CancelledError';
  return error;
}

/**
 * High global concurrency with a per-host cap so one domain cannot stall the pool
 * or trigger aggressive rate limits.
 */
async function mapPool(items, { globalLimit, perHostLimit, signal }, mapper, onProgress) {
  const results = new Array(items.length);
  let nextIndex = 0;
  let completed = 0;
  const hostActive = new Map();
  const hostWaiters = new Map();

  const wakeAllWaiters = () => {
    for (const [, queue] of hostWaiters) {
      while (queue.length) queue.shift()();
    }
    hostWaiters.clear();
  };

  if (signal) {
    signal.addEventListener('abort', wakeAllWaiters, { once: true });
  }

  const acquireHost = async (host) => {
    const waitStarted = Date.now();
    while (true) {
      if (signal?.aborted) throw abortError();
      const active = hostActive.get(host) || 0;
      if (active < perHostLimit) {
        hostActive.set(host, active + 1);
        return;
      }
      // Don't queue forever behind stuck same-host requests
      if (Date.now() - waitStarted > BOOKMARK_MAX_MS) {
        hostActive.set(host, active + 1);
        return;
      }
      await Promise.race([
        new Promise((resolve) => {
          if (signal?.aborted) {
            resolve();
            return;
          }
          const queue = hostWaiters.get(host) || [];
          queue.push(resolve);
          hostWaiters.set(host, queue);
        }),
        sleep(250, signal)
      ]);
    }
  };

  const releaseHost = (host) => {
    const queue = hostWaiters.get(host);
    if (queue?.length) {
      const next = queue.shift();
      if (!queue.length) hostWaiters.delete(host);
      next();
      return;
    }
    const active = (hostActive.get(host) || 1) - 1;
    if (active <= 0) hostActive.delete(host);
    else hostActive.set(host, active);
  };

  async function worker() {
    while (true) {
      if (signal?.aborted) return;

      const index = nextIndex;
      nextIndex += 1;
      if (index >= items.length) return;

      const item = items[index];
      const host = hostnameOf(item.url);
      try {
        await acquireHost(host);
      } catch {
        return;
      }
      if (signal?.aborted) {
        releaseHost(host);
        return;
      }

      try {
        // Absolute safety net: never let one bookmark block a worker forever
        results[index] = await Promise.race([
          mapper(item, index),
          sleep(BOOKMARK_MAX_MS + 1500, signal).then(() => ({
            ...item,
            status: null,
            broken: false,
            skipped: false,
            timedOut: true,
            attempts: 0,
            statuses: []
          }))
        ]);
      } finally {
        releaseHost(host);
      }
      completed += 1;
      onProgress?.(completed, items.length, results[index]);
    }
  }

  const workers = Array.from(
    { length: Math.min(globalLimit, items.length) },
    () => worker()
  );
  await Promise.all(workers);
  return results;
}

function getProgressElements() {
  return {
    dialog: document.getElementById('clear-404-progress-dialog'),
    contentEl: document.querySelector('#clear-404-progress-dialog .clear-404-progress-content'),
    orbEl: document.getElementById('clear-404-progress-orb'),
    successEl: document.getElementById('clear-404-progress-success'),
    titleEl: document.getElementById('clear-404-progress-title'),
    statusEl: document.getElementById('clear-404-progress-status'),
    detailEl: document.getElementById('clear-404-progress-detail'),
    trackEl: document.getElementById('clear-404-progress-track'),
    countEl: document.getElementById('clear-404-progress-count'),
    barEl: document.getElementById('clear-404-progress-bar'),
    foundEl: document.getElementById('clear-404-progress-found'),
    metaEl: document.getElementById('clear-404-progress-meta'),
    stopBtn: document.getElementById('clear-404-progress-stop'),
    closeBtn: document.getElementById('clear-404-progress-close')
  };
}

/**
 * Show animated scan progress overlay.
 * @param {{ title?: string, cancellable?: boolean, speed?: number }} options
 */
export function showClear404Progress({ title, cancellable = false, speed = 1 } = {}) {
  const els = getProgressElements();
  if (!els.dialog) return null;

  const abortController = new AbortController();
  let speedMultiplier = Math.max(1, Math.floor(Number(speed) || 1));

  // Reset success UI from any previous run
  els.contentEl?.classList.remove('is-success');
  if (els.orbEl) {
    els.orbEl.hidden = false;
    els.orbEl.setAttribute('aria-hidden', 'false');
  }
  if (els.successEl) {
    els.successEl.hidden = true;
    els.successEl.setAttribute('aria-hidden', 'true');
  }
  if (els.trackEl) els.trackEl.hidden = false;
  if (els.metaEl) els.metaEl.hidden = false;
  if (els.closeBtn) {
    els.closeBtn.hidden = true;
    els.closeBtn.disabled = true;
  }

  if (els.titleEl) {
    els.titleEl.textContent = title
      || i18n('clear404ProgressTitle', 'Checking bookmarks…');
  }
  if (els.statusEl) {
    els.statusEl.textContent = i18n('clear404ProgressPreparing', 'Preparing…');
  }
  if (els.detailEl) {
    els.detailEl.textContent = i18n(
      'clear404ScanSpeed',
      'Scan speed ×$1',
      [String(speedMultiplier)]
    );
  }
  if (els.countEl) els.countEl.textContent = '0 / 0';
  if (els.foundEl) {
    els.foundEl.textContent = i18n(
      'clear404ProgressFound',
      'Confirmed broken: $1',
      ['0']
    );
  }
  if (els.barEl) {
    els.barEl.style.width = '0%';
    els.barEl.classList.add('is-indeterminate');
  }

  const stopBtn = els.stopBtn;
  const closeBtn = els.closeBtn;
  const onStop = () => {
    if (abortController.signal.aborted) return;
    if (stopBtn) {
      stopBtn.disabled = true;
      stopBtn.textContent = i18n('clear404Stopping', 'Stopping…');
    }
    if (els.statusEl) {
      els.statusEl.textContent = i18n('clear404Stopping', 'Stopping…');
    }
    abortController.abort();
  };

  if (stopBtn) {
    if (cancellable) {
      stopBtn.hidden = false;
      stopBtn.disabled = false;
      stopBtn.textContent = i18n('clear404Stop', 'Stop');
      stopBtn.addEventListener('click', onStop);
    } else {
      stopBtn.hidden = true;
      stopBtn.disabled = true;
    }
  }

  els.dialog.style.display = 'block';
  els.dialog.setAttribute('aria-busy', 'true');

  let foundBroken = 0;
  let lastPaint = 0;
  let closed = false;

  const paint = (payload = {}, force = false) => {
    const now = performance.now();
    if (!force && now - lastPaint < 50) return;
    lastPaint = now;

    const {
      phase,
      done = 0,
      total = 0,
      currentTitle = '',
      currentUrl = '',
      verifyingAttempt = 0,
      verifyingTotal = VERIFY_ATTEMPTS,
      speed: payloadSpeed
    } = payload;

    if (payloadSpeed != null) {
      const nextSpeed = Math.max(1, Math.floor(Number(payloadSpeed) || 1));
      if (nextSpeed !== speedMultiplier) {
        speedMultiplier = nextSpeed;
        if (els.detailEl && phase !== 'deleting' && phase !== 'done') {
          els.detailEl.textContent = i18n(
            'clear404ScanSpeed',
            'Scan speed ×$1',
            [String(speedMultiplier)]
          );
        }
      }
    }

    if (els.barEl) {
      if (total > 0) {
        els.barEl.classList.remove('is-indeterminate');
        const pct = Math.max(0, Math.min(100, (done / total) * 100));
        els.barEl.style.width = `${pct}%`;
      } else {
        els.barEl.classList.add('is-indeterminate');
        els.barEl.style.width = '30%';
      }
    }

    if (els.countEl) {
      els.countEl.textContent = total > 0 ? `${done} / ${total}` : '…';
    }

    if (els.statusEl && !abortController.signal.aborted) {
      if (phase === 'verifying') {
        els.statusEl.textContent = i18n(
          'clear404ProgressVerifying',
          'Re-checking suspected link ($1/$2)…',
          [String(verifyingAttempt), String(verifyingTotal)]
        );
      } else if (phase === 'deleting') {
        els.statusEl.textContent = i18n('clear404Deleting', 'Deleting…');
      } else if (phase === 'done') {
        els.statusEl.textContent = i18n('clear404ProgressDone', 'Scan complete');
      } else if (phase === 'cancelled') {
        els.statusEl.textContent = i18n('clear404Stopped', 'Scan stopped');
      } else {
        els.statusEl.textContent = i18n(
          'clear404Scanning',
          'Checking bookmarks… ($1/$2)',
          [String(done), String(total || '…')]
        );
      }
    }

    if (els.detailEl) {
      const speedLabel = i18n(
        'clear404ScanSpeed',
        'Scan speed ×$1',
        [String(speedMultiplier)]
      );
      const label = currentTitle || currentUrl || '';
      els.detailEl.textContent = label ? `${speedLabel} · ${label}` : speedLabel;
      els.detailEl.title = currentUrl || label || speedLabel;
    }

    if (els.foundEl) {
      els.foundEl.textContent = i18n(
        'clear404ProgressFound',
        'Confirmed broken: $1',
        [String(foundBroken)]
      );
    }
  };

  const hideDialog = () => {
    els.dialog.style.display = 'none';
    els.contentEl?.classList.remove('is-success');
    if (els.orbEl) {
      els.orbEl.hidden = false;
      els.orbEl.setAttribute('aria-hidden', 'false');
    }
    if (els.successEl) {
      els.successEl.hidden = true;
      els.successEl.setAttribute('aria-hidden', 'true');
    }
    if (els.trackEl) els.trackEl.hidden = false;
    if (els.metaEl) els.metaEl.hidden = false;
    if (closeBtn) {
      closeBtn.hidden = true;
      closeBtn.disabled = true;
    }
  };

  return {
    signal: abortController.signal,
    get aborted() {
      return abortController.signal.aborted;
    },
    update(payload) {
      if (typeof payload.found === 'number') {
        foundBroken = payload.found;
      }
      paint(payload);
    },
    noteBroken() {
      foundBroken += 1;
      if (els.foundEl) {
        els.foundEl.textContent = i18n(
          'clear404ProgressFound',
          'Confirmed broken: $1',
          [String(foundBroken)]
        );
      }
    },
    setFound(count) {
      foundBroken = count;
      if (els.foundEl) {
        els.foundEl.textContent = i18n(
          'clear404ProgressFound',
          'Confirmed broken: $1',
          [String(foundBroken)]
        );
      }
    },
    /**
     * Switch to success state and wait until the user clicks Close.
     */
    showSuccess({ checked = 0, empty = false } = {}) {
      return new Promise((resolve) => {
        if (closed) {
          resolve();
          return;
        }

        stopBtn?.removeEventListener('click', onStop);
        if (stopBtn) {
          stopBtn.hidden = true;
          stopBtn.disabled = true;
        }

        els.contentEl?.classList.add('is-success');
        if (els.orbEl) {
          els.orbEl.hidden = true;
          els.orbEl.setAttribute('aria-hidden', 'true');
        }
        if (els.successEl) {
          els.successEl.hidden = false;
          els.successEl.setAttribute('aria-hidden', 'false');
          // restart CSS animation
          els.successEl.classList.remove('is-animating');
          void els.successEl.offsetWidth;
          els.successEl.classList.add('is-animating');
        }
        if (els.trackEl) els.trackEl.hidden = false;
        if (els.metaEl) els.metaEl.hidden = false;
        if (els.barEl) {
          els.barEl.classList.remove('is-indeterminate');
          els.barEl.style.width = '100%';
        }
        if (els.countEl) {
          const total = checked > 0 ? checked : 0;
          els.countEl.textContent = total > 0 ? `${total} / ${total}` : '100%';
        }
        if (els.foundEl) {
          els.foundEl.textContent = i18n(
            'clear404ProgressFound',
            'Confirmed broken: $1',
            ['0']
          );
        }
        if (els.detailEl) els.detailEl.textContent = '';

        if (els.titleEl) {
          els.titleEl.textContent = i18n('clear404SuccessTitle', 'All clear');
        }
        if (els.statusEl) {
          els.statusEl.textContent = empty
            ? (i18n('clear404NoBookmarks', 'No bookmarks to check in this folder'))
            : (i18n('clear404NoneFound', 'No 404 bookmarks found'));
        }
        if (!empty && checked > 0 && els.detailEl) {
          els.detailEl.textContent = i18n(
            'clear404SuccessChecked',
            'Checked $1 bookmarks',
            [String(checked)]
          );
        }

        els.dialog.setAttribute('aria-busy', 'false');

        if (closeBtn) {
          closeBtn.hidden = false;
          closeBtn.disabled = false;
          closeBtn.textContent = i18n('clear404Close', 'Close');
          const onClose = () => {
            closeBtn.removeEventListener('click', onClose);
            closed = true;
            hideDialog();
            resolve();
          };
          closeBtn.addEventListener('click', onClose);
          window.setTimeout(() => closeBtn.focus(), 0);
        } else {
          closed = true;
          hideDialog();
          resolve();
        }
      });
    },
    close({ cancelled = false } = {}) {
      if (closed) return;
      closed = true;
      stopBtn?.removeEventListener('click', onStop);
      if (stopBtn) {
        stopBtn.hidden = true;
        stopBtn.disabled = true;
      }
      if (closeBtn) {
        closeBtn.hidden = true;
        closeBtn.disabled = true;
      }
      if (els.barEl) {
        els.barEl.classList.remove('is-indeterminate');
        els.barEl.style.width = cancelled
          ? els.barEl.style.width || '0%'
          : '100%';
      }
      paint({ phase: cancelled ? 'cancelled' : 'done', done: 1, total: 1 }, true);
      if (cancelled && els.statusEl) {
        els.statusEl.textContent = i18n('clear404Stopped', 'Scan stopped');
      }
      els.dialog.setAttribute('aria-busy', 'false');
      window.setTimeout(() => {
        hideDialog();
      }, cancelled ? 120 : 180);
    }
  };
}

/**
 * Resolve pool limits from plan scan-speed multiplier (1 / 2 / 4).
 */
export function clear404ConcurrencyForSpeed(speedMultiplier = 1) {
  const speed = Math.max(1, Math.min(8, Math.floor(Number(speedMultiplier) || 1)));
  return {
    speed,
    globalLimit: BASE_CONCURRENCY * speed,
    perHostLimit: Math.min(8, BASE_PER_HOST_CONCURRENCY * speed)
  };
}

/**
 * Check all bookmarks under a folder (including nested). Only hard 404/410 count as broken,
 * and only after VERIFY_ATTEMPTS consecutive broken responses.
 */
export async function findBrokenBookmarks(folderId, {
  onProgress,
  signal,
  speedMultiplier = 1
} = {}) {
  const { bookmarks } = await collectFolderBookmarks(folderId);
  const checkable = bookmarks.filter((b) => isCheckableUrl(b.url));
  const { speed, globalLimit, perHostLimit } = clear404ConcurrencyForSpeed(speedMultiplier);

  if (checkable.length === 0) {
    return {
      bookmarks: [],
      broken: [],
      checked: 0,
      total: bookmarks.length,
      cancelled: false,
      speed
    };
  }

  const progress = {
    done: 0,
    total: checkable.length,
    found: 0
  };

  onProgress?.({
    phase: 'scanning',
    done: 0,
    total: progress.total,
    found: 0,
    speed
  });

  // Prevent HTTP Basic Auth login dialogs (e.g. 1Panel) during scan
  await setAuthSuppression(true);

  try {
    const results = await mapPool(
      checkable,
      {
        globalLimit,
        perHostLimit,
        signal
      },
      async (bookmark) => {
        const result = await checkBookmarkUrl(
          bookmark,
          (item, attempt, attemptsTotal) => {
            onProgress?.({
              phase: 'verifying',
              done: progress.done,
              total: progress.total,
              found: progress.found,
              currentTitle: item.title,
              currentUrl: item.url,
              verifyingAttempt: attempt,
              verifyingTotal: attemptsTotal,
              speed
            });
          },
          { signal }
        );
        return result;
      },
      (done, total, result) => {
        progress.done = done;
        if (result?.broken) progress.found += 1;
        onProgress?.({
          phase: signal?.aborted ? 'cancelled' : 'scanning',
          done,
          total,
          found: progress.found,
          currentTitle: result?.title,
          currentUrl: result?.url,
          result,
          speed
        });
      }
    );

    const completed = results.filter(Boolean);
    const broken = completed.filter((item) => item.broken);
    return {
      bookmarks: completed,
      broken,
      checked: completed.length,
      total: bookmarks.length,
      cancelled: Boolean(signal?.aborted),
      speed
    };
  } finally {
    await setAuthSuppression(false);
  }
}

async function removeBookmark(id) {
  return new Promise((resolve, reject) => {
    chrome.bookmarks.remove(id, () => {
      if (chrome.runtime.lastError) {
        reject(new Error(chrome.runtime.lastError.message));
        return;
      }
      resolve(true);
    });
  });
}

export async function deleteBrokenBookmarks(brokenBookmarks, { onProgress } = {}) {
  let deleted = 0;
  const total = brokenBookmarks.length;
  for (const bookmark of brokenBookmarks) {
    try {
      await removeBookmark(bookmark.id);
      deleted += 1;
    } catch (error) {
      console.warn('Failed to delete broken bookmark', bookmark.id, error);
    }
    onProgress?.({
      phase: 'deleting',
      done: deleted,
      total,
      currentTitle: bookmark.title,
      currentUrl: bookmark.url
    });
  }
  return deleted;
}

async function updateBookmarkDetails(id, changes) {
  return new Promise((resolve, reject) => {
    chrome.bookmarks.update(id, changes, (result) => {
      if (chrome.runtime.lastError) {
        reject(new Error(chrome.runtime.lastError.message));
        return;
      }
      resolve(result);
    });
  });
}

/**
 * Open the shared edit-dialog above the Clear 404 modal.
 * Reparents to document.body so it escapes the bookmarks stacking context.
 * @returns {Promise<{ title: string, url: string } | null>}
 */
function openClear404EditDialog(bookmark) {
  return new Promise((resolve) => {
    const editDialog = document.getElementById('edit-dialog');
    const form = document.getElementById('edit-form');
    const nameInput = document.getElementById('edit-name');
    const urlInput = document.getElementById('edit-url');
    const titleEl = editDialog?.querySelector('h2');
    const cancelButton = editDialog?.querySelector('.cancel-button');
    const closeButton = editDialog?.querySelector('.close-button');
    const clear404Dialog = document.getElementById('clear-404-dialog');

    if (!editDialog || !form || !nameInput || !urlInput) {
      resolve(null);
      return;
    }

    const previousZIndex = editDialog.style.zIndex;
    const previousParent = editDialog.parentNode;
    const placeholder = document.createComment('edit-dialog-home');
    previousParent.insertBefore(placeholder, editDialog);
    document.body.appendChild(editDialog);

    editDialog.classList.add('clear-404-edit-elevated');
    editDialog.style.zIndex = '1300';
    if (clear404Dialog) {
      clear404Dialog.classList.add('clear-404-dialog-behind-edit');
    }

    nameInput.value = bookmark.title || '';
    urlInput.value = bookmark.url || '';
    if (titleEl) {
      titleEl.textContent = i18n('editDialogTitle', 'Edit Bookmark');
    }

    editDialog.style.display = 'block';
    window.setTimeout(() => {
      urlInput.focus();
      urlInput.select();
    }, 0);

    const cleanup = (result) => {
      form.onsubmit = null;
      if (cancelButton) cancelButton.onclick = null;
      if (closeButton) closeButton.onclick = null;
      editDialog.style.display = 'none';
      editDialog.classList.remove('clear-404-edit-elevated');
      editDialog.style.zIndex = previousZIndex;
      clear404Dialog?.classList.remove('clear-404-dialog-behind-edit');
      if (placeholder.parentNode) {
        placeholder.parentNode.insertBefore(editDialog, placeholder);
        placeholder.remove();
      }
      resolve(result);
    };

    form.onsubmit = async (event) => {
      event.preventDefault();
      const title = nameInput.value.trim();
      let url = urlInput.value.trim();
      if (!title || !url) return;
      if (!/^https?:\/\//i.test(url)) {
        url = `https://${url}`;
      }
      try {
        await updateBookmarkDetails(bookmark.id, { title, url });
        cleanup({ title, url });
      } catch (error) {
        console.warn('Failed to update bookmark', bookmark.id, error);
        cleanup(null);
      }
    };

    if (cancelButton) {
      cancelButton.onclick = () => cleanup(null);
    }
    if (closeButton) {
      closeButton.onclick = () => cleanup(null);
    }
  });
}

/**
 * Preview modal: list 404 bookmarks with per-item Visit / Edit / Delete, plus Delete All.
 * @returns {Promise<{ action: 'deleteAll' | 'done' | 'cancel', remaining: Array, deletedCount: number, editedCount: number }>}
 */
export function showClear404Preview(brokenBookmarks) {
  return new Promise((resolve) => {
    const dialog = document.getElementById('clear-404-dialog');
    const listEl = document.getElementById('clear-404-preview-list');
    const summaryEl = document.getElementById('clear-404-preview-summary');
    const confirmBtn = document.getElementById('clear-404-confirm-button');
    const cancelBtn = document.getElementById('clear-404-cancel-button');
    const closeBtn = document.getElementById('clear-404-close-button');

    const remaining = brokenBookmarks.map((b) => ({ ...b }));
    let deletedCount = 0;
    let editedCount = 0;

    const visitLabel = i18n('clear404Visit', 'Visit');
    const editLabel = i18n('clear404Edit', 'Edit');
    const deleteLabel = i18n('clear404DeleteOne', 'Delete');
    const deleteAllLabel = i18n('clear404ConfirmButton', 'Delete All');

    if (!dialog || !listEl || !confirmBtn || !cancelBtn) {
      const ok = window.confirm(
        i18n(
          'clear404PreviewSummary',
          'Found $1 broken bookmarks (404/410). Delete them?',
          [String(remaining.length)]
        )
      );
      resolve({
        action: ok ? 'deleteAll' : 'cancel',
        remaining,
        deletedCount: 0,
        editedCount: 0
      });
      return;
    }

    const updateSummary = () => {
      if (summaryEl) {
        summaryEl.textContent = i18n(
          'clear404PreviewSummary',
          'Found $1 broken bookmarks (404/410). Delete them?',
          [String(remaining.length)]
        );
      }
      confirmBtn.disabled = remaining.length === 0;
      confirmBtn.textContent = deleteAllLabel;
    };

    const finish = (action) => {
      cleanup();
      resolve({
        action,
        remaining: [...remaining],
        deletedCount,
        editedCount
      });
    };

    const cleanup = () => {
      confirmBtn.removeEventListener('click', onConfirm);
      cancelBtn.removeEventListener('click', onCancel);
      closeBtn?.removeEventListener('click', onCancel);
      dialog.style.display = 'none';
    };

    const onConfirm = () => finish('deleteAll');
    const onCancel = () => finish(
      (deletedCount > 0 || editedCount > 0) ? 'done' : 'cancel'
    );

    const renderList = () => {
      listEl.innerHTML = '';
      updateSummary();

      if (remaining.length === 0) {
        const empty = document.createElement('div');
        empty.className = 'clear-404-item clear-404-item-empty';
        empty.textContent = i18n('clear404NoneFound', 'No 404 bookmarks found');
        listEl.appendChild(empty);
        return;
      }

      for (const bookmark of remaining) {
        const item = document.createElement('div');
        item.className = 'clear-404-item';
        item.dataset.bookmarkId = bookmark.id;

        const body = document.createElement('div');
        body.className = 'clear-404-item-body';

        const title = document.createElement('div');
        title.className = 'clear-404-item-title';
        title.textContent = bookmark.title || bookmark.url;

        const meta = document.createElement('div');
        meta.className = 'clear-404-item-meta';
        const statusLabel = bookmark.status != null ? String(bookmark.status) : '404';
        meta.textContent = `${statusLabel} · ${bookmark.url}`;

        body.appendChild(title);
        body.appendChild(meta);

        const actions = document.createElement('div');
        actions.className = 'clear-404-item-actions';

        const setBusy = (busy) => {
          visitBtn.disabled = busy;
          editBtn.disabled = busy;
          deleteBtn.disabled = busy;
        };

        const visitBtn = document.createElement('button');
        visitBtn.type = 'button';
        visitBtn.className = 'clear-404-item-btn clear-404-item-btn-visit';
        visitBtn.textContent = visitLabel;
        visitBtn.title = visitLabel;
        visitBtn.addEventListener('click', (event) => {
          event.preventDefault();
          event.stopPropagation();
          try {
            window.open(bookmark.url, '_blank', 'noopener,noreferrer');
          } catch (error) {
            console.warn('Failed to open bookmark URL', error);
          }
        });

        const editBtn = document.createElement('button');
        editBtn.type = 'button';
        editBtn.className = 'clear-404-item-btn clear-404-item-btn-edit';
        editBtn.textContent = editLabel;
        editBtn.title = editLabel;
        editBtn.addEventListener('click', async (event) => {
          event.preventDefault();
          event.stopPropagation();
          setBusy(true);
          try {
            const updated = await openClear404EditDialog(bookmark);
            if (!updated) {
              setBusy(false);
              return;
            }
            editedCount += 1;
            const idx = remaining.findIndex((b) => b.id === bookmark.id);
            if (idx >= 0) remaining.splice(idx, 1);
            item.classList.add('is-removing');
            window.setTimeout(() => {
              renderList();
              if (remaining.length === 0) {
                finish('done');
              }
            }, 160);
          } catch (error) {
            console.warn('Failed to edit broken bookmark', bookmark.id, error);
            setBusy(false);
          }
        });

        const deleteBtn = document.createElement('button');
        deleteBtn.type = 'button';
        deleteBtn.className = 'clear-404-item-btn clear-404-item-btn-delete';
        deleteBtn.textContent = deleteLabel;
        deleteBtn.title = deleteLabel;
        deleteBtn.addEventListener('click', async (event) => {
          event.preventDefault();
          event.stopPropagation();
          setBusy(true);
          try {
            await removeBookmark(bookmark.id);
            const idx = remaining.findIndex((b) => b.id === bookmark.id);
            if (idx >= 0) remaining.splice(idx, 1);
            deletedCount += 1;
            item.classList.add('is-removing');
            window.setTimeout(() => {
              renderList();
              if (remaining.length === 0) {
                finish('done');
              }
            }, 160);
          } catch (error) {
            console.warn('Failed to delete broken bookmark', bookmark.id, error);
            setBusy(false);
          }
        });

        actions.appendChild(visitBtn);
        actions.appendChild(editBtn);
        actions.appendChild(deleteBtn);

        item.appendChild(body);
        item.appendChild(actions);
        listEl.appendChild(item);
      }
    };

    confirmBtn.textContent = deleteAllLabel;
    confirmBtn.disabled = false;
    renderList();
    dialog.style.display = 'block';

    confirmBtn.addEventListener('click', onConfirm);
    cancelBtn.addEventListener('click', onCancel);
    closeBtn?.addEventListener('click', onCancel);
  });
}

export function getClear404ErrorMessage(error) {
  const code = String(error?.message || error || '').slice(0, 120);
  if (code === 'PERMISSION_DENIED') {
    return i18n(
      'clear404PermissionDenied',
      'Network permission is required to check bookmarks'
    );
  }
  return i18n('clear404Error', 'Failed to clear 404 bookmarks: $1', [code]);
}
