/**
 * Shared truncation utilities for tool outputs.
 *
 * Truncation is based on two independent limits - whichever is hit first wins:
 * - Line limit (default: 2000 lines)
 * - Byte limit (default: 50KB)
 *
 * Never returns partial lines (except bash tail truncation edge case).
 */
export const DEFAULT_MAX_LINES = 2000;
export const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB
export const GREP_MAX_LINE_LENGTH = 500; // Max chars per grep match line
const runtimeBuffer = globalThis.Buffer;
const nonAsciiPattern = /[^\x00-\x7f]/;
function utf8ByteLength(content) {
    if (runtimeBuffer)
        return runtimeBuffer.byteLength(content, "utf8");
    const firstNonAscii = content.search(nonAsciiPattern);
    if (firstNonAscii === -1)
        return content.length;
    let bytes = firstNonAscii;
    for (let i = firstNonAscii; i < content.length; i++) {
        const code = content.charCodeAt(i);
        if (code <= 0x7f) {
            bytes += 1;
        }
        else if (code <= 0x7ff) {
            bytes += 2;
        }
        else if (code >= 0xd800 && code <= 0xdbff && i + 1 < content.length) {
            const next = content.charCodeAt(i + 1);
            if (next >= 0xdc00 && next <= 0xdfff) {
                bytes += 4;
                i++;
            }
            else {
                bytes += 3;
            }
        }
        else {
            bytes += 3;
        }
    }
    return bytes;
}
function replaceUnpairedSurrogates(content) {
    let output = "";
    for (let i = 0; i < content.length; i++) {
        const code = content.charCodeAt(i);
        if (code >= 0xd800 && code <= 0xdbff) {
            if (i + 1 < content.length) {
                const next = content.charCodeAt(i + 1);
                if (next >= 0xdc00 && next <= 0xdfff) {
                    output += content[i] + content[i + 1];
                    i++;
                    continue;
                }
            }
            output += "�";
        }
        else if (code >= 0xdc00 && code <= 0xdfff) {
            output += "�";
        }
        else {
            output += content[i];
        }
    }
    return output;
}
/**
 * Format bytes as human-readable size.
 */
export function formatSize(bytes) {
    if (bytes < 1024) {
        return `${bytes}B`;
    }
    else if (bytes < 1024 * 1024) {
        return `${(bytes / 1024).toFixed(1)}KB`;
    }
    else {
        return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
    }
}
/**
 * Truncate content from the head (keep first N lines/bytes).
 * Suitable for file reads where you want to see the beginning.
 *
 * Never returns partial lines. If first line exceeds byte limit,
 * returns empty content with firstLineExceedsLimit=true.
 */
export function truncateHead(content, options = {}) {
    const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
    const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
    const totalBytes = utf8ByteLength(content);
    const lines = content.split("\n");
    const totalLines = lines.length;
    // Check if no truncation needed
    if (totalLines <= maxLines && totalBytes <= maxBytes) {
        return {
            content,
            truncated: false,
            truncatedBy: null,
            totalLines,
            totalBytes,
            outputLines: totalLines,
            outputBytes: totalBytes,
            lastLinePartial: false,
            firstLineExceedsLimit: false,
            maxLines,
            maxBytes,
        };
    }
    // Check if first line alone exceeds byte limit
    const firstLineBytes = utf8ByteLength(lines[0]);
    if (firstLineBytes > maxBytes) {
        return {
            content: "",
            truncated: true,
            truncatedBy: "bytes",
            totalLines,
            totalBytes,
            outputLines: 0,
            outputBytes: 0,
            lastLinePartial: false,
            firstLineExceedsLimit: true,
            maxLines,
            maxBytes,
        };
    }
    // Collect complete lines that fit
    const outputLinesArr = [];
    let outputBytesCount = 0;
    let truncatedBy = "lines";
    for (let i = 0; i < lines.length && i < maxLines; i++) {
        const line = lines[i];
        const lineBytes = utf8ByteLength(line) + (i > 0 ? 1 : 0); // +1 for newline
        if (outputBytesCount + lineBytes > maxBytes) {
            truncatedBy = "bytes";
            break;
        }
        outputLinesArr.push(line);
        outputBytesCount += lineBytes;
    }
    // If we exited due to line limit
    if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
        truncatedBy = "lines";
    }
    const outputContent = outputLinesArr.join("\n");
    const finalOutputBytes = utf8ByteLength(outputContent);
    return {
        content: outputContent,
        truncated: true,
        truncatedBy,
        totalLines,
        totalBytes,
        outputLines: outputLinesArr.length,
        outputBytes: finalOutputBytes,
        lastLinePartial: false,
        firstLineExceedsLimit: false,
        maxLines,
        maxBytes,
    };
}
/**
 * Truncate content from the tail (keep last N lines/bytes).
 * Suitable for bash output where you want to see the end (errors, final results).
 *
 * May return partial first line if the last line of original content exceeds byte limit.
 */
