import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

export const PUBLISH_SKILL_NAME = 'static-page-publish';
export const PUBLISH_ROOT_DIR = 'MindSpace';
export const PUBLIC_ZONE_DIR = 'public';
export const PUBLISH_SKILL_DIR = path.join(__dirname, 'skills', PUBLISH_SKILL_NAME);
export const WORKSPACE_HINTS_FILENAME = '.tkmindhints';
export const LEGACY_WORKSPACE_HINTS_FILENAME = '.goosehints';

function renderBrandingBlock(userAddressName) {
  const name = userAddressName || '用户';
  return `## 品牌与称呼（硬性）

- 你是 **TKMind** 助手；介绍产品时用 TKMind，不要称 goose、Goose、goosed
- 与用户对话时，用 **${name}** 称呼用户（可辅以「你/您」），**禁止**把用户叫作 TKMind
- 问候示例：「${name}，下午好」——不要用「TKMind，下午好」
- 不要描述本工作区为「Rust goose 项目」或「goose AI 框架」
- 本工作区是 TKMind **MindSpace 用户空间**，用于文件管理与静态页面生成
`;
}

/** Preferred name when addressing the user in chat (display name > username > slug). */
const INTERNAL_ID_PATTERN =
  /^wx_[a-z0-9_]{4,64}$|^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;

export function resolveUserAddressName({ displayName, username, slug } = {}) {
  const preferred = String(displayName ?? '').trim();
  if (preferred && !INTERNAL_ID_PATTERN.test(preferred)) return preferred;
  const uname = String(username ?? '').trim();
  if (uname && !INTERNAL_ID_PATTERN.test(uname)) return uname;
  const fallback = String(slug ?? '').trim();
  if (fallback && !INTERNAL_ID_PATTERN.test(fallback)) return fallback;
  return '用户';
}

export function resolvePublicBaseUrl(env = process.env) {
  return (env.H5_PUBLIC_BASE_URL ?? 'https://m.tkmind.cn').replace(/\/$/, '');
}

export const PUBLISH_KEY_UUID =
  /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;

/** Stable per-user directory key (UUID). */
export function resolvePublishKey(user) {
  if (!user?.id) throw new Error('缺少用户 ID');
  return String(user.id).trim().toLowerCase();
}

/** @deprecated Use resolvePublishKey — kept as alias for URL/path segments. */
export function resolvePublishSlug(user) {
  return resolvePublishKey(user);
}

export function resolveUsernameSlug(user) {
  if (!user?.username) throw new Error('缺少用户名');
  return String(user.username).trim().toLowerCase();
}

export function resolvePublishDir(h5Root, user) {
  return path.join(h5Root, PUBLISH_ROOT_DIR, resolvePublishKey(user));
}

export function resolveLegacyPublishDir(h5Root, user) {
  if (!user?.username) return null;
  return path.join(h5Root, PUBLISH_ROOT_DIR, resolveUsernameSlug(user));
}

function mergePublishTrees(sourceDir, targetDir) {
  if (!fs.existsSync(sourceDir)) return;
  for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
    const from = path.join(sourceDir, entry.name);
    const to = path.join(targetDir, entry.name);
    if (entry.isDirectory()) {
      fs.mkdirSync(to, { recursive: true });
      mergePublishTrees(from, to);
    } else if (!fs.existsSync(to)) {
      fs.copyFileSync(from, to);
    }
  }
}

/** Move username-based MindSpace/<username>/ into MindSpace/<userId>/ once. */
export function migrateUserPublishDir(h5Root, user, legacyUsersRoot = null) {
  const target = resolvePublishDir(h5Root, user);
  fs.mkdirSync(target, { recursive: true });
  const legacyDirs = [
    resolveLegacyPublishDir(h5Root, user),
    legacyUsersRoot ? path.join(legacyUsersRoot, resolveUsernameSlug(user)) : null,
  ].filter(Boolean);
  for (const legacyDir of legacyDirs) {
    if (path.resolve(legacyDir) === path.resolve(target)) continue;
    mergePublishTrees(legacyDir, target);
  }
  return target;
}

