import { logger } from "../util/logger.js";
import { redactBody, redactUrl } from "../util/redact.js";
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { Agent, fetch as fetch$1 } from "undici";
//#region extensions/weixin/src/api/api.ts
function readPackageJson() {
	try {
		const dir = path.dirname(fileURLToPath(import.meta.url));
		const pkgPath = path.resolve(dir, "..", "..", "package.json");
		return JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
	} catch {
		return {};
	}
}
const pkg = readPackageJson();
const CHANNEL_VERSION = pkg.version ?? "unknown";
/** iLink-App-Id: from package.json `ilink_appid`. */
const ILINK_APP_ID = pkg.ilink_appid ?? "";
/**
* iLink-App-ClientVersion: uint32 encoded as 0x00MMNNPP
* High 8 bits fixed to 0; remaining bits: major<<16 | minor<<8 | patch.
*/
function buildClientVersion(version) {
	const parts = version.split(".").map((p) => parseInt(p, 10));
	const major = parts[0] ?? 0;
	const minor = parts[1] ?? 0;
	const patch = parts[2] ?? 0;
	return (major & 255) << 16 | (minor & 255) << 8 | patch & 255;
}
const ILINK_APP_CLIENT_VERSION = buildClientVersion(pkg.version ?? "0.0.0");
/** Build the `base_info` payload included in every API request. */
function buildBaseInfo() {
	return { channel_version: CHANNEL_VERSION };
}
/** Default timeout for long-poll getUpdates requests. */
const DEFAULT_LONG_POLL_TIMEOUT_MS = 35e3;
/** Extra time beyond client long-poll timeout for undici `headersTimeout` (server may idle before first byte). */
const LONG_POLL_HEADERS_SLACK_MS = 15e3;
/** Default timeout for regular API requests (sendMessage, getUploadUrl). */
const DEFAULT_API_TIMEOUT_MS = 15e3;
/** Default timeout for lightweight API requests (getConfig, sendTyping). */
const DEFAULT_CONFIG_TIMEOUT_MS = 1e4;
function ensureTrailingSlash(url) {
	return url.endsWith("/") ? url : `${url}/`;
}
function isUndiciHeadersTimeout(err) {
	return typeof err === "object" && err !== null && "code" in err && err.code === "UND_ERR_HEADERS_TIMEOUT";
}
/** X-WECHAT-UIN header: random uint32 -> decimal string -> base64. */
function randomWechatUin() {
	const uint32 = crypto.randomBytes(4).readUInt32BE(0);
	return Buffer.from(String(uint32), "utf-8").toString("base64");
}
/** Build headers shared by GET and POST requests. */
function buildCommonHeaders(routeTag) {
	const headers = {
		"iLink-App-Id": ILINK_APP_ID,
		"iLink-App-ClientVersion": String(ILINK_APP_CLIENT_VERSION)
	};
	const tag = routeTag?.trim();
	if (tag) headers.SKRouteTag = tag;
	return headers;
}
function buildHeaders(opts) {
	const headers = {
		"Content-Type": "application/json",
		AuthorizationType: "ilink_bot_token",
		"Content-Length": String(Buffer.byteLength(opts.body, "utf-8")),
		"X-WECHAT-UIN": randomWechatUin(),
		...buildCommonHeaders(opts.routeTag)
	};
	if (opts.token?.trim()) headers.Authorization = `Bearer ${opts.token.trim()}`;
	logger.debug(`requestHeaders: ${JSON.stringify({
		...headers,
		Authorization: headers.Authorization ? "Bearer ***" : void 0
	})}`);
	return headers;
}
/**
* GET fetch wrapper (QR login and other GET endpoints).
*/
async function apiGetFetch(params) {
	const base = ensureTrailingSlash(params.baseUrl);
	const url = new URL(params.endpoint, base);
	const hdrs = buildCommonHeaders(params.routeTag);
	logger.debug(`GET ${redactUrl(url.toString())}`);
	const timeoutMs = params.timeoutMs;
	const controller = timeoutMs != null && timeoutMs > 0 ? new AbortController() : void 0;
	const t = controller != null && timeoutMs != null ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
	try {
		const res = await fetch(url.toString(), {
			method: "GET",
			headers: hdrs,
			...controller ? { signal: controller.signal } : {},
			keepalive: true
		});
		if (t !== void 0) clearTimeout(t);
		const rawText = await res.text();
		logger.debug(`${params.label} status=${res.status} raw=${redactBody(rawText)}`);
		if (!res.ok) throw new Error(`${params.label} ${res.status}: ${rawText}`);
		return rawText;
	} catch (err) {
		if (t !== void 0) clearTimeout(t);
		throw err;
	}
}
/**
* Common POST fetch wrapper: POST JSON to a Weixin API endpoint with timeout + abort.
*/
async function apiPostFetch(params) {
	const base = ensureTrailingSlash(params.baseUrl);
	const url = new URL(params.endpoint, base);
	const hdrs = buildHeaders({
		token: params.token,
		body: params.body,
		routeTag: params.routeTag
	});
	logger.debug(`POST ${redactUrl(url.toString())} body=${redactBody(params.body)}`);
	const controller = new AbortController();
	const t = setTimeout(() => controller.abort(), params.timeoutMs);
	const signal = params.abortSignal != null ? AbortSignal.any([controller.signal, params.abortSignal]) : controller.signal;
	const keepalive = params.keepalive ?? true;
	try {
		const init = {
			method: "POST",
			headers: hdrs,
			body: params.body,
			signal,
			keepalive
		};
		if (params.dispatcher) init.dispatcher = params.dispatcher;
		const res = await fetch$1(url.toString(), init);
		clearTimeout(t);
		const rawText = await res.text();
		logger.debug(`${params.label} status=${res.status} raw=${redactBody(rawText)}`);
		if (!res.ok) throw new Error(`${params.label} ${res.status}: ${rawText}`);
		return rawText;
	} catch (err) {
		clearTimeout(t);
		throw err;
	}
}
/**
* Long-poll getUpdates. Server should hold the request until new messages or timeout.
*
* On client-side timeout (no server response within timeoutMs), returns an empty response
* with ret=0 so the caller can simply retry. This is normal for long-poll.
*/
async function getUpdates(params) {
	const timeout = params.timeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS;
	const headersTimeout = timeout + LONG_POLL_HEADERS_SLACK_MS;
	const agent = new Agent({
		connectTimeout: 6e4,
		headersTimeout,
		bodyTimeout: headersTimeout
	});
	try {
		const rawText = await apiPostFetch({
			baseUrl: params.baseUrl,
			endpoint: "ilink/bot/getupdates",
			body: JSON.stringify({
				get_updates_buf: params.get_updates_buf ?? "",
				base_info: buildBaseInfo()
			}),
			token: params.token,
			timeoutMs: timeout,
			label: "getUpdates",
			routeTag: params.routeTag,
			dispatcher: agent,
			keepalive: true,
			abortSignal: params.abortSignal
		});
		return JSON.parse(rawText);
	} catch (err) {
		if (params.abortSignal?.aborted && err instanceof Error && err.name === "AbortError") throw err;
		if (err instanceof Error && err.name === "AbortError") {
			logger.debug(`getUpdates: client-side timeout after ${timeout}ms, returning empty response`);
			return {
				ret: 0,
				msgs: [],
				get_updates_buf: params.get_updates_buf
			};
		}
		if (isUndiciHeadersTimeout(err)) {
			logger.debug(`getUpdates: undici headers timeout (UND_ERR_HEADERS_TIMEOUT), returning empty response`);
			return {
				ret: 0,
				msgs: [],
				get_updates_buf: params.get_updates_buf ?? ""
			};
		}
		throw err;
	} finally {
		try {
			await agent.close();
		} catch {}
	}
}
/** Get a pre-signed CDN upload URL for a file. */
async function getUploadUrl(params) {
	const rawText = await apiPostFetch({
		baseUrl: params.baseUrl,
		endpoint: "ilink/bot/getuploadurl",
		body: JSON.stringify({
			filekey: params.filekey,
			media_type: params.media_type,
			to_user_id: params.to_user_id,
			rawsize: params.rawsize,
			rawfilemd5: params.rawfilemd5,
			filesize: params.filesize,
			thumb_rawsize: params.thumb_rawsize,
			thumb_rawfilemd5: params.thumb_rawfilemd5,
			thumb_filesize: params.thumb_filesize,
			no_need_thumb: params.no_need_thumb,
			aeskey: params.aeskey,
			base_info: buildBaseInfo()
		}),
		token: params.token,
		timeoutMs: params.timeoutMs ?? DEFAULT_API_TIMEOUT_MS,
		label: "getUploadUrl",
		routeTag: params.routeTag
	});
	return JSON.parse(rawText);
}
function assertSendMessageOk(resp) {
	if (typeof resp.ret === "number" && resp.ret !== 0) throw new Error(`sendMessage ret=${resp.ret} errmsg=${resp.errmsg ?? ""}`);
	if (typeof resp.errcode === "number" && resp.errcode !== 0) throw new Error(`sendMessage errcode=${resp.errcode} errmsg=${resp.errmsg ?? ""}`);
}
/** Send a single message downstream. Returns parsed body (incl. refreshed `context_token` when present). */
async function sendMessage(params) {
	const t = (await apiPostFetch({
		baseUrl: params.baseUrl,
		endpoint: "ilink/bot/sendmessage",
		body: JSON.stringify({
			...params.body,
			base_info: buildBaseInfo()
		}),
		token: params.token,
		timeoutMs: params.timeoutMs ?? DEFAULT_API_TIMEOUT_MS,
		label: "sendMessage",
		routeTag: params.routeTag
	})).trim();
	if (!t.startsWith("{")) return {};
	let resp;
	try {
		resp = JSON.parse(t);
	} catch (e) {
		throw new Error(`sendMessage invalid JSON: ${String(e)}`);
	}
	assertSendMessageOk(resp);
	return resp;
}
/** Fetch bot config (includes typing_ticket) for a given user. */
async function getConfig(params) {
	const rawText = await apiPostFetch({
		baseUrl: params.baseUrl,
		endpoint: "ilink/bot/getconfig",
		body: JSON.stringify({
			ilink_user_id: params.ilinkUserId,
			context_token: params.contextToken,
			base_info: buildBaseInfo()
		}),
		token: params.token,
		timeoutMs: params.timeoutMs ?? DEFAULT_CONFIG_TIMEOUT_MS,
		label: "getConfig",
		routeTag: params.routeTag
	});
	return JSON.parse(rawText);
}
/** Send a typing indicator to a user. */
async function sendTyping(params) {
	await apiPostFetch({
		baseUrl: params.baseUrl,
		endpoint: "ilink/bot/sendtyping",
		body: JSON.stringify({
			...params.body,
			base_info: buildBaseInfo()
		}),
		token: params.token,
		timeoutMs: params.timeoutMs ?? DEFAULT_CONFIG_TIMEOUT_MS,
		label: "sendTyping",
		routeTag: params.routeTag
	});
}
//#endregion
export { apiGetFetch, buildBaseInfo, getConfig, getUpdates, getUploadUrl, sendMessage, sendTyping };

//# sourceMappingURL=api.js.map