import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { resolveAgentGooseMode } from './policies.mjs';

/** Spawned as a separate Node process by goosed; must sit beside bundled portal runtime. */
export function resolveSandboxMcpServerPath() {
  return path.join(path.dirname(fileURLToPath(import.meta.url)), 'mindspace-sandbox-mcp.mjs');
}

export const CAPABILITY_CATALOG = [
  {
    key: 'shell',
    label: 'Shell 命令',
    description: '执行 bash/shell 命令（developer.shell）',
    risk: 'high',
    category: 'developer',
  },
  {
    key: 'static_publish',
    label: '用户沙箱目录',
    description:
      '在 MindSpace/<用户ID>/ 内可使用 sandbox-fs 的 write_file/edit_file/read_file/create_dir（以及按权限开放的 list_dir；不可越出该目录）',
    risk: 'medium',
    category: 'publisher',
  },
  {
    key: 'private_data_space',
    label: '用户私有数据空间',
    description:
      '为用户提供唯一的私有 SQLite 数据空间，供 Agent 创建问卷、表单、清单等私有结构化数据表',
    risk: 'medium',
    category: 'publisher',
  },
  {
    key: 'filesystem',
    label: '文件读写（全目录）',
    description: '在工作区内任意读写文件（developer.write / edit），仅高级用户',
    risk: 'high',
    category: 'developer',
  },
  {
    key: 'code_browse',
    label: '代码浏览',
    description: '目录树、代码结构分析（developer.tree、analyze）',
    risk: 'low',
    category: 'developer',
  },
  {
    key: 'image_read',
    label: '图片读取',
    description: '读取本地或网络图片（developer.read_image）',
    risk: 'low',
    category: 'developer',
  },
  {
    key: 'skills',
    label: '技能加载',
    description: '加载技能与知识（skills / summon.load）',
    risk: 'low',
    category: 'knowledge',
  },
  {
    key: 'subagent',
    label: '子任务派发',
    description: 'delegate 子 Agent 执行复杂任务（summon）',
    risk: 'high',
    category: 'agent',
  },
  {
    key: 'code_sandbox',
    label: '沙箱脚本',
    description: 'execute_typescript 在沙箱中批量调用工具（code_execution）',
    risk: 'high',
    category: 'agent',
  },
  {
    key: 'chat_recall',
    label: '对话回溯',
    description: '检索历史会话（chatrecall）',
    risk: 'low',
    category: 'memory',
  },
  {
    key: 'context_memory',
    label: '项目记忆',
    description: '会话级项目记忆注入（projectmemory，H5 引导用）',
    risk: 'low',
    category: 'memory',
  },
  {
    key: 'memory_store',
    label: '长期记忆',
    description: '记住/检索用户偏好（memory 扩展）',
    risk: 'medium',
    category: 'memory',
  },
  {
    key: 'extension_admin',
    label: '扩展管理',
    description: '搜索、启用、禁用扩展（extensionmanager）',
    risk: 'high',
    category: 'admin',
  },
  {
    key: 'apps',
    label: '应用管理',
    description: '创建与管理 TKMind 应用（apps）',
    risk: 'medium',
    category: 'agent',
  },
  {
    key: 'todo',
    label: '待办事项',
    description: '任务列表跟踪（todo）',
    risk: 'low',
    category: 'productivity',
  },
  {
    key: 'computer',
    label: '电脑控制',
    description: '自动化脚本、网页抓取、文档处理（computercontroller）',
    risk: 'high',
    category: 'automation',
  },
  {
    key: 'charts',
    label: '数据可视化',
    description: '图表与可视化（autovisualiser）',
    risk: 'low',
    category: 'automation',
  },
  {
    key: 'aider',
    label: 'Aider 编码',
    description: '多文件编码委托（aider）',
    risk: 'high',
    category: 'developer',
  },
  {
    key: 'openhands',
    label: 'OpenHands 编码',
    description: '复杂多文件编码与仓库级任务委托（openhands）',
    risk: 'high',
    category: 'developer',
  },
];

/** Capabilities that must never be enabled for regular users, even via DB overrides. */
export const USER_NON_GRANTABLE_CAPABILITIES = new Set(['extension_admin']);

export function clampUserCapabilities(capabilities) {
  const clamped = { ...capabilities };
  for (const key of USER_NON_GRANTABLE_CAPABILITIES) {
    clamped[key] = false;
  }
  return clamped;
}

export const DEFAULT_USER_CAPABILITIES = Object.fromEntries(
  CAPABILITY_CATALOG.map(({ key }) => {
    const defaults = {
      shell: false,
      static_publish: false,
      private_data_space: true,
      filesystem: false,
      code_browse: false,
      image_read: true,
      skills: true,
      subagent: false,
      code_sandbox: false,
      chat_recall: true,
      context_memory: true,
      memory_store: true,
      extension_admin: false,
      apps: false,
      todo: false,
      computer: false,
      charts: false,
      aider: false,
      openhands: false,
    };
    return [key, defaults[key] ?? false];
  }),
);