export function buildPublicUrl(publicBaseUrl, publishKey, filename = '') {
  const base = `${publicBaseUrl}/${PUBLISH_ROOT_DIR}/${encodeURIComponent(publishKey)}`;
  if (!filename) return `${base}/`;
  const clean = filename.replace(/^\/+/, '');
  return `${base}/${clean.split('/').map(encodeURIComponent).join('/')}`;
}

/** Public-zone HTML pages live under \`public/\`; share URLs must include that segment. */
export function buildPublicZonePageUrl(publicBaseUrl, publishKey, filename) {
  const clean = String(filename ?? '').replace(/^\/+/, '');
  const relative = clean.startsWith(`${PUBLIC_ZONE_DIR}/`) ? clean : `${PUBLIC_ZONE_DIR}/${clean}`;
  return buildPublicUrl(publicBaseUrl, publishKey, relative);
}

export function renderPublishSkill({ slug, username, publicBaseUrl, publishDir, displayName }) {
  const addressName = resolveUserAddressName({ displayName, username, slug });
  const exampleUrl = buildPublicZonePageUrl(publicBaseUrl, slug, 'report.html');
  return `---
name: ${PUBLISH_SKILL_NAME}
description: 在专属 MindSpace 目录生成可公开访问的静态 HTML 报告与页面（TKMind H5）
---

# 静态页面 / 报告发布（TKMind）

当用户需要**网页、HTML 报告、可视化页面、可分享链接**时，**必须先加载本技能**并按以下规则执行。

## 硬性约束（不可违反）

1. **唯一可写目录**：\`${publishDir}\`
2. **禁止**使用绝对路径（如 \`/Users/...\`、\`../\` 跳出目录）
3. **允许**在本目录内使用 \`write_file\`、\`edit_file\`、\`read_file\`、\`list_dir\`；**禁止**访问此目录外的路径（含 \`../\`、其它用户目录、项目根目录；系统会在 OS 层拦截越界访问）
4. **禁止**子 Agent、扩展管理、修改工作区外文件
5. 页面默认写入 \`public/\` 分区，使用相对路径如 \`public/report.html\`、\`public/assets/chart.png\`
6. 生成 HTML 后，向用户提供可访问链接，格式：
   \`${publicBaseUrl}/${PUBLISH_ROOT_DIR}/${slug}/public/<文件名>\`
   （写入 \`public/页面.html\` 时 URL **必须**含 \`public/\`；仅当 HTML 直接写在 workspace 根目录时才省略）

## 推荐工作流

1. 确认需求（标题、章节、是否要图表/样式）
2. 使用 \`write_file\` 创建 \`public/页面.html\`（可含内联 CSS；需要时在 \`public/assets/\` 或工作区 \`assets/\` 放资源）
3. 在 \`<head>\` 写入 **mindspace-cover** 元数据（见下文，必须与页面主题一致）
4. 页面内资源使用**相对路径**（\`assets/foo.png\`），不要用磁盘绝对路径
5. 保存 HTML 后，服务端会**立即**生成同名预览图 \`<文件名>.thumbnail.svg\`（Agent 交互阶段即生效，无需等用户保存到「我的空间」）
6. 完成后按「回复格式」返回可点击链接

## 回复格式（必须）

向用户交付页面时，**必须使用 Markdown 可点击链接**，让用户在聊天里直接点开预览：

\`\`\`markdown
[普拉提 618 活动页](${exampleUrl})
\`\`\`

要求：
- **必须**使用 \`[标题](URL)\` 格式，不要只给裸 URL
- 标题用页面真实主题（不要用「点击这里」「链接」）
- 可同时给出本地相对路径（如 \`public/pilates-618.html\`）与公网链接
- 说明：静态文件保存即生效，**无需重启**

## 信息流预览图（必须）

每个 HTML 页面都必须在 \`<head>\` 包含与**页面主题一致**的预览元数据。系统据此生成 **精美的 3:4 信息流封面**（「我的空间」卡片 + 工作区 \`.thumbnail.svg\` + 保存弹窗预览）：

\`\`\`html
<meta name="description" content="一句话摘要，显示在预览图副标题">
<meta name="mindspace-cover" content='{"tag":"运动","emoji":"🧘","accent":"#eeb04e","accent2":"#1a0a2e","subtitle":"618 限时 5 节课","cover":"assets/hero.jpg"}'>
\`\`\`

| 字段 | 要求 |
|------|------|
| \`tag\` | 与页面主题一致：旅行 / 美食 / 报告 / **运动** / **活动** 等；决定默认配色 |
| \`accent\` / \`accent2\` | 从页面主色提取，与 hero/背景一致 |
| \`subtitle\` | 一句话卖点；未写时用 description |
| \`cover\` / \`image\` | **必须**指向高质量主图（相对 HTML 或 https）；见下文 |
| \`emoji\` | 可选；也可写在 title 中 |

### 精美预览图（必须达标）

保存 HTML 后，服务端会**立即**生成同名预览图 \`<文件名>.thumbnail.svg\`（Agent 交互阶段即生效，无需等用户保存到「我的空间」）。要产出**可在信息流中直接展示的精美封面**，必须：

1. **视觉类页面**（旅行、美食、活动、运动、品牌、产品、促销等）**必须**在 \`assets/\` 放置高质量 hero 主图（建议宽度 ≥1200px），并在 \`cover\` 字段引用
2. **纯文字报告**可仅用配色 + tag，但仍须保证 \`accent\` / \`subtitle\` 与页面风格一致
3. \`tag\`、\`accent\`、\`accent2\`、\`subtitle\` 必须与页面实际视觉一致；**禁止**省略 mindspace-cover 或填无关默认值
4. 若缺少 hero 主图，封面会退化为简陋默认图，**视为未达标**

**禁止**使用与页面无关的通用风景图逻辑；促销/运动/品牌页必须写明 \`tag\`、\`accent\` 和 \`cover\`。

## HTML 模板建议

- 完整 \`<!DOCTYPE html>\`，\`lang="zh-CN"\`
- 移动端友好：\`<meta name="viewport" content="width=device-width, initial-scale=1">\`
- 深色/浅色与内容一致；主色在 CSS 与 mindspace-cover 中保持一致

## 查找文件（CSV、文档等）

- 只在**当前工作区**内从 \`.\` 搜索（如 \`list_dir\`、\`list_dir oa\`，或 shell 可用时 \`find . -name '*.csv'\`）
- 用户说的「OA 区」等视为工作区**子目录**（如 \`oa/\`），用相对路径查找
- **禁止**搜索上级目录、其它用户目录、\`${PUBLISH_ROOT_DIR}\` 根目录、\`/Users\`、项目根目录
- 找不到时告知用户上传或提供相对路径，**不要**扩大到工作区外

## 禁止事项

- 不要写入当前工作区以外的任何目录
- shell 仅用于本目录内整理文件/简单脚本；不要 \`rm -rf\` 越界路径、不要安装系统级依赖

## 示例

用户：「写一份越南旅游指南网页」

正确做法：
- \`write_file\` → \`public/vietnam-guide.html\`（内容完整）
- 回复链接：\`${buildPublicZonePageUrl(publicBaseUrl, slug, 'vietnam-guide.html')}\`
`;
}

