// File.read / write / list 实现 —— 在 ~/FFAI Workspace/ scope 内操作。
//
// 安全约束：
// - 路径必须收敛在 workspace root 内（resolveSafe 阻断 ../）
// - read/write 单次 ≤ MAX_BYTES（防 OOM；超 size LLM 应分片或拒绝）
// - list 单次 ≤ MAX_ENTRIES（防巨型目录扫死）

import * as fs from "node:fs/promises";
import * as path from "node:path";
import { ensureWorkspaceRoot, resolveSafe } from "./scope";

const MAX_BYTES = 8 * 1024 * 1024; // 8 MiB；超的让 LLM 拆
const MAX_ENTRIES = 1000;

/**
 * resolveSafe 只做字符串路径校验，无法挡 pre-existing 符号链接（如 ~/FFAI Workspace/evil → /etc/passwd）。
 * 任何**读路径**（readFile/listDir）都过这一层：跑 fs.realpath 后再核对仍在 rootAbs 内。
 *
 * writeFile 不强制走 realpath：用 `wx` flag 原子独占创建，写入只会落在 rootAbs 内的新 inode；
 * 假如 LLM 给 `path=evil/foo.txt` 想穿透链接，realpath 会因 evil 是符号链接但 foo.txt 不存在
 * 而 mkdir/`wx` 仍按字符串路径在链接目标里建——所以 writeFile 也跑一次 realpath 检查父目录。
 */
async function assertRealpathInRoot(rootAbs: string, full: string): Promise<string> {
  let real: string;
  try {
    real = await fs.realpath(full);
  } catch (err) {
    // 文件还不存在（write 新建）时 realpath ENOENT —— 用父目录校验
    if ((err as NodeJS.ErrnoException).code === "ENOENT") {
      real = path.join(await fs.realpath(path.dirname(full)), path.basename(full));
    } else {
      throw err;
    }
  }
  if (real !== rootAbs && !real.startsWith(rootAbs + path.sep)) {
    throw new Error(`symlink_escape:resolved=${real}`);
  }
  return real;
}

export interface ReadArgs {
  readonly path: string;
  /** "utf8" → 返回字符串；"base64" → 返回 base64 串；默认 utf8 */
  readonly encoding?: "utf8" | "base64";
}

export interface ReadResult {
  readonly path: string;
  readonly encoding: "utf8" | "base64";
  readonly content: string;
  readonly sizeBytes: number;
}

export async function readFile(args: ReadArgs): Promise<ReadResult> {
  const root = await ensureWorkspaceRoot();
  const full = resolveSafe(root, args.path);
  await assertRealpathInRoot(path.resolve(root), full);
  const stat = await fs.stat(full);
  if (stat.size > MAX_BYTES) {
    throw new Error(`file_too_large:${stat.size}>${MAX_BYTES}`);
  }
  const buf = await fs.readFile(full);
  const encoding = args.encoding ?? "utf8";
  return {
    path: args.path,
    encoding,
    content: encoding === "base64" ? buf.toString("base64") : buf.toString("utf8"),
    sizeBytes: stat.size,
  };
}

export interface WriteArgs {
  readonly path: string;
  readonly content: string;
  readonly encoding?: "utf8" | "base64";
  /** 文件存在时是否覆盖；默认 false（拒写） */
  readonly overwrite?: boolean;
}

export interface WriteResult {
  readonly path: string;
  readonly bytesWritten: number;
}

export async function writeFile(args: WriteArgs): Promise<WriteResult> {
  const root = await ensureWorkspaceRoot();
  const full = resolveSafe(root, args.path);
  const buf = Buffer.from(args.content, args.encoding ?? "utf8");
  if (buf.byteLength > MAX_BYTES) {
    throw new Error(`content_too_large:${buf.byteLength}>${MAX_BYTES}`);
  }
  await fs.mkdir(path.dirname(full), { recursive: true });
  // realpath 检查放 mkdir 之后：父目录已落地，可解析；防符号链接父目录指向 root 外
  await assertRealpathInRoot(path.resolve(root), full);
  // wx = O_CREAT | O_EXCL：原子独占创建，避免 access-then-write TOCTOU
  const flag = args.overwrite ? "w" : "wx";
  try {
    await fs.writeFile(full, buf, { flag });
  } catch (err) {
    if ((err as NodeJS.ErrnoException).code === "EEXIST") {
      throw new Error("file_exists_no_overwrite");
    }
    throw err;
  }
  return { path: args.path, bytesWritten: buf.byteLength };
}

export interface ListArgs {
  readonly path: string;
  /** 截断输出避免巨型目录刷屏；默认 100；上限 MAX_ENTRIES */
  readonly limit?: number;
  /** 默认 false：跳过每条 stat 节省 syscall；agent 需要 size 时显式开 */
  readonly includeSize?: boolean;
}

export interface ListEntry {
  readonly name: string;
  readonly path: string;
  readonly type: "file" | "directory" | "symlink" | "other";
  readonly sizeBytes?: number;
}

export interface ListResult {
  readonly path: string;
  readonly entries: readonly ListEntry[];
  readonly truncated: boolean;
}

export async function listDir(args: ListArgs): Promise<ListResult> {
  const root = await ensureWorkspaceRoot();
  const full = resolveSafe(root, args.path);
  await assertRealpathInRoot(path.resolve(root), full);
  const limit = Math.min(Math.max(args.limit ?? 100, 1), MAX_ENTRIES);
  const dirents = await fs.readdir(full, { withFileTypes: true });
  const sliced = dirents.slice(0, limit);
  const entries: ListEntry[] = await Promise.all(
    sliced.map(async (d) => {
      const relPath = path.join(args.path, d.name);
      let type: ListEntry["type"] = "other";
      if (d.isFile()) type = "file";
      else if (d.isDirectory()) type = "directory";
      else if (d.isSymbolicLink()) type = "symlink";
      let sizeBytes: number | undefined;
      if (args.includeSize && type === "file") {
        try {
          sizeBytes = (await fs.stat(path.join(full, d.name))).size;
        } catch {
          // 软失败：单条 stat 出错不影响整体 list
        }
      }
      return { name: d.name, path: relPath, type, sizeBytes };
    }),
  );
  return {
    path: args.path,
    entries,
    truncated: dirents.length > limit,
  };
}