const CATALOG_KEYS = new Set(CAPABILITY_CATALOG.map((item) => item.key));

export function catalogKeys() {
  return [...CATALOG_KEYS];
}

export function isValidCapabilityKey(key) {
  return CATALOG_KEYS.has(key);
}

function makeExtension(type, name, tools = []) {
  return {
    type,
    name,
    description: '',
    display_name: name,
    bundled: true,
    available_tools: tools,
  };
}

export const SANDBOX_DEVELOPER_TOOLS = ['write', 'edit', 'shell', 'tree', 'read_image'];

/** static_publish sandbox: only file writes inside MindSpace/<userId>/ — no tree/shell by default */
export function sandboxDeveloperTools(capabilities) {
  const tools = ['write', 'edit'];
  if (capabilities.shell) {
    tools.push('shell', 'tree');
  } else if (capabilities.code_browse) {
    tools.push('tree');
  }
  if (capabilities.image_read) tools.push('read_image');
  return tools;
}

/**
 * Tools exposed by the sandbox MCP server (mindspace-sandbox-mcp.mjs) for static_publish users.
 * read_file is always included because edit_file requires reading first.
 */
export function sandboxMcpTools(capabilities) {
  const tools = [];
  if (capabilities.static_publish) {
    tools.push('read_file', 'write_file', 'edit_file', 'create_dir');
    if (capabilities.shell || capabilities.code_browse) tools.push('list_dir');
  }
  if (capabilities.private_data_space) {
    tools.push(
      'private_data_info',
      'private_data_schema',
      'private_data_query',
      'private_data_execute',
      'schedule_create_item',
      'schedule_create_reminder',
      'schedule_list_items',
    );
  }
  return tools;
}

export function developerToolsFromPolicy(sessionPolicy) {
  const developer = sessionPolicy?.extensionOverrides?.find((ext) => ext.name === 'developer');
  const sandboxFs = sessionPolicy?.extensionOverrides?.find((ext) => ext.name === 'sandbox-fs');
  return (sandboxFs ?? developer)?.available_tools ?? [];
}

function mergeDeveloperTools(capabilities) {
  const tools = [];
  if (capabilities.shell) tools.push('shell');
  if (capabilities.filesystem) tools.push('write', 'edit');
  if (capabilities.code_browse) tools.push('tree');
  if (capabilities.image_read) tools.push('read_image');
  return tools;
}

function sandboxMcpEnvs(sandboxMcp, mcpTools) {
  const envs = {
    SANDBOX_ROOT: sandboxMcp.sandboxRoot,
    ALLOWED_TOOLS: mcpTools.join(','),
  };
  if (sandboxMcp.userId) envs.PRIVATE_DATA_USER_ID = sandboxMcp.userId;
  for (const key of [
    'DATABASE_URL',
    'MYSQL_HOST',
    'MYSQL_PORT',
    'MYSQL_USER',
    'MYSQL_PASSWORD',
    'MYSQL_DATABASE',
    'PRIVATE_DATA_MAX_BYTES',
  ]) {
    if (process.env[key]) envs[key] = process.env[key];
  }
  return envs;
}

/**
 * Build goose agent/start extension_overrides from resolved capability flags.
 * Returns null when the caller should use server defaults (admin / unrestricted).
 *
 * sandboxMcp: { serverPath, sandboxRoot, nodeExecPath? }
 *   When provided for a static_publish user, the built-in developer extension is replaced
 *   by a sandboxed stdio MCP that enforces filesystem boundaries at the OS level.
 */
