/**
 * browser-tool.js — 浏览器控制工具
 *
 * 单一 tool，通过 action 字段选择子命令。
 * 感知主要基于 AXTree snapshot（文本，便宜），截图为辅助。
 *
 * 每个动作的 details 都包含 { running, url, thumbnail? } 状态字段，
 * 供 chat.js 拦截后推送 browser_status WS 事件给前端。
 *
 * 操作：
 * - start    启动浏览器
 * - stop     关闭浏览器
 * - navigate 导航到 URL
 * - snapshot  获取当前页面的无障碍树
 * - screenshot 截取当前页面截图
 * - click    点击元素（by ref）
 * - type     输入文本
 * - scroll   滚动页面
 * - select   选择下拉选项
 * - key      按键
 * - wait     等待页面加载
 * - evaluate 执行页面 JavaScript
 * - show     将浏览器窗口置前
 */

import { Type, StringEnum } from "../pi-sdk/index.ts";
import { BrowserManager } from "../browser/browser-manager.ts";
import { t } from "../i18n.ts";
import { toolOk } from "./tool-result.ts";
import { getToolSessionPath } from "./tool-session.ts";
import {
  browserScreenshotMediaItem,
  persistBrowserScreenshotFile,
} from "../session-files/browser-screenshot-file.ts";
import { redactLogText } from "../log-redactor.ts";
import { summarizeBrowserActionParams } from "./browser-action-log.ts";
import { modelSupportsDirectImageInput } from "../../shared/model-capabilities.ts";

const BROWSER_ACTIONS = [
  "start", "stop", "navigate", "snapshot", "screenshot", "click", "type",
  "scroll", "select", "key", "wait", "evaluate", "show",
];

/** Browser 专用错误：content 显示格式化文本，details.error 保留原始消息 */
function browserError(rawMsg: any, details: Record<string, any> = {}) {
  return {
    content: [{ type: "text", text: t("error.browserError", { msg: rawMsg }) }],
    details: { ...details, error: rawMsg },
  };
}

/**
 * 创建浏览器工具
 * @param {(() => string|null)|undefined} getSessionPath - 返回当前 sessionPath 的回调
 * @param {object} [options]
 * @param {(sessionPath:string|null) => object|null} [options.getSessionModel] - 返回执行 session 的模型对象
 * @param {() => { prepare?: Function }|null} [options.getVisionBridge] - 视觉辅助桥
 * @param {() => boolean} [options.isVisionAuxiliaryEnabled] - 视觉辅助总开关
 * @param {() => string|null} [options.getHanakoHome] - 返回 HANA_HOME
 * @param {(entry: object) => object} [options.registerSessionFile] - 注册 session 文件
 * @param {boolean} [options.screenshotEnabled] - false 时从 schema 屏蔽 screenshot
 * @returns {import('../pi-sdk/index.ts').ToolDefinition}
 */
