import { y as resolveStateDir } from "./paths-mvMm5bYV.js";
import { t as formatCliCommand } from "./command-format-CKGmlpAQ.js";
import { f as normalizeUniqueSingleOrTrimmedStringList } from "./string-normalization-WNUDCpXX.js";
import { c as JsonFileReadError, p as tryReadJsonSync } from "./json-files-2umMHm0W.js";
import { t as sanitizeTerminalText } from "./safe-text-BkQYdJi1.js";
import { o as callGateway } from "./call-BcwNCr8V.js";
import { n as normalizeDeviceAuthScopes } from "./device-auth-C-STNejO.js";
import { t as note } from "./note-DuS7ttKk.js";
import { l as roleScopesAllow } from "./pairing-token-BEUQW6ez.js";
import { c as listApprovedPairedDeviceRoles, g as summarizeDeviceTokens, l as listDevicePairing } from "./device-pairing-BVDQX5DE.js";
import path from "node:path";
//#region src/commands/doctor-device-pairing.ts
function hasNumberVersion(value) {
	return "version" in value && typeof value.version === "number";
}
function isDeviceAuthStoreTokenEntry(value) {
	return typeof value === "object" && value !== null && "token" in value && typeof value.token === "string" && "role" in value && typeof value.role === "string" && "scopes" in value && Array.isArray(value.scopes) && value.scopes.every((scope) => typeof scope === "string") && "updatedAtMs" in value && typeof value.updatedAtMs === "number";
}
function normalizeGatewayPairedDevice(device) {
	return {
		...device,
		tokenSummaries: device.tokens ?? []
	};
}
function normalizeLocalPairedDevice(device) {
	return {
		...device,
		tokenSummaries: summarizeDeviceTokens(device.tokens) ?? []
	};
}
async function loadDoctorPairingSnapshot(params) {
	if (params.healthOk) try {
		const payload = await callGateway({
			method: "device.pair.list",
			timeoutMs: 5e3,
			config: params.cfg
		});
		return {
			pending: payload.pending,
			paired: payload.paired.map((device) => normalizeGatewayPairedDevice(device))
		};
	} catch {}
	if (params.cfg.gateway?.mode === "remote") return null;
	const local = await listDevicePairing();
	return {
		pending: local.pending,
		paired: local.paired.map((device) => normalizeLocalPairedDevice(device))
	};
}
function resolveApprovedScopes(device) {
	return normalizeDeviceAuthScopes(device.approvedScopes ?? device.scopes);
}
function formatScopes(scopes) {
	return scopes.length > 0 ? scopes.join(", ") : "none";
}
function formatRoles(roles) {
	return roles.length > 0 ? roles.join(", ") : "none";
}
function quoteCliArg(value) {
	if (/^[A-Za-z0-9_/:=.,@%+-]+$/.test(value)) return value;
	return `'${value.replaceAll("'", "'\\''")}'`;
}
function formatCliArgs(args) {
	return formatCliCommand(args.map(quoteCliArg).join(" "));
}
function describeDevice(params) {
	const label = sanitizeTerminalText(params.displayName?.trim() || "") || sanitizeTerminalText(params.clientId?.trim() || "");
	return label ? `${label} (${params.deviceId})` : params.deviceId;
}
function findTokenSummary(device, role) {
	const normalizedRole = role.trim();
	return device.tokenSummaries.find((entry) => entry.role === normalizedRole && !entry.revokedAtMs);
}
function hasPendingScopeUpgrade(params) {
	for (const role of params.requestedRoles) {
		if (!params.approvedRoles.includes(role)) continue;
		const requestedForRole = params.pendingScopes.filter((scope) => role === "operator" ? scope.startsWith("operator.") : !scope.startsWith("operator."));
		if (requestedForRole.length === 0) continue;
		if (!roleScopesAllow({
			role,
			requestedScopes: requestedForRole,
			allowedScopes: params.approvedScopes
		})) return true;
	}
	return false;
}
function resolvePendingPairingIssue(pending, paired) {
	const deviceLabel = describeDevice({
		deviceId: pending.deviceId,
		displayName: pending.displayName,
		clientId: pending.clientId
	});
	const approveCommand = formatCliArgs([
		"openclaw",
		"devices",
		"approve",
		pending.requestId
	]);
	const inspectCommand = formatCliArgs([
		"openclaw",
		"devices",
		"list"
	]);
	if (!paired) return {
		kind: "first-time",
		pending,
		deviceLabel,
		approveCommand,
		inspectCommand
	};
	if (paired.publicKey !== pending.publicKey) return {
		kind: "public-key-repair",
		pending,
		deviceLabel,
		approveCommand,
		inspectCommand,
		removeCommand: formatCliArgs([
			"openclaw",
			"devices",
			"remove",
			pending.deviceId
		])
	};
	const requestedRoles = normalizeUniqueSingleOrTrimmedStringList([pending.roles, pending.role].flat());
	const approvedRoles = listApprovedPairedDeviceRoles(paired);
	if (requestedRoles.some((role) => !approvedRoles.includes(role))) return {
		kind: "role-upgrade",
		pending,
		deviceLabel,
		approveCommand,
		inspectCommand,
		approvedRoles,
		requestedRoles
	};
	const approvedScopes = resolveApprovedScopes(paired);
	const requestedScopes = normalizeDeviceAuthScopes(pending.scopes);
	if (hasPendingScopeUpgrade({
		requestedRoles,
		pendingScopes: requestedScopes,
		approvedRoles,
		approvedScopes
	})) return {
		kind: "scope-upgrade",
		pending,
		deviceLabel,
		approveCommand,
		inspectCommand,
		approvedScopes,
		requestedScopes
	};
	return {
		kind: "repair",
		pending,
		deviceLabel,
		approveCommand,
		inspectCommand
	};
}
function formatPendingPairingIssue(issue) {
	switch (issue.kind) {
		case "first-time": return `- Pending device pairing request ${issue.pending.requestId} for ${issue.deviceLabel}. Review with ${issue.inspectCommand}, then approve with ${issue.approveCommand}.`;
		case "public-key-repair": return `- Pending device repair ${issue.pending.requestId} for ${issue.deviceLabel}: the current device identity no longer matches the approved pairing record. This commonly loops on pairing-required for an already paired device. Remove the stale record with ${issue.removeCommand}, then rerun ${issue.inspectCommand} and approve with ${issue.approveCommand}.`;
		case "role-upgrade": return `- Pending role upgrade ${issue.pending.requestId} for ${issue.deviceLabel}: approved roles [${formatRoles(issue.approvedRoles)}], requested roles [${formatRoles(issue.requestedRoles)}]. Review with ${issue.inspectCommand}, then approve with ${issue.approveCommand}.`;
		case "scope-upgrade": return `- Pending scope upgrade ${issue.pending.requestId} for ${issue.deviceLabel}: approved scopes [${formatScopes(issue.approvedScopes)}], requested scopes [${formatScopes(issue.requestedScopes)}]. Review with ${issue.inspectCommand}, then approve with ${issue.approveCommand}.`;
		case "repair": return `- Pending device repair ${issue.pending.requestId} for ${issue.deviceLabel}: the device is already paired, but a new approval is still required before the requested auth can be used. Review with ${issue.inspectCommand}, then approve with ${issue.approveCommand}.`;
	}
	throw new Error("Unsupported pending pairing issue");
}
function collectPendingPairingIssues(snapshot) {
	const pairedByDeviceId = new Map(snapshot.paired.map((device) => [device.deviceId, device]));
	return snapshot.pending.map((pending) => formatPendingPairingIssue(resolvePendingPairingIssue(pending, pairedByDeviceId.get(pending.deviceId))));
}
function collectPairedRecordIssues(snapshot) {
	const lines = [];
	for (const device of snapshot.paired) {
		const deviceLabel = describeDevice({
			deviceId: device.deviceId,
			displayName: device.displayName,
			clientId: device.clientId
		});
		const approvedRoles = listApprovedPairedDeviceRoles(device);
		const approvedScopes = resolveApprovedScopes(device);
		if (approvedRoles.includes("operator") && approvedScopes.length === 0) lines.push(`- Paired device ${deviceLabel} is missing its approved operator scope baseline. Scope upgrades can get stuck in pairing-required until the device repairs or is re-approved.`);
		for (const role of approvedRoles) {
			const token = findTokenSummary(device, role);
			const rotateCommand = formatCliArgs([
				"openclaw",
				"devices",
				"rotate",
				"--device",
				device.deviceId,
				"--role",
				role
			]);
			if (!token) {
				lines.push(`- Paired device ${deviceLabel} has no active ${role} device token even though the role is approved. This commonly ends in pairing-required or device-token-mismatch. Rotate a fresh token with ${rotateCommand}.`);
				continue;
			}
			if (token.scopes.length > 0 && !roleScopesAllow({
				role,
				requestedScopes: token.scopes,
				allowedScopes: approvedScopes
			})) lines.push(`- Paired device ${deviceLabel} has a ${role} token outside the approved scope baseline [${formatScopes(approvedScopes)}]. Rotate it with ${rotateCommand}.`);
		}
	}
	return lines;
}
function readJsonFile(filePath) {
	return tryReadJsonSync(filePath);
}
function readLocalIdentity(env = process.env) {
	const identity = readJsonFile(path.join(resolveStateDir(env), "identity", "device.json"));
	if (!identity || typeof identity !== "object" || !hasNumberVersion(identity) || identity.version !== 1 || !("deviceId" in identity) || typeof identity.deviceId !== "string" || !identity.deviceId.trim()) return null;
	return {
		version: 1,
		deviceId: identity.deviceId
	};
}
function readLocalDeviceAuthStore(env = process.env) {
	const store = readJsonFile(path.join(resolveStateDir(env), "identity", "device-auth.json"));
	if (!store || typeof store !== "object" || !hasNumberVersion(store) || store.version !== 1 || !("deviceId" in store) || typeof store.deviceId !== "string" || !store.deviceId.trim() || !("tokens" in store) || typeof store.tokens !== "object" || store.tokens === null) return null;
	const tokens = {};
	for (const [role, entry] of Object.entries(store.tokens)) {
		if (!isDeviceAuthStoreTokenEntry(entry)) return null;
		tokens[role] = entry;
	}
	return {
		version: 1,
		deviceId: store.deviceId,
		tokens
	};
}
function collectLocalDeviceAuthIssues(snapshot) {
	const identity = readLocalIdentity();
	const store = readLocalDeviceAuthStore();
	if (!identity || !store || store.deviceId !== identity.deviceId) return [];
	const paired = snapshot.paired.find((device) => device.deviceId === identity.deviceId);
	if (!paired) return [];
	const deviceLabel = describeDevice({
		deviceId: paired.deviceId,
		displayName: paired.displayName,
		clientId: paired.clientId
	});
	const lines = [];
	const approvedRoles = new Set(listApprovedPairedDeviceRoles(paired));
	for (const entry of Object.values(store.tokens)) {
		const role = entry.role.trim();
		if (!role) continue;
		const pairedToken = findTokenSummary(paired, role);
		if (!pairedToken) {
			if (approvedRoles.has(role)) continue;
			lines.push(`- Local cached ${role} device auth for ${deviceLabel} no longer has a matching active gateway token, and that role is no longer approved for this device. Reconnect with shared gateway auth to refresh local auth, or remove the stale cached ${role} auth entry.`);
			continue;
		}
		const rotateCommand = formatCliArgs([
			"openclaw",
			"devices",
			"rotate",
			"--device",
			paired.deviceId,
			"--role",
			role
		]);
		const gatewayIssuedAtMs = pairedToken.rotatedAtMs ?? pairedToken.createdAtMs;
		if (entry.updatedAtMs < gatewayIssuedAtMs) {
			lines.push(`- Local cached ${role} device token for ${deviceLabel} predates the gateway rotation. This is a stale device-token pattern and can fail with device token mismatch. Reconnect with shared gateway auth to refresh it, or rotate again with ${rotateCommand}.`);
			continue;
		}
		const cachedScopes = normalizeDeviceAuthScopes(entry.scopes);
		const pairedScopes = normalizeDeviceAuthScopes(pairedToken.scopes);
		if (cachedScopes.join("\n") !== pairedScopes.join("\n")) lines.push(`- Local cached ${role} device scopes for ${deviceLabel} differ from the gateway record. Cached scopes [${formatScopes(cachedScopes)}], gateway scopes [${formatScopes(pairedScopes)}]. Reconnect with shared gateway auth to refresh it, or rotate with ${rotateCommand}.`);
	}
	return lines;
}
function formatPairingStoreReadIssue(error) {
	const problem = error.reason === "parse" ? "contains invalid JSON" : "could not be read";
	return `- Device pairing store ${error.filePath} ${problem}. OpenClaw refused to treat it as empty to avoid overwriting approved pairings. Fix the JSON or file permissions, or move it aside and re-pair devices.`;
}
async function noteDevicePairingHealth(params) {
	let snapshot;
	try {
		snapshot = await loadDoctorPairingSnapshot(params);
	} catch (error) {
		if (error instanceof JsonFileReadError) {
			note(formatPairingStoreReadIssue(error), "Device pairing");
			return;
		}
		throw error;
	}
	if (!snapshot) return;
	const lines = [
		...collectPendingPairingIssues(snapshot),
		...collectPairedRecordIssues(snapshot),
		...collectLocalDeviceAuthIssues(snapshot)
	];
	if (lines.length === 0) return;
	note(lines.join("\n"), "Device pairing");
}
//#endregion
export { noteDevicePairingHealth };
