/**
 * ============================================================
 *  src/tools/shared/fileUtils.ts —— Read/Edit/Write 共享工具函数
 * ------------------------------------------------------------
 *  从 kakadeai 主仓库提炼的核心能力，剥离 UI/LSP/权限等高级层：
 *    1. 设备文件路径拦截（防止 /dev/zero 等阻塞）
 *    2. 路径安全校验 + 规范化（null byte / UNC / ~ 展开）
 *    3. 行尾符检测与保持（CRLF vs LF）
 *    4. 引号风格保留（弯引号 ↔ 直引号归一化）
 * ============================================================
 */
import { statSync } from 'node:fs';
import { open, readFile } from 'node:fs/promises';
import { homedir } from 'node:os';
import { extname, resolve } from 'node:path';
import { getWorkingDir } from '../../bootstrap/workingDir.js';
// -------------------- 1. 设备文件路径拦截 --------------------
const BLOCKED_DEVICE_PATHS = new Set([
    '/dev/zero',
    '/dev/random',
    '/dev/urandom',
    '/dev/full',
    '/dev/stdin',
    '/dev/tty',
    '/dev/console',
    '/dev/stdout',
    '/dev/stderr',
    '/dev/fd/0',
    '/dev/fd/1',
    '/dev/fd/2',
]);
const WINDOWS_BLOCKED_NAMES = new Set(['NUL', 'CON', 'PRN', 'AUX', 'COM1', 'COM2', 'LPT1']);
export function isBlockedDevicePath(filePath) {
    // 用 forward-slash 归一化：path.normalize 在 Windows 上会把 / 转成 \，
    // 黑名单是 POSIX 字面量永远对不上，所以直接做反斜杠 → 正斜杠的归一化
    const slashed = filePath.replaceAll('\\', '/');
    if (BLOCKED_DEVICE_PATHS.has(slashed))
        return true;
    if (slashed.startsWith('/proc/') &&
        (slashed.endsWith('/fd/0') || slashed.endsWith('/fd/1') || slashed.endsWith('/fd/2'))) {
        return true;
    }
    const baseName = slashed.split('/').pop() ?? '';
    if (WINDOWS_BLOCKED_NAMES.has(baseName.toUpperCase())) {
        return true;
    }
    return false;
}
export function validateAndResolvePath(rawPath, workingDir) {
    if (rawPath.includes('\0')) {
        return { ok: false, error: '路径包含非法字符（null byte）' };
    }
    // 在 resolve 之前先按原始路径检查设备文件：
    // Windows 上 resolve('D:\\workdir', '/dev/zero') → 'D:\\dev\\zero'，已丢失 POSIX 语义
    if (isBlockedDevicePath(rawPath)) {
        return { ok: false, error: `不允许读取设备文件：${rawPath}。该路径可能产生无限输出或阻塞进程。` };
    }
    const expanded = expandPath(rawPath);
    const resolved = resolve(workingDir, expanded);
    // resolve 后再查一次（兜底 Windows 设备名如 NUL/CON 经 resolve 后仍能 baseName 命中）
    if (isBlockedDevicePath(resolved)) {
        return { ok: false, error: `不允许读取设备文件：${resolved}。该路径可能产生无限输出或阻塞进程。` };
    }
    if (process.platform === 'win32' && /^\\\\/.test(resolved)) {
        return { ok: false, error: '不支持 UNC 路径（\\\\server\\share 格式），请使用本地路径' };
    }
    return { ok: true, resolvedPath: resolved };
}
function expandPath(p) {
    if (p.startsWith('~/') || p === '~') {
        return resolve(homedir(), p.slice(2));
    }
    return p;
}
/**
 * 工具入口便捷封装：自动以 getWorkingDir() 为锚点做路径规范化 + 安全校验。
 * 等价于 `validateAndResolvePath(rawPath, getWorkingDir())`。
 * 让 Read / Write / Edit / MultiEdit 不必各自 import getWorkingDir。
 */
