/**
 * agent_loop.js
 * Lightweight Agent execution loop: plan, step status, tool logs, retry,
 * checkpoints, and permission confirmation.
 */
(function () {
  'use strict';

  const MAX_LOGS = 80;
  const MAX_TOOL_LOGS = 80;
  const MAX_AUDIT_RUNS = 60;
  const LONG_TASK_THRESHOLD_MS = 8000;
  const HEARTBEAT_INTERVAL_MS = 2000;
  const STALE_AFTER_MS = 30000;
  const AUDIT_KEY = 'agentAuditRuns';
  const ACTIVE_RUN_KEY = 'agentLoopActiveRun';
  const MUTATING_TOOLS = new Set([
    'fs.write',
    'fs.copy',
    'fs.move',
    'fs.delete',
    'skill.write',
    'project.generate',
    'cloudflare.pages.deploy',
    'shell.exec',
    'clipboard.write',
    'checkpoint.restore'
  ]);
  const DANGEROUS_BROWSER_TEXT = /提交|确认|删除|同意|通过|批准|驳回|支付|发布|send|submit|delete|approve|confirm/i;
  const DEFAULT_POLICY = {
    lowRisk: 'auto',
    mediumRisk: 'confirm',
    highRisk: 'confirm',
    checkpoint: 'auto',
    retryCount: 1,
    trustedPaths: [],
    toolRules: [],
    pathRules: []
  };

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

  function parseTime(value) {
    const time = Date.parse(value || '');
    return Number.isFinite(time) ? time : Date.now();
  }

  function formatElapsed(ms) {
    const totalSeconds = Math.max(0, Math.floor(ms / 1000));
    const seconds = totalSeconds % 60;
    const minutesTotal = Math.floor(totalSeconds / 60);
    const minutes = minutesTotal % 60;
    const hours = Math.floor(minutesTotal / 60);
    if (hours > 0) return `${hours}h ${minutes}m`;
    if (minutes > 0) return `${minutes}m ${seconds}s`;
    return `${seconds}s`;
  }

  function storageGet(keys) {
    return new Promise((resolve) => {
      if (typeof chrome === 'undefined' || !chrome.storage?.local) return resolve({});
      chrome.storage.local.get(keys, value => resolve(value || {}));
    });
  }

  function storageSet(value) {
    return new Promise((resolve) => {
      if (typeof chrome === 'undefined' || !chrome.storage?.local) return resolve();
      chrome.storage.local.set(value, () => resolve());
    });
  }

  function storageRemove(keys) {
    return new Promise((resolve) => {
      if (typeof chrome === 'undefined' || !chrome.storage?.local?.remove) return resolve();
      chrome.storage.local.remove(keys, () => resolve());
    });
  }

  function id(prefix) {
    return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
  }

  function escapeHtml(value) {
    return String(value ?? '')
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;');
  }

  function clip(value, max = 1200) {
    const text = typeof value === 'string' ? value : JSON.stringify(value ?? '', null, 2);
    if (text.length <= max) return text;
    return text.slice(0, max) + '\n...[truncated]';
  }

  function redact(value, depth = 0) {
    if (depth > 4) return '[depth-limit]';
    if (value == null) return value;
    if (typeof value === 'string') return value.length > 1000 ? value.slice(0, 1000) + '\n...[truncated]' : value;
    if (typeof value !== 'object') return value;
    if (Array.isArray(value)) return value.slice(0, 20).map(item => redact(item, depth + 1));
    const out = {};
    for (const [key, val] of Object.entries(value)) {
      if (/api[-_]?key|authorization|cookie|token|secret|password/i.test(key)) {
        out[key] = '[redacted]';
      } else if (/content|body|responseBody|postData/i.test(key) && typeof val === 'string') {
        out[key] = val.length > 1200 ? val.slice(0, 1200) + '\n...[truncated]' : val;
      } else {
        out[key] = redact(val, depth + 1);
      }
    }
    return out;
  }

  function isFailure(result) {
    if (!result) return false;
    if (result.error || result.permissionDenied) return true;
    if (result.ok === false) return true;
    if (result.result && result.result.ok === false) return true;
    return false;
  }

  function isRetryableError(errorOrResult) {
    const text = JSON.stringify(errorOrResult || '').toLowerCase();
    return /timeout|timed out|network|failed to fetch|connection|rate limit|429|500|502|503|504|temporar/.test(text);
  }

  function normalizeRules(rawRules = []) {
    const allowed = ['auto', 'confirm', 'block'];
    const list = Array.isArray(rawRules)
      ? rawRules
      : String(rawRules || '').split('\n').map(line => {
        const match = line.match(/^\s*([^:=]+?)\s*[:=]\s*(auto|confirm|block)\s*$/i);
        return match ? { pattern: match[1], action: match[2].toLowerCase() } : null;
      }).filter(Boolean);
    return list.map(rule => ({
      pattern: String(rule.pattern || rule.tool || rule.path || '').trim(),
      action: String(rule.action || '').trim().toLowerCase(),
      reason: String(rule.reason || '').trim()
    })).filter(rule => rule.pattern && allowed.includes(rule.action));
  }

  function wildcardMatch(pattern, value) {
    const raw = String(pattern || '').trim();
    const text = String(value || '');
    if (!raw) return false;
    const escaped = raw.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
    return new RegExp(`^${escaped}$`).test(text);
  }

  function normalizePolicy(raw = {}) {
    const select = (value, allowed, fallback) => allowed.includes(value) ? value : fallback;
    const trustedPaths = Array.isArray(raw.trustedPaths)
      ? raw.trustedPaths.map(x => String(x || '').trim()).filter(Boolean)
      : String(raw.trustedPaths || '').split('\n').map(x => x.trim()).filter(Boolean);
    return {
      lowRisk: select(raw.lowRisk, ['auto', 'confirm', 'block'], DEFAULT_POLICY.lowRisk),
      mediumRisk: select(raw.mediumRisk, ['auto', 'confirm', 'block'], DEFAULT_POLICY.mediumRisk),
      highRisk: select(raw.highRisk, ['confirm', 'block'], DEFAULT_POLICY.highRisk),
      checkpoint: select(raw.checkpoint, ['auto', 'ask', 'off'], DEFAULT_POLICY.checkpoint),
      retryCount: Math.max(0, Math.min(5, Number(raw.retryCount ?? DEFAULT_POLICY.retryCount) || 0)),
      trustedPaths,
      toolRules: normalizeRules(raw.toolRules),
      pathRules: normalizeRules(raw.pathRules)
    };
  }

  function inferObjectiveSteps(objective, skillName) {
    const text = String(objective || '').toLowerCase();
    const steps = [
      { id: 'understand', title: '理解任务与上下文', status: 'completed' },
      { id: 'plan', title: '制定执行计划', status: 'in_progress' }
    ];
    if (/文件|目录|名单|fs\.|本地|skill|项目|前端|html|写入|删除|移动|复制/.test(text)) {
      steps.push({ id: 'permission', title: '确认本地权限与回滚点', status: 'pending' });
    }
    if (/页面|浏览器|点击|填写|抓接口|xhr|fetch|network|接口/.test(text)) {
      steps.push({ id: 'browser', title: '执行浏览器与接口操作', status: 'pending' });
    }
    steps.push(
      { id: 'tools', title: skillName ? `执行 Skill：${skillName}` : '调用必要工具', status: 'pending' },
      { id: 'verify', title: '检查结果与错误', status: 'pending' },
      { id: 'final', title: '汇总交付结果', status: 'pending' }
    );
    return steps;
  }

  class AgentLoopRuntime {
    constructor() {
      this.current = null;
      this.collapsed = true;
      this.policy = { ...DEFAULT_POLICY };
      this.policyLoaded = false;
      this.auditTrail = [];
      this.auditLoaded = false;
      this.elapsedTimer = null;
      this.persistTimer = null;
      this.lastHeartbeatWriteAt = 0;
      this.dismissed = false;
      this._bindDom();
    }

    _bindDom() {
      if (typeof document === 'undefined') return;
      document.addEventListener('DOMContentLoaded', () => {
        const toggle = document.getElementById('agentLoopToggle');
        if (toggle) {
          toggle.addEventListener('click', () => {
            this.collapsed = !this.collapsed;
            this.render();
          });
        }
        const clear = document.getElementById('agentLoopClear');
        if (clear) {
          clear.addEventListener('click', () => this.dismiss());
        }
        this.restoreActiveRun();
      });
    }

    async restoreActiveRun() {
      if (this.current) return null;
      const stored = await storageGet([ACTIVE_RUN_KEY]);
      const saved = stored[ACTIVE_RUN_KEY];
      if (!saved?.runId) return null;
      const run = JSON.parse(JSON.stringify(saved));
      if (run.status === 'running') {
        const heartbeatAt = parseTime(run.heartbeatAt || run.updatedAt || run.startedAt);
        if (Date.now() - heartbeatAt > STALE_AFTER_MS) {
          run.status = 'stale';
          run.staleAt = nowIso();
          run.updatedAt = run.updatedAt || run.heartbeatAt || run.startedAt;
          run.logs = Array.isArray(run.logs) ? run.logs : [];
          run.logs.push({
            id: id('log'),
            at: nowIso(),
            level: 'warn',
            message: '执行连接已中断，可基于审计与上下文续接',
            data: { heartbeatAt: run.heartbeatAt || '' }
          });
        }
      }
      this.current = run;
      this.render();
      this.persistCurrent(true);
      return this.snapshot();
    }

    persistCurrent(immediate = false) {
      if (typeof chrome === 'undefined' || !chrome.storage?.local) return;
      const write = () => {
        this.persistTimer = null;
        if (!this.current) {
          storageRemove([ACTIVE_RUN_KEY]);
          return;
        }
        storageSet({ [ACTIVE_RUN_KEY]: this.snapshot() });
      };
      if (immediate) {
        if (this.persistTimer) {
          clearTimeout(this.persistTimer);
          this.persistTimer = null;
        }
        write();
        return;
      }
      if (!this.persistTimer) this.persistTimer = setTimeout(write, 250);
    }

    async loadPolicy(force = false) {
      if (this.policyLoaded && !force) return this.policy;
      if (typeof chrome === 'undefined' || !chrome.storage?.local) {
        this.policyLoaded = true;
        return this.policy;
      }
      try {
        const stored = await chrome.storage.local.get(['agentPolicySettings']);
        this.policy = normalizePolicy(stored.agentPolicySettings || {});
      } catch (_) {
        this.policy = { ...DEFAULT_POLICY };
      }
      this.policyLoaded = true;
      return this.policy;
    }

    async savePolicy(policy = {}) {
      this.policy = normalizePolicy(policy);
      this.policyLoaded = true;
      if (typeof chrome !== 'undefined' && chrome.storage?.local) {
        await chrome.storage.local.set({ agentPolicySettings: this.policy });
      }
      if (this.current) this.log('info', '权限策略已更新', this.policy);
      this.render();
      return { ok: true, policy: this.policy };
    }

    ensureRun(meta = {}) {
      if (this.dismissed) return null;
      if (!this.current) return this.startRun(meta);
      return this.current;
    }

    startRun(meta = {}) {
      this.dismissed = false;
      const title = meta.title || meta.skillName || 'Agent 执行任务';
      this.current = {
        runId: id('run'),
        title,
        source: meta.source || 'chat',
        objective: meta.objective || meta.userMessage || '',
        skillName: meta.skillName || '',
        status: 'running',
        startedAt: nowIso(),
        updatedAt: nowIso(),
        heartbeatAt: nowIso(),
        steps: (meta.steps && meta.steps.length ? meta.steps : inferObjectiveSteps(meta.objective || meta.userMessage || '', meta.skillName))
          .map((step, index) => ({
            id: step.id || `step_${index + 1}`,
            title: step.title || step.name || `步骤 ${index + 1}`,
            status: step.status || 'pending',
            message: step.message || '',
            updatedAt: nowIso()
          })),
        logs: [],
        toolRuns: [],
        checkpoints: [],
        permissions: []
      };
      this.log('info', '执行任务已创建', { title, source: this.current.source });
      this.render();
      this.persistCurrent(true);
      return this.snapshot();
    }

    reset() {
      this.dismissed = false;
      this.current = null;
      this.stopElapsedTicker();
      this.persistCurrent(true);
      this.render();
    }

    dismiss() {
      this.dismissed = true;
      this.current = null;
      this.stopElapsedTicker();
      this.persistCurrent(true);
      this.render();
    }

    startElapsedTicker() {
      if (this.elapsedTimer || typeof setInterval !== 'function') return;
      this.elapsedTimer = setInterval(() => {
        if (this.current?.status === 'running') {
          const now = Date.now();
          if (now - this.lastHeartbeatWriteAt >= HEARTBEAT_INTERVAL_MS) {
            this.current.heartbeatAt = nowIso();
            this.lastHeartbeatWriteAt = now;
            this.persistCurrent(true);
          }
        }
        this.render();
      }, 1000);
    }

    stopElapsedTicker() {
      if (!this.elapsedTimer || typeof clearInterval !== 'function') return;
      clearInterval(this.elapsedTimer);
      this.elapsedTimer = null;
    }

    setCollapsed(collapsed = true) {
      this.collapsed = !!collapsed;
      this.render();
      return { ok: true, collapsed: this.collapsed };
    }

    snapshot() {
      return this.current ? JSON.parse(JSON.stringify(this.current)) : null;
    }

    async loadAudit(force = false) {
      if (this.auditLoaded && !force) return this.auditTrail;
      if (typeof chrome === 'undefined' || !chrome.storage?.local) {
        this.auditLoaded = true;
        return this.auditTrail;
      }
      try {
        const stored = await chrome.storage.local.get([AUDIT_KEY]);
        this.auditTrail = Array.isArray(stored[AUDIT_KEY]) ? stored[AUDIT_KEY] : [];
      } catch (_) {
        this.auditTrail = [];
      }
      this.auditLoaded = true;
      return this.auditTrail;
    }

    persistAudit() {
      if (typeof chrome === 'undefined' || !chrome.storage?.local) return;
      chrome.storage.local.set({ [AUDIT_KEY]: this.auditTrail.slice(0, MAX_AUDIT_RUNS) }, () => {
        void chrome.runtime?.lastError;
      });
    }

    recordAuditSnapshot() {
      if (!this.current) return null;
      const snapshot = this.snapshot();
      const entry = {
        runId: snapshot.runId,
        title: snapshot.title,
        objective: snapshot.objective,
        skillName: snapshot.skillName,
        status: snapshot.status,
        source: snapshot.source,
        startedAt: snapshot.startedAt,
        updatedAt: snapshot.updatedAt,
        toolCount: snapshot.toolRuns.length,
        failedToolCount: snapshot.toolRuns.filter(item => item.status === 'failed').length,
        permissionCount: snapshot.permissions.length,
        checkpointCount: snapshot.checkpoints.length,
        snapshot
      };
      const idx = this.auditTrail.findIndex(item => item.runId === entry.runId);
      if (idx >= 0) this.auditTrail.splice(idx, 1);
      this.auditTrail.unshift(entry);
      if (this.auditTrail.length > MAX_AUDIT_RUNS) this.auditTrail.length = MAX_AUDIT_RUNS;
      this.auditLoaded = true;
      this.persistAudit();
      return entry;
    }

    async listAudit(args = {}) {
      await this.loadAudit();
      const limit = Math.max(1, Math.min(100, Number(args.limit || 20)));
      return this.auditTrail.slice(0, limit).map(entry => ({
        runId: entry.runId,
        title: entry.title,
        objective: entry.objective,
        skillName: entry.skillName,
        status: entry.status,
        source: entry.source,
        startedAt: entry.startedAt,
        updatedAt: entry.updatedAt,
        toolCount: entry.toolCount || 0,
        failedToolCount: entry.failedToolCount || 0,
        permissionCount: entry.permissionCount || 0,
        checkpointCount: entry.checkpointCount || 0
      }));
    }

    async replayAudit(runIdOrArgs = {}) {
      const runId = typeof runIdOrArgs === 'string' ? runIdOrArgs : (runIdOrArgs.runId || runIdOrArgs.id);
      if (!runId) return { ok: false, error: '缺少 runId' };
      await this.loadAudit();
      const entry = this.auditTrail.find(item => item.runId === runId);
      if (!entry?.snapshot) return { ok: false, error: '未找到审计记录' };
      this.current = JSON.parse(JSON.stringify(entry.snapshot));
      this.current.status = 'replayed';
      this.current.updatedAt = nowIso();
      this.log('info', '已回放历史审计记录', { runId });
      this.render();
      return { ok: true, run: this.snapshot() };
    }

    setPlan(args = {}) {
      const run = this.ensureRun({ title: args.title || 'Agent 执行任务', objective: args.objective || '' });
      if (!run) return { ok: false, error: '监控面板已隐藏' };
      if (args.title) run.title = String(args.title);
      if (args.objective) run.objective = String(args.objective);
      if (Array.isArray(args.steps) && args.steps.length) {
        run.steps = args.steps.map((step, index) => ({
          id: step.id || `step_${index + 1}`,
          title: step.title || step.name || `步骤 ${index + 1}`,
          status: step.status || 'pending',
          message: step.message || '',
          updatedAt: nowIso()
        }));
      }
      this.log('info', '计划已更新', { steps: run.steps.length });
      this.render();
      this.persistCurrent();
      return { ok: true, plan: this.snapshot() };
    }

    setStep(args = {}) {
      const run = this.ensureRun({});
      if (!run) return { ok: false, error: '监控面板已隐藏' };
      const stepId = args.stepId || args.id;
      let step = stepId ? run.steps.find(item => item.id === stepId) : null;
      if (!step && stepId) {
        step = { id: stepId, title: args.title || String(stepId), status: 'pending', message: '', updatedAt: nowIso() };
        run.steps.push(step);
      }
      if (!step && args.title) {
        step = { id: stepId || id('step'), title: String(args.title), status: 'pending', message: '', updatedAt: nowIso() };
        run.steps.push(step);
      }
      if (!step) return { ok: false, error: '未找到步骤' };
      step.status = args.status || step.status;
      step.message = args.message || step.message;
      step.updatedAt = nowIso();
      run.updatedAt = nowIso();
      this.render();
      this.persistCurrent();
      return { ok: true, step };
    }

    finishRun(status = 'completed', message = '') {
      if (!this.current) return { ok: true, status: 'idle' };
      this.current.status = status;
      this.current.updatedAt = nowIso();
      this.current.heartbeatAt = this.current.heartbeatAt || this.current.updatedAt;
      if (status === 'completed') {
        for (const step of this.current.steps) {
          if (step.status === 'pending' || step.status === 'in_progress') {
            step.status = 'completed';
            step.updatedAt = nowIso();
          }
        }
      }
      this.log(status === 'failed' ? 'error' : 'info', message || `执行状态：${status}`);
      this.recordAuditSnapshot();
      this.render();
      this.persistCurrent(true);
      return { ok: true, status };
    }

    log(level, message, data = {}) {
      const run = this.ensureRun({});
      if (!run) return { ok: false, error: '监控面板已隐藏' };
      run.logs.push({
        id: id('log'),
        at: nowIso(),
        level,
        message,
        data: redact(data)
      });
      if (run.logs.length > MAX_LOGS) run.logs.splice(0, run.logs.length - MAX_LOGS);
      run.updatedAt = nowIso();
      this.persistCurrent();
      return { ok: true };
    }

    assessRisk(tool, args = {}) {
      const risk = {
        level: 'low',
        requiresConfirm: false,
        blocked: false,
        needsCheckpoint: false,
        reason: '',
        tool,
        args: redact(args)
      };

      if (MUTATING_TOOLS.has(tool)) {
        risk.level = 'medium';
        risk.requiresConfirm = true;
        risk.reason = '该操作会修改文件、剪贴板、下载文件或执行命令';
      }
      if (tool === 'fs.delete' || tool === 'shell.exec' || tool === 'cloudflare.pages.deploy') {
        risk.level = 'high';
        risk.reason = tool === 'fs.delete'
          ? '该操作会删除本地文件或目录'
          : tool === 'cloudflare.pages.deploy'
            ? '该操作会把本地交付物发布到公网 Cloudflare Pages'
            : '该操作会在本地执行命令';
      }
      if (tool === 'checkpoint.restore') {
        risk.level = 'high';
        risk.reason = '该操作会恢复回滚点并覆盖或删除本地路径';
      }
      const isDryRunFileMutation = (tool === 'fs.copy' || tool === 'fs.move' || tool === 'fs.delete') && args.dryRun !== false;
      if (isDryRunFileMutation) {
        risk.requiresConfirm = false;
        risk.reason = '当前为预演模式，不会实际改动文件';
      }
      if (tool === 'browser.click' && (args.intent === 'approval-submit' || DANGEROUS_BROWSER_TEXT.test([args.text, ...(args.texts || [])].join(' ')))) {
        risk.level = 'high';
        risk.requiresConfirm = true;
        risk.reason = '该点击可能提交、批准或删除页面数据';
      }
      if (/^(fs\.|skill\.write|project\.generate|shell\.exec)/.test(tool)) {
        risk.needsCheckpoint = true;
      }
      if (isDryRunFileMutation) {
        risk.level = 'low';
        risk.requiresConfirm = false;
        risk.needsCheckpoint = false;
        risk.previewOnly = true;
      }
      return risk;
    }

    applyPolicy(risk, args = {}) {
      const policy = this.policy || DEFAULT_POLICY;
      if (risk.previewOnly) {
        risk.requiresConfirm = false;
        risk.needsCheckpoint = false;
        return risk;
      }
      const paths = this.inferCheckpointPaths(risk.tool, args);
      const toolRule = (policy.toolRules || []).find(rule => wildcardMatch(rule.pattern, risk.tool));
      const pathRule = (policy.pathRules || []).find(rule => paths.some(path => path.startsWith(rule.pattern) || wildcardMatch(rule.pattern, path)));
      const forcedAction = toolRule?.action || pathRule?.action || '';
      if (forcedAction) {
        const source = toolRule ? `工具规则 ${toolRule.pattern}` : `路径规则 ${pathRule.pattern}`;
        if (forcedAction === 'block') {
          risk.blocked = true;
          risk.requiresConfirm = false;
          risk.reason = toolRule?.reason || pathRule?.reason || `${source} 已阻止该操作`;
          return risk;
        }
        risk.requiresConfirm = forcedAction === 'confirm';
        risk.reason = toolRule?.reason || pathRule?.reason || `${source} 要求 ${forcedAction === 'confirm' ? '确认' : '自动执行'}`;
      }

      const mode = policy[`${risk.level}Risk`] || (risk.level === 'low' ? 'auto' : 'confirm');
      const trusted = Array.isArray(policy.trustedPaths) ? policy.trustedPaths : [];
      const inTrustedPath = paths.length > 0 && paths.every(path => trusted.some(prefix => prefix && path.startsWith(prefix)));

      if (!forcedAction && mode === 'block') {
        risk.blocked = true;
        risk.requiresConfirm = false;
        risk.reason = risk.reason || `策略阻止 ${risk.level} 风险操作`;
      } else if (!forcedAction && mode === 'confirm') {
        risk.requiresConfirm = true;
      } else if (!forcedAction && mode === 'auto') {
        risk.requiresConfirm = false;
      }

      if (inTrustedPath && risk.level !== 'high' && mode !== 'block') {
        risk.requiresConfirm = false;
        risk.reason = risk.reason ? `${risk.reason}；路径位于信任目录` : '路径位于信任目录';
      }

      if (policy.checkpoint === 'off') risk.needsCheckpoint = false;
      if (policy.checkpoint === 'ask') risk.requiresConfirm = true;
      return risk;
    }

    async requestPermission(risk) {
      const run = this.ensureRun({});
      const record = {
        id: id('perm'),
        at: nowIso(),
        tool: risk.tool,
        level: risk.level,
        reason: risk.reason,
        status: 'pending',
        args: risk.args
      };
      run.permissions.push(record);
      this.setStep({ id: 'permission', status: 'in_progress', message: `等待确认：${risk.tool}` });
      this.render();

      const summary = [
        `詹老师的AIPROS请求执行：${risk.tool}`,
        `风险级别：${risk.level}`,
        risk.reason ? `原因：${risk.reason}` : '',
        '',
        clip(risk.args, 800),
        '',
        '是否允许继续？'
      ].filter(Boolean).join('\n');
      let approved = true;
      if (typeof window !== 'undefined' && typeof window.confirm === 'function') {
        approved = window.confirm(summary);
      }
      record.status = approved ? 'approved' : 'denied';
      record.decidedAt = nowIso();
      this.setStep({ id: 'permission', status: approved ? 'completed' : 'failed', message: approved ? '权限已确认' : '用户已取消操作' });
      this.log(approved ? 'info' : 'warn', approved ? '权限确认通过' : '权限确认被取消', { tool: risk.tool });
      this.render();
      return approved;
    }

    inferCheckpointPaths(tool, args = {}) {
      const paths = [];
      const add = (value) => {
        if (typeof value === 'string' && value.startsWith('/')) paths.push(value);
      };
      if (tool === 'fs.write' || tool === 'fs.delete') add(args.path);
      if (tool === 'fs.copy' || tool === 'fs.move') {
        add(args.src || args.source);
        add(args.dst || args.target);
      }
      if (tool === 'skill.write') {
        add(args.path);
        add(args.skillsDir);
      }
      if (tool === 'project.generate') {
        add(args.targetDir || args.outputDir);
      }
      if (tool === 'shell.exec') add(args.cwd);
      return Array.from(new Set(paths));
    }

    async callNative(tool, args = {}) {
      if (typeof chrome === 'undefined' || !chrome.runtime?.sendMessage) {
        return { ok: false, error: '浏览器环境不可用' };
      }
      return await new Promise((resolve) => {
        chrome.runtime.sendMessage({ type: 'NATIVE_TOOL', tool, args }, (resp) => {
          if (chrome.runtime.lastError) {
            resolve({ ok: false, error: chrome.runtime.lastError.message });
            return;
          }
          if (resp?.ok && resp.data?.ok) resolve({ ok: true, result: resp.data.result || resp.data });
          else resolve({ ok: false, error: resp?.error || resp?.data?.error || '本地 Agent 不可用' });
        });
      });
    }

    async createCheckpoint(tool, args = {}) {
      const paths = this.inferCheckpointPaths(tool, args);
      if (!paths.length) return null;
      const resp = await this.callNative('checkpoint.create', {
        label: `${tool} 自动回滚点`,
        tool,
        paths,
        args: redact(args)
      });
      if (!resp.ok) {
        this.log('warn', '回滚点创建失败或已跳过', { tool, error: resp.error });
        return null;
      }
      const checkpoint = resp.result;
      this.current?.checkpoints.push({
        id: checkpoint.id,
        label: checkpoint.label,
        tool,
        paths,
        createdAt: checkpoint.createdAt
      });
      this.setStep({ id: 'permission', status: 'completed', message: `已创建回滚点：${checkpoint.id}` });
      this.log('info', '回滚点已创建', { checkpointId: checkpoint.id, paths });
      this.render();
      return checkpoint;
    }

    addToolStep(tool) {
      const run = this.ensureRun({});
      let step = run.steps.find(item => item.id === `tool:${tool}`);
      if (!step) {
        step = {
          id: `tool:${tool}`,
          title: `调用工具：${tool}`,
          status: 'pending',
          message: '',
          updatedAt: nowIso()
        };
        const finalIndex = run.steps.findIndex(item => item.id === 'verify');
        if (finalIndex >= 0) run.steps.splice(finalIndex, 0, step);
        else run.steps.push(step);
      }
      return step;
    }

    startTool(tool, args, meta = {}) {
      const run = this.ensureRun({});
      const step = this.addToolStep(tool);
      step.status = 'in_progress';
      step.message = `第 ${meta.attempt || 1} 次执行`;
      step.updatedAt = nowIso();
      const toolRun = {
        id: id('tool'),
        tool,
        args: redact(args),
        status: 'running',
        attempt: meta.attempt || 1,
        checkpointId: meta.checkpointId || '',
        startedAt: nowIso()
      };
      run.toolRuns.push(toolRun);
      if (run.toolRuns.length > MAX_TOOL_LOGS) run.toolRuns.splice(0, run.toolRuns.length - MAX_TOOL_LOGS);
      this.log('info', `调用工具：${tool}`, { attempt: meta.attempt || 1, args: redact(args) });
      this.render();
      return toolRun;
    }

    completeTool(toolRun, result) {
      toolRun.status = isFailure(result) ? 'failed' : 'completed';
      toolRun.result = redact(result);
      toolRun.finishedAt = nowIso();
      const step = this.current?.steps.find(item => item.id === `tool:${toolRun.tool}`);
      if (step) {
        step.status = toolRun.status;
        step.message = toolRun.status === 'completed' ? '执行完成' : '执行失败';
        step.updatedAt = nowIso();
      }
      this.log(toolRun.status === 'completed' ? 'info' : 'error', `${toolRun.tool} ${toolRun.status === 'completed' ? '完成' : '失败'}`, { result: redact(result) });
      this.render();
    }

    failTool(toolRun, error) {
      toolRun.status = 'failed';
      toolRun.error = String(error?.message || error || '工具执行失败');
      toolRun.finishedAt = nowIso();
      const step = this.current?.steps.find(item => item.id === `tool:${toolRun.tool}`);
      if (step) {
        step.status = 'failed';
        step.message = toolRun.error;
        step.updatedAt = nowIso();
      }
      this.log('error', `${toolRun.tool} 失败`, { error: toolRun.error });
      this.render();
    }

    async executeTool({ tool, args = {}, executor, retries = 1 }) {
      const run = this.ensureRun({ objective: `执行工具：${tool}` });
      await this.loadPolicy();
      this.setStep({ id: 'tools', status: 'in_progress', message: `准备调用 ${tool}` });
      const risk = this.applyPolicy(this.assessRisk(tool, args), args);
      if (risk.blocked) {
        this.log('error', '权限策略已阻止工具调用', { tool, level: risk.level, reason: risk.reason });
        this.setStep({ id: 'permission', status: 'failed', message: '权限策略阻止' });
        return { ok: false, error: risk.reason || '权限策略阻止了该操作', blockedByPolicy: true, tool };
      }

      if (risk.requiresConfirm) {
        const approved = await this.requestPermission(risk);
        if (!approved) {
          return { ok: false, error: '用户取消了权限确认', permissionDenied: true, tool };
        }
        if (tool === 'checkpoint.restore') args.confirm = true;
        if ((tool === 'fs.copy' || tool === 'fs.move' || tool === 'fs.delete') && args.dryRun === false) args.confirm = true;
      }

      let checkpoint = null;
      if (risk.needsCheckpoint) {
        checkpoint = await this.createCheckpoint(tool, args);
      }

      const policyRetries = Number(this.policy?.retryCount);
      const maxAttempts = Math.max(1, Number(Number.isFinite(policyRetries) ? policyRetries : retries || 1) + 1);
      for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
        const toolRun = this.startTool(tool, args, { attempt, checkpointId: checkpoint?.id });
        try {
          const result = await executor();
          if (isFailure(result) && attempt < maxAttempts && isRetryableError(result)) {
            toolRun.status = 'retrying';
            toolRun.result = redact(result);
            this.log('warn', `${tool} 失败，准备重试`, { attempt, result: redact(result) });
            this.render();
            await new Promise(resolve => setTimeout(resolve, 400 * attempt));
            continue;
          }
          this.completeTool(toolRun, result);
          this.setStep({ id: 'verify', status: isFailure(result) ? 'failed' : 'completed', message: isFailure(result) ? '工具返回错误' : '工具结果已记录' });
          return result;
        } catch (error) {
          if (attempt < maxAttempts && isRetryableError(error)) {
            toolRun.status = 'retrying';
            toolRun.error = String(error.message || error);
            this.log('warn', `${tool} 异常，准备重试`, { attempt, error: toolRun.error });
            this.render();
            await new Promise(resolve => setTimeout(resolve, 400 * attempt));
            continue;
          }
          this.failTool(toolRun, error);
          this.setStep({ id: 'verify', status: 'failed', message: String(error.message || error) });
          return { ok: false, error: String(error.message || error), tool };
        }
      }
      return { ok: false, error: '工具重试次数已用完', tool };
    }

    render() {
      if (typeof document === 'undefined') return;
      const panel = document.getElementById('agentLoopPanel');
      if (!panel) return;
      const run = this.current;
      if (!run) {
        panel.style.display = 'none';
        this.stopElapsedTicker();
        return;
      }
      panel.style.display = 'block';
      panel.classList.toggle('collapsed', this.collapsed);
      panel.classList.toggle('expanded', !this.collapsed);
      const title = document.getElementById('agentLoopTitle');
      const status = document.getElementById('agentLoopStatus');
      const elapsed = document.getElementById('agentLoopElapsed');
      const body = document.getElementById('agentLoopBody');
      const toggle = document.getElementById('agentLoopToggle');
      const stepsEl = document.getElementById('agentLoopSteps');
      const logsEl = document.getElementById('agentLoopLogs');
      const checkpointsEl = document.getElementById('agentLoopCheckpoints');
      if (title) title.textContent = run.title || 'Agent 执行任务';
      if (status) {
        const statusText = {
          running: '执行中',
          failed: '失败',
          completed: '完成',
          replayed: '回放',
          stale: '待续接'
        };
        status.textContent = statusText[run.status] || '完成';
        status.className = 'agent-loop-status ' + run.status;
      }
      if (elapsed) {
        const startAt = parseTime(run.startedAt);
        const endAt = run.status === 'running' ? Date.now() : parseTime(run.updatedAt || run.startedAt);
        const elapsedMs = Math.max(0, endAt - startAt);
        const showElapsed = elapsedMs >= LONG_TASK_THRESHOLD_MS;
        elapsed.textContent = `已处理 ${formatElapsed(elapsedMs)}`;
        elapsed.className = `agent-loop-elapsed ${showElapsed ? 'visible' : ''} ${escapeHtml(run.status)}`;
        elapsed.title = showElapsed ? `任务已处理 ${formatElapsed(elapsedMs)}` : '';
      }
      if (run.status === 'running') this.startElapsedTicker();
      else this.stopElapsedTicker();
      if (toggle) {
        toggle.title = this.collapsed ? '展开监控' : '收起监控';
        toggle.setAttribute('aria-label', this.collapsed ? '展开监控' : '收起监控');
        toggle.setAttribute('aria-expanded', String(!this.collapsed));
      }
      if (body) body.style.display = this.collapsed ? 'none' : 'block';
      if (stepsEl) {
        stepsEl.innerHTML = run.steps.map(step => `
          <div class="agent-step ${escapeHtml(step.status)}">
            <span class="agent-step-dot"></span>
            <span class="agent-step-title">${escapeHtml(step.title)}</span>
            ${step.message ? `<span class="agent-step-msg">${escapeHtml(step.message)}</span>` : ''}
          </div>
        `).join('');
      }
      if (logsEl) {
        const logs = run.logs.slice(-8).reverse();
        logsEl.innerHTML = logs.map(log => `
          <div class="agent-log ${escapeHtml(log.level)}">
            <span class="agent-log-time">${escapeHtml(log.at.slice(11, 19))}</span>
            <span class="agent-log-text">${escapeHtml(log.message)}</span>
          </div>
        `).join('');
      }
      if (checkpointsEl) {
        checkpointsEl.innerHTML = run.checkpoints.length
          ? run.checkpoints.slice(-4).reverse().map(cp => `
            <div class="agent-checkpoint">
              <span>${escapeHtml(cp.id)}</span>
              <span>${escapeHtml(cp.tool || '')}</span>
            </div>
          `).join('')
          : '<div class="agent-empty">暂无回滚点</div>';
      }
    }
  }

  const runtime = new AgentLoopRuntime();
  globalThis.AgentLoop = runtime;
  globalThis.AiscAgentLoop = runtime;
})();