export function renderWorkspaceHints({ slug, username, publishDir, displayName }) {
  const addressName = resolveUserAddressName({ displayName, username, slug });
  return `# TKMind 用户工作区边界

你是用户 **${addressName}** 的专属 TKMind 助手。当前会话**唯一**文件根目录：
\`${publishDir}\`

${renderBrandingBlock(addressName)}
## 查找 / 读取文件（CSV、文档、OA 区等）

1. **只能**在本工作区内搜索，例如 \`list_dir\`、\`list_dir oa\`，或 shell 可用时 \`find . -name '*.csv'\`、\`rg keyword .\`
2. 用户说的「OA 区」「文档区」等，**先当作工作区内的子目录**（如 \`oa/\`、\`docs/\`），只用相对路径查找
3. **绝对禁止**：
   - 去上级目录、\`${PUBLISH_ROOT_DIR}\` 根目录、其它用户名目录、项目根目录搜索
   - 使用 \`/Users\`、\`/home\`、Desktop、Documents、Downloads 等主机路径
   - 在回复中建议「在 MindSpace 目录或附近目录搜索」——你没有这个权限
4. 找不到文件时：说明在其工作区内未找到，请用户上传或提供相对路径；**不得**扩大到工作区外

## 公网链接 vs 本地文件

- 公网 URL（如 \`https://…/MindSpace/<用户>/\`）**仅供用户浏览器打开已发布的 HTML 页面**
- **禁止**用公网 URL、curl、wget 或 read_image 去「列目录」「读 CSV」——静态站点不提供目录索引
- 有 \`shell\` 时用 \`ls oa/\`、\`find .\`、\`cat oa/file.csv\`；有 \`tree\` 时用 \`tree oa/\`
- 不要说「我没有 shell」——若系统提示已列出 shell，就必须用 shell 在工作区内操作
`;
}