export function truncateTail(content, options = {}) {
    const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
    const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
    const totalBytes = utf8ByteLength(content);
    const lines = content.split("\n");
    if (lines.length > 1 && lines[lines.length - 1] === "")
        lines.pop();
    const totalLines = lines.length;
    // Check if no truncation needed
    if (totalLines <= maxLines && totalBytes <= maxBytes) {
        return {
            content,
            truncated: false,
            truncatedBy: null,
            totalLines,
            totalBytes,
            outputLines: totalLines,
            outputBytes: totalBytes,
            lastLinePartial: false,
            firstLineExceedsLimit: false,
            maxLines,
            maxBytes,
        };
    }
    // Work backwards from the end
    const outputLinesArr = [];
    let outputBytesCount = 0;
    let truncatedBy = "lines";
    let lastLinePartial = false;
    for (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) {
        const line = lines[i];
        const lineBytes = utf8ByteLength(line) + (outputLinesArr.length > 0 ? 1 : 0); // +1 for newline
        if (outputBytesCount + lineBytes > maxBytes) {
            truncatedBy = "bytes";
            // Edge case: if we haven't added ANY lines yet and this line exceeds maxBytes,
            // take the end of the line (partial)
            if (outputLinesArr.length === 0) {
                const truncatedLine = truncateStringToBytesFromEnd(line, maxBytes);
                outputLinesArr.unshift(truncatedLine);
                outputBytesCount = utf8ByteLength(truncatedLine);
                lastLinePartial = true;
            }
            break;
        }
        outputLinesArr.unshift(line);
        outputBytesCount += lineBytes;
    }
    // If we exited due to line limit
    if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
        truncatedBy = "lines";
    }
    const outputContent = outputLinesArr.join("\n");
    const finalOutputBytes = utf8ByteLength(outputContent);
    return {
        content: outputContent,
        truncated: true,
        truncatedBy,
        totalLines,
        totalBytes,
        outputLines: outputLinesArr.length,
        outputBytes: finalOutputBytes,
        lastLinePartial,
        firstLineExceedsLimit: false,
        maxLines,
        maxBytes,
    };
}
/**
 * Truncate a string to fit within a byte limit (from the end).
 * Handles multi-byte UTF-8 characters correctly.
 */
function truncateStringToBytesFromEnd(str, maxBytes) {
    if (maxBytes <= 0)
        return "";
    let outputBytes = 0;
    let start = str.length;
    let needsReplacement = false;
    for (let i = str.length; i > 0;) {
        let characterStart = i - 1;
        const code = str.charCodeAt(characterStart);
        let characterBytes;
        let unpairedSurrogate = false;
        if (code >= 0xdc00 && code <= 0xdfff && characterStart > 0) {
            const previous = str.charCodeAt(characterStart - 1);
            if (previous >= 0xd800 && previous <= 0xdbff) {
                characterStart--;
                characterBytes = 4;
            }
            else {
                characterBytes = 3;
                unpairedSurrogate = true;
            }
        }
        else if (code >= 0xd800 && code <= 0xdfff) {
            characterBytes = 3;
            unpairedSurrogate = true;
        }
        else {
            characterBytes = code <= 0x7f ? 1 : code <= 0x7ff ? 2 : 3;
        }
        if (outputBytes + characterBytes > maxBytes)
            break;
        outputBytes += characterBytes;
        start = characterStart;
        needsReplacement ||= unpairedSurrogate;
        i = characterStart;
    }
    const output = str.slice(start);
    return needsReplacement ? replaceUnpairedSurrogates(output) : output;
}
/**
 * Truncate a single line to max characters, adding [truncated] suffix.
 * Used for grep match lines.
 */
export function truncateLine(line, maxChars = GREP_MAX_LINE_LENGTH) {
    if (line.length <= maxChars) {
        return { text: line, wasTruncated: false };
    }
    return { text: `${line.slice(0, maxChars)}... [truncated]`, wasTruncated: true };
}
