/**
 * scheduler.js
 * Automation task registry, trigger matching, execution, and logs.
 */
(function () {
  'use strict';

  const TASKS_KEY = 'automationTasks';
  const LOGS_KEY = 'automationLogs';
  const QUEUE_KEY = 'automationRunQueue';
  const CRON_KEYS_KEY = 'automationCronKeys';
  const BUILTIN_SEEDED_KEY = 'builtinAutomationTasksSeeded';
  const TICK_ALARM = 'aisc_automation_tick';
  const MAX_LOGS = 80;
  const MAX_QUEUE = 120;
  const RUNNING_LEASE_MS = 10 * 60 * 1000;
  const DAILY_WECHAT_TASK_ID = 'builtin_daily_wechat_article_0800_beijing';
  let queueProcessing = false;

  function storageGet(keys) {
    return new Promise((resolve, reject) => {
      chrome.storage.local.get(keys, (value) => {
        const err = chrome.runtime.lastError;
        if (err) reject(new Error(err.message));
        else resolve(value || {});
      });
    });
  }

  function storageSet(value) {
    return new Promise((resolve, reject) => {
      chrome.storage.local.set(value, () => {
        const err = chrome.runtime.lastError;
        if (err) reject(new Error(err.message));
        else resolve();
      });
    });
  }

  function makeId(prefix = 'task') {
    return prefix + '_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);
  }

  function normalizeTask(task = {}) {
    const trigger = task.trigger || {};
    return {
      id: task.id || makeId(),
      name: String(task.name || '未命名自动化任务').trim(),
      enabled: task.enabled !== false,
      trigger: {
        type: trigger.type || 'manual',
        rule: trigger.rule || '',
        urlPattern: trigger.urlPattern || trigger.pattern || '',
        description: trigger.description || '',
        timeZone: trigger.timeZone || ''
      },
      instruction: String(task.instruction || task.description || '').trim(),
      sourceUrl: task.sourceUrl || '',
      schedule: task.schedule || null,
      steps: Array.isArray(task.steps) ? task.steps : [],
      createdAt: task.createdAt || new Date().toISOString(),
      updatedAt: new Date().toISOString(),
      lastRunAt: task.lastRunAt || '',
      lastStatus: task.lastStatus || '',
      runCount: Number(task.runCount || 0),
      lastError: task.lastError || ''
    };
  }

  async function listTasks() {
    const stored = await storageGet([TASKS_KEY]);
    return Array.isArray(stored[TASKS_KEY]) ? stored[TASKS_KEY].map(normalizeTask) : [];
  }

  async function saveTasks(tasks) {
    await storageSet({ [TASKS_KEY]: tasks.map(normalizeTask) });
  }

  async function saveTask(task) {
    const tasks = await listTasks();
    const normalized = normalizeTask(task);
    const idx = tasks.findIndex(t => t.id === normalized.id);
    if (idx >= 0) {
      normalized.createdAt = tasks[idx].createdAt || normalized.createdAt;
      tasks[idx] = { ...tasks[idx], ...normalized, updatedAt: new Date().toISOString() };
    } else {
      tasks.unshift(normalized);
    }
    await saveTasks(tasks);
    await syncTickAlarm();
    return normalized;
  }

  async function deleteTask(id) {
    const tasks = await listTasks();
    const next = tasks.filter(t => t.id !== id);
    await saveTasks(next);
    await syncTickAlarm();
    return { deleted: tasks.length - next.length };
  }

  async function toggleTask(id, enabled) {
    const tasks = await listTasks();
    const target = tasks.find(t => t.id === id);
    if (!target) throw new Error('任务不存在');
    target.enabled = enabled !== false;
    target.updatedAt = new Date().toISOString();
    await saveTasks(tasks);
    await syncTickAlarm();
    return target;
  }

  async function appendLog(entry) {
    const stored = await storageGet([LOGS_KEY]);
    const logs = Array.isArray(stored[LOGS_KEY]) ? stored[LOGS_KEY] : [];
    logs.unshift({
      id: entry.id || makeId('log'),
      taskId: entry.taskId || '',
      taskName: entry.taskName || '',
      status: entry.status || 'info',
      trigger: entry.trigger || '',
      message: entry.message || '',
      details: entry.details || null,
      createdAt: entry.createdAt || new Date().toISOString()
    });
    await storageSet({ [LOGS_KEY]: logs.slice(0, MAX_LOGS) });
  }

  async function listLogs() {
    const stored = await storageGet([LOGS_KEY]);
    return Array.isArray(stored[LOGS_KEY]) ? stored[LOGS_KEY] : [];
  }

  function normalizeQueueEntry(entry = {}) {
    return {
      id: entry.id || makeId('run'),
      taskId: entry.taskId || entry.taskSnapshot?.id || '',
      taskName: entry.taskName || entry.taskSnapshot?.name || '',
      trigger: entry.trigger || 'manual',
      status: entry.status || 'queued',
      attempts: Number(entry.attempts || 0),
      maxAttempts: Math.max(1, Math.min(5, Number(entry.maxAttempts || 1))),
      nextRunAt: entry.nextRunAt || '',
      leaseUntil: entry.leaseUntil || '',
      createdAt: entry.createdAt || new Date().toISOString(),
      updatedAt: entry.updatedAt || new Date().toISOString(),
      startedAt: entry.startedAt || '',
      finishedAt: entry.finishedAt || '',
      error: entry.error || '',
      result: entry.result || null,
      taskSnapshot: entry.taskSnapshot || null
    };
  }

  async function readQueue() {
    const stored = await storageGet([QUEUE_KEY]);
    return Array.isArray(stored[QUEUE_KEY]) ? stored[QUEUE_KEY].map(normalizeQueueEntry) : [];
  }

  async function saveQueue(queue) {
    const normalized = queue.map(normalizeQueueEntry);
    const active = normalized.filter(entry => !['success', 'canceled'].includes(entry.status));
    const done = normalized.filter(entry => ['success', 'canceled'].includes(entry.status)).slice(0, MAX_QUEUE - active.length);
    await storageSet({ [QUEUE_KEY]: [...active, ...done].slice(0, MAX_QUEUE) });
  }

  async function listQueue(args = {}) {
    const queue = await readQueue();
    if (args.includeDone) return queue;
    return queue.filter(entry => !['success', 'canceled'].includes(entry.status));
  }

  async function enqueueRun(input, trigger = 'manual', options = {}) {
    const tasks = await listTasks();
    const task = typeof input === 'string'
      ? tasks.find(t => t.id === input)
      : normalizeTask(input);
    if (!task) throw new Error('任务不存在');
    const now = new Date().toISOString();
    const entry = normalizeQueueEntry({
      id: makeId('run'),
      taskId: task.id,
      taskName: task.name,
      trigger,
      status: 'queued',
      maxAttempts: options.maxAttempts || task.queue?.maxAttempts || 1,
      taskSnapshot: task,
      createdAt: now,
      updatedAt: now
    });
    const queue = await readQueue();
    queue.unshift(entry);
    await saveQueue(queue);
    await appendLog({ taskId: task.id, taskName: task.name, status: 'queued', trigger, message: '已加入长期任务队列', details: { runId: entry.id } });
    if (options.process !== false) {
      processRunQueue().catch(e => console.warn('[automation] queue process failed:', e.message));
    }
    return entry;
  }

  function isRunnableQueueEntry(entry, now = new Date()) {
    if (entry.status === 'queued') {
      return !entry.nextRunAt || new Date(entry.nextRunAt) <= now;
    }
    if (entry.status === 'running' && entry.leaseUntil) {
      return new Date(entry.leaseUntil) <= now;
    }
    return false;
  }

  async function cancelQueuedRun(id) {
    const queue = await readQueue();
    const entry = queue.find(item => item.id === id);
    if (!entry) throw new Error('队列任务不存在');
    if (entry.status === 'running') throw new Error('任务正在执行，暂不支持取消');
    entry.status = 'canceled';
    entry.updatedAt = new Date().toISOString();
    entry.finishedAt = entry.updatedAt;
    await saveQueue(queue);
    return entry;
  }

  async function retryQueuedRun(id) {
    const queue = await readQueue();
    const entry = queue.find(item => item.id === id);
    if (!entry) throw new Error('队列任务不存在');
    entry.status = 'queued';
    entry.error = '';
    entry.nextRunAt = '';
    entry.finishedAt = '';
    entry.updatedAt = new Date().toISOString();
    await saveQueue(queue);
    processRunQueue().catch(e => console.warn('[automation] queue retry failed:', e.message));
    return entry;
  }

  function normalizeDow(value) {
    const names = { SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6 };
    const upper = String(value).toUpperCase();
    if (upper in names) return names[upper];
    const num = Number(value);
    if (num === 7) return 0;
    return num;
  }

  function cronParts(date = new Date(), timeZone = '') {
    if (!timeZone) {
      return {
        minute: date.getMinutes(),
        hour: date.getHours(),
        day: date.getDate(),
        month: date.getMonth() + 1,
        dow: date.getDay()
      };
    }
    const weekdayMap = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
    const parts = new Intl.DateTimeFormat('en-US', {
      timeZone,
      year: 'numeric',
      month: '2-digit',
      day: '2-digit',
      hour: '2-digit',
      minute: '2-digit',
      weekday: 'short',
      hourCycle: 'h23'
    }).formatToParts(date).reduce((acc, part) => {
      acc[part.type] = part.value;
      return acc;
    }, {});
    return {
      minute: Number(parts.minute),
      hour: Number(parts.hour),
      day: Number(parts.day),
      month: Number(parts.month),
      dow: weekdayMap[parts.weekday] ?? date.getDay()
    };
  }

  function matchField(field, actual, min, max, isDow = false) {
    const raw = String(field || '*').trim();
    if (raw === '*') return true;
    return raw.split(',').some(part => {
      const value = part.trim();
      if (!value) return false;
      const stepMatch = value.match(/^\*\/(\d+)$/);
      if (stepMatch) {
        const step = Math.max(1, Number(stepMatch[1]));
        return (actual - min) % step === 0;
      }
      const rangeMatch = value.match(/^([A-Za-z0-9]+)-([A-Za-z0-9]+)$/);
      if (rangeMatch) {
        const start = isDow ? normalizeDow(rangeMatch[1]) : Number(rangeMatch[1]);
        const end = isDow ? normalizeDow(rangeMatch[2]) : Number(rangeMatch[2]);
        if (!Number.isFinite(start) || !Number.isFinite(end)) return false;
        if (start <= end) return actual >= start && actual <= end;
        return actual >= start || actual <= end;
      }
      const expected = isDow ? normalizeDow(value) : Number(value);
      return Number.isFinite(expected) && expected >= min && expected <= max && actual === expected;
    });
  }

  function matchesCron(rule = '', date = new Date(), timeZone = '') {
    const parts = String(rule || '').trim().split(/\s+/);
    if (parts.length !== 5) return false;
    const [minute, hour, day, month, dow] = parts;
    const actual = cronParts(date, timeZone);
    return matchField(minute, actual.minute, 0, 59) &&
      matchField(hour, actual.hour, 0, 23) &&
      matchField(day, actual.day, 1, 31) &&
      matchField(month, actual.month, 1, 12) &&
      matchField(dow, actual.dow, 0, 6, true);
  }

  function zonedDateText(date = new Date(), timeZone = 'Asia/Shanghai') {
    const parts = new Intl.DateTimeFormat('en-CA', {
      timeZone,
      year: 'numeric',
      month: '2-digit',
      day: '2-digit'
    }).formatToParts(date).reduce((acc, part) => {
      acc[part.type] = part.value;
      return acc;
    }, {});
    return `${parts.year}-${parts.month}-${parts.day}`;
  }

  function makeRunNow(date = new Date()) {
    return {
      iso: date.toISOString(),
      localDate: zonedDateText(date, Intl.DateTimeFormat().resolvedOptions().timeZone),
      beijingDate: zonedDateText(date, 'Asia/Shanghai')
    };
  }

  function minuteKey(date = new Date()) {
    const pad = n => String(n).padStart(2, '0');
    return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
  }

  function urlMatches(pattern, url) {
    const value = String(pattern || '').trim();
    if (!value || !url) return false;
    try {
      return new RegExp(value).test(url);
    } catch (_) {
      return String(url).includes(value);
    }
  }

  function getPathValue(source, path) {
    return String(path || '').split('.').reduce((acc, key) => {
      if (acc == null) return '';
      return acc[key];
    }, source);
  }

  function resolveTemplates(value, state) {
    if (typeof value === 'string') {
      return value.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, path) => {
        const resolved = getPathValue(state, path);
        if (resolved == null) return '';
        return typeof resolved === 'object' ? JSON.stringify(resolved) : String(resolved);
      });
    }
    if (Array.isArray(value)) return value.map(item => resolveTemplates(item, state));
    if (value && typeof value === 'object') {
      const out = {};
      Object.entries(value).forEach(([key, val]) => { out[key] = resolveTemplates(val, state); });
      return out;
    }
    return value;
  }

  function stepTool(step = {}) {
    return step.tool || step.type || step.action || '';
  }

  async function downloadText(args = {}) {
    if (!chrome.downloads?.download) throw new Error('下载能力不可用，请确认插件已授予 downloads 权限');
    const content = String(args.content ?? '');
    const filename = String(args.filename || args.path || '詹老师的AIPROS自动化结果.txt').replace(/^\/+/, '');
    const mime = args.mime || args.contentType || 'text/plain;charset=utf-8';
    const url = `data:${mime},${encodeURIComponent(content)}`;
    const downloadId = await new Promise((resolve, reject) => {
      chrome.downloads.download({ url, filename, saveAs: args.saveAs === true }, (id) => {
        const err = chrome.runtime.lastError;
        if (err) reject(new Error(err.message));
        else resolve(id);
      });
    });
    return { downloaded: true, downloadId, filename };
  }

  async function notify(args = {}) {
    const title = String(args.title || '詹老师的AIPROS自动化');
    const message = String(args.message || args.body || '任务已完成');
    if (chrome.notifications?.create) {
      await new Promise(resolve => {
        chrome.notifications.create('', {
          type: 'basic',
          iconUrl: 'icons/icon48.png',
          title,
          message
        }, () => resolve());
      });
    }
    return { notified: true, title, message };
  }

  function readLLMSettings(stored = {}) {
    const settings = stored.llmSettings || {};
    return {
      apiKey: settings.api_key || settings.apiKey || settings.aliyun_api_key || '',
      apiUrl: settings.api_url || settings.apiUrl || settings.aliyun_api_url || 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
      model: settings.model || settings.aliyun_model || 'qwen-plus'
    };
  }

  async function runAssistant(args = {}, state = {}) {
    const stored = await storageGet(['llmSettings']);
    const { apiKey, apiUrl, model } = readLLMSettings(stored);
    if (!apiKey) throw new Error('LLM API未配置，请先在设置里保存 API Key');
    const contextText = JSON.stringify({
      task: state.task?.name || '',
      trigger: state.trigger || '',
      pageContext: state.context || null,
      last: state.last || null,
      results: state.results || []
    }, null, 2).slice(0, 12000);
    const instruction = args.instruction || args.prompt || state.task?.instruction || '请执行这个自动化任务。';
    const resp = await fetch(apiUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + apiKey
      },
      body: JSON.stringify({
        model,
        messages: [
          { role: 'system', content: '你是「詹老师的AIPROS」自动化执行器。按用户自然语言任务执行，输出简洁、结构化、可直接使用的中文结果。无人值守自动化场景下，如果信息不足，不要停下来追问；基于已有信息完成，并把缺口标为“待补充”。' },
          { role: 'user', content: `【用户任务】\n${instruction}\n\n【自动化上下文】\n${contextText}` }
        ],
        temperature: args.temperature ?? 0.3,
        max_tokens: args.max_tokens || 4096
      })
    });
    if (!resp.ok) throw new Error('LLM API返回错误 ' + resp.status + ': ' + (await resp.text()).slice(0, 300));
    const data = await resp.json();
    return { text: data.choices?.[0]?.message?.content || '' };
  }

  async function runSkill(args = {}, state = {}) {
    const skillName = args.skill || args.name;
    if (!skillName) throw new Error('skill.run 缺少 skill 名称');
    const stored = await storageGet(['customSkills', 'llmSettings']);
    let content = stored.customSkills?.[skillName]?.content || '';
    if (!content) {
      const url = chrome.runtime.getURL(`skills/${skillName}.md`);
      const resp = await fetch(url);
      if (resp.ok) content = await resp.text();
    }
    if (!content) throw new Error('未找到 Skill: ' + skillName);
    const { apiKey, apiUrl, model } = readLLMSettings(stored);
    if (!apiKey) throw new Error('LLM API未配置，请先在设置里保存 API Key');
    const contextText = JSON.stringify({
      pageContext: state.context || null,
      last: state.last || null,
      results: state.results || []
    }, null, 2).slice(0, 12000);
    const resp = await fetch(apiUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + apiKey
      },
      body: JSON.stringify({
        model,
        messages: [
          { role: 'system', content: '你是「詹老师的AIPROS」自动化执行器。请按Skill规范执行任务，只输出最终结果。无人值守自动化场景下，如果信息不足，不要停下来追问；基于已有信息完成，并把缺口标为“待补充”。' },
          { role: 'user', content: `【Skill】\n${content}\n\n【自动化上下文】\n${contextText}\n\n【用户补充】\n${args.instruction || args.prompt || '请执行。'}` }
        ],
        temperature: args.temperature ?? 0.3,
        max_tokens: args.max_tokens || 4096
      })
    });
    if (!resp.ok) throw new Error('LLM API返回错误 ' + resp.status + ': ' + (await resp.text()).slice(0, 300));
    const data = await resp.json();
    return { skill: skillName, text: data.choices?.[0]?.message?.content || '' };
  }

  async function executeStep(step, state) {
    const tool = stepTool(step);
    const args = resolveTemplates(step.args || step.params || {}, state);
    switch (tool) {
      case 'browser.open': return await globalThis.AiscCdp.open(args);
      case 'browser.navigate': return await globalThis.AiscCdp.navigate(args);
      case 'browser.click': return await globalThis.AiscCdp.click(args);
      case 'browser.type':
      case 'browser.fill': return await globalThis.AiscCdp.type(args);
      case 'browser.press': return await globalThis.AiscCdp.press(args);
      case 'browser.waitFor': return await globalThis.AiscCdp.waitFor(args);
      case 'browser.getText':
      case 'page.readText': return await globalThis.AiscCdp.getText(args);
      case 'browser.screenshot': return await globalThis.AiscCdp.screenshot(args);
      case 'browser.pdf': return await globalThis.AiscCdp.pdf(args);
      case 'browser.network.start': return await globalThis.AiscCdp.networkStart(args);
      case 'browser.network.get': return await globalThis.AiscCdp.networkGet(args);
      case 'browser.network.clear': return await globalThis.AiscCdp.networkClear(args);
      case 'browser.network.stop': return await globalThis.AiscCdp.networkStop(args);
      case 'browser.acceptance.check': return await globalThis.AiscCdp.acceptanceCheck(args);
      case 'browser.close': return await globalThis.AiscCdp.close(args);
      case 'page.read': {
        if (typeof globalThis.safeExtractActivePage !== 'function') throw new Error('页面读取器未就绪');
        return await globalThis.safeExtractActivePage();
      }
      case 'web.fetch': {
        if (typeof globalThis.fetchWebPageContext !== 'function') throw new Error('网页解析器未就绪');
        return await globalThis.fetchWebPageContext(args.url);
      }
      case 'web.search': {
        if (typeof globalThis.webSearch !== 'function') throw new Error('联网搜索器未就绪');
        const query = args.query || args.q;
        if (!query) throw new Error('web.search 缺少 query');
        const maxResults = Math.max(1, Math.min(10, Number(args.max_results || args.maxResults || 5)));
        return { query, results: await globalThis.webSearch(query, maxResults) };
      }
      case 'assistant.run':
      case 'ai.run': return await runAssistant(args, state);
      case 'skill.run': return await runSkill(args, state);
      case 'download.text':
      case 'download.html': return await downloadText(args);
      case 'notify':
      case 'notify.send': return await notify(args);
      case 'fs.write':
      case 'fs.read':
      case 'fs.list':
      case 'shell.exec':
      case 'clipboard.read':
      case 'clipboard.write':
      case 'screen.capture':
      case 'window.activate':
        if (!globalThis.AiscNative?.call) throw new Error('Native Agent 通道未就绪');
        {
          const nativeResult = await globalThis.AiscNative.call(tool, args);
          if (nativeResult?.ok === false) throw new Error(nativeResult.error || 'Native Agent 调用失败');
          return nativeResult;
        }
      default:
        throw new Error('未知自动化步骤工具: ' + tool);
    }
  }

  async function runTask(input, trigger = 'manual') {
    const tasks = await listTasks();
    const task = typeof input === 'string'
      ? tasks.find(t => t.id === input)
      : normalizeTask(input);
    if (!task) throw new Error('任务不存在');
    if (trigger !== 'manual' && task.enabled === false) return { skipped: true, reason: 'disabled' };

    const state = { task, trigger, now: makeRunNow(), results: [], last: null, context: null };
    await appendLog({ taskId: task.id, taskName: task.name, status: 'running', trigger, message: '开始执行' });
    try {
      for (let i = 0; i < task.steps.length; i++) {
        const step = task.steps[i];
        const result = await executeStep(step, state);
        state.last = result;
        state.results.push({ index: i + 1, tool: stepTool(step), result });
        if (stepTool(step) === 'page.read' || stepTool(step) === 'web.fetch') state.context = result;
      }
      const persisted = tasks.find(t => t.id === task.id);
      if (persisted) {
        persisted.lastRunAt = new Date().toISOString();
        persisted.lastStatus = 'success';
        persisted.lastError = '';
        persisted.runCount = Number(persisted.runCount || 0) + 1;
        persisted.updatedAt = new Date().toISOString();
        await saveTasks(tasks);
      }
      await appendLog({
        taskId: task.id,
        taskName: task.name,
        status: 'success',
        trigger,
        message: '执行完成',
        details: state.results
      });
      return { ok: true, taskId: task.id, results: state.results };
    } catch (e) {
      const persisted = tasks.find(t => t.id === task.id);
      if (persisted) {
        persisted.lastRunAt = new Date().toISOString();
        persisted.lastStatus = 'error';
        persisted.lastError = e.message || String(e);
        persisted.runCount = Number(persisted.runCount || 0) + 1;
        persisted.updatedAt = new Date().toISOString();
        await saveTasks(tasks);
      }
      await appendLog({
        taskId: task.id,
        taskName: task.name,
        status: 'error',
        trigger,
        message: e.message || String(e),
        details: state.results
      });
      throw e;
    }
  }

  async function processRunQueue(args = {}) {
    if (queueProcessing && !args.force) return { processed: 0, busy: true };
    queueProcessing = true;
    let processed = 0;
    try {
      const now = new Date();
      const limit = Math.max(1, Math.min(10, Number(args.limit || 1)));
      const queue = await readQueue();
      const candidates = queue.filter(entry => {
        if (args.entryId && entry.id !== args.entryId) return false;
        return isRunnableQueueEntry(entry, now);
      }).slice(0, limit);

      for (const entry of candidates) {
        entry.status = 'running';
        entry.attempts = Number(entry.attempts || 0) + 1;
        entry.startedAt = entry.startedAt || new Date().toISOString();
        entry.updatedAt = new Date().toISOString();
        entry.leaseUntil = new Date(Date.now() + RUNNING_LEASE_MS).toISOString();
        await saveQueue(queue);

        try {
          const result = await runTask(entry.taskSnapshot || entry.taskId, entry.trigger || 'queue');
          entry.status = 'success';
          entry.result = result;
          entry.error = '';
          entry.finishedAt = new Date().toISOString();
          entry.updatedAt = entry.finishedAt;
          entry.leaseUntil = '';
          processed += 1;
        } catch (e) {
          entry.error = e.message || String(e);
          entry.updatedAt = new Date().toISOString();
          entry.leaseUntil = '';
          if (entry.attempts < entry.maxAttempts) {
            entry.status = 'queued';
            entry.nextRunAt = new Date(Date.now() + Math.min(300000, 15000 * entry.attempts)).toISOString();
          } else {
            entry.status = 'error';
            entry.finishedAt = entry.updatedAt;
          }
          processed += 1;
        }
        await saveQueue(queue);
      }
      return { processed, queue: await listQueue({ includeDone: true }) };
    } finally {
      queueProcessing = false;
    }
  }

  async function runCronDue() {
    const now = new Date();
    const key = minuteKey(now);
    const stored = await storageGet([CRON_KEYS_KEY]);
    const cronKeys = stored[CRON_KEYS_KEY] || {};
    const tasks = await listTasks();
    for (const task of tasks) {
      if (!task.enabled || task.trigger?.type !== 'cron') continue;
      if (!matchesCron(task.trigger.rule, now, task.trigger.timeZone || '')) continue;
      const taskMinuteKey = task.id + ':' + key;
      if (cronKeys[task.id] === taskMinuteKey) continue;
      cronKeys[task.id] = taskMinuteKey;
      await storageSet({ [CRON_KEYS_KEY]: cronKeys });
      enqueueRun(task.id, 'cron').catch(e => console.warn('[automation] cron enqueue failed:', e.message));
    }
  }

  async function runUrlMatches(tabId, tabUrl) {
    const tasks = await listTasks();
    for (const task of tasks) {
      if (!task.enabled || task.trigger?.type !== 'url') continue;
      if (!urlMatches(task.trigger.urlPattern || task.trigger.rule, tabUrl)) continue;
      enqueueRun(task.id, 'url:' + tabId).catch(e => console.warn('[automation] url enqueue failed:', e.message));
    }
  }

  async function syncTickAlarm() {
    if (!chrome.alarms?.create) return;
    const tasks = await listTasks().catch(() => []);
    const hasCron = tasks.some(t => t.enabled && t.trigger?.type === 'cron');
    if (hasCron) chrome.alarms.create(TICK_ALARM, { periodInMinutes: 1 });
    else chrome.alarms.clear(TICK_ALARM);
  }

  function dailyWechatArticleTask() {
    return {
      id: DAILY_WECHAT_TASK_ID,
      name: '每日公众号文章',
      enabled: true,
      instruction: '每天早上 8 点（北京时间）联网搜索企业AI、流程管理、Agent落地、AI产品和数字化转型相关公开信息，选择一个最适合当天写的选题，调用「公众号写作」Skill，生成完整公众号 HTML。',
      schedule: {
        preset: 'daily',
        time: '08:00',
        hour: 8,
        minute: 0,
        weekdays: [],
        monthDay: 1,
        intervalValue: 30,
        intervalUnit: 'minutes',
        timeZone: 'Asia/Shanghai'
      },
      trigger: {
        type: 'cron',
        rule: '0 8 * * *',
        urlPattern: '',
        timeZone: 'Asia/Shanghai',
        description: '每天 08:00（北京时间）自动运行'
      },
      steps: [
        {
          tool: 'web.search',
          args: {
            query: '企业AI Agent落地 流程管理 数字化转型 AI产品 最新案例 {{now.beijingDate}}',
            max_results: 8
          }
        },
        {
          tool: 'skill.run',
          args: {
            skill: '公众号写作',
            temperature: 0.55,
            max_tokens: 12000,
            instruction: '请基于上一步联网搜索结果，选择一个今天最值得写的企业AI/流程管理/Agent落地选题，写成一篇可直接发布的公众号文章。必须输出完整 HTML，包含移动端排版、一键复制、Canvas 转 PNG 头图/金句/信息框/流程图/步骤卡/CTA 和作者名片。不要追问，不要输出 Markdown。'
          }
        },
        {
          tool: 'download.html',
          args: {
            filename: '詹老师的AIPROS每日公众号文章/{{now.beijingDate}}_公众号文章.html',
            mime: 'text/html;charset=utf-8',
            content: '{{last.text}}'
          }
        },
        {
          tool: 'notify',
          args: {
            title: '每日公众号文章已生成',
            message: '{{task.name}} 已完成，HTML 已下载。'
          }
        }
      ]
    };
  }

  async function ensureBuiltinAutomationTasks() {
    const stored = await storageGet([TASKS_KEY, BUILTIN_SEEDED_KEY]);
    const tasks = Array.isArray(stored[TASKS_KEY]) ? stored[TASKS_KEY].map(normalizeTask) : [];
    if (stored[BUILTIN_SEEDED_KEY] && tasks.some(t => t.id === DAILY_WECHAT_TASK_ID)) return;
    if (!tasks.some(t => t.id === DAILY_WECHAT_TASK_ID)) {
      tasks.unshift(normalizeTask(dailyWechatArticleTask()));
      await saveTasks(tasks);
    }
    await storageSet({ [BUILTIN_SEEDED_KEY]: true });
    await syncTickAlarm();
  }

  async function handleMessage(message) {
    switch (message.type) {
      case 'AUTOMATION_LIST':
        return { tasks: await listTasks(), logs: await listLogs() };
      case 'AUTOMATION_SAVE':
        return { task: await saveTask(message.task || {}) };
      case 'AUTOMATION_DELETE':
        return await deleteTask(message.id);
      case 'AUTOMATION_TOGGLE':
        return { task: await toggleTask(message.id, message.enabled) };
      case 'AUTOMATION_RUN': {
        const queued = await enqueueRun(message.id, 'manual', { process: false });
        const queueResult = await processRunQueue({ entryId: queued.id });
        return { queued, queueResult };
      }
      case 'AUTOMATION_RUN_DRAFT': {
        const queued = await enqueueRun(message.task || {}, 'manual', { process: false });
        const queueResult = await processRunQueue({ entryId: queued.id });
        return { queued, queueResult };
      }
      case 'AUTOMATION_QUEUE':
        return { queue: await listQueue({ includeDone: message.includeDone !== false }) };
      case 'AUTOMATION_QUEUE_ENQUEUE':
        return { queued: await enqueueRun(message.id || message.task || {}, message.trigger || 'manual', { process: message.process !== false }) };
      case 'AUTOMATION_QUEUE_PROCESS':
        return await processRunQueue({ limit: message.limit, force: message.force });
      case 'AUTOMATION_QUEUE_RETRY':
        return { queued: await retryQueuedRun(message.id) };
      case 'AUTOMATION_QUEUE_CANCEL':
        return { queued: await cancelQueuedRun(message.id) };
      case 'AUTOMATION_LOGS':
        return { logs: await listLogs() };
      case 'AUTOMATION_NOTIFY':
        return await notify(message.args || message);
      default:
        throw new Error('未知自动化消息: ' + message.type);
    }
  }

  function init() {
    chrome.alarms?.onAlarm?.addListener((alarm) => {
      if (alarm.name === TICK_ALARM) runCronDue().catch(e => console.warn('[automation] tick failed:', e.message));
    });
    chrome.tabs?.onUpdated?.addListener((tabId, changeInfo, tab) => {
      if (changeInfo.status !== 'complete') return;
      const url = tab?.url || changeInfo.url || '';
      if (!url) return;
      runUrlMatches(tabId, url).catch(e => console.warn('[automation] url trigger failed:', e.message));
    });
    chrome.runtime?.onMessage?.addListener((message, sender, sendResponse) => {
      if (!message?.type || !String(message.type).startsWith('AUTOMATION_')) return false;
      (async () => {
        try {
          const data = await handleMessage(message);
          sendResponse({ ok: true, data });
        } catch (e) {
          sendResponse({ ok: false, error: e.message || String(e) });
        }
      })();
      return true;
    });
    syncTickAlarm().catch(() => {});
    ensureBuiltinAutomationTasks().catch(e => console.warn('[automation] seed builtin tasks failed:', e.message));
    processRunQueue().catch(e => console.warn('[automation] resume queue failed:', e.message));
  }

  globalThis.AiscAutomation = {
    init,
    listTasks,
    saveTask,
    deleteTask,
    toggleTask,
    runTask,
    listLogs,
    listQueue,
    enqueueRun,
    processRunQueue,
    retryQueuedRun,
    cancelQueuedRun,
    handleMessage,
    matchesCron
  };

  init();
})();