export function buildSandboxSessionConstraints({ baseConstraints, developerTools = [] }) {
  const tools = developerTools.length > 0 ? developerTools : ['write_file', 'edit_file'];
  const hasSandboxMcp = tools.includes('write_file') || tools.includes('read_file');
  const hasShell = tools.includes('shell');
  const hasListDir = tools.includes('list_dir') || tools.includes('tree');

  const lines = [
    baseConstraints,
    '',
    '## 当前会话可用文件工具',
    `- ${tools.join(', ')}`,
    hasSandboxMcp
      ? '- 文件工具由**沙箱 MCP** 提供：所有路径在 OS 层强制限制在工作区内，越界访问会立即报错'
      : '',
  ].filter((l) => l !== '');

  if (hasShell) {
    lines.push(
      '- **shell 已开启**：只能在本工作区内执行（如 `ls oa/`、`find . -name "*.csv"`、`cat oa/文件.csv`）',
      '- **禁止** curl/wget 或访问公网 URL 来读取工作区文件',
    );
  } else {
    lines.push(
      '- **shell 未开启**：不能执行 shell；也不要用公网 URL 代替本地读文件',
    );
  }
  if (hasListDir) {
    lines.push('- **list_dir 可用**：`list_dir` 或 `list_dir oa` 查看目录结构');
  } else {
    lines.push('- 需要列目录或读 CSV 时，请用户上传文件内容或开启 list_dir 能力');
  }
  lines.push(
    '',
    '## 生成 / 发布 HTML 页面',
    '- 你有 write_file/edit_file 工具：**必须由你**写入 `public/xxx.html`（或工作区根目录 `.html`）',
    '- 开始前执行 load_skill → `static-page-publish`，按技能说明写入 mindspace-cover 元数据',
    '- **禁止**让用户手动保存到 public 或说无法生成页面（除非 write_file 调用失败）',
    '- 完成后回复 `[页面标题](公网URL)` 可点击链接；写入 `public/` 时 URL 必须含 `/public/` 路径段',
  );
  return lines.join('\n');
}

