import os from "node:os";
import { approveDevicePairing, listDevicePairing } from "openclaw/plugin-sdk";
import qrcode from "qrcode-terminal";

//#region extensions/device-pair/index.ts
function renderQrAscii(data) {
	return new Promise((resolve) => {
		qrcode.generate(data, { small: true }, (output) => {
			resolve(output);
		});
	});
}
const DEFAULT_GATEWAY_PORT = 18789;
function normalizeUrl(raw, schemeFallback) {
	const candidate = raw.trim();
	if (!candidate) return null;
	const parsedUrl = parseNormalizedGatewayUrl(candidate);
	if (parsedUrl) return parsedUrl;
	const hostPort = candidate.split("/", 1)[0]?.trim() ?? "";
	return hostPort ? `${schemeFallback}://${hostPort}` : null;
}
function parseNormalizedGatewayUrl(raw) {
	try {
		const parsed = new URL(raw);
		const scheme = parsed.protocol.slice(0, -1);
		const normalizedScheme = scheme === "http" ? "ws" : scheme === "https" ? "wss" : scheme;
		if (!(normalizedScheme === "ws" || normalizedScheme === "wss")) return null;
		if (!parsed.hostname) return null;
		return `${normalizedScheme}://${parsed.hostname}${parsed.port ? `:${parsed.port}` : ""}`;
	} catch {
		return null;
	}
}
function parsePositiveInteger(raw) {
	if (!raw) return null;
	const parsed = Number.parseInt(raw, 10);
	return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
}
function resolveGatewayPort(cfg) {
	const envPort = parsePositiveInteger(process.env.OPENCLAW_GATEWAY_PORT?.trim()) ?? parsePositiveInteger(process.env.CLAWDBOT_GATEWAY_PORT?.trim());
	if (envPort) return envPort;
	const configPort = cfg.gateway?.port;
	if (typeof configPort === "number" && Number.isFinite(configPort) && configPort > 0) return configPort;
	return DEFAULT_GATEWAY_PORT;
}
function resolveScheme(cfg, opts) {
	if (opts?.forceSecure) return "wss";
	return cfg.gateway?.tls?.enabled === true ? "wss" : "ws";
}
function isPrivateIPv4(address) {
	const parts = address.split(".");
	if (parts.length != 4) return false;
	const octets = parts.map((part) => Number.parseInt(part, 10));
	if (octets.some((value) => !Number.isFinite(value) || value < 0 || value > 255)) return false;
	const [a, b] = octets;
	if (a === 10) return true;
	if (a === 172 && b >= 16 && b <= 31) return true;
	if (a === 192 && b === 168) return true;
	return false;
}
function isTailnetIPv4(address) {
	const parts = address.split(".");
	if (parts.length !== 4) return false;
	const octets = parts.map((part) => Number.parseInt(part, 10));
	if (octets.some((value) => !Number.isFinite(value) || value < 0 || value > 255)) return false;
	const [a, b] = octets;
	return a === 100 && b >= 64 && b <= 127;
}
function pickMatchingIPv4(predicate) {
	const nets = os.networkInterfaces();
	for (const entries of Object.values(nets)) {
		if (!entries) continue;
		for (const entry of entries) {
			const family = entry?.family;
			const isIpv4 = family === "IPv4" || String(family) === "4";
			if (!entry || entry.internal || !isIpv4) continue;
			const address = entry.address?.trim() ?? "";
			if (!address) continue;
			if (predicate(address)) return address;
		}
	}
	return null;
}
function pickLanIPv4() {
	return pickMatchingIPv4(isPrivateIPv4);
}
function pickTailnetIPv4() {
	return pickMatchingIPv4(isTailnetIPv4);
}
async function resolveTailnetHost(api) {
	for (const candidate of ["tailscale", "/Applications/Tailscale.app/Contents/MacOS/Tailscale"]) try {
		const result = await api.runtime.system.runCommandWithTimeout([
			candidate,
			"status",
			"--json"
		], { timeoutMs: 5e3 });
		if (result.code !== 0) continue;
		const raw = result.stdout.trim();
		if (!raw) continue;
		const parsed = parsePossiblyNoisyJsonObject(raw);
		const self = typeof parsed.Self === "object" && parsed.Self !== null ? parsed.Self : void 0;
		const dns = typeof self?.DNSName === "string" ? self.DNSName : void 0;
		if (dns && dns.length > 0) return dns.replace(/\.$/, "");
		const ips = Array.isArray(self?.TailscaleIPs) ? self?.TailscaleIPs : [];
		if (ips.length > 0) return ips[0] ?? null;
	} catch {
		continue;
	}
	return null;
}
function parsePossiblyNoisyJsonObject(raw) {
	const start = raw.indexOf("{");
	const end = raw.lastIndexOf("}");
	if (start === -1 || end <= start) return {};
	try {
		return JSON.parse(raw.slice(start, end + 1));
	} catch {
		return {};
	}
}
function resolveAuth(cfg) {
	const mode = cfg.gateway?.auth?.mode;
	const token = pickFirstDefined([
		process.env.OPENCLAW_GATEWAY_TOKEN,
		process.env.CLAWDBOT_GATEWAY_TOKEN,
		cfg.gateway?.auth?.token
	]) ?? void 0;
	const password = pickFirstDefined([
		process.env.OPENCLAW_GATEWAY_PASSWORD,
		process.env.CLAWDBOT_GATEWAY_PASSWORD,
		cfg.gateway?.auth?.password
	]) ?? void 0;
	if (mode === "token" || mode === "password") return resolveRequiredAuth(mode, {
		token,
		password
	});
	if (token) return {
		token,
		label: "token"
	};
	if (password) return {
		password,
		label: "password"
	};
	return { error: "Gateway auth is not configured (no token or password)." };
}
function pickFirstDefined(candidates) {
	for (const value of candidates) {
		const trimmed = value?.trim();
		if (trimmed) return trimmed;
	}
	return null;
}
function resolveRequiredAuth(mode, values) {
	if (mode === "token") return values.token ? {
		token: values.token,
		label: "token"
	} : { error: "Gateway auth is set to token, but no token is configured." };
	return values.password ? {
		password: values.password,
		label: "password"
	} : { error: "Gateway auth is set to password, but no password is configured." };
}
async function resolveGatewayUrl(api) {
	const cfg = api.config;
	const pluginCfg = api.pluginConfig ?? {};
	const scheme = resolveScheme(cfg);
	const port = resolveGatewayPort(cfg);
	if (typeof pluginCfg.publicUrl === "string" && pluginCfg.publicUrl.trim()) {
		const url = normalizeUrl(pluginCfg.publicUrl, scheme);
		if (url) return {
			url,
			source: "plugins.entries.device-pair.config.publicUrl"
		};
		return { error: "Configured publicUrl is invalid." };
	}
	const tailscaleMode = cfg.gateway?.tailscale?.mode ?? "off";
	if (tailscaleMode === "serve" || tailscaleMode === "funnel") {
		const host = await resolveTailnetHost(api);
		if (!host) return { error: "Tailscale Serve is enabled, but MagicDNS could not be resolved." };
		return {
			url: `wss://${host}`,
			source: `gateway.tailscale.mode=${tailscaleMode}`
		};
	}
	const remoteUrl = cfg.gateway?.remote?.url;
	if (typeof remoteUrl === "string" && remoteUrl.trim()) {
		const url = normalizeUrl(remoteUrl, scheme);
		if (url) return {
			url,
			source: "gateway.remote.url"
		};
	}
	const bind = cfg.gateway?.bind ?? "loopback";
	if (bind === "custom") {
		const host = cfg.gateway?.customBindHost?.trim();
		if (host) return {
			url: `${scheme}://${host}:${port}`,
			source: "gateway.bind=custom"
		};
		return { error: "gateway.bind=custom requires gateway.customBindHost." };
	}
	if (bind === "tailnet") {
		const host = pickTailnetIPv4();
		if (host) return {
			url: `${scheme}://${host}:${port}`,
			source: "gateway.bind=tailnet"
		};
		return { error: "gateway.bind=tailnet set, but no tailnet IP was found." };
	}
	if (bind === "lan") {
		const host = pickLanIPv4();
		if (host) return {
			url: `${scheme}://${host}:${port}`,
			source: "gateway.bind=lan"
		};
		return { error: "gateway.bind=lan set, but no private LAN IP was found." };
	}
	return { error: "Gateway is only bound to loopback. Set gateway.bind=lan, enable tailscale serve, or configure plugins.entries.device-pair.config.publicUrl." };
}
function encodeSetupCode(payload) {
	const json = JSON.stringify(payload);
	return Buffer.from(json, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
}
function formatSetupReply(payload, authLabel) {
	return [
		"Pairing setup code generated.",
		"",
		"1) Open the iOS app → Settings → Gateway",
		"2) Paste the setup code below and tap Connect",
		"3) Back here, run /pair approve",
		"",
		"Setup code:",
		encodeSetupCode(payload),
		"",
		`Gateway: ${payload.url}`,
		`Auth: ${authLabel}`
	].join("\n");
}
function formatSetupInstructions() {
	return [
		"Pairing setup code generated.",
		"",
		"1) Open the iOS app → Settings → Gateway",
		"2) Paste the setup code from my next message and tap Connect",
		"3) Back here, run /pair approve"
	].join("\n");
}
function formatPendingRequests(pending) {
	if (pending.length === 0) return "No pending device pairing requests.";
	const lines = ["Pending device pairing requests:"];
	for (const req of pending) {
		const label = req.displayName?.trim() || req.deviceId;
		const platform = req.platform?.trim();
		const ip = req.remoteIp?.trim();
		const parts = [
			`- ${req.requestId}`,
			label ? `name=${label}` : null,
			platform ? `platform=${platform}` : null,
			ip ? `ip=${ip}` : null
		].filter(Boolean);
		lines.push(parts.join(" · "));
	}
	return lines.join("\n");
}
function register(api) {
	api.registerCommand({
		name: "pair",
		description: "Generate setup codes and approve device pairing requests.",
		acceptsArgs: true,
		handler: async (ctx) => {
			const tokens = (ctx.args?.trim() ?? "").split(/\s+/).filter(Boolean);
			const action = tokens[0]?.toLowerCase() ?? "";
			api.logger.info?.(`device-pair: /pair invoked channel=${ctx.channel} sender=${ctx.senderId ?? "unknown"} action=${action || "new"}`);
			if (action === "status" || action === "pending") return { text: formatPendingRequests((await listDevicePairing()).pending) };
			if (action === "approve") {
				const requested = tokens[1]?.trim();
				const list = await listDevicePairing();
				if (list.pending.length === 0) return { text: "No pending device pairing requests." };
				let pending;
				if (requested) if (requested.toLowerCase() === "latest") pending = [...list.pending].toSorted((a, b) => (b.ts ?? 0) - (a.ts ?? 0))[0];
				else pending = list.pending.find((entry) => entry.requestId === requested);
				else if (list.pending.length === 1) pending = list.pending[0];
				else return { text: `${formatPendingRequests(list.pending)}\n\nMultiple pending requests found. Approve one explicitly:
/pair approve <requestId>
Or approve the most recent:
/pair approve latest` };
				if (!pending) return { text: "Pairing request not found." };
				const approved = await approveDevicePairing(pending.requestId);
				if (!approved) return { text: "Pairing request not found." };
				const label = approved.device.displayName?.trim() || approved.device.deviceId;
				const platform = approved.device.platform?.trim();
				return { text: `✅ Paired ${label}${platform ? ` (${platform})` : ""}.` };
			}
			const auth = resolveAuth(api.config);
			if (auth.error) return { text: `Error: ${auth.error}` };
			const urlResult = await resolveGatewayUrl(api);
			if (!urlResult.url) return { text: `Error: ${urlResult.error ?? "Gateway URL unavailable."}` };
			const payload = {
				url: urlResult.url,
				token: auth.token,
				password: auth.password
			};
			if (action === "qr") {
				const qrAscii = await renderQrAscii(encodeSetupCode(payload));
				const authLabel = auth.label ?? "auth";
				const channel = ctx.channel;
				const target = ctx.senderId?.trim() || ctx.from?.trim() || ctx.to?.trim() || "";
				if (channel === "telegram" && target) try {
					const send = api.runtime?.channel?.telegram?.sendMessageTelegram;
					if (send) {
						await send(target, [
							"Scan this QR code with the OpenClaw iOS app:",
							"",
							"```",
							qrAscii,
							"```"
						].join("\n"), {
							...ctx.messageThreadId != null ? { messageThreadId: ctx.messageThreadId } : {},
							...ctx.accountId ? { accountId: ctx.accountId } : {}
						});
						return { text: [
							`Gateway: ${payload.url}`,
							`Auth: ${authLabel}`,
							"",
							"After scanning, come back here and run `/pair approve` to complete pairing."
						].join("\n") };
					}
				} catch (err) {
					api.logger.warn?.(`device-pair: telegram QR send failed, falling back (${String(err?.message ?? err)})`);
				}
				api.logger.info?.(`device-pair: QR fallback channel=${channel} target=${target}`);
				return { text: [
					"Scan this QR code with the OpenClaw iOS app:",
					"",
					"```",
					qrAscii,
					"```",
					"",
					...[
						`Gateway: ${payload.url}`,
						`Auth: ${authLabel}`,
						"",
						"After scanning, run `/pair approve` to complete pairing."
					]
				].join("\n") };
			}
			const channel = ctx.channel;
			const target = ctx.senderId?.trim() || ctx.from?.trim() || ctx.to?.trim() || "";
			const authLabel = auth.label ?? "auth";
			if (channel === "telegram" && target) try {
				const runtimeKeys = Object.keys(api.runtime ?? {});
				const channelKeys = Object.keys(api.runtime?.channel ?? {});
				api.logger.debug?.(`device-pair: runtime keys=${runtimeKeys.join(",") || "none"} channel keys=${channelKeys.join(",") || "none"}`);
				const send = api.runtime?.channel?.telegram?.sendMessageTelegram;
				if (!send) throw new Error(`telegram runtime unavailable (runtime keys: ${runtimeKeys.join(",")}; channel keys: ${channelKeys.join(",")})`);
				await send(target, formatSetupInstructions(), {
					...ctx.messageThreadId != null ? { messageThreadId: ctx.messageThreadId } : {},
					...ctx.accountId ? { accountId: ctx.accountId } : {}
				});
				api.logger.info?.(`device-pair: telegram split send ok target=${target} account=${ctx.accountId ?? "none"} thread=${ctx.messageThreadId ?? "none"}`);
				return { text: encodeSetupCode(payload) };
			} catch (err) {
				api.logger.warn?.(`device-pair: telegram split send failed, falling back to single message (${String(err?.message ?? err)})`);
			}
			return { text: formatSetupReply(payload, authLabel) };
		}
	});
}

//#endregion
export { register as default };