export function buildAgentExtensionPolicy(
  capabilities,
  { unrestricted = false, policies = null, sandboxMcp = null } = {},
) {
  if (unrestricted) {
    return { extensionOverrides: null, enableContextMemory: true, gooseMode: 'auto' };
  }

  const extensions = [];
  if (capabilities.static_publish || (capabilities.private_data_space && sandboxMcp)) {
    if (sandboxMcp?.serverPath && sandboxMcp?.sandboxRoot) {
      // Sandboxed stdio MCP: enforces SANDBOX_ROOT at the OS level.
      // Replaces the built-in developer extension so path traversal is impossible.
      const mcpTools = sandboxMcpTools(capabilities);
      if (mcpTools.length > 0) {
        extensions.push({
          type: 'stdio',
          name: 'sandbox-fs',
          description:
            '工作区沙箱文件系统与用户私有数据空间。用户私有数据空间是当前用户唯一的 SQLite 数据库，适合问卷、表单、清单、调研数据和分析中间表；不要用于账号、计费、权限、审计、公开平台数据或跨用户数据。',
          display_name: 'sandbox-fs',
          bundled: false,
          cmd: sandboxMcp.nodeExecPath ?? process.execPath,
          // sandboxRoot passed as argv[2] so it works even if goosed doesn't forward envs
          args: [sandboxMcp.serverPath, sandboxMcp.sandboxRoot],
          // envs (goosed field name) as belt-and-suspenders backup
          envs: sandboxMcpEnvs(sandboxMcp, mcpTools),
          available_tools: mcpTools,
        });
      }
      // Keep developer extension only for image reading when applicable.
      if (capabilities.image_read) {
        extensions.push(makeExtension('platform', 'developer', ['read_image']));
      }
    } else if (capabilities.static_publish) {
      // Fallback when sandbox MCP is not configured: use built-in developer extension.
      // This is the legacy path — file operations are NOT boundary-enforced.
      const sandboxTools = sandboxDeveloperTools(capabilities);
      if (sandboxTools.length > 0) {
        extensions.push(makeExtension('platform', 'developer', sandboxTools));
      }
    }
    if (capabilities.static_publish) {
      extensions.push(makeExtension('platform', 'skills', []));
      extensions.push(makeExtension('platform', 'summon', ['load_skill']));
      extensions.push(makeExtension('platform', 'projectmemory', []));
    }
  } else {
    const developerTools = mergeDeveloperTools(capabilities);
    if (developerTools.length > 0) {
      extensions.push(makeExtension('platform', 'developer', developerTools));
    }
    if (capabilities.code_browse) {
      extensions.push(makeExtension('platform', 'analyze', []));
    }
    if (capabilities.skills) {
      extensions.push(makeExtension('platform', 'skills', []));
    }
    if (capabilities.skills || capabilities.subagent) {
      const summonTools = [];
      if (capabilities.skills) summonTools.push('load', 'load_skill');
      if (capabilities.subagent) summonTools.push('delegate');
      extensions.push(makeExtension('platform', 'summon', summonTools));
    }
  }
  if (capabilities.code_sandbox) {
    extensions.push(makeExtension('platform', 'code_execution', []));
  }
  if (capabilities.chat_recall) {
    extensions.push(makeExtension('platform', 'chatrecall', []));
  }
  if (capabilities.context_memory) {
    extensions.push(makeExtension('platform', 'projectmemory', []));
  }
  if (capabilities.memory_store) {
    extensions.push(makeExtension('builtin', 'memory', []));
  }
  if (capabilities.extension_admin) {
    extensions.push(makeExtension('platform', 'extensionmanager', []));
  }
  if (capabilities.apps) {
    extensions.push(makeExtension('platform', 'apps', []));
  }
  if (capabilities.todo) {
    extensions.push(makeExtension('platform', 'todo', []));
  }
  if (capabilities.computer) {
    extensions.push(makeExtension('builtin', 'computercontroller', []));
  }
  if (capabilities.charts) {
    extensions.push(makeExtension('builtin', 'autovisualiser', []));
  }
  if (capabilities.aider) {
    extensions.push(makeExtension('platform', 'aider', []));
  }
  if (capabilities.openhands) {
    extensions.push(makeExtension('platform', 'openhands', []));
  }

  return {
    extensionOverrides: extensions,
    enableContextMemory: Boolean(
      capabilities.context_memory || capabilities.chat_recall || capabilities.static_publish,
    ),
    gooseMode: resolveAgentGooseMode(capabilities, policies),
  };
}

/**
 * Narrow policy for in-preview page edit sub-sessions: patch API via shell when allowed,
 * otherwise reply-only patches via mindspace-page-update blocks.
 */
export function buildPageEditAgentPolicy(basePolicy) {
  if (basePolicy?.unrestricted) {
    return {
      ...basePolicy,
      enableContextMemory: false,
      gooseMode: 'auto',
    };
  }

  const baseDeveloper = basePolicy?.extensionOverrides?.find((ext) => ext.name === 'developer');
  const canShell =
    baseDeveloper?.available_tools?.includes('shell') ||
    basePolicy?.capabilities?.shell === true;
  const extensions = canShell ? [makeExtension('platform', 'developer', ['shell'])] : [];

  return {
    ...basePolicy,
    extensionOverrides: extensions,
    enableContextMemory: false,
    gooseMode: 'auto',
  };
}

export function normalizeCapabilityPatch(patch) {
  const normalized = {};
  for (const [key, value] of Object.entries(patch ?? {})) {
    if (!isValidCapabilityKey(key)) continue;
    normalized[key] = Boolean(value);
  }
  return normalized;
}
