import { runExec } from "./exec.js";
import { existsSync } from "node:fs";
import { execFileSync } from "node:child_process";
//#region src/infra/tailscale.ts
function parsePossiblyNoisyJsonObject(stdout) {
	const trimmed = stdout.trim();
	const start = trimmed.indexOf("{");
	const end = trimmed.lastIndexOf("}");
	if (start >= 0 && end > start) return JSON.parse(trimmed.slice(start, end + 1));
	return JSON.parse(trimmed);
}
function normalizeOptionalString(value) {
	if (typeof value !== "string") return;
	const trimmed = value.trim();
	return trimmed.length > 0 ? trimmed : void 0;
}
function isTailnetIPv4(ip) {
	const trimmed = ip.trim();
	if (!/^\d+\.\d+\.\d+\.\d+$/.test(trimmed)) return false;
	return trimmed.startsWith("100.");
}
function parseTailnetIpv4Lines(stdout) {
	for (const line of stdout.split(/\r?\n/)) {
		const ip = line.trim();
		if (isTailnetIPv4(ip)) return ip;
	}
}
const WELL_KNOWN_TAILSCALE_PATHS = [
	"/opt/homebrew/bin/tailscale",
	"/usr/local/bin/tailscale",
	"/Applications/Tailscale.app/Contents/MacOS/Tailscale"
];
var TailscaleCliNotFoundError = class extends Error {
	constructor() {
		super("Tailscale CLI not found. Install Tailscale (https://tailscale.com/download) and sign in, or ensure the tailscale command is on PATH (macOS app: /Applications/Tailscale.app).");
		this.name = "TailscaleCliNotFoundError";
	}
};
async function checkBinary(path) {
	if (!path || !existsSync(path)) return false;
	try {
		await Promise.race([runExec(path, ["--version"], { timeoutMs: 3e3 }), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("timeout")), 3e3))]);
		return true;
	} catch {
		return false;
	}
}
async function findTailscaleBinary() {
	for (const path of WELL_KNOWN_TAILSCALE_PATHS) if (await checkBinary(path)) return path;
	try {
		const { stdout } = await runExec("which", ["tailscale"], { timeoutMs: 3e3 });
		const fromPath = stdout.trim();
		if (fromPath && await checkBinary(fromPath)) return fromPath;
	} catch {}
	try {
		const { stdout } = await runExec("find", [
			"/Applications",
			"-maxdepth",
			"3",
			"-name",
			"Tailscale",
			"-path",
			"*/Tailscale.app/Contents/MacOS/Tailscale"
		], { timeoutMs: 5e3 });
		const found = stdout.trim().split("\n")[0];
		if (found && await checkBinary(found)) return found;
	} catch {}
	return null;
}
let cachedTailscaleBinary = null;
function getTestTailscaleBinaryOverride(env = process.env) {
	const forcedBinary = env.XOPC_TEST_TAILSCALE_BINARY?.trim();
	if (!forcedBinary) return null;
	if (env.VITEST || env.NODE_ENV === "test") return forcedBinary;
	return null;
}
async function requireTailscaleBinary() {
	const forcedBinary = getTestTailscaleBinaryOverride();
	if (forcedBinary) {
		cachedTailscaleBinary = forcedBinary;
		return forcedBinary;
	}
	if (cachedTailscaleBinary) return cachedTailscaleBinary;
	const resolved = await findTailscaleBinary();
	if (!resolved) throw new TailscaleCliNotFoundError();
	cachedTailscaleBinary = resolved;
	return resolved;
}
async function getTailscaleBinary() {
	try {
		return await requireTailscaleBinary();
	} catch (err) {
		if (err instanceof TailscaleCliNotFoundError) return "tailscale";
		throw err;
	}
}
function resetTailscaleBinaryCacheForTest() {
	cachedTailscaleBinary = null;
}
function findTailscaleBinarySync() {
	const forced = getTestTailscaleBinaryOverride();
	if (forced) return forced;
	if (cachedTailscaleBinary) return cachedTailscaleBinary;
	for (const path of WELL_KNOWN_TAILSCALE_PATHS) if (existsSync(path)) return path;
	return null;
}
function getTailnetIPv4Sync() {
	try {
		const bin = findTailscaleBinarySync();
		if (!bin) return;
		return parseTailnetIpv4Lines(execFileSync(bin, ["ip", "-4"], {
			encoding: "utf8",
			timeout: 3e3
		}));
	} catch {
		return;
	}
}
async function getTailnetIPv4() {
	try {
		const { stdout } = await runExec(await getTailscaleBinary(), ["ip", "-4"], { timeoutMs: 5e3 });
		return parseTailnetIpv4Lines(stdout);
	} catch {
		return;
	}
}
async function getTailnetHostname(exec = runExec) {
	const { stdout } = await exec(await getTailscaleBinary(), ["status", "--json"], {
		timeoutMs: 5e3,
		maxBuffer: 4e5
	});
	const parsed = stdout ? parsePossiblyNoisyJsonObject(stdout) : {};
	const self = typeof parsed.Self === "object" && parsed.Self !== null ? parsed.Self : void 0;
	const dns = typeof self?.DNSName === "string" ? self.DNSName : void 0;
	const ips = Array.isArray(self?.TailscaleIPs) ? parsed.Self.TailscaleIPs ?? [] : [];
	if (dns && dns.length > 0) return dns.replace(/\.$/, "");
	if (ips.length > 0) return ips[0];
	throw new Error("Could not determine Tailscale DNS or IP");
}
function extractExecErrorText(err) {
	const errOutput = err;
	return {
		stdout: typeof errOutput.stdout === "string" ? errOutput.stdout : "",
		stderr: typeof errOutput.stderr === "string" ? errOutput.stderr : "",
		message: typeof errOutput.message === "string" ? errOutput.message : "",
		code: typeof errOutput.code === "string" ? errOutput.code : ""
	};
}
function isPermissionDeniedError(err) {
	const { stdout, stderr, message, code } = extractExecErrorText(err);
	if (code.toUpperCase() === "EACCES") return true;
	const combined = `${stdout}\n${stderr}\n${message}`.toLowerCase();
	return combined.includes("permission denied") || combined.includes("access denied") || combined.includes("operation not permitted") || combined.includes("requires root") || combined.includes("must be run as root") || combined.includes("requires sudo");
}
async function execWithSudoFallback(exec, bin, args, opts) {
	try {
		return await exec(bin, args, opts);
	} catch (err) {
		if (!isPermissionDeniedError(err)) throw err;
		try {
			return await exec("sudo", [
				"-n",
				bin,
				...args
			], opts);
		} catch {
			throw err;
		}
	}
}
function wrapTailscaleExecError(err) {
	const { code, message } = extractExecErrorText(err);
	if (code === "ENOENT" || message.includes("ENOENT")) return new TailscaleCliNotFoundError();
	return err instanceof Error ? err : new Error(String(err));
}
async function enableTailscaleServe(port, exec = runExec) {
	const tailscaleBin = await requireTailscaleBinary();
	try {
		await execWithSudoFallback(exec, tailscaleBin, [
			"serve",
			"--bg",
			"--yes",
			`${port}`
		], {
			maxBuffer: 2e5,
			timeoutMs: 15e3
		});
	} catch (err) {
		throw wrapTailscaleExecError(err);
	}
}
async function disableTailscaleServe(exec = runExec) {
	const tailscaleBin = await requireTailscaleBinary();
	try {
		await execWithSudoFallback(exec, tailscaleBin, ["serve", "reset"], {
			maxBuffer: 2e5,
			timeoutMs: 15e3
		});
	} catch (err) {
		throw wrapTailscaleExecError(err);
	}
}
async function enableTailscaleFunnel(port, exec = runExec) {
	const tailscaleBin = await requireTailscaleBinary();
	try {
		await execWithSudoFallback(exec, tailscaleBin, [
			"funnel",
			"--bg",
			"--yes",
			`${port}`
		], {
			maxBuffer: 2e5,
			timeoutMs: 15e3
		});
	} catch (err) {
		throw wrapTailscaleExecError(err);
	}
}
async function disableTailscaleFunnel(exec = runExec) {
	const tailscaleBin = await requireTailscaleBinary();
	try {
		await execWithSudoFallback(exec, tailscaleBin, ["funnel", "reset"], {
			maxBuffer: 2e5,
			timeoutMs: 15e3
		});
	} catch (err) {
		throw wrapTailscaleExecError(err);
	}
}
const whoisCache = /* @__PURE__ */ new Map();
function readRecord(value) {
	return value && typeof value === "object" ? value : null;
}
function parseWhoisIdentity(payload) {
	const userProfile = readRecord(payload.UserProfile) ?? readRecord(payload.userProfile) ?? readRecord(payload.User);
	const login = normalizeOptionalString(userProfile?.LoginName) ?? normalizeOptionalString(userProfile?.Login) ?? normalizeOptionalString(userProfile?.login) ?? normalizeOptionalString(payload.LoginName) ?? normalizeOptionalString(payload.login);
	if (!login) return null;
	return {
		login,
		name: normalizeOptionalString(userProfile?.DisplayName) ?? normalizeOptionalString(userProfile?.Name) ?? normalizeOptionalString(userProfile?.displayName) ?? normalizeOptionalString(payload.DisplayName) ?? normalizeOptionalString(payload.name)
	};
}
async function readTailscaleWhoisIdentity(ip, exec = runExec, opts) {
	const normalized = ip.trim();
	if (!normalized) return null;
	const now = Date.now();
	const cached = whoisCache.get(normalized);
	if (cached && cached.expiresAt > now) return cached.value;
	const cacheTtlMs = opts?.cacheTtlMs ?? 6e4;
	const errorTtlMs = opts?.errorTtlMs ?? 5e3;
	try {
		const { stdout } = await exec(await getTailscaleBinary(), [
			"whois",
			"--json",
			normalized
		], {
			timeoutMs: opts?.timeoutMs ?? 5e3,
			maxBuffer: 2e5
		});
		const identity = parseWhoisIdentity(stdout ? parsePossiblyNoisyJsonObject(stdout) : {});
		whoisCache.set(normalized, {
			value: identity,
			expiresAt: Date.now() + cacheTtlMs
		});
		return identity;
	} catch {
		whoisCache.set(normalized, {
			value: null,
			expiresAt: Date.now() + errorTtlMs
		});
		return null;
	}
}
async function isTailscaleInstalled() {
	return await findTailscaleBinary() !== null;
}
//#endregion
export { TailscaleCliNotFoundError, disableTailscaleFunnel, disableTailscaleServe, enableTailscaleFunnel, enableTailscaleServe, findTailscaleBinary, getTailnetHostname, getTailnetIPv4, getTailnetIPv4Sync, getTailscaleBinary, getTestTailscaleBinaryOverride, isTailscaleInstalled, readTailscaleWhoisIdentity, requireTailscaleBinary, resetTailscaleBinaryCacheForTest };

//# sourceMappingURL=tailscale.js.map