import { logger } from "../util/logger.js";
import { generateId } from "../util/random.js";
import { MessageItemType } from "../api/types.js";
import { canonicalWeixinPeerId, normalizeWeixinAccountId } from "../auth/weixin-account-id.js";
import { resolveWeixinRootDir } from "../storage/state-dir.js";
import fs from "node:fs";
import path from "node:path";
//#region extensions/weixin/src/messaging/inbound.ts
/**
* contextToken is issued per-message by the Weixin getupdates API and must
* be echoed verbatim in every outbound send. The in-memory map is the primary
* lookup; a disk-backed file per account ensures tokens survive gateway restarts.
*
* `sendTo` is the last inbound `from_user_id` verbatim. ilink often requires `to_user_id`
* in sendmessage to match that string exactly (session/cron peer ids are sanitized).
*/
const contextTokenStore = /* @__PURE__ */ new Map();
function parsePersistedPeerVal(val) {
	if (typeof val === "string" && val.trim()) return { token: val.trim() };
	if (val && typeof val === "object" && "token" in val) {
		const o = val;
		if (typeof o.token !== "string" || !o.token.trim()) return null;
		const sendTo = typeof o.sendTo === "string" && o.sendTo.trim() ? o.sendTo.trim() : void 0;
		return {
			token: o.token.trim(),
			...sendTo ? { sendTo } : {}
		};
	}
	return null;
}
/**
* Session keys use {@link canonicalWeixinPeerId} (same as `buildSessionKey`); cron `delivery.to`
* and outbound `ctx.to` use that shape. ilink `from_user_id` is often `…@im.wechat` while the
* session peer is `…-im-wechat`. Normalize so context_token cache keys match lookups.
*/
function normalizeIlinkUserIdForContext(userId) {
	return canonicalWeixinPeerId(userId);
}
/** ilink may use a shorter openid in one path and a suffixed id in another; treat as same peer when unambiguous. */
function ilinkPeerKeysLikelySame(storedKey, queryPeer) {
	const a = normalizeIlinkUserIdForContext(storedKey);
	const b = normalizeIlinkUserIdForContext(queryPeer);
	if (a === b) return true;
	if (a.length < 12 || b.length < 12) return false;
	return a.startsWith(b) || b.startsWith(a);
}
function contextTokenKey(accountId, userId) {
	return `${normalizeWeixinAccountId(accountId)}:${normalizeIlinkUserIdForContext(userId)}`;
}
function canonicalContextTokenFilePath(accountId) {
	const norm = normalizeWeixinAccountId(accountId);
	return path.join(resolveWeixinRootDir(), "accounts", `${norm}.context-tokens.json`);
}
/** Persist all context tokens for a given account to disk. */
function persistContextTokens(accountId) {
	const prefix = `${normalizeWeixinAccountId(accountId)}:`;
	const tokens = {};
	for (const [k, v] of contextTokenStore) if (k.startsWith(prefix)) {
		const peerSuffix = k.slice(prefix.length);
		if (v.sendTo?.trim()) tokens[peerSuffix] = {
			token: v.token,
			sendTo: v.sendTo.trim()
		};
		else tokens[peerSuffix] = v.token;
	}
	const filePath = canonicalContextTokenFilePath(accountId);
	try {
		const dir = path.dirname(filePath);
		fs.mkdirSync(dir, { recursive: true });
		fs.writeFileSync(filePath, JSON.stringify(tokens, null, 0), "utf-8");
	} catch (err) {
		logger.warn(`persistContextTokens: failed to write ${filePath}: ${String(err)}`);
	}
}
/**
* Restore persisted context tokens for an account into the in-memory map.
* Called once during gateway startAccount to survive restarts.
*/
function restoreContextTokens(accountId) {
	let count = 0;
	const norm = normalizeWeixinAccountId(accountId);
	const filePath = canonicalContextTokenFilePath(accountId);
	try {
		if (fs.existsSync(filePath)) {
			const raw = fs.readFileSync(filePath, "utf-8");
			const tokens = JSON.parse(raw);
			for (const [userId, val] of Object.entries(tokens)) {
				const entry = parsePersistedPeerVal(val);
				if (entry?.token) {
					contextTokenStore.set(contextTokenKey(norm, userId), entry);
					count++;
				}
			}
		}
	} catch (err) {
		logger.warn(`restoreContextTokens: failed to read ${filePath}: ${String(err)}`);
	}
	if (count > 0) logger.info(`restoreContextTokens: restored ${count} tokens for account=${norm}`);
}
/** Remove all context tokens for a given account (memory + disk). */
function clearContextTokensForAccount(accountId) {
	const norm = normalizeWeixinAccountId(accountId);
	const prefix = `${norm}:`;
	for (const k of [...contextTokenStore.keys()]) if (k.startsWith(prefix)) contextTokenStore.delete(k);
	try {
		const filePath = canonicalContextTokenFilePath(accountId);
		if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
	} catch (err) {
		logger.warn(`clearContextTokensForAccount: failed to remove context tokens file: ${String(err)}`);
	}
	logger.info(`clearContextTokensForAccount: cleared tokens for account=${norm}`);
}
function tryHydratePeerEntryFromDisk(accountId, userId) {
	const peer = normalizeIlinkUserIdForContext(userId);
	const norm = normalizeWeixinAccountId(accountId);
	const filePath = canonicalContextTokenFilePath(accountId);
	if (!fs.existsSync(filePath)) return void 0;
	try {
		const raw = fs.readFileSync(filePath, "utf-8");
		const tokens = JSON.parse(raw);
		for (const [jsonKey, val] of Object.entries(tokens)) {
			const entry = parsePersistedPeerVal(val);
			if (!entry?.token) continue;
			if (!ilinkPeerKeysLikelySame(jsonKey, peer)) continue;
			contextTokenStore.set(contextTokenKey(norm, peer), entry);
			persistContextTokens(accountId);
			return entry;
		}
	} catch (err) {
		logger.debug(`tryHydratePeerEntryFromDisk: ${filePath}: ${String(err)}`);
	}
}
function getWeixinContextPeerEntry(accountId, userId) {
	const k = contextTokenKey(accountId, userId);
	let e = contextTokenStore.get(k);
	if (e?.token) return e;
	const hydrated = tryHydratePeerEntryFromDisk(accountId, userId);
	if (hydrated) return hydrated;
	return contextTokenStore.get(k);
}
/** Last inbound `from_user_id` for API `to_user_id` (verbatim), when known. */
function getWeixinOutboundSendUserId(accountId, userId) {
	return getWeixinContextPeerEntry(accountId, userId)?.sendTo?.trim() || void 0;
}
/** Store a context token for a given account+user pair (memory + disk). */
function setContextToken(accountId, userId, token, options) {
	const k = contextTokenKey(accountId, userId);
	const prev = contextTokenStore.get(k);
	const sendTo = options && options.sendToUserId !== void 0 ? options.sendToUserId.trim() || void 0 : prev?.sendTo;
	const next = {
		token,
		...sendTo ? { sendTo } : {}
	};
	logger.debug(`setContextToken: key=${k} sendTo=${sendTo ? "yes" : "no"}`);
	contextTokenStore.set(k, next);
	persistContextTokens(accountId);
}
/** Retrieve the cached context token for a given account+user pair. */
function getContextToken(accountId, userId) {
	const k = contextTokenKey(accountId, userId);
	const e = getWeixinContextPeerEntry(accountId, userId);
	logger.debug(`getContextToken: key=${k} found=${e?.token !== void 0} storeSize=${contextTokenStore.size}`);
	return e?.token;
}
/**
* Find all accountIds that have an active contextToken for the given userId.
* Used to infer the sending bot account from the recipient address when
* accountId is not explicitly provided (e.g. cron delivery).
*
* Returns all matching accountIds (not just the first) so the caller can
* detect ambiguity when multiple accounts have sessions with the same user.
*/
function findAccountIdsByContextToken(accountIds, userId) {
	return accountIds.filter((id) => contextTokenStore.has(contextTokenKey(id, userId)));
}
/** In-memory entries whose peer id matches (exact or ilink prefix/suffix variant). */
function findContextTokenEntriesByPeer(peerUserId) {
	const peer = normalizeIlinkUserIdForContext(peerUserId);
	const out = [];
	for (const [k, entry] of contextTokenStore) {
		if (!entry?.token?.trim()) continue;
		const idx = k.indexOf(":");
		if (idx <= 0 || idx >= k.length - 1) continue;
		const accountId = k.slice(0, idx);
		if (!ilinkPeerKeysLikelySame(k.slice(idx + 1), peer)) continue;
		if (accountId) out.push({
			accountId,
			token: entry.token,
			...entry.sendTo?.trim() ? { sendTo: entry.sendTo.trim() } : {}
		});
	}
	return out;
}
/**
* When the preferred account id does not match the token filename, scan every
* `accounts/*.context-tokens.json` for this peer (case-insensitive user key).
*/
function tryHydrateAnyAccountContextTokenFromDisk(peerUserId) {
	const peer = normalizeIlinkUserIdForContext(peerUserId);
	const accountsDir = path.join(resolveWeixinRootDir(), "accounts");
	if (!fs.existsSync(accountsDir)) return void 0;
	const hits = [];
	let names;
	try {
		names = fs.readdirSync(accountsDir);
	} catch {
		return;
	}
	const suffix = ".context-tokens.json";
	for (const name of names) {
		if (!name.endsWith(suffix)) continue;
		const accountFromFile = name.slice(0, -20);
		const filePath = path.join(accountsDir, name);
		try {
			const raw = fs.readFileSync(filePath, "utf-8");
			const tokens = JSON.parse(raw);
			for (const [jsonKey, val] of Object.entries(tokens)) {
				const entry = parsePersistedPeerVal(val);
				if (!entry?.token) continue;
				if (!ilinkPeerKeysLikelySame(jsonKey, peer)) continue;
				hits.push({
					accountId: normalizeWeixinAccountId(accountFromFile),
					token: entry.token,
					...entry.sendTo?.trim() ? { sendTo: entry.sendTo.trim() } : {}
				});
			}
		} catch {}
	}
	if (hits.length === 0) return void 0;
	const byAccount = /* @__PURE__ */ new Map();
	for (const h of hits) if (!byAccount.has(h.accountId)) byAccount.set(h.accountId, {
		token: h.token,
		sendTo: h.sendTo
	});
	if (byAccount.size > 1) return void 0;
	const [accountId, hit] = [...byAccount.entries()][0];
	setContextToken(accountId, peer, hit.token, hit.sendTo ? { sendToUserId: hit.sendTo } : void 0);
	return {
		accountId,
		token: hit.token,
		sendTo: hit.sendTo
	};
}
function generateMessageSid() {
	return generateId("weixin");
}
/** Returns true if the message item is a media type (image, video, file, or voice). */
function isMediaItem(item) {
	return item.type === MessageItemType.IMAGE || item.type === MessageItemType.VIDEO || item.type === MessageItemType.FILE || item.type === MessageItemType.VOICE;
}
function bodyFromItemList(itemList) {
	if (!itemList?.length) return "";
	for (const item of itemList) {
		if (item.type === MessageItemType.TEXT && item.text_item?.text != null) {
			const text = String(item.text_item.text);
			const ref = item.ref_msg;
			if (!ref) return text;
			if (ref.message_item && isMediaItem(ref.message_item)) return text;
			const parts = [];
			if (ref.title) parts.push(ref.title);
			if (ref.message_item) {
				const refBody = bodyFromItemList([ref.message_item]);
				if (refBody) parts.push(refBody);
			}
			if (!parts.length) return text;
			return `[引用: ${parts.join(" | ")}]\n${text}`;
		}
		if (item.type === MessageItemType.VOICE && item.voice_item?.text) return item.voice_item.text;
	}
	return "";
}
/**
* Convert a WeixinMessage from getUpdates to the inbound MsgContext for the core pipeline.
* Media: only pass MediaPath (local file, after CDN download + decrypt).
* We never pass MediaUrl — the upstream CDN URL is encrypted/auth-only.
* Priority when multiple media types present: image > video > file > voice.
*/
function weixinMessageToMsgContext(msg, accountId, opts) {
	const from_user_id = msg.from_user_id ?? "";
	const ctx = {
		Body: bodyFromItemList(msg.item_list),
		From: from_user_id,
		To: from_user_id,
		AccountId: accountId,
		OriginatingChannel: "weixin",
		OriginatingTo: from_user_id,
		MessageSid: generateMessageSid(),
		Timestamp: msg.create_time_ms,
		Provider: "weixin",
		ChatType: "direct"
	};
	if (msg.context_token) ctx.context_token = msg.context_token;
	if (opts?.decryptedPicPath) {
		ctx.MediaPath = opts.decryptedPicPath;
		ctx.MediaType = "image/*";
	} else if (opts?.decryptedVideoPath) {
		ctx.MediaPath = opts.decryptedVideoPath;
		ctx.MediaType = "video/mp4";
	} else if (opts?.decryptedFilePath) {
		ctx.MediaPath = opts.decryptedFilePath;
		ctx.MediaType = opts.fileMediaType ?? "application/octet-stream";
	} else if (opts?.decryptedVoicePath) {
		ctx.MediaPath = opts.decryptedVoicePath;
		ctx.MediaType = opts.voiceMediaType ?? "audio/wav";
	}
	return ctx;
}
/** Extract the context_token from an inbound WeixinMsgContext. */
function getContextTokenFromMsgContext(ctx) {
	return ctx.context_token;
}
//#endregion
export { clearContextTokensForAccount, findAccountIdsByContextToken, findContextTokenEntriesByPeer, getContextToken, getContextTokenFromMsgContext, getWeixinOutboundSendUserId, isMediaItem, restoreContextTokens, setContextToken, tryHydrateAnyAccountContextTokenFromDisk, weixinMessageToMsgContext };

//# sourceMappingURL=inbound.js.map