export function resolveToolPath(rawPath) {
    return validateAndResolvePath(rawPath, getWorkingDir());
}
export function detectLineEndingsForString(content) {
    let crlfCount = 0;
    let lfCount = 0;
    for (let i = 0; i < content.length; i++) {
        if (content[i] === '\n') {
            if (i > 0 && content[i - 1] === '\r') {
                crlfCount++;
            }
            else {
                lfCount++;
            }
        }
    }
    return crlfCount > lfCount ? 'CRLF' : 'LF';
}
export async function detectFileLineEndings(filePath) {
    try {
        const handle = await readFile(filePath, { encoding: 'utf8' });
        const head = handle.slice(0, 4096);
        return detectLineEndingsForString(head);
    }
    catch {
        return 'LF';
    }
}
export function applyLineEnding(content, ending) {
    if (ending === 'CRLF') {
        return content.replaceAll('\r\n', '\n').split('\n').join('\r\n');
    }
    return content;
}
// -------------------- 3.5 二进制扩展名拦截 --------------------
// 上游 D:\code\kakadeai\constants\files.ts:5-112 BINARY_EXTENSIONS Set 的裁剪版：
// 故意剔除 .log / .csv / .tsv / .dat / .data / .lock —— 这些经常被人当文本读。
// 保留纯二进制：图片 / 文档 / 可执行 / 编译产物 / 压缩包 / 多媒体 / 字体 / 数据库 / wasm。
export const BINARY_EXTENSIONS = new Set([
    '.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.ico', '.tiff', '.tif',
    '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
    '.exe', '.dll', '.so', '.dylib', '.o', '.a',
    '.pyc', '.pyo', '.class', '.jar',
    '.zip', '.tar', '.gz', '.bz2', '.7z', '.rar', '.iso',
    '.mp3', '.mp4', '.mov', '.avi', '.mkv', '.wav', '.flac', '.ogg',
    '.ttf', '.otf', '.woff', '.woff2',
    '.sqlite', '.sqlite3', '.db',
    '.psd', '.ai', '.bin', '.wasm',
]);
export function hasBinaryExtension(filePath) {
    const ext = extname(filePath).toLowerCase();
    return BINARY_EXTENSIONS.has(ext);
}
// -------------------- 3.7 自动 snapshot（Edit/MultiEdit 拦截路径用） --------------------
/**
 * 自动读取文件前 N 行作为 snippet，给 Edit/MultiEdit 在 assertFresh 拦截时注入到错误响应里。
 *
 * 双限制：
 *   - SNAPSHOT_MAX_LINES = 200 行（防止小行长文件吃 context）
 *   - SNAPSHOT_MAX_BYTES = 32KB（防止超长单行 minified 文件撑爆 context）
 *   两个阈值任一命中都拒绝读全量。
 *
 * 失败 reason：
 *   - 'binary'      → 命中二进制扩展名黑名单
 *   - 'too-large'   → 文件 > 32KB（不读全文，让模型走显式 Read）
 *   - 'unreadable'  → stat 或 read 失败
 *
 * 调用方负责：
 *   1. 在拿到 ok=true 后调 recordRead(filePath, { truncated: snap.truncated })
 *   2. 把 snippet 拼到 error 字符串里
 *   3. ok=false 时根据 reason 给模型可读的 fallback 消息
 */
const SNAPSHOT_MAX_LINES = 200;
const SNAPSHOT_MAX_BYTES = 32 * 1024;
export async function tryAutoSnapshot(filePath) {
    // 1. 二进制扩展名拒绝
    if (hasBinaryExtension(filePath)) {
        return { ok: false, reason: 'binary' };
    }
    // 2. stat 拿大小
    let sizeBytes;
    try {
        const st = statSync(filePath);
        if (!st.isFile())
            return { ok: false, reason: 'unreadable' };
        sizeBytes = st.size;
    }
    catch {
        return { ok: false, reason: 'unreadable' };
    }
    // 3. 字节阈值
    if (sizeBytes > SNAPSHOT_MAX_BYTES) {
        return { ok: false, reason: 'too-large', sizeBytes };
    }
    // 4. 读全文（已知 <= 32KB）
    let content;
    try {
        content = await readFile(filePath, 'utf8');
    }
    catch {
        return { ok: false, reason: 'unreadable' };
    }
    // 5. 行数阈值：超过 200 行截断
    const allLines = content.split('\n');
    const totalLines = allLines.length;
    const truncated = totalLines > SNAPSHOT_MAX_LINES;
    const lines = truncated ? allLines.slice(0, SNAPSHOT_MAX_LINES) : allLines;
    // 拼带行号的 snippet（与 Read 工具输出风格一致）
    const numbered = lines
        .map((line, i) => `${String(i + 1).padStart(6, ' ')}\t${line}`)
        .join('\n');
    return { ok: true, snippet: numbered, truncated, totalLines };
}
/**
 * 读文件前 3 字节，返回 BOM 类型。
 * - EF BB BF → utf8-bom
 * - FF FE    → utf16le
 * - 其它 / 空 / 出错 → utf8（默认）
 */
export async function detectFileBomEncoding(filePath) {
    let fh = null;
    try {
        fh = await open(filePath, 'r');
        const buf = Buffer.alloc(3);
        const { bytesRead } = await fh.read(buf, 0, 3, 0);
        if (bytesRead >= 3 && buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf) {
            return 'utf8-bom';
        }
        if (bytesRead >= 2 && buf[0] === 0xff && buf[1] === 0xfe) {
            return 'utf16le';
        }
        return 'utf8';
    }
    catch {
        return 'utf8';
    }
    finally {
        if (fh) {
            try {
                await fh.close();
            }
            catch { }
        }
    }
}
// -------------------- 4. 引号风格保留 --------------------
export const LEFT_SINGLE_CURLY_QUOTE = '\u2018';
export const RIGHT_SINGLE_CURLY_QUOTE = '\u2019';
export const LEFT_DOUBLE_CURLY_QUOTE = '\u201C';
export const RIGHT_DOUBLE_CURLY_QUOTE = '\u201D';
export function normalizeQuotes(str) {
    return str
        .replaceAll(LEFT_SINGLE_CURLY_QUOTE, "'")
        .replaceAll(RIGHT_SINGLE_CURLY_QUOTE, "'")
        .replaceAll(LEFT_DOUBLE_CURLY_QUOTE, '"')
        .replaceAll(RIGHT_DOUBLE_CURLY_QUOTE, '"');
}
export function findActualString(fileContent, searchString) {
    if (fileContent.includes(searchString)) {
        return searchString;
    }
    const normalizedSearch = normalizeQuotes(searchString);
    const normalizedFile = normalizeQuotes(fileContent);
    const searchIndex = normalizedFile.indexOf(normalizedSearch);
    if (searchIndex !== -1) {
        return fileContent.substring(searchIndex, searchIndex + searchString.length);
    }
    return null;
}
export function preserveQuoteStyle(oldString, actualOldString, newString) {
    if (oldString === actualOldString) {
        return newString;
    }
    const hasDoubleQuotes = actualOldString.includes(LEFT_DOUBLE_CURLY_QUOTE) ||
        actualOldString.includes(RIGHT_DOUBLE_CURLY_QUOTE);
    const hasSingleQuotes = actualOldString.includes(LEFT_SINGLE_CURLY_QUOTE) ||
        actualOldString.includes(RIGHT_SINGLE_CURLY_QUOTE);
    if (!hasDoubleQuotes && !hasSingleQuotes) {
        return newString;
    }
    let result = newString;
    if (hasDoubleQuotes) {
        result = applyCurlyDoubleQuotes(result);
    }
    if (hasSingleQuotes) {
        result = applyCurlySingleQuotes(result);
    }
    return result;
}
function isOpeningContext(chars, index) {
    if (index === 0)
        return true;
    const prev = chars[index - 1];
    return (prev === ' ' ||
        prev === '\t' ||
        prev === '\n' ||
        prev === '\r' ||
        prev === '(' ||
        prev === '[' ||
        prev === '{' ||
        prev === '\u2014' ||
        prev === '\u2013');
}
function applyCurlyDoubleQuotes(str) {
    const chars = [...str];
    const result = [];
    for (let i = 0; i < chars.length; i++) {
        if (chars[i] === '"') {
            result.push(isOpeningContext(chars, i)
                ? LEFT_DOUBLE_CURLY_QUOTE
                : RIGHT_DOUBLE_CURLY_QUOTE);
        }
        else {
            result.push(chars[i]);
        }
    }
    return result.join('');
}
function applyCurlySingleQuotes(str) {
    const chars = [...str];
    const result = [];
    for (let i = 0; i < chars.length; i++) {
        if (chars[i] === "'") {
            const prev = i > 0 ? chars[i - 1] : undefined;
            const next = i < chars.length - 1 ? chars[i + 1] : undefined;
            const prevIsLetter = prev !== undefined && /\p{L}/u.test(prev);
            const nextIsLetter = next !== undefined && /\p{L}/u.test(next);
            if (prevIsLetter && nextIsLetter) {
                result.push(RIGHT_SINGLE_CURLY_QUOTE);
            }
            else {
                result.push(isOpeningContext(chars, i)
                    ? LEFT_SINGLE_CURLY_QUOTE
                    : RIGHT_SINGLE_CURLY_QUOTE);
            }
        }
        else {
            result.push(chars[i]);
        }
    }
    return result.join('');
}
// -------------------- 5. 字符串搜索/替换共享工具 --------------------
/** 统计 needle 在 haystack 中出现多少次（不重叠）。 */
export function countOccurrences(haystack, needle) {
    if (needle.length === 0)
        return 0;
    let count = 0;
    let pos = 0;
    while ((pos = haystack.indexOf(needle, pos)) !== -1) {
        count++;
        pos += needle.length;
    }
    return count;
}
/**
 * 全部替换。
 * 不用 String.prototype.replaceAll(string, string) 与 String.prototype.replace 是
 * 为了规避 replacement 字符串中 `$&` / `$1` 等被当作回填模式解释的问题。
 */
export function splitReplaceAll(haystack, needle, replacement) {
    return haystack.split(needle).join(replacement);
}
