(function (root, factory) {
  "use strict";
  const config = typeof module === "object" && module.exports
    ? require("./capture-config.js")
    : root.SDMCaptureConfig;
  const api = factory(config);
  if (typeof module === "object" && module.exports) module.exports = api;
  root.SDMCaptureQueue = api;
})(typeof globalThis !== "undefined" ? globalThis : this, function (config) {
  "use strict";

  const STATUS = Object.freeze({
    WAITING: "waiting",
    COLLECTING: "collecting",
    SUCCESS: "success",
    PARTIAL: "partial_success",
    FAILED: "failed"
  });
  const FINAL_STATUSES = new Set([STATUS.SUCCESS, STATUS.PARTIAL, STATUS.FAILED]);

  function nowIso() {
    return new Date().toISOString();
  }

  function taskIdentity(account, storeId, domain, asin) {
    return `${String(account || "").toLowerCase()}|${String(storeId || "").toLowerCase()}|${config.normalizeHost(domain)}|${config.normalizeAsin(asin)}`;
  }

  function taskId(identity, asin) {
    let first = 2166136261;
    let second = 2246822519;
    for (let index = 0; index < identity.length; index++) {
      const code = identity.charCodeAt(index);
      first = Math.imul(first ^ code, 16777619) >>> 0;
      second = Math.imul(second ^ code, 3266489917) >>> 0;
    }
    return `cap_${asin}_${first.toString(16).padStart(8, "0")}${second.toString(16).padStart(8, "0")}`;
  }

  function createCaptureQueue(storage, settings = config.SETTINGS) {
    if (!storage?.get || !storage?.set) throw new Error("持久化任务队列缺少 storage 适配器");
    let mutation = Promise.resolve();

    function locked(operation) {
      const next = mutation.then(operation, operation);
      mutation = next.catch(() => {});
      return next;
    }

    async function readTasks() {
      const result = await storage.get(settings.TASK_STORAGE_KEY);
      const tasks = result?.[settings.TASK_STORAGE_KEY];
      return Array.isArray(tasks) ? tasks : [];
    }

    async function writeTasks(tasks) {
      const sorted = [...tasks]
        .sort((a, b) => String(b.updatedAt || "").localeCompare(String(a.updatedAt || "")))
        .slice(0, settings.MAX_TASKS);
      await storage.set({ [settings.TASK_STORAGE_KEY]: sorted });
      return sorted;
    }

    async function recoverInterrupted(wakeWaiting = false) {
      return locked(async () => {
        const tasks = await readTasks();
        let recovered = 0;
        const updated = tasks.map(task => {
          const interrupted = task.status === STATUS.COLLECTING && Number(task.leaseUntil || 0) <= Date.now();
          const delayedWaiting = wakeWaiting && task.status === STATUS.WAITING;
          if (!interrupted && !delayedWaiting) return task;
          recovered += 1;
          return {
            ...task,
            status: STATUS.WAITING,
            nextAttemptAt: 0,
            error: interrupted ? "扩展后台已重启，任务正在恢复" : task.error,
            updatedAt: nowIso()
          };
        });
        if (recovered) await writeTasks(updated);
        return recovered;
      });
    }

    async function enqueue(targets, options = {}) {
      return locked(async () => {
        const tasks = await readTasks();
        const account = String(options.account || "").trim();
        const storeId = String(options.storeId || "").trim();
        const profileId = String(options.profileId || "").trim();
        const accepted = [];
        const duplicates = [];
        const seenInput = new Set();
        for (const raw of Array.isArray(targets) ? targets : [targets]) {
          const asin = config.normalizeAsin(raw?.asin || config.asinFromUrl(raw?.url));
          const domain = config.normalizeHost(raw?.domain || raw?.url);
          const site = config.siteForHost(domain);
          if (!account || !asin || !site) continue;
          const idempotencyKey = taskIdentity(account, storeId, domain, asin);
          if (seenInput.has(idempotencyKey)) continue;
          seenInput.add(idempotencyKey);
          const existingIndex = tasks.findIndex(task => task.idempotencyKey === idempotencyKey);
          const existing = existingIndex >= 0 ? tasks[existingIndex] : null;
          if (existing) {
            if (options.force) {
              const timestamp = nowIso();
              const refreshed = {
                ...existing,
                storeId,
                profileId,
                batchId: String(options.batchId || raw?.batchId || ""),
                includeVariants: Boolean(options.includeVariants),
                seedProduct: raw?.seedProduct && typeof raw.seedProduct === "object" ? raw.seedProduct : null,
                seedIsCurrentPage: Boolean(raw?.seedIsCurrentPage),
                previewProduct: raw?.previewProduct && typeof raw.previewProduct === "object" ? raw.previewProduct : (raw?.preview && typeof raw.preview === "object" ? raw.preview : existing.previewProduct || null),
                status: STATUS.WAITING,
                attempts: 0,
                error: "",
                errorCode: "",
                variantProgress: null,
                nextAttemptAt: 0,
                updatedAt: timestamp
              };
              tasks[existingIndex] = refreshed;
              accepted.push(refreshed);
              continue;
            }
            if (raw?.seedProduct && existing.status !== STATUS.SUCCESS && existing.status !== STATUS.PARTIAL) {
              const timestamp = nowIso();
              const refreshed = {
                ...existing,
                profileId,
                seedProduct: raw.seedProduct,
                seedIsCurrentPage: Boolean(raw?.seedIsCurrentPage),
                status: STATUS.WAITING,
                attempts: 0,
                error: "",
                errorCode: "",
                variantProgress: null,
                nextAttemptAt: 0,
                updatedAt: timestamp
              };
              tasks[existingIndex] = refreshed;
              accepted.push(refreshed);
              continue;
            }
            duplicates.push(existing);
            continue;
          }
          const timestamp = nowIso();
          const task = {
            id: taskId(idempotencyKey, asin),
            idempotencyKey,
            account,
            storeId,
            profileId,
            asin,
            domain,
            site: site.site,
            country: site.country,
            url: config.canonicalProductUrl(domain, asin),
            seedProduct: raw?.seedProduct && typeof raw.seedProduct === "object" ? raw.seedProduct : null,
            seedIsCurrentPage: Boolean(raw?.seedIsCurrentPage),
            previewProduct: raw?.previewProduct && typeof raw.previewProduct === "object" ? raw.previewProduct : (raw?.preview && typeof raw.preview === "object" ? raw.preview : null),
            includeVariants: Boolean(options.includeVariants),
            batchId: String(options.batchId || raw?.batchId || ""),
            status: STATUS.WAITING,
            attempts: 0,
            maxRetries: settings.REQUEST_RETRIES,
            error: "",
            errorCode: "",
            partialErrors: [],
            variantProgress: null,
            queueOrder: Date.now() * 1000 + accepted.length,
            createdAt: timestamp,
            updatedAt: timestamp
          };
          tasks.unshift(task);
          accepted.push(task);
        }
        if (accepted.length) await writeTasks(tasks);
        return { accepted, duplicates };
      });
    }

    function publicTask(task) {
      const { variantProgress, ...visible } = task || {};
      return visible;
    }

    async function list(account = "", limit = 100) {
      const tasks = await readTasks();
      return tasks
        .filter(task => !account || task.account === account)
        .slice(0, Math.max(1, Number(limit) || 100))
        .map(publicTask);
    }

    async function update(id, patch) {
      return locked(async () => {
        const tasks = await readTasks();
        const index = tasks.findIndex(task => task.id === id);
        if (index < 0) return null;
        tasks[index] = { ...tasks[index], ...patch, id: tasks[index].id, updatedAt: nowIso() };
        await writeTasks(tasks);
        return tasks[index];
      });
    }

    async function claim(limit = settings.TASK_CONCURRENCY) {
      return locked(async () => {
        const tasks = await readTasks();
        const claimed = [];
        const indexes = tasks.map((task, index) => ({ task, index }))
          .filter(({ task }) => task.status === STATUS.WAITING && Number(task.nextAttemptAt || 0) <= Date.now())
          .sort((left, right) => {
            const orderDiff = Number(left.task.queueOrder || 0) - Number(right.task.queueOrder || 0);
            return orderDiff || String(left.task.createdAt || left.task.updatedAt || "").localeCompare(String(right.task.createdAt || right.task.updatedAt || ""));
          })
          .map(item => item.index);
        for (const index of indexes) {
          if (claimed.length >= limit) break;
          if (tasks[index].status !== STATUS.WAITING) continue;
          if (Number(tasks[index].nextAttemptAt || 0) > Date.now()) continue;
          tasks[index] = {
            ...tasks[index],
            status: STATUS.COLLECTING,
            attempts: Number(tasks[index].attempts || 0) + 1,
            error: "",
            errorCode: "",
            nextAttemptAt: 0,
            leaseUntil: Date.now() + Number(settings.TASK_LEASE_MS || 120000),
            updatedAt: nowIso()
          };
          claimed.push(tasks[index]);
        }
        if (claimed.length) await writeTasks(tasks);
        return claimed;
      });
    }

    async function heartbeat(id) {
      return locked(async () => {
        const tasks = await readTasks();
        const index = tasks.findIndex(task => task.id === id && task.status === STATUS.COLLECTING);
        if (index < 0) return null;
        tasks[index] = {
          ...tasks[index],
          leaseUntil: Date.now() + Math.max(1000, Number(settings.TASK_LEASE_MS || 120000)),
          heartbeatAt: nowIso(),
          updatedAt: nowIso()
        };
        await writeTasks(tasks);
        return tasks[index];
      });
    }

    async function yieldTask(id, patch = {}) {
      return locked(async () => {
        const tasks = await readTasks();
        const index = tasks.findIndex(task => task.id === id && task.status === STATUS.COLLECTING);
        if (index < 0) return null;
        const queueOrder = Math.max(0, ...tasks.map(task => Number(task.queueOrder || 0))) + 1;
        tasks[index] = {
          ...tasks[index], ...patch, id: tasks[index].id,
          status: STATUS.WAITING, resume: true, leaseUntil: 0, heartbeatAt: "",
          nextAttemptAt: 0, queueOrder, updatedAt: nowIso()
        };
        await writeTasks(tasks);
        return tasks[index];
      });
    }

    async function retryFailed(id = "", account = "") {
      return locked(async () => {
        const tasks = await readTasks();
        const retried = [];
        for (let index = 0; index < tasks.length; index++) {
          const task = tasks[index];
          if (task.status !== STATUS.FAILED) continue;
          if (task.errorCode === "USER_CANCELLED") continue;
          if (id && task.id !== id) continue;
          if (account && task.account !== account) continue;
          tasks[index] = { ...task, status: STATUS.WAITING, attempts: 0, error: "", errorCode: "", partialErrors: [], variantProgress: null, updatedAt: nowIso() };
          retried.push(tasks[index]);
        }
        if (retried.length) await writeTasks(tasks);
        return retried;
      });
    }

    async function cancelBatch(batchId, account = "") {
      return locked(async () => {
        const targetBatch = String(batchId || "");
        if (!targetBatch) return [];
        const tasks = await readTasks();
        const cancelled = [];
        for (let index = 0; index < tasks.length; index++) {
          const task = tasks[index];
          if (task.batchId !== targetBatch || (account && task.account !== account)) continue;
          if (task.status === STATUS.SUCCESS || task.status === STATUS.PARTIAL_SUCCESS || task.errorCode === "USER_CANCELLED") continue;
          tasks[index] = {
            ...task,
            status: STATUS.FAILED,
            error: "用户已停止本次批量采集",
            errorCode: "USER_CANCELLED",
            nextAttemptAt: 0,
            updatedAt: nowIso()
          };
          cancelled.push(tasks[index]);
        }
        if (cancelled.length) await writeTasks(tasks);
        return cancelled;
      });
    }

    async function batchSummary(batchId, account = "") {
      const targetBatch = String(batchId || "");
      const tasks = (await readTasks()).filter(task => task.batchId === targetBatch && (!account || task.account === account));
      const counts = { waiting: 0, collecting: 0, success: 0, partial_success: 0, failed: 0, cancelled: 0 };
      tasks.forEach(task => {
        if (task.errorCode === "USER_CANCELLED") counts.cancelled += 1;
        else if (Object.prototype.hasOwnProperty.call(counts, task.status)) counts[task.status] += 1;
      });
      return { batchId: targetBatch, total: tasks.length, active: counts.waiting + counts.collecting, counts, tasks: tasks.map(publicTask) };
    }

    async function remove(id) {
      return locked(async () => {
        const tasks = await readTasks();
        const filtered = tasks.filter(task => task.id !== id);
        if (filtered.length !== tasks.length) await writeTasks(filtered);
        return filtered.length !== tasks.length;
      });
    }

    async function purgeFinal(account = "") {
      return locked(async () => {
        const targetAccount = String(account || "").trim();
        const tasks = await readTasks();
        const filtered = tasks.filter(task => !(
          FINAL_STATUSES.has(task.status)
          && (!targetAccount || task.account === targetAccount)
        ));
        if (filtered.length !== tasks.length) await writeTasks(filtered);
        return tasks.length - filtered.length;
      });
    }

    async function releaseProduct(target = {}) {
      return locked(async () => {
        const account = String(target.account || "").trim();
        const storeId = String(target.storeId || "").trim();
        const asin = config.normalizeAsin(target.asin || config.asinFromUrl(target.url));
        const domain = config.normalizeHost(target.domain || target.url);
        if (!account || !asin) return { tasks: 0, cache: 0 };
        const tasks = await readTasks();
        const filtered = tasks.filter(task => !(
          task.account === account
          && (!storeId || task.storeId === storeId)
          && task.asin === asin
          && (!domain || config.normalizeHost(task.domain) === domain)
        ));
        if (filtered.length !== tasks.length) await writeTasks(filtered);
        const cache = await readCache();
        let removedCache = 0;
        for (const key of Object.keys(cache)) {
          const exact = domain ? key === cacheKey(domain, asin, account) : key.startsWith(`${account.toLowerCase()}|`) && key.endsWith(`|${asin}`);
          if (!exact) continue;
          delete cache[key];
          removedCache += 1;
        }
        if (removedCache) await storage.set({ [settings.CACHE_STORAGE_KEY]: cache });
        return { tasks: tasks.length - filtered.length, cache: removedCache };
      });
    }

    async function readCache() {
      const result = await storage.get(settings.CACHE_STORAGE_KEY);
      const cache = result?.[settings.CACHE_STORAGE_KEY];
      return cache && typeof cache === "object" && !Array.isArray(cache) ? cache : {};
    }

    function cacheKey(domain, asin, account = "") {
      return `${String(account || "").toLowerCase()}|${config.normalizeHost(domain)}|${config.normalizeAsin(asin)}`;
    }

    async function getCached(domain, asin, account = "") {
      const key = cacheKey(domain, asin, account);
      const cache = await readCache();
      const entry = cache[key];
      if (!entry) return null;
      if (Number(entry.expiresAt || 0) <= Date.now()) {
        delete cache[key];
        await storage.set({ [settings.CACHE_STORAGE_KEY]: cache });
        return null;
      }
      return entry.value || null;
    }

    async function setCached(domain, asin, value, account = "") {
      return locked(async () => {
        const cache = await readCache();
        const key = cacheKey(domain, asin, account);
        cache[key] = { value, expiresAt: Date.now() + settings.CACHE_TTL_MS, updatedAt: nowIso() };
        const entries = Object.entries(cache)
          .filter(([, entry]) => Number(entry.expiresAt || 0) > Date.now())
          .sort((a, b) => String(b[1].updatedAt || "").localeCompare(String(a[1].updatedAt || "")))
          .slice(0, settings.MAX_CACHE_ENTRIES);
        await storage.set({ [settings.CACHE_STORAGE_KEY]: Object.fromEntries(entries) });
        return value;
      });
    }

    async function clearCache(account = "") {
      if (!account) {
        await storage.set({ [settings.CACHE_STORAGE_KEY]: {} });
        return;
      }
      const cache = await readCache();
      const prefix = `${String(account).toLowerCase()}|`;
      const filtered = Object.fromEntries(Object.entries(cache).filter(([key]) => !key.startsWith(prefix)));
      await storage.set({ [settings.CACHE_STORAGE_KEY]: filtered });
    }

    async function cacheSize(account = "") {
      const cache = await readCache();
      const prefix = account ? `${String(account).toLowerCase()}|` : "";
      return Object.entries(cache).filter(([key, entry]) => (!prefix || key.startsWith(prefix)) && Number(entry.expiresAt || 0) > Date.now()).length;
    }

    async function summary(account = "") {
      const tasks = await list(account, settings.MAX_TASKS);
      const counts = { waiting: 0, collecting: 0, success: 0, partial_success: 0, failed: 0 };
      tasks.forEach(task => { if (Object.hasOwn(counts, task.status)) counts[task.status] += 1; });
      return { counts, total: tasks.length, cacheSize: await cacheSize(account), tasks };
    }

    return { recoverInterrupted, enqueue, list, update, claim, heartbeat, yieldTask, retryFailed, cancelBatch, batchSummary, remove, purgeFinal, releaseProduct, getCached, setCached, clearCache, summary };
  }

  return { STATUS, FINAL_STATUSES, taskIdentity, createCaptureQueue };
});
