const DEFAULT_TIMEOUT_MS = 15_000;
const MAX_TIMEOUT_MS = 30_000;
const DEFAULT_MAX_BYTES = 64 * 1024;
const HARD_MAX_BYTES = 512 * 1024;
const FORBIDDEN_HEADERS = new Set([
    "connection",
    "content-length",
    "cookie",
    "host",
    "proxy-authorization",
    "transfer-encoding",
]);
function clampNumber(raw, fallback, min, max) {
    if (typeof raw !== "number" || !Number.isFinite(raw))
        return fallback;
    return Math.min(Math.max(Math.floor(raw), min), max);
}
function normalizeUrl(rawUrl) {
    if (typeof rawUrl !== "string" || !rawUrl.trim()) {
        return "url is required";
    }
    let url;
    try {
        url = new URL(rawUrl);
    }
    catch {
        return `invalid URL: ${rawUrl}`;
    }
    if (url.protocol !== "http:" && url.protocol !== "https:") {
        return `unsupported URL protocol '${url.protocol}' (only http/https are allowed)`;
    }
    if (url.username || url.password) {
        return "credentials in URLs are not allowed";
    }
    return url;
}
function normalizeHeaders(raw) {
    if (raw == null)
        return {};
    if (typeof raw !== "object" || Array.isArray(raw)) {
        return "headers must be an object of string values";
    }
    const headers = {};
    for (const [key, value] of Object.entries(raw)) {
        const lower = key.toLowerCase();
        if (FORBIDDEN_HEADERS.has(lower)) {
            return `header '${key}' is not allowed`;
        }
        if (typeof value !== "string") {
            return `header '${key}' must be a string`;
        }
        headers[key] = value;
    }
    return headers;
}
async function readResponseText(resp, maxBytes) {
    const reader = resp.body?.getReader();
    if (!reader) {
        const buf = Buffer.from(await resp.arrayBuffer());
        if (buf.byteLength > maxBytes) {
            return `response exceeded maxBytes (${buf.byteLength} > ${maxBytes})`;
        }
        return { text: buf.toString("utf-8"), bytes: buf.byteLength };
    }
    const chunks = [];
    let total = 0;
    while (true) {
        const { done, value } = await reader.read();
        if (done)
            break;
        const chunk = Buffer.from(value);
        total += chunk.byteLength;
        if (total > maxBytes) {
            await reader.cancel().catch(() => { });
            return `response exceeded maxBytes (${total} > ${maxBytes})`;
        }
        chunks.push(chunk);
    }
    const body = Buffer.concat(chunks, total);
    return { text: body.toString("utf-8"), bytes: total };
}
function formatBody(text, contentType) {
    if (!contentType.toLowerCase().includes("json"))
        return text;
    try {
        return JSON.stringify(JSON.parse(text), null, 2);
    }
    catch {
        return text;
    }
}
export async function fetchTextResource(rawUrl, opts = {}) {
    const url = normalizeUrl(rawUrl);
    if (typeof url === "string")
        return { ok: false, error: url };
    const headers = normalizeHeaders(opts.headers);
    if (typeof headers === "string")
        return { ok: false, error: headers };
    const maxBytes = clampNumber(opts.maxBytes, DEFAULT_MAX_BYTES, 1, HARD_MAX_BYTES);
    const timeoutMs = clampNumber(opts.timeoutMs, DEFAULT_TIMEOUT_MS, 1, MAX_TIMEOUT_MS);
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), timeoutMs);
    try {
        const resp = await fetch(url.toString(), {
            method: "GET",
            headers,
            redirect: "follow",
            signal: controller.signal,
        });
        const contentType = resp.headers.get("content-type") ?? "";
        const body = await readResponseText(resp, maxBytes);
        if (typeof body === "string") {
            return {
                ok: false,
                status: resp.status,
                statusText: resp.statusText,
                finalUrl: resp.url,
                contentType,
                error: body,
            };
        }
        return {
            ok: resp.ok,
            status: resp.status,
            statusText: resp.statusText,
            finalUrl: resp.url,
            contentType,
            bytes: body.bytes,
            text: formatBody(body.text, contentType),
            error: resp.ok ? undefined : `HTTP ${resp.status} ${resp.statusText}`,
        };
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        return { ok: false, error: msg };
    }
    finally {
        clearTimeout(timeout);
    }
}
export async function executeWebFetchAction(args) {
    const method = args.method == null ? "GET" : args.method;
    if (method !== "GET") {
        return "Error: web_fetch only supports method='GET'.";
    }
    const result = await fetchTextResource(args.url, {
        headers: args.headers,
        maxBytes: args.maxBytes,
        timeoutMs: args.timeoutMs,
    });
    if (!result.ok) {
        const status = result.status != null ? `HTTP ${result.status} ${result.statusText ?? ""}: ` : "";
        return `Error: web_fetch failed: ${status}${result.error ?? "unknown error"}`;
    }
    return [
        `URL: ${result.finalUrl}`,
        `Status: ${result.status} ${result.statusText}`,
        `Content-Type: ${result.contentType || "(none)"}`,
        `Bytes: ${result.bytes ?? 0}`,
        "",
        result.text ?? "",
    ].join("\n");
}
//# sourceMappingURL=web-fetch-executor.js.map