export function createBrowserTool(getSessionPath: any, options: {
  screenshotEnabled?: boolean;
  getSessionModel?: (sessionPath: string | null) => any;
  getVisionBridge?: () => any;
  isVisionAuxiliaryEnabled?: () => boolean;
  getHanakoHome?: () => string | null;
  registerSessionFile?: (entry: any) => any;
} = {}) {
  const browser = BrowserManager.instance();
  const screenshotEnabled = options.screenshotEnabled !== false;
  const actionValues = screenshotEnabled
    ? BROWSER_ACTIONS
    : BROWSER_ACTIONS.filter((action) => action !== "screenshot");

  /** 操作日志 per-session（每次 start 时清空，记录所有操作供回看纠错） */
  const _actionLogs = new Map(); // sessionPath → action[]
  const ACTION_LOG_MAX_SESSIONS = 20;  // 最多保留 20 个 session 的日志
  const ACTION_LOG_MAX_PER_SESSION = 200; // 每个 session 最多 200 条

  function getActionLog(sessionPath: any) {
    return _actionLogs.get(sessionPath) || [];
  }

  function logAction(sessionPath: any, action: any, params: any, resultSummary: any, error?: any) {
    if (!_actionLogs.has(sessionPath)) {
      _actionLogs.set(sessionPath, []);
      // 淘汰最早的 session 日志
      if (_actionLogs.size > ACTION_LOG_MAX_SESSIONS) {
        _actionLogs.delete(_actionLogs.keys().next().value);
      }
    }
    const log = _actionLogs.get(sessionPath);
    log.push({
      ts: new Date().toISOString(),
      action,
      params: summarizeBrowserActionParams(action, params),
      result: error ? `ERROR: ${redactLogText(error)}` : redactLogText(resultSummary),
      url: redactLogText(browser.currentUrl(sessionPath)),
    });
    // 截断过长的单 session 日志
    if (log.length > ACTION_LOG_MAX_PER_SESSION) {
      log.splice(0, log.length - ACTION_LOG_MAX_PER_SESSION);
    }
  }

  /** 当前状态快照（附加到每个 action 的 details），运行时自动带缩略图 */
  async function statusFields(sessionPath: any) {
    const running = browser.isRunning(sessionPath);
    const url = browser.currentUrl(sessionPath);
    const activeTab = browser.activeTab?.(sessionPath) || null;
    const tabs = browser.getTabs?.(sessionPath) || [];
    const fields: Record<string, any> = {
      running,
      url,
      tabId: activeTab?.tabId || null,
      title: activeTab?.title || "",
      tabs,
    };
    if (running) {
      const thumbnail = await browser.thumbnail(sessionPath);
      if (thumbnail) {
        fields.thumbnail = thumbnail;
        fields.thumbnailCapturedAt = Date.now();
        fields.thumbnailUrl = url;
      }
    }
    return fields;
  }

  async function safeStatusFields(sessionPath: any) {
    try {
      return await statusFields(sessionPath);
    } catch {
      return {
        running: browser.isRunning(sessionPath),
        url: browser.currentUrl(sessionPath),
      };
    }
  }

  function resolveSessionPath(ctx: any) {
    return getToolSessionPath(ctx) || getSessionPath?.() || null;
  }

  function isExplicitTextOnlyModel(model: any) {
    return Array.isArray(model?.input) && !modelSupportsDirectImageInput(model);
  }

  return {
    name: "browser",
    label: "Browser",
    description: "Control a headless browser (navigate, click, type, scroll, screenshot, evaluate JS). Element [ref] ids from snapshot become stale after page changes; always use refs from the latest snapshot.",
    parameters: Type.Object({
      action: StringEnum(actionValues, { description: "Which operation to run. Required params per action: navigate→url; click→ref; type→text (optional ref, pressEnter); scroll→direction (optional amount); select→ref+value; key→key; wait→(optional timeout, state); evaluate→expression. start, stop, snapshot, screenshot, show take no extra params." }),
      url: Type.Optional(Type.String({ description: "URL (required for navigate)" })),
      tabId: Type.Optional(Type.String({ description: "Optional browser tab id. Defaults to the active tab." })),
      ref: Type.Optional(Type.Number({ description: "Element ref number (used for click/type/select)" })),
      text: Type.Optional(Type.String({ description: "Input text (required for type)" })),
      direction: Type.Optional(StringEnum(
        ["up", "down"],
        { description: "Scroll direction (required for scroll)" },
      )),
      amount: Type.Optional(Type.Number({ description: "Scroll amount (optional for scroll, default 3)" })),
      value: Type.Optional(Type.String({ description: "Option value (required for select)" })),
      key: Type.Optional(Type.String({ description: "Key name (required for key), e.g. Enter, Escape, Tab, Control+a" })),
      expression: Type.Optional(Type.String({ description: "JavaScript expression (required for evaluate)" })),
      timeout: Type.Optional(Type.Number({ description: "Timeout in milliseconds (optional for wait, default 5000)" })),
      state: Type.Optional(Type.String({ description: "Wait state (optional for wait): domcontentloaded / load / stable / networkidle (idle is accepted)" })),
      pressEnter: Type.Optional(Type.Boolean({ description: "Press Enter after typing (optional for type)" })),
    }),

    execute: async (_toolCallId, params, _signal, _onUpdate, ctx) => {
      try {
        const sessionPath = resolveSessionPath(ctx);

        switch (params.action) {

          // ── start ──
          case "start": {
            if (browser.isRunning(sessionPath)) {
              logAction(sessionPath, "start", null, "already_running");
              return toolOk(t("error.browserAlreadyRunning"), { status: "already_running", ...await statusFields(sessionPath) });
            }
            _actionLogs.delete(sessionPath);
            await browser.launch(sessionPath);
            logAction(sessionPath, "start", null, "launched");
            return toolOk(t("error.browserLaunched"), { status: "launched", ...await statusFields(sessionPath) });
          }

          // ── stop ──
          case "stop": {
            if (!browser.isRunning(sessionPath)) {
              return toolOk(t("error.browserNotRunning"), { status: "not_running", running: false, url: null });
            }
            logAction(sessionPath, "stop", null, "closed");
            const sessionLog = [...getActionLog(sessionPath)];
            await browser.close(sessionPath);
            _actionLogs.delete(sessionPath);
            return toolOk(t("error.browserClosed"), { status: "closed", running: false, url: null, actionLog: sessionLog });
          }

          // ── navigate ──
          case "navigate": {
            if (!params.url) return browserError(t("error.browserNavigateNeedUrl"));
            const result = await browser.navigate(params.url, sessionPath, { tabId: params.tabId });
            logAction(sessionPath, "navigate", { url: params.url }, result.title);
            return toolOk(
              t("error.browserNavigated", { title: result.title, url: result.url, snapshot: result.snapshot }),
              { action: "navigate", ...await statusFields(sessionPath), title: result.title },
            );
          }

          // ── snapshot ──
          case "snapshot": {
            const text = await browser.snapshot(sessionPath, params.tabId || null);
            return toolOk(text, { action: "snapshot", ...await statusFields(sessionPath) });
          }

          // ── screenshot ──
          case "screenshot": {
            const model = ctx?.model || options.getSessionModel?.(sessionPath) || null;
            const textOnlyNeedsAuxiliary = isExplicitTextOnlyModel(model);
            const auxiliaryAvailable = options.isVisionAuxiliaryEnabled?.() === true;
            if (!screenshotEnabled || (textOnlyNeedsAuxiliary && !auxiliaryAvailable)) {
              const msg = "browser screenshot is unavailable because the current model does not support image input";
              return {
                content: [{ type: "text", text: t("error.browserError", { msg }) }],
                details: { action: "screenshot", visionAdapted: false, visionError: msg, error: msg },
              };
            }
            const { base64, mimeType } = await browser.screenshot(sessionPath, params.tabId || null);
            const screenshotFile = await persistBrowserScreenshotFile({
              hanakoHome: options.getHanakoHome?.(),
              sessionPath,
              base64,
              mimeType,
              registerSessionFile: options.registerSessionFile,
            } as any);
            const mediaItem = browserScreenshotMediaItem(screenshotFile);
            const details = {
              action: "screenshot",
              mimeType,
              ...await statusFields(sessionPath),
              ...(screenshotFile || {}),
              screenshotFile,
              ...(mediaItem ? { media: { items: [mediaItem] } } : {}),
            };
            const image = { type: "image", mimeType, data: base64 };
            return { content: [image], details };
          }

          // ── click ──
          case "click": {
            if (params.ref == null) return browserError(t("error.browserClickNeedRef"));
            const snapshot = await browser.click(params.ref, sessionPath, params.tabId || null);
            logAction(sessionPath, "click", { ref: params.ref }, `clicked [${params.ref}]`);
            return toolOk(t("error.browserClicked", { ref: params.ref, snapshot }), { action: "click", ref: params.ref, ...await statusFields(sessionPath) });
          }

          // ── type ──
          case "type": {
            if (params.text == null) return browserError(t("error.browserTypeNeedText"));
            const snapshot = await browser.type(params.text, params.ref, { pressEnter: params.pressEnter ?? false }, sessionPath, params.tabId || null);
            logAction(sessionPath, "type", { ref: params.ref, text: params.text, pressEnter: params.pressEnter ?? false }, "typed");
            return toolOk(
              t("error.browserTyped", { target: params.ref != null ? ` to [${params.ref}]` : "", snapshot }),
              { action: "type", ref: params.ref, ...await statusFields(sessionPath) },
            );
          }

          // ── scroll ──
          case "scroll": {
            if (!params.direction) return browserError(t("error.browserScrollNeedDir"));
            const snapshot = await browser.scroll(params.direction, params.amount ?? 3, sessionPath, params.tabId || null);
            logAction(sessionPath, "scroll", { direction: params.direction, amount: params.amount }, "scrolled");
            return toolOk(
              t("error.browserScrolled", { dir: params.direction, snapshot }),
              { action: "scroll", direction: params.direction, ...await statusFields(sessionPath) },
            );
          }

          // ── select ──
          case "select": {
            if (params.ref == null) return browserError(t("error.browserSelectNeedRef"));
            if (!params.value) return browserError(t("error.browserSelectNeedValue"));
            const snapshot = await browser.select(params.ref, params.value, sessionPath, params.tabId || null);
            return toolOk(
              t("error.browserSelected", { ref: params.ref, value: params.value, snapshot }),
              { action: "select", ref: params.ref, value: params.value, ...await statusFields(sessionPath) },
            );
          }

          // ── key ──
          case "key": {
            if (!params.key) return browserError(t("error.browserKeyNeedKey"));
            const snapshot = await browser.pressKey(params.key, sessionPath, params.tabId || null);
            return toolOk(t("error.browserKeyPressed", { key: params.key, snapshot }), { action: "key", key: params.key, ...await statusFields(sessionPath) });
          }

          // ── wait ──
          case "wait": {
            const snapshot = await browser.wait({
              timeout: params.timeout ?? 5000,
              state: params.state ?? "domcontentloaded",
            }, sessionPath, params.tabId || null);
            return toolOk(t("error.browserWaitDone", { snapshot }), { action: "wait", ...await statusFields(sessionPath) });
          }

          // ── evaluate ──
          case "evaluate": {
            if (!params.expression) return browserError(t("error.browserEvalNeedExpr"));
            const result = await browser.evaluate(params.expression, sessionPath, params.tabId || null);
            const truncated = result.length > 30000
              ? result.slice(0, 30000) + t("error.browserOutputTruncated")
              : result;
            return toolOk(truncated, { action: "evaluate", ...await statusFields(sessionPath) });
          }

          // ── show ──
          case "show": {
            await browser.show(sessionPath, params.tabId || null);
            return toolOk(t("error.browserShown"), { action: "show", ...await statusFields(sessionPath) });
          }

          default:
            return browserError(t("error.browserUnknownAction", { action: params.action }));
        }
      } catch (error) {
        const sessionPath = resolveSessionPath(ctx);
        logAction(sessionPath, params.action, params, null, error.message);
        return browserError(t("error.browserActionFailed", { msg: error.message }), {
          action: params.action,
          ...await safeStatusFields(sessionPath),
          ...(error.browserFatal || error.code === "BROWSER_SESSION_UNAVAILABLE" ? { fatal: true } : {}),
        });
      }
    },
  };
}