export function buildPublishConstraints({ slug, username, publicBaseUrl, publishDir, displayName }) {
  const addressName = resolveUserAddressName({ displayName, username, slug });
  return [
    '## TKMind 用户空间沙箱（硬性约束）',
    '',
    '- 你是 **TKMind** 助手；与用户对话时用 **' + addressName + '** 称呼用户，禁止把用户叫作 TKMind',
    '- 禁止称 goose / Goose / goosed 或「Rust goose 项目」',
    `- 当前用户 **${addressName}** 的 Agent 工作区（唯一文件根目录）：\`${publishDir}\``,
    '- 用户上传落在分区子目录：`oa/`、`private/`、`public/`（均在上述工作区内）',
    `- 公网 HTML 前缀（公开区页面）：\`${buildPublicUrl(publicBaseUrl, slug, `${PUBLIC_ZONE_DIR}/`)}\``,
    '- **查找文件**：在工作区内对应分区搜索（如 `oa/2025-12-06T13-34_export.csv`），不要搜其它用户目录或公网 URL',
    '- **禁止**：访问 `assets/` 内部路径、其它用户目录、主机绝对路径；禁止用公网 URL 列目录或读 CSV',
    '- **路径规则**：只用相对路径；禁止 `../`；工作区外的路径会被系统拒绝（OS 层强制，非软约束）',
    '- **生成页面（必须亲自完成）**：先 `load_skill` → `static-page-publish`，再用 `write_file`/`edit_file` 写入 `public/页面.html`',
    '- **禁止**让用户「手动保存到 public 目录」或说「我无法生成页面」——除非 write_file 已失败并报告错误',
    '- 完成后给出 Markdown 可点击公网链接 `[标题](URL)`；写入 `public/页面.html` 时 URL 为 `.../MindSpace/<用户ID>/public/页面.html`',
    `- 发布技能：\`${PUBLISH_SKILL_NAME}\`（生成页面前应 load_skill）`,
  ].join('\n');
}

export function ensureWorkspaceHintsInstalled(publishDir, context) {
  const hintsPath = path.join(publishDir, WORKSPACE_HINTS_FILENAME);
  const legacyPath = path.join(publishDir, LEGACY_WORKSPACE_HINTS_FILENAME);
  const content = renderWorkspaceHints(context);
  fs.writeFileSync(hintsPath, content, 'utf8');
  if (fs.existsSync(legacyPath)) {
    fs.unlinkSync(legacyPath);
  }
  return hintsPath;
}

export function ensurePublishSkillInstalled(publishDir, context) {
  const skillRoot = path.join(publishDir, '.agents', 'skills', PUBLISH_SKILL_NAME);
  fs.mkdirSync(skillRoot, { recursive: true });
  const skillPath = path.join(skillRoot, 'SKILL.md');
  const content = renderPublishSkill(context);
  const existing = fs.existsSync(skillPath) ? fs.readFileSync(skillPath, 'utf8') : '';
  if (existing !== content) {
    fs.writeFileSync(skillPath, content, 'utf8');
  }
  return skillPath;
}

export function ensureUserPublishLayout({
  h5Root,
  publicBaseUrl,
  user,
  legacyUsersRoot = null,
  installWorkspaceHints = false,
}) {
  migrateUserPublishDir(h5Root, user, legacyUsersRoot);
  const slug = resolvePublishKey(user);
  const username = user.username ? resolveUsernameSlug(user) : slug;
  const displayName = user.displayName ?? user.display_name ?? null;
  const publishDir = resolvePublishDir(h5Root, user);
  fs.mkdirSync(path.join(publishDir, 'assets'), { recursive: true });

  const context = { slug, username, displayName, publicBaseUrl, publishDir };

  if (installWorkspaceHints) {
    ensureWorkspaceHintsInstalled(publishDir, context);
    ensurePublishSkillInstalled(publishDir, context);
  }

  return {
    slug,
    username,
    displayName,
    publishDir,
    publicBaseUrl,
    publicUrl: buildPublicUrl(publicBaseUrl, slug),
    skillName: PUBLISH_SKILL_NAME,
    constraints: buildPublishConstraints(context),
  };
}

export function isPathInsidePublishDir(publishDir, requestedPath) {
  const resolvedPublish = path.resolve(publishDir);
  const resolved = path.resolve(requestedPath);
  return resolved === resolvedPublish || resolved.startsWith(`${resolvedPublish}${path.sep}`);
}
