import { execSync, spawnSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
import { homedir, tmpdir } from "node:os";
import { join } from "node:path";
import { generateAndSetWallpaper, getMacHardwareModelName } from "./generate-wallpaper.js";
import { checkCliclickInstalled, checkFfmpegInstalled, checkWhisperCppInstalled, checkWhisperModelIntegrity, checkGhInstalled, checkPm2Installed, checkChromeInstalled, checkScreenRecordingPermission, checkAccessibilityPermission, checkAutomationPermission, checkFullDiskAccessPermission, checkCameraPermission, checkMicrophonePermission, checkContactsPermission, checkCalendarPermission, checkRemindersPermission, checkPhotosPermission, checkSpeechRecognitionPermission, checkInputMonitoringPermission, checkDesktopFolderPermission, checkDocumentsFolderPermission, checkDownloadsFolderPermission, grantAllTerminalPermissions, grantAllNodePermissions, getNodeBinaryPath, getCliclickBinaryPath, openSystemSettingsForPermission, pinHomebrewBinaries, saveTccPaths, WHISPER_MODEL_DIR, WHISPER_MODEL_PATH, WHISPER_MODEL_URL, } from "./macos-permissions.js";
const GREEN = "\x1b[32m";
const RED = "\x1b[31m";
const YELLOW = "\x1b[33m";
const DIM = "\x1b[2m";
const RESET = "\x1b[0m";
const BOLD = "\x1b[1m";
const DISPLAYPLACER_LABEL = "com.user.displayplacer";
const MAC_MINI_DISPLAY_SETTINGS = "res:1504x846 hz:60 color_depth:8 scaling:on origin:(0,0) degree:0";
const DISPLAYPLACER_PLIST_PATH = join(homedir(), "Library", "LaunchAgents", `${DISPLAYPLACER_LABEL}.plist`);
const CLAW_SETUP_STATE_DIR = "/opt/claw-setup/state";
const MACHINE_ACCOUNT_STATE_PATH = join(CLAW_SETUP_STATE_DIR, "machine-account.json");
const ACCOUNT_BOOTSTRAP_STATE_PATH = join(CLAW_SETUP_STATE_DIR, "account-bootstrap.json");
function ok(msg) {
    console.log(`  ${GREEN}✓${RESET} ${msg}`);
}
function fail(msg) {
    console.log(`  ${RED}✗${RESET} ${msg}`);
}
function warn(msg) {
    console.log(`  ${YELLOW}!${RESET} ${msg}`);
}
function heading(msg) {
    console.log(`\n${BOLD}${msg}${RESET}\n`);
}
function getBinaryPath(name) {
    try {
        const { stdout, status } = spawnSync("which", [name], { encoding: "utf-8" });
        const path = stdout.trim();
        return status === 0 && path.length > 0 ? path : null;
    }
    catch {
        return null;
    }
}
function isBinaryAvailable(name) {
    return getBinaryPath(name) !== null;
}
function readClawSetupState(path) {
    if (!existsSync(path))
        return null;
    try {
        return JSON.parse(readFileSync(path, "utf-8"));
    }
    catch {
        return null;
    }
}
function getConsoleUser() {
    try {
        return execSync("stat -f %Su /dev/console 2>/dev/null", { encoding: "utf-8" }).trim();
    }
    catch {
        return "";
    }
}
function guardClawSetupAccountContext() {
    const consoleUser = getConsoleUser();
    const currentUser = execSync("id -un", { encoding: "utf-8" }).trim();
    const machineState = readClawSetupState(MACHINE_ACCOUNT_STATE_PATH);
    const bootstrapState = readClawSetupState(ACCOUNT_BOOTSTRAP_STATE_PATH);
    console.log(`${YELLOW}!${RESET} macOS provisioning is moving to claw-setup.`);
    console.log(`  Prefer: ${DIM}bash /opt/claw-setup/main.sh --from=configure-macos${RESET}`);
    if (machineState?.runtimeAccount && machineState.runtimeAccount !== consoleUser) {
        fail(`Recorded runtime account is ${machineState.runtimeAccount}, but current console user is ${consoleUser || currentUser}.`);
        console.log("  Switch to the recorded account or re-run claw-setup with an explicit account mode.");
        process.exit(1);
    }
    if (bootstrapState?.phase === "account-prepared") {
        const requiredLogin = bootstrapState.requiresLoginAs ?? machineState?.runtimeAccount ?? "visionclaw";
        if (consoleUser !== requiredLogin) {
            fail(`claw-setup bootstrap is waiting for login as ${requiredLogin}.`);
            console.log(`  Log in as ${requiredLogin} and re-run claw-setup to finish user-level configuration.`);
            process.exit(1);
        }
    }
}
function confirm(message) {
    const result = spawnSync("bash", ["-c", `read -r -p "${message} [Y/n] " ans; [[ -z "$ans" || "$ans" =~ ^[Yy] ]]`], { stdio: "inherit" });
    return result.status === 0;
}
// ─── Passwordless Sudo ──────────────────────────────────────────────────────
function isPasswordlessSudoConfigured() {
    const username = (process.env.SUDO_USER ?? process.env.USER ?? "").trim()
        || execSync("id -un", { encoding: "utf-8" }).trim();
    const sudoersPath = `/etc/sudoers.d/${username}`;
    try {
        const content = execSync(`sudo cat "${sudoersPath}" 2>/dev/null`, { encoding: "utf-8" });
        return content.includes("NOPASSWD: ALL");
    }
    catch {
        return false;
    }
}
function configurePasswordlessSudo() {
    heading("Passwordless Sudo");
    if (isPasswordlessSudoConfigured()) {
        ok("Passwordless sudo already configured.");
        return;
    }
    fail("Passwordless sudo — not configured");
    console.log("\n  This allows VisionClaw to run sudo commands without a password prompt.");
    console.log("  A sudoers drop-in file will be created at /etc/sudoers.d/<username>.\n");
    if (!confirm("  Proceed?")) {
        warn("Skipped passwordless sudo configuration.");
        return;
    }
    const username = (process.env.SUDO_USER ?? process.env.USER ?? "").trim()
        || execSync("id -un", { encoding: "utf-8" }).trim();
    const result = spawnSync("sudo", ["sh", "-c", `echo '${username} ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/${username} && chmod 440 /etc/sudoers.d/${username}`], { stdio: "inherit" });
    if (result.status === 0 && isPasswordlessSudoConfigured()) {
        ok("Passwordless sudo configured.");
    }
    else {
        fail("Failed to configure passwordless sudo.");
    }
}
// ─── System Privilege Admin Authorization Rule ──────────────────────────────
/**
 * Authorization right used by `AuthorizationExecuteWithPrivileges()` and
 * `security execute-with-privileges`. This is the right that triggers the
 * "<App> would like to administer your computer" SecurityAgent dialog —
 * which several macOS-internal utilities (cron/crontab spool access,
 * Admin framework operations, some installer paths) request implicitly.
 *
 * Default rule on macOS 26 (Tahoe): class=user, shared=false, timeout=300.
 * `shared=false` means each invocation re-prompts even within the same
 * session, which is exactly the symptom we observed with the watchdog.
 *
 * Setting class=allow makes authd grant the right without any UI prompt.
 * This is intentional for VisionClaw's threat model (the host is a
 * personal desktop agent that already has passwordless `NOPASSWD: ALL`
 * sudo configured above), so this does not raise the privilege ceiling.
 *
 * Confirmed via `security authorizationdb read system.privilege.admin`
 * on macOS 26.3.1; the comment in that rule explicitly states it is
 * "Used by AuthorizationExecuteWithPrivileges(...)".
 */
const SYSTEM_PRIVILEGE_ADMIN_RIGHT = "system.privilege.admin";
export function parseSecurityAuthorizationStatus(output) {
    const match = /\b(YES|NO)\s+\((-?\d+)\)/.exec(output);
    if (!match)
        return null;
    return { verdict: match[1], code: Number(match[2]) };
}
function readAuthorizationRuleShape(right) {
    try {
        const result = spawnSync("security", ["authorizationdb", "read", right], { encoding: "utf-8", timeout: 10_000, stdio: ["pipe", "pipe", "pipe"] });
        const readOutput = `${result.stdout}${result.stderr}`;
        const classMatch = /<key>class<\/key>\s*<string>([^<]+)<\/string>/.exec(readOutput);
        const ruleBlockMatch = /<key>rule<\/key>\s*(?:<string>([^<]+)<\/string>|<array>([\s\S]*?)<\/array>)/.exec(readOutput);
        const ruleNames = [];
        if (ruleBlockMatch?.[1]) {
            ruleNames.push(ruleBlockMatch[1]);
        }
        if (ruleBlockMatch?.[2]) {
            const stringMatches = ruleBlockMatch[2].matchAll(/<string>([^<]+)<\/string>/g);
            for (const match of stringMatches) {
                ruleNames.push(match[1]);
            }
        }
        return {
            className: classMatch?.[1] ?? null,
            ruleNames,
            readOutput,
        };
    }
    catch {
        return null;
    }
}
function isAuthorizationRuleAllowed(shape) {
    if (!shape)
        return false;
    if (shape.className === "allow")
        return true;
    return shape.className === "rule" && shape.ruleNames.includes("allow");
}
function writeAuthorizationAllowRule(right) {
    const rulePlist = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>class</key>
  <string>allow</string>
  <key>comment</key>
  <string>Allow unattended local administration for VisionClaw hosts.</string>
  <key>version</key>
  <integer>0</integer>
</dict>
</plist>
`;
    return spawnSync("sudo", ["security", "authorizationdb", "write", right], { encoding: "utf-8", input: rulePlist, timeout: 10_000, stdio: ["pipe", "pipe", "pipe"] });
}
export function isSystemPrivilegeAdminAllowed() {
    if (process.platform !== "darwin")
        return true;
    return isAuthorizationRuleAllowed(readAuthorizationRuleShape(SYSTEM_PRIVILEGE_ADMIN_RIGHT));
}
function configureSystemPrivilegeAdmin() {
    heading("System Administration Authorization");
    if (isSystemPrivilegeAdminAllowed()) {
        ok(`${SYSTEM_PRIVILEGE_ADMIN_RIGHT} — grants allow (no admin prompts).`);
        return;
    }
    fail(`${SYSTEM_PRIVILEGE_ADMIN_RIGHT} — does not grant allow`);
    console.log(`\n  This authd right backs the "<App> would like to administer your`);
    console.log(`  computer." dialog. Default class=user re-prompts on every call`);
    console.log(`  (shared=false), which can wedge background utilities like the`);
    console.log(`  watchdog when no user is at the keyboard.`);
    console.log(`\n  Setting it to 'allow' makes authd grant the right without a UI`);
    console.log(`  prompt. This matches the host's threat model since passwordless`);
    console.log(`  sudo is already configured above — both grant unattended root`);
    console.log(`  access to local processes.`);
    console.log(`\n  Revert with: ${DIM}sudo security authorizationdb remove ${SYSTEM_PRIVILEGE_ADMIN_RIGHT}${RESET}`);
    console.log(`  (removal restores the default rule on next read.)\n`);
    if (!confirm("  Set system.privilege.admin to 'allow'?")) {
        warn("Skipped system.privilege.admin configuration.");
        return;
    }
    // Write a full plist instead of the `allow` shorthand. Some macOS releases
    // expand the shorthand to class=rule, which is valid only if the nested rule
    // is exactly `allow` and makes readback failures harder to diagnose.
    const result = writeAuthorizationAllowRule(SYSTEM_PRIVILEGE_ADMIN_RIGHT);
    const securityStdout = typeof result.stdout === "string" ? result.stdout : result.stdout.toString("utf-8");
    const securityStderr = typeof result.stderr === "string" ? result.stderr : result.stderr.toString("utf-8");
    const securityOutput = `${securityStdout}${securityStderr}`.trim();
    if (securityOutput.length > 0) {
        console.log(securityOutput);
    }
    const securityStatus = parseSecurityAuthorizationStatus(securityOutput);
    const currentShape = readAuthorizationRuleShape(SYSTEM_PRIVILEGE_ADMIN_RIGHT);
    if (isAuthorizationRuleAllowed(currentShape)) {
        const ruleSuffix = currentShape?.className === "rule" && currentShape.ruleNames.length > 0
            ? ` (rule=${currentShape.ruleNames.join(",")})`
            : "";
        ok(`${SYSTEM_PRIVILEGE_ADMIN_RIGHT} — class=${currentShape?.className ?? "allow"}${ruleSuffix}.`);
        return;
    }
    const readbackSummary = currentShape
        ? `class=${currentShape.className ?? "unknown"}, rule=${currentShape.ruleNames.join(",") || "none"}`
        : "class=unknown, rule=none";
    if (securityStatus) {
        fail(`Failed to set ${SYSTEM_PRIVILEGE_ADMIN_RIGHT} to 'allow' (security reported ${securityStatus.verdict} (${securityStatus.code}), readback ${readbackSummary}).`);
    }
    else {
        fail(`Failed to set ${SYSTEM_PRIVILEGE_ADMIN_RIGHT} to 'allow' (sudo exit ${result.status ?? "?"}, readback ${readbackSummary}).`);
    }
    console.log(`  ${YELLOW}Tip:${RESET} on macOS 15.1+ this can fail with -60005 if the auth.db is`);
    console.log(`  in an inconsistent state. Try ${DIM}sudo security authorizationdb reset${RESET}`);
    console.log(`  and re-run prepare-mac.\n`);
}
const BREW_PACKAGES = [
    { name: "cliclick", formula: "cliclick", check: checkCliclickInstalled },
    { name: "ffmpeg", formula: "ffmpeg", check: checkFfmpegInstalled },
    { name: "whisper-cpp", formula: "whisper-cpp", check: checkWhisperCppInstalled },
    { name: "gh", formula: "gh", check: checkGhInstalled },
    { name: "cloudflared", formula: "cloudflare/cloudflare/cloudflared", check: () => isBinaryAvailable("cloudflared") },
    { name: "dockutil", formula: "dockutil", check: () => isBinaryAvailable("dockutil") },
    { name: "displayplacer", formula: "displayplacer", check: () => isBinaryAvailable("displayplacer") },
];
async function installBrewPackages() {
    heading("Homebrew Packages");
    const missing = [];
    for (const pkg of BREW_PACKAGES) {
        const installed = await pkg.check();
        if (installed) {
            ok(`${pkg.name} — installed`);
        }
        else {
            fail(`${pkg.name} — not installed`);
            missing.push(pkg);
        }
    }
    if (missing.length > 0) {
        const formulae = missing.map((p) => p.formula).join(" ");
        console.log(`\n  Will install: ${missing.map((p) => p.name).join(", ")}`);
        if (!confirm("  Proceed?")) {
            warn("Skipped brew package installation.");
        }
        else {
            console.log(`\n  ${DIM}brew install ${formulae}${RESET}\n`);
            const result = spawnSync("brew", ["install", ...missing.map((p) => p.formula)], {
                stdio: "inherit",
            });
            if (result.status === 0) {
                ok("Brew packages installed successfully.");
            }
            else {
                fail("Some brew packages failed to install. Check the output above.");
            }
        }
    }
    else {
        console.log(`\n  All brew packages are installed.`);
    }
    // Always pin node and cliclick so `brew upgrade` doesn't change their Cellar
    // paths, which would invalidate macOS TCC permissions. Safe to call on re-run
    // since `brew pin` on an already-pinned formula is a no-op.
    pinHomebrewBinaries();
    ok("Pinned node and cliclick (brew pin) to prevent path-breaking upgrades.");
}
// ─── npm Global Packages ─────────────────────────────────────────────────────
async function installNpmGlobals() {
    heading("npm Global Packages");
    const pm2Installed = await checkPm2Installed();
    if (pm2Installed) {
        ok("pm2 — installed");
        return;
    }
    fail("pm2 — not installed");
    console.log("\n  pm2 is required as the process manager in production.");
    if (!confirm("  Install pm2 globally via npm?")) {
        warn("Skipped pm2 installation. Install manually: npm install -g pm2");
        return;
    }
    console.log(`\n  ${DIM}npm install -g pm2${RESET}\n`);
    const result = spawnSync("npm", ["install", "-g", "pm2"], {
        stdio: "inherit",
    });
    if (result.status === 0) {
        ok("pm2 installed successfully.");
    }
    else {
        fail("pm2 installation failed. Check the output above.");
    }
}
// ─── Whisper Model ───────────────────────────────────────────────────────────
function downloadWhisperModel() {
    heading("Whisper Model");
    const integrity = checkWhisperModelIntegrity();
    if (integrity === "valid") {
        ok(`Model found at ${WHISPER_MODEL_PATH}`);
        return;
    }
    if (integrity === "incomplete") {
        warn(`Model file exists but is incomplete or corrupt.`);
        console.log(`  Removing partial file and re-downloading…\n`);
        unlinkSync(WHISPER_MODEL_PATH);
    }
    else {
        fail("Whisper model not found.");
    }
    console.log(`  Download location: ${WHISPER_MODEL_PATH}`);
    console.log(`  Size: ~1.5 GB (one-time download)\n`);
    if (!confirm("  Download now?")) {
        warn("Skipped whisper model download.");
        return;
    }
    mkdirSync(WHISPER_MODEL_DIR, { recursive: true });
    console.log("");
    const result = spawnSync("curl", ["-L", "--progress-bar", "-o", WHISPER_MODEL_PATH, WHISPER_MODEL_URL], { stdio: "inherit" });
    if (result.status !== 0) {
        fail("Whisper model download failed. You can retry later.");
        return;
    }
    // Verify downloaded file integrity
    const postCheck = checkWhisperModelIntegrity();
    if (postCheck === "valid") {
        ok("Whisper model downloaded and verified successfully.");
    }
    else {
        fail("Downloaded file appears incomplete. Please retry.");
    }
}
function readCommandOutput(command) {
    try {
        return execSync(command, { encoding: "utf-8" });
    }
    catch {
        return null;
    }
}
function readFirstParsedCommandOutput(commands, parser) {
    for (const command of commands) {
        const output = readCommandOutput(command);
        if (output === null) {
            continue;
        }
        const parsed = parser(output);
        if (parsed !== null) {
            return parsed;
        }
    }
    return null;
}
function parseIntegerOutput(output) {
    const value = Number.parseInt(output.trim(), 10);
    return Number.isNaN(value) ? null : value;
}
function parseBooleanOutput(output) {
    const normalized = output.trim().toLowerCase();
    if (normalized === "1" || normalized === "true") {
        return true;
    }
    if (normalized === "0" || normalized === "false") {
        return false;
    }
    return null;
}
function parseScreenLockStatusOutput(output) {
    const normalized = output.trim().toLowerCase();
    if (normalized.includes("password is not required")
        || normalized.includes("screenlock is off")
        || normalized.includes("screen lock is off")
        || normalized.includes("screenlock off")
        || normalized.includes("screen lock off")) {
        return false;
    }
    if (normalized.includes("screenlock delay is")
        || normalized.includes("screen lock delay is")
        || normalized.includes("password is required")
        || normalized.includes("screenlock immediate")
        || normalized.includes("screen lock immediate")) {
        return true;
    }
    return null;
}
function getPmsetValue(source, key) {
    const output = readCommandOutput("pmset -g custom 2>/dev/null");
    if (output === null) {
        return null;
    }
    const section = source === "battery" ? "Battery Power:" : "AC Power:";
    const sectionIdx = output.indexOf(section);
    if (sectionIdx === -1)
        return null;
    const nextSectionIdx = output.indexOf(source === "battery" ? "AC Power:" : "\n\n", sectionIdx + section.length);
    const block = nextSectionIdx === -1
        ? output.slice(sectionIdx)
        : output.slice(sectionIdx, nextSectionIdx);
    const match = new RegExp(`^\\s*${key}\\s+(\\d+)`, "m").exec(block);
    return match ? Number.parseInt(match[1], 10) : null;
}
function getScreensaverIdleTime() {
    return readFirstParsedCommandOutput([
        "defaults -currentHost read com.apple.screensaver idleTime 2>/dev/null",
        "defaults read com.apple.screensaver idleTime 2>/dev/null",
    ], parseIntegerOutput);
}
const WALLPAPER_PLIST_PATH = join(homedir(), "Library/Application Support/com.apple.wallpaper/Store/Index.plist");
function hasBatteryPower() {
    return readCommandOutput("pmset -g custom 2>/dev/null")?.includes("Battery Power:") ?? false;
}
function hasWallpaperIdleChoices(section) {
    const choices = readCommandOutput(`/usr/libexec/PlistBuddy -c "Print ${section}:Idle:Content:Choices" "${WALLPAPER_PLIST_PATH}" 2>/dev/null`);
    if (choices === null)
        return false;
    const trimmed = choices.trim();
    if (trimmed.length === 0)
        return false;
    // macOS 26+ always has a default entry with Provider = default and empty
    // Files. This does NOT mean a wallpaper screen saver is actually selected.
    // Only treat it as enabled when the Files array has real entries or there
    // are multiple choice dictionaries.
    const dictCount = (trimmed.match(/^\s*Dict\s*\{/gm) ?? []).length;
    if (dictCount > 1)
        return true;
    const filesMatch = /Files\s*=\s*Array\s*\{([^}]*)\}/s.exec(trimmed);
    if (filesMatch && filesMatch[1].trim().length > 0)
        return true;
    const providerMatch = /Provider\s*=\s*(\S+)/i.exec(trimmed);
    if (providerMatch && providerMatch[1] !== "default")
        return true;
    return false;
}
function isWallpaperScreenSaverEnabled() {
    if (!existsSync(WALLPAPER_PLIST_PATH))
        return false;
    if (hasWallpaperIdleChoices("AllSpacesAndDisplays"))
        return true;
    if (hasWallpaperIdleChoices("SystemDefault"))
        return true;
    const allSpacesType = readCommandOutput(`/usr/libexec/PlistBuddy -c "Print AllSpacesAndDisplays:Type" "${WALLPAPER_PLIST_PATH}" 2>/dev/null`);
    if (allSpacesType?.trim() === "linked")
        return true;
    const systemDefaultType = readCommandOutput(`/usr/libexec/PlistBuddy -c "Print SystemDefault:Type" "${WALLPAPER_PLIST_PATH}" 2>/dev/null`);
    return systemDefaultType?.trim() === "linked";
}
function disableWallpaperScreenSaver() {
    if (!existsSync(WALLPAPER_PLIST_PATH))
        return;
    const sections = ["AllSpacesAndDisplays", "SystemDefault"];
    for (const section of sections) {
        try {
            execSync(`/usr/libexec/PlistBuddy -c "Delete ${section}:Idle:Content:Choices" "${WALLPAPER_PLIST_PATH}" 2>/dev/null`);
        }
        catch { /* section may not exist */ }
        try {
            execSync(`/usr/libexec/PlistBuddy -c "Add ${section}:Idle:Content:Choices array" "${WALLPAPER_PLIST_PATH}" 2>/dev/null`);
        }
        catch { /* already exists after deletion failed */ }
    }
    try {
        execSync("killall WallpaperAgent 2>/dev/null", { shell: "/bin/bash" });
    }
    catch { /* may not be running */ }
}
function getAskForPassword() {
    const screenLockStatus = readFirstParsedCommandOutput(["sysadminctl -screenLock status 2>&1"], parseScreenLockStatusOutput);
    if (screenLockStatus !== null) {
        return screenLockStatus;
    }
    const legacyAskForPassword = readFirstParsedCommandOutput([
        "defaults -currentHost read com.apple.screensaver askForPassword 2>/dev/null",
        "defaults read com.apple.screensaver askForPassword 2>/dev/null",
    ], parseBooleanOutput);
    if (legacyAskForPassword !== null) {
        return legacyAskForPassword;
    }
    return true;
}
function disableAskForPassword() {
    if (isBinaryAvailable("sysadminctl")) {
        const result = spawnSync("sysadminctl", ["-screenLock", "off", "-password", "-"], { stdio: "inherit" });
        if (result.status === 0) {
            return;
        }
        // sysadminctl may fail on Mac Mini (no login password, MDM-managed, etc.)
        // Fall through to the defaults-based approach instead of throwing.
    }
    execSync("defaults write com.apple.screensaver askForPassword -bool false");
}
export function isPowerStrategyConfigured() {
    const hasBattery = hasBatteryPower();
    const acDisplay = getPmsetValue("ac", "displaysleep");
    const screensaverIdle = getScreensaverIdleTime();
    const screensaverOff = screensaverIdle === 0;
    const wallpaperOff = !isWallpaperScreenSaverEnabled();
    if (hasBattery) {
        // Keep display on at all times on battery — displaysleep must be 0.
        const batteryDisplay = getPmsetValue("battery", "displaysleep");
        if (batteryDisplay !== 0)
            return false;
        // `disablesleep` does not exist in the battery profile on macOS;
        // use `sleep 0` instead to verify system sleep is disabled on battery.
        const batterySleep = getPmsetValue("battery", "sleep");
        if (batterySleep !== 0)
            return false;
    }
    if (acDisplay !== 0 || !screensaverOff || !wallpaperOff)
        return false;
    // When display sleep is off (AC) and screensaver is disabled, the screen lock
    // after screensaver can never trigger. On Mac Mini (no battery, always on AC)
    // this means askForPassword is irrelevant — skip the check to avoid a false
    // negative when sysadminctl reports a delay that cannot actually fire.
    if (hasBattery) {
        const askPassword = getAskForPassword();
        if (askPassword)
            return false;
    }
    return true;
}
function configurePowerStrategy() {
    heading("Power & Screen Strategy");
    const hasBattery = hasBatteryPower();
    const settings = [];
    if (hasBattery) {
        settings.push({
            label: "Display sleep on battery — never",
            check: () => getPmsetValue("battery", "displaysleep") === 0,
            apply: () => { execSync("sudo pmset -b displaysleep 0", { stdio: "inherit" }); },
        }, {
            // macOS does not expose `disablesleep` in the battery profile.
            // Setting `sleep 0` on battery is the correct way to prevent
            // system sleep when on battery power (e.g. lid closed on a laptop
            // used as a headless server).
            label: "System sleep on battery — disabled",
            check: () => getPmsetValue("battery", "sleep") === 0,
            apply: () => { execSync("sudo pmset -b sleep 0", { stdio: "inherit" }); },
        });
    }
    settings.push({
        label: "Display sleep on AC — never",
        check: () => getPmsetValue("ac", "displaysleep") === 0,
        apply: () => { execSync("sudo pmset -c displaysleep 0", { stdio: "inherit" }); },
    }, {
        label: "Screen saver — disabled",
        check: () => {
            const idle = getScreensaverIdleTime();
            return idle === 0;
        },
        apply: () => {
            execSync("defaults -currentHost write com.apple.screensaver idleTime -int 0");
        },
    }, {
        label: "Wallpaper screen saver — disabled",
        check: () => !isWallpaperScreenSaverEnabled(),
        apply: () => { disableWallpaperScreenSaver(); },
    });
    if (hasBattery) {
        settings.push({
            label: "Password after screen saver — off",
            check: () => !getAskForPassword(),
            apply: () => { disableAskForPassword(); },
        });
    }
    const needsFix = [];
    for (const s of settings) {
        if (s.check()) {
            ok(s.label);
        }
        else {
            fail(s.label);
            needsFix.push(s);
        }
    }
    if (needsFix.length === 0) {
        console.log(`\n  Power & screen settings are all correct.`);
        return;
    }
    console.log(`\n  Will configure: ${needsFix.map((s) => s.label).join(", ")}`);
    console.log("  Note: pmset changes require sudo, and screen lock changes may prompt for your login password.\n");
    if (!confirm("  Proceed?")) {
        warn("Skipped power strategy configuration.");
        return;
    }
    for (const s of needsFix) {
        try {
            s.apply();
            if (s.check()) {
                ok(s.label);
            }
            else {
                fail(`${s.label}: setting did not take effect`);
            }
        }
        catch (err) {
            fail(`${s.label}: ${err instanceof Error ? err.message : String(err)}`);
        }
    }
}
// ─── Automatic Software Update ───────────────────────────────────────────────
/**
 * Read a boolean key from the system-level SoftwareUpdate plist.
 * These are stored in /Library/Preferences/com.apple.SoftwareUpdate and
 * require `sudo defaults` to write, but can be read without elevation.
 */
function getSoftwareUpdateSetting(key) {
    return readFirstParsedCommandOutput([
        `defaults read /Library/Preferences/com.apple.SoftwareUpdate ${key} 2>/dev/null`,
    ], parseBooleanOutput);
}
/**
 * Check whether all macOS automatic software update mechanisms are disabled.
 * Returns true only when every auto-update toggle is off.
 *
 * The five relevant keys in /Library/Preferences/com.apple.SoftwareUpdate:
 *  - AutomaticCheckEnabled:             Check for updates
 *  - AutomaticDownload:                 Download new updates when available
 *  - AutomaticallyInstallMacOSUpdates:  Install macOS updates
 *  - CriticalUpdateInstall:             Install system data files & security updates
 *  - ConfigDataInstall:                 Install config data (XProtect, MRT, etc.)
 */
export function isAutoUpdateDisabled() {
    const keys = [
        "AutomaticCheckEnabled",
        "AutomaticDownload",
        "AutomaticallyInstallMacOSUpdates",
        "CriticalUpdateInstall",
        "ConfigDataInstall",
    ];
    for (const key of keys) {
        const value = getSoftwareUpdateSetting(key);
        // If the key is missing (null), macOS treats it as enabled by default.
        // If it is true/1, auto-update is still on. Both cases → not disabled.
        if (value === null || value)
            return false;
    }
    return true;
}
function configureAutoUpdate() {
    heading("Automatic Software Update");
    const settings = [
        {
            label: "Check for updates — off",
            check: () => getSoftwareUpdateSetting("AutomaticCheckEnabled") === false,
            apply: () => { execSync("sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticCheckEnabled -bool false", { stdio: "inherit" }); },
        },
        {
            label: "Download new updates — off",
            check: () => getSoftwareUpdateSetting("AutomaticDownload") === false,
            apply: () => { execSync("sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticDownload -bool false", { stdio: "inherit" }); },
        },
        {
            label: "Install macOS updates — off",
            check: () => getSoftwareUpdateSetting("AutomaticallyInstallMacOSUpdates") === false,
            apply: () => { execSync("sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticallyInstallMacOSUpdates -bool false", { stdio: "inherit" }); },
        },
        {
            label: "Install critical / security updates — off",
            check: () => getSoftwareUpdateSetting("CriticalUpdateInstall") === false,
            apply: () => { execSync("sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate CriticalUpdateInstall -bool false", { stdio: "inherit" }); },
        },
        {
            label: "Install config data files (XProtect) — off",
            check: () => getSoftwareUpdateSetting("ConfigDataInstall") === false,
            apply: () => { execSync("sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate ConfigDataInstall -bool false", { stdio: "inherit" }); },
        },
    ];
    const needsFix = [];
    for (const s of settings) {
        if (s.check()) {
            ok(s.label);
        }
        else {
            fail(s.label);
            needsFix.push(s);
        }
    }
    if (needsFix.length === 0) {
        console.log(`\n  Automatic software update is fully disabled.`);
        return;
    }
    console.log(`\n  Will disable: ${needsFix.map((s) => s.label).join(", ")}`);
    console.log("  Note: Changing software update settings requires sudo.\n");
    if (!confirm("  Proceed?")) {
        warn("Skipped automatic update configuration.");
        return;
    }
    for (const s of needsFix) {
        try {
            s.apply();
            if (s.check()) {
                ok(s.label);
            }
            else {
                fail(`${s.label}: setting did not take effect`);
            }
        }
        catch (err) {
            fail(`${s.label}: ${err instanceof Error ? err.message : String(err)}`);
        }
    }
}
// ─── Desktop Customization ───────────────────────────────────────────────────
function setDesktopBackground() {
    console.log("  Generating desktop wallpaper (prepare-mac stage)...");
    try {
        generateAndSetWallpaper();
        ok("Desktop wallpaper generated and set.");
    }
    catch (err) {
        fail(`Failed to set desktop wallpaper: ${err instanceof Error ? err.message : String(err)}`);
    }
}
function disableWidgets() {
    console.log("  Disabling desktop widgets...");
    try {
        execSync("defaults write com.apple.WindowManager StandardHideWidgets -bool true");
        execSync("defaults write com.apple.WindowManager StageManagerHideWidgets -bool true");
        ok("Widgets disabled.");
    }
    catch (err) {
        fail(`Failed to disable widgets: ${err instanceof Error ? err.message : String(err)}`);
    }
}
const DOCK_APPS = [
    { label: "Apps", path: "/System/Applications/Apps.app", alternatives: ["/System/Applications/Launchpad.app"] },
    { label: "Google Chrome", path: "/Applications/Google Chrome.app" },
    { label: "Terminal", path: "/System/Applications/Utilities/Terminal.app" },
    { label: "Feishu", path: "/Applications/Feishu.app", alternatives: ["/Applications/Lark.app"] },
    { label: "WeChat", path: "/Applications/WeChat.app" },
    { label: "System Settings", path: "/System/Applications/System Settings.app", alternatives: ["/System/Applications/System Preferences.app"] },
];
function resolveDockApp(app) {
    if (existsSync(app.path)) {
        return { label: app.label, path: app.path };
    }
    for (const alt of app.alternatives ?? []) {
        if (existsSync(alt)) {
            const altLabel = alt.split("/").pop()?.replace(".app", "") ?? app.label;
            return { label: altLabel, path: alt };
        }
    }
    return null;
}
function getDockItems() {
    try {
        const output = execSync("dockutil --list", { encoding: "utf-8" });
        return output.split("\n").filter(Boolean).map((line) => {
            const parts = line.split("\t");
            return { label: parts[0], path: parts[1] ?? "", section: parts[2] ?? "persistentApps" };
        });
    }
    catch {
        return [];
    }
}
function configureDock() {
    heading("Dock Configuration");
    if (!isBinaryAvailable("dockutil")) {
        warn("dockutil not found — skipping Dock configuration.");
        return;
    }
    try {
        const resolved = [];
        for (const app of DOCK_APPS) {
            const found = resolveDockApp(app);
            if (found) {
                resolved.push(found);
            }
            else {
                const tried = [app.path, ...(app.alternatives ?? [])].join(", ");
                warn(`${app.label} not found (checked: ${tried}) — skipping`);
            }
        }
        // Get current persistentApps only (leave persistentOthers like Downloads/Trash alone)
        const currentApps = getDockItems().filter((i) => i.section === "persistentApps");
        const resolvedPaths = new Set(resolved.map((r) => r.path));
        // Remove apps that shouldn't be in the dock (skip Finder — always pinned by macOS)
        for (const item of currentApps) {
            if (item.label === "Finder")
                continue;
            const itemPath = decodeURIComponent(item.path.replace(/^file:\/\//, "").replace(/\/$/, ""));
            if (!resolvedPaths.has(itemPath)) {
                try {
                    execSync(`dockutil --remove '${item.label}' --no-restart`, { stdio: "ignore" });
                }
                catch {
                    warn(`Failed to remove ${item.label} from Dock`);
                }
            }
        }
        // Re-read dock state after removals to avoid duplicates
        const afterRemoval = getDockItems().filter((i) => i.section === "persistentApps");
        const currentPaths = new Set(afterRemoval.map((i) => decodeURIComponent(i.path.replace(/^file:\/\//, "").replace(/\/$/, ""))));
        for (const app of resolved) {
            if (currentPaths.has(app.path))
                continue;
            try {
                execSync(`dockutil --add '${app.path}' --no-restart`, { stdio: "ignore" });
            }
            catch {
                warn(`Failed to add ${app.label} to Dock`);
            }
        }
        execSync("killall Dock 2>/dev/null || true", { shell: "/bin/bash", stdio: "ignore" });
        ok(`Dock configured with ${resolved.length} apps.`);
    }
    catch (err) {
        fail(`Failed to configure Dock: ${err instanceof Error ? err.message : String(err)}`);
    }
}
function configureDesktop() {
    heading("Desktop Customization");
    setDesktopBackground();
    disableWidgets();
}
function isMacMini() {
    const model = getMacHardwareModelName();
    if (!model)
        return false;
    const normalized = model.toLowerCase();
    return normalized.startsWith("macmini") || normalized.includes("mac mini");
}
/**
 * Query `displayplacer list` and extract the first persistent screen ID.
 * The output contains lines like:
 *   Persistent screen id: 6EC41A8F-5D06-4112-B413-3026F3346262
 */
function getDisplayId(displayplacerPath) {
    try {
        const output = execSync(`"${displayplacerPath}" list`, {
            encoding: "utf-8",
            shell: "/bin/bash",
            timeout: 10_000,
        });
        const match = /Persistent screen id:\s*(\S+)/i.exec(output);
        return match?.[1] ?? null;
    }
    catch {
        return null;
    }
}
function buildDisplayArgument(displayId) {
    return `id:${displayId} ${MAC_MINI_DISPLAY_SETTINGS}`;
}
function escapeXml(value) {
    return value
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&apos;");
}
function generateDisplayplacerPlist(displayplacerPath, displayArgument) {
    return `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>${DISPLAYPLACER_LABEL}</string>
  <key>ProgramArguments</key>
  <array>
    <string>${escapeXml(displayplacerPath)}</string>
    <string>${escapeXml(displayArgument)}</string>
  </array>
  <key>RunAtLoad</key>
  <true/>
</dict>
</plist>
`;
}
function isDisplayplacerLaunchAgentLoaded() {
    const result = spawnSync("launchctl", ["list", DISPLAYPLACER_LABEL], {
        encoding: "utf-8",
        stdio: ["pipe", "pipe", "pipe"],
    });
    return result.status === 0;
}
export function isMacMiniDisplayResolutionConfigured() {
    if (!isMacMini()) {
        return true;
    }
    const displayplacerPath = getBinaryPath("displayplacer");
    if (displayplacerPath === null || !existsSync(DISPLAYPLACER_PLIST_PATH)) {
        return false;
    }
    try {
        const plist = readFileSync(DISPLAYPLACER_PLIST_PATH, "utf-8");
        return plist.includes(displayplacerPath)
            && plist.includes(MAC_MINI_DISPLAY_SETTINGS)
            && isDisplayplacerLaunchAgentLoaded();
    }
    catch {
        return false;
    }
}
export function configureMacMiniDisplayResolution() {
    heading("Display Resolution");
    if (!isMacMini()) {
        ok("Not a Mac mini — skipping display resolution setup.");
        return;
    }
    const model = getMacHardwareModelName();
    if (model) {
        console.log(`  Detected ${model}; applying Mac mini display resolution...`);
    }
    const displayplacerPath = getBinaryPath("displayplacer");
    if (displayplacerPath === null) {
        fail("displayplacer not found.");
        console.log(`  Install it with ${DIM}brew install displayplacer${RESET} and re-run prepare-mac.\n`);
        return;
    }
    const displayId = getDisplayId(displayplacerPath);
    if (displayId === null) {
        fail("Could not detect a display ID from displayplacer list.");
        console.log(`  Run ${DIM}displayplacer list${RESET} manually to verify your display is detected.\n`);
        return;
    }
    ok(`Detected display ID: ${displayId}`);
    const displayArgument = buildDisplayArgument(displayId);
    const applyResult = spawnSync(displayplacerPath, [displayArgument], { stdio: "inherit" });
    if (applyResult.status === 0) {
        ok("Display resolution applied.");
    }
    else {
        fail("Failed to apply display resolution.");
    }
    try {
        mkdirSync(join(homedir(), "Library", "LaunchAgents"), { recursive: true });
        writeFileSync(DISPLAYPLACER_PLIST_PATH, generateDisplayplacerPlist(displayplacerPath, displayArgument), "utf-8");
        ok(`LaunchAgent plist written to ${DIM}${DISPLAYPLACER_PLIST_PATH}${RESET}`);
    }
    catch (err) {
        fail(`Failed to write LaunchAgent plist: ${err instanceof Error ? err.message : String(err)}`);
        return;
    }
    if (isDisplayplacerLaunchAgentLoaded()) {
        spawnSync("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}`, DISPLAYPLACER_PLIST_PATH], { stdio: "inherit" });
    }
    const loadResult = spawnSync("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, DISPLAYPLACER_PLIST_PATH], { stdio: "inherit" });
    if (loadResult.status === 0) {
        ok("LaunchAgent installed to reapply display resolution at login.");
    }
    else {
        fail("Failed to load display resolution LaunchAgent.");
    }
}
function isDockConfigured() {
    if (!isBinaryAvailable("dockutil"))
        return false;
    const currentPaths = getDockItems()
        .filter((i) => i.section === "persistentApps")
        .map((i) => decodeURIComponent(i.path.replace(/^file:\/\//, "").replace(/\/$/, "")));
    const expected = DOCK_APPS.map((app) => resolveDockApp(app)?.path).filter((p) => p !== undefined);
    return expected.every((p) => currentPaths.some((cp) => cp === p || cp.endsWith(p)));
}
// ─── Chrome Check ────────────────────────────────────────────────────────────
function isChromeDefaultBrowser() {
    try {
        // Use LSCopyDefaultHandlerForURLScheme — the authoritative CoreServices API.
        // Parsing the LaunchServices plist directly is unreliable because an absent
        // https entry means the OS default (Safari) is active, not Chrome.
        const result = spawnSync("swift", ["-e", [
                "import Foundation",
                "import CoreServices",
                "let h = LSCopyDefaultHandlerForURLScheme(\"https\" as CFString)?.takeRetainedValue() as String? ?? \"\"",
                "print(h)",
            ].join("\n")], { encoding: "utf-8", timeout: 15_000 });
        return result.stdout.trim().toLowerCase() === "com.google.chrome";
    }
    catch {
        return false;
    }
}
function setChromeAsDefaultBrowser() {
    console.log("  Setting Google Chrome as the default browser...");
    // Use Swift to call LSSetDefaultHandlerForURLScheme — the only reliable
    // programmatic way to change the default browser without extra brew deps.
    compileAndRunSwift("default-browser", [
        "import Foundation",
        "import CoreServices",
        "let bundleID = \"com.google.chrome\" as CFString",
        "LSSetDefaultHandlerForURLScheme(\"http\" as CFString, bundleID)",
        "LSSetDefaultHandlerForURLScheme(\"https\" as CFString, bundleID)",
        "LSSetDefaultHandlerForURLScheme(\"x-web-search\" as CFString, bundleID)",
    ].join("\n"));
}
async function checkChrome() {
    heading("Google Chrome");
    const installed = await checkChromeInstalled();
    if (!installed) {
        fail("Google Chrome not found.");
        console.log("  Chrome is required for browser automation (Playwright).\n");
        if (!confirm("  Open Chrome download page?")) {
            warn("Skipped Chrome. Install it manually from https://www.google.com/chrome/");
            return;
        }
        execSync("open https://www.google.com/chrome/");
        console.log("  Install Chrome, then press Enter to continue...");
        spawnSync("bash", ["-c", "read -r -p ''"], { stdio: "inherit" });
        return;
    }
    ok("Google Chrome found.");
    if (isChromeDefaultBrowser()) {
        ok("Google Chrome is the default browser.");
    }
    else {
        setChromeAsDefaultBrowser();
        if (isChromeDefaultBrowser()) {
            ok("Google Chrome set as default browser.");
        }
        else {
            warn("Could not verify Chrome as default browser — you may need to set it manually in System Settings > Desktop & Dock > Default web browser.");
        }
    }
}
// ─── Playwriter Extension ───────────────────────────────────────────────────
const PLAYWRITER_EXTENSION_ID = "jfeammnjpkecdekppnclgkkffahnhfhe";
const PLAYWRITER_UPDATE_URL = "https://clients2.google.com/service/update2/crx";
export function isPlaywriterExtensionInstalled() {
    const chromeDataDir = join(homedir(), "Library/Application Support/Google/Chrome");
    const profiles = [];
    try {
        if (existsSync(chromeDataDir)) {
            for (const entry of readdirSync(chromeDataDir)) {
                if (entry === "Default" || entry.startsWith("Profile ")) {
                    const extDir = join(chromeDataDir, entry, "Extensions", PLAYWRITER_EXTENSION_ID);
                    if (existsSync(extDir))
                        profiles.push(entry);
                }
            }
        }
    }
    catch {
        // Chrome data dir may not exist
    }
    return { installed: profiles.length > 0, profiles };
}
export function isPlaywriterPolicyConfigured() {
    try {
        const plistPath = "/Library/Managed Preferences/com.google.Chrome.plist";
        if (!existsSync(plistPath))
            return false;
        const content = execSync(`/usr/libexec/PlistBuddy -c "Print ExtensionInstallForcelist" "${plistPath}" 2>/dev/null`, { encoding: "utf-8", timeout: 5000 });
        return content.includes(PLAYWRITER_EXTENSION_ID);
    }
    catch {
        return false;
    }
}
export function installPlaywriterExtension() {
    return installPlaywriterPolicy();
}
function installPlaywriterPolicy() {
    const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>ExtensionInstallForcelist</key>
    <array>
        <string>${PLAYWRITER_EXTENSION_ID};${PLAYWRITER_UPDATE_URL}</string>
    </array>
</dict>
</plist>`;
    const username = (process.env.SUDO_USER ?? process.env.USER ?? "").trim()
        || execSync("id -un", { encoding: "utf-8" }).trim();
    try {
        const tmpPlist = join(tmpdir(), "vc-playwriter-policy.plist");
        writeFileSync(tmpPlist, plistContent, "utf-8");
        // System-level
        execSync('sudo mkdir -p "/Library/Managed Preferences"', { stdio: "pipe", timeout: 10_000 });
        execSync(`sudo cp "${tmpPlist}" "/Library/Managed Preferences/com.google.Chrome.plist"`, { stdio: "pipe", timeout: 10_000 });
        execSync('sudo chmod 644 "/Library/Managed Preferences/com.google.Chrome.plist"', { stdio: "pipe", timeout: 10_000 });
        // User-level
        if (username) {
            const userDir = `/Library/Managed Preferences/${username}`;
            execSync(`sudo mkdir -p "${userDir}"`, { stdio: "pipe", timeout: 10_000 });
            execSync(`sudo cp "${tmpPlist}" "${userDir}/com.google.Chrome.plist"`, { stdio: "pipe", timeout: 10_000 });
            execSync(`sudo chmod 644 "${userDir}/com.google.Chrome.plist"`, { stdio: "pipe", timeout: 10_000 });
        }
        unlinkSync(tmpPlist);
        // External Extensions JSON (fallback)
        const chromeDataDir = join(homedir(), "Library/Application Support/Google/Chrome");
        const extDir = join(chromeDataDir, "External Extensions");
        mkdirSync(extDir, { recursive: true });
        writeFileSync(join(extDir, `${PLAYWRITER_EXTENSION_ID}.json`), JSON.stringify({ external_update_url: PLAYWRITER_UPDATE_URL }, null, 2), "utf-8");
        // Refresh cfprefsd cache
        try {
            execSync("sudo killall cfprefsd 2>/dev/null", { stdio: "pipe", timeout: 10_000 });
        }
        catch { /* not critical */ }
        return true;
    }
    catch {
        return false;
    }
}
function _checkPlaywriterExtension() {
    heading("Playwriter Extension");
    // 1. Check extension in Chrome profiles
    const extStatus = isPlaywriterExtensionInstalled();
    if (extStatus.installed) {
        ok(`Playwriter extension found in: ${extStatus.profiles.join(", ")}`);
    }
    else if (isPlaywriterPolicyConfigured()) {
        warn("Playwriter extension policy configured but extension not yet installed.");
        console.log("  Chrome needs to be restarted for the policy to take effect.\n");
    }
    else {
        fail("Playwriter extension not installed in any Chrome profile.");
        console.log("  The extension will be installed via Chrome enterprise policy.\n");
        if (confirm("  Install Playwriter extension via Chrome policy?")) {
            if (installPlaywriterPolicy()) {
                ok("Playwriter extension policy written.");
                console.log("  Restart Chrome for the extension to be installed automatically.\n");
            }
            else {
                fail("Failed to write Playwriter extension policy.");
            }
        }
        else {
            warn("Skipped Playwriter extension policy installation.");
        }
    }
    // 2. Check CLI
    if (isBinaryAvailable("playwriter")) {
        try {
            const { stdout } = spawnSync("playwriter", ["--version"], { encoding: "utf-8", timeout: 5000 });
            ok(`Playwriter CLI installed (${stdout.split("\n")[0].trim() || "version unknown"})`);
        }
        catch {
            ok("Playwriter CLI installed.");
        }
    }
    else {
        fail("Playwriter CLI not installed.");
        console.log("  Required for browser automation via Playwriter MCP.\n");
        if (confirm("  Install Playwriter CLI globally via npm?")) {
            console.log(`\n  ${DIM}npm install -g playwriter${RESET}\n`);
            const result = spawnSync("npm", ["install", "-g", "playwriter"], { stdio: "inherit" });
            if (result.status === 0) {
                ok("Playwriter CLI installed successfully.");
            }
            else {
                fail("Playwriter CLI installation failed. Check the output above.");
            }
        }
        else {
            warn("Skipped Playwriter CLI installation.");
        }
    }
}
// ─── macOS Permissions ───────────────────────────────────────────────────────
/**
 * Resolve the macOS SDK path via xcrun so we can pass -sdk explicitly to
 * swiftc, avoiding compiler/SDK version mismatches after partial Command
 * Line Tools updates.
 */
function getXcrunSdkPath() {
    try {
        return execSync("xcrun --sdk macosx --show-sdk-path 2>/dev/null", {
            encoding: "utf-8",
            timeout: 10_000,
        }).trim() || null;
    }
    catch {
        return null;
    }
}
/**
 * Compile a Swift source snippet to a temp binary and run it.
 * Used to trigger native macOS permission prompts (AVFoundation, EventKit, etc.)
 * that require framework-level API calls the shell can't make directly.
 *
 * The binary is run with stdio inherited so that the macOS consent dialog
 * remains properly associated with the Terminal process. The generous timeout
 * gives the user time to respond to the dialog before execSync kills the child.
 *
 * Passes -sdk explicitly when available to work around Xcode Command Line
 * Tools compiler/SDK version mismatches.
 */
function compileAndRunSwift(label, code) {
    const base = join(tmpdir(), `visionclaw-perm-${label}`);
    const src = `${base}.swift`;
    const bin = base;
    try {
        writeFileSync(src, code, "utf-8");
        const sdkPath = getXcrunSdkPath();
        const sdkFlag = sdkPath ? ` -sdk "${sdkPath}"` : "";
        execSync(`swiftc -O${sdkFlag} -o "${bin}" "${src}" 2>/dev/null`, { timeout: 30_000 });
        spawnSync(bin, [], { stdio: "ignore", timeout: 60_000 });
    }
    catch { /* expected if permission denied or compile issues */ }
    try {
        unlinkSync(src);
    }
    catch { /* cleanup */ }
    try {
        unlinkSync(bin);
    }
    catch { /* cleanup */ }
}
function waitForUser() {
    spawnSync("bash", ["-c", `read -r -p "  Press Enter to continue... "`], { stdio: "inherit" });
}
function ensureConfigOwnership() {
    heading("Config Directory Ownership");
    const configDir = join(homedir(), ".config");
    mkdirSync(configDir, { recursive: true });
    const userFromEnv = process.env.SUDO_USER ?? process.env.USER;
    const username = userFromEnv && userFromEnv.trim().length > 0
        ? userFromEnv.trim()
        : execSync("id -un", { encoding: "utf-8" }).trim();
    console.log(`  Ensuring ${configDir} is owned by ${username}...`);
    const chownResult = spawnSync("sudo", ["chown", "-R", username, configDir], { stdio: "inherit" });
    if (chownResult.status === 0) {
        ok(`${configDir} ownership fixed.`);
        return;
    }
    fail(`Failed to fix ${configDir} ownership.`);
}
const PERMISSION_CHECKS = [
    {
        label: "Screen Recording",
        service: "screenRecording",
        check: checkScreenRecordingPermission,
        trigger: () => {
            const tmpFile = `/tmp/visionclaw-perm-trigger-${Date.now()}.png`;
            try {
                execSync(`screencapture -x -C "${tmpFile}"`, { stdio: "ignore", timeout: 10_000 });
            }
            catch { /* expected if not yet granted */ }
            try {
                unlinkSync(tmpFile);
            }
            catch { /* cleanup best-effort */ }
        },
        settingsPanel: "Screen Recording",
    },
    {
        label: "Accessibility",
        service: "accessibility",
        check: checkAccessibilityPermission,
        trigger: () => {
            try {
                execSync("cliclick p", { stdio: "ignore", timeout: 10_000 });
            }
            catch { /* expected if not yet granted */ }
        },
        settingsPanel: "Accessibility",
        manualOnly: true,
    },
    {
        label: "Automation (AppleScript)",
        service: "appleEvents",
        check: checkAutomationPermission,
        trigger: () => {
            try {
                execSync(`osascript -e 'tell application "System Events" to return name of first process'`, { stdio: "ignore", timeout: 10_000 });
            }
            catch { /* expected if not yet granted */ }
        },
        settingsPanel: "Automation",
    },
    {
        label: "Full Disk Access",
        service: "fullDiskAccess",
        check: checkFullDiskAccessPermission,
        trigger: () => {
            try {
                execSync(`ls "${process.env.HOME ?? ""}/Library/Mail" >/dev/null 2>&1`, { timeout: 10_000 });
            }
            catch { /* expected if not yet granted */ }
        },
        settingsPanel: "Full Disk Access",
        manualOnly: true,
    },
    {
        label: "Camera",
        service: "camera",
        check: checkCameraPermission,
        trigger: () => {
            compileAndRunSwift("camera", [
                "import AVFoundation",
                "import Foundation",
                "let sem = DispatchSemaphore(value: 0)",
                "AVCaptureDevice.requestAccess(for: .video) { _ in sem.signal() }",
                "sem.wait()",
            ].join("\n"));
        },
        settingsPanel: "Camera",
    },
    {
        label: "Microphone",
        service: "microphone",
        check: checkMicrophonePermission,
        trigger: () => {
            compileAndRunSwift("mic", [
                "import AVFoundation",
                "import Foundation",
                "let sem = DispatchSemaphore(value: 0)",
                "AVCaptureDevice.requestAccess(for: .audio) { _ in sem.signal() }",
                "sem.wait()",
            ].join("\n"));
        },
        settingsPanel: "Microphone",
    },
    {
        label: "Contacts",
        service: "contacts",
        check: checkContactsPermission,
        trigger: () => {
            compileAndRunSwift("contacts", [
                "import Contacts",
                "import Foundation",
                "let store = CNContactStore()",
                "let sem = DispatchSemaphore(value: 0)",
                "store.requestAccess(for: .contacts) { _, _ in sem.signal() }",
                "sem.wait()",
            ].join("\n"));
        },
        settingsPanel: "Contacts",
    },
    {
        label: "Calendar",
        service: "calendar",
        check: checkCalendarPermission,
        trigger: () => {
            compileAndRunSwift("calendar", [
                "import EventKit",
                "import Foundation",
                "let store = EKEventStore()",
                "let sem = DispatchSemaphore(value: 0)",
                "if #available(macOS 17.0, *) {",
                "  store.requestFullAccessToEvents { _, _ in sem.signal() }",
                "} else {",
                "  store.requestAccess(to: .event) { _, _ in sem.signal() }",
                "}",
                "sem.wait()",
            ].join("\n"));
        },
        settingsPanel: "Calendars",
    },
    {
        label: "Reminders",
        service: "reminders",
        check: checkRemindersPermission,
        trigger: () => {
            compileAndRunSwift("reminders", [
                "import EventKit",
                "import Foundation",
                "let store = EKEventStore()",
                "let sem = DispatchSemaphore(value: 0)",
                "if #available(macOS 17.0, *) {",
                "  store.requestFullAccessToReminders { _, _ in sem.signal() }",
                "} else {",
                "  store.requestAccess(to: .reminder) { _, _ in sem.signal() }",
                "}",
                "sem.wait()",
            ].join("\n"));
        },
        settingsPanel: "Reminders",
    },
    {
        label: "Photos",
        service: "photos",
        check: checkPhotosPermission,
        trigger: () => {
            compileAndRunSwift("photos", [
                "import Photos",
                "import Foundation",
                "let sem = DispatchSemaphore(value: 0)",
                "PHPhotoLibrary.requestAuthorization(for: .readWrite) { _ in sem.signal() }",
                "sem.wait()",
            ].join("\n"));
        },
        settingsPanel: "Photos",
    },
    {
        label: "Speech Recognition",
        service: "speechRecognition",
        check: checkSpeechRecognitionPermission,
        trigger: () => {
            compileAndRunSwift("speech", [
                "import Speech",
                "import Foundation",
                "let sem = DispatchSemaphore(value: 0)",
                "SFSpeechRecognizer.requestAuthorization { _ in sem.signal() }",
                "sem.wait()",
            ].join("\n"));
        },
        settingsPanel: "Speech Recognition",
    },
    {
        label: "Input Monitoring",
        service: "inputMonitoring",
        check: checkInputMonitoringPermission,
        trigger: () => { },
        settingsPanel: "Input Monitoring",
        manualOnly: true,
    },
    {
        label: "Desktop Folder",
        service: "desktopFolder",
        check: checkDesktopFolderPermission,
        trigger: () => {
            try {
                execSync(`ls "${process.env.HOME ?? ""}/Desktop" >/dev/null 2>&1`, { timeout: 10_000 });
            }
            catch { /* expected if not yet granted */ }
        },
        settingsPanel: "Files and Folders",
    },
    {
        label: "Documents Folder",
        service: "documentsFolder",
        check: checkDocumentsFolderPermission,
        trigger: () => {
            try {
                execSync(`ls "${process.env.HOME ?? ""}/Documents" >/dev/null 2>&1`, { timeout: 10_000 });
            }
            catch { /* expected if not yet granted */ }
        },
        settingsPanel: "Files and Folders",
    },
    {
        label: "Downloads Folder",
        service: "downloadsFolder",
        check: checkDownloadsFolderPermission,
        trigger: () => {
            try {
                execSync(`ls "${process.env.HOME ?? ""}/Downloads" >/dev/null 2>&1`, { timeout: 10_000 });
            }
            catch { /* expected if not yet granted */ }
        },
        settingsPanel: "Files and Folders",
    },
];
function sleep(ms) {
    spawnSync("sleep", [String(ms / 1000)]);
}
/**
 * Trigger each macOS permission one at a time so the consent dialog can
 * appear and the user can respond before we move on to the next one.
 *
 * Flow per permission:
 *   1. Check if already granted → skip with checkmark
 *   2. Trigger the native API that prompts macOS consent dialog
 *   3. Wait briefly for the dialog to appear on screen
 *   4. Wait for user to handle the dialog and press Enter
 *   5. Re-check; if still not granted, offer retry / System Settings / skip
 */
async function triggerPermissionDialogs() {
    console.log("  We will request each permission one at a time.");
    console.log("  Please handle each macOS dialog before pressing Enter.\n");
    const total = PERMISSION_CHECKS.length;
    for (let i = 0; i < PERMISSION_CHECKS.length; i++) {
        const perm = PERMISSION_CHECKS[i];
        const prefix = `[${i + 1}/${total}]`;
        const alreadyGranted = await perm.check();
        if (alreadyGranted) {
            ok(`${prefix} ${perm.label} — already granted`);
            continue;
        }
        if (perm.manualOnly) {
            console.log(`\n  ${prefix} ${BOLD}${perm.label}${RESET} — requires manual grant`);
            console.log(`    This permission cannot be triggered programmatically.`);
            console.log(`    System Settings will open to the correct panel.`);
            if (perm.service === "accessibility") {
                const nodePath = getNodeBinaryPath() ?? "node";
                const cliclickPath = getCliclickBinaryPath() ?? "cliclick";
                console.log(`    Add ALL of the following binaries:`);
                console.log(`      • Terminal`);
                console.log(`      • cliclick (${cliclickPath})`);
                console.log(`      • Node (${nodePath})`);
                console.log(`    cliclick calls the Accessibility API directly at runtime.\n`);
            }
            else {
                console.log(`    Add Terminal and Node (${getNodeBinaryPath() ?? "node"}), then press Enter.\n`);
            }
            openSystemSettingsForPermission(perm.service);
            waitForUser();
            const nowGranted = await perm.check();
            if (nowGranted) {
                ok(`${prefix} ${perm.label} — granted`);
            }
            else {
                warn(`${prefix} ${perm.label} — still not granted (you may need to restart Terminal)`);
            }
            continue;
        }
        // Retry loop: trigger → wait → check → retry/open settings/skip
        // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- loop re-evaluates after retry
        while (true) {
            console.log(`\n  ${prefix} ${BOLD}${perm.label}${RESET}`);
            console.log(`    Triggering permission request...`);
            perm.trigger();
            sleep(1500);
            console.log(`    A macOS dialog should have appeared. Click ${BOLD}Allow${RESET}, then press Enter.`);
            waitForUser();
            const granted = await perm.check();
            if (granted) {
                ok(`${prefix} ${perm.label} — granted`);
                break;
            }
            console.log(`    ${YELLOW}Permission not detected yet.${RESET}`);
            console.log(`    ${DIM}(r)${RESET} Retry trigger    ${DIM}(s)${RESET} Open System Settings    ${DIM}(Enter)${RESET} Skip`);
            const choice = spawnSync("bash", ["-c", `read -r -n1 -p "    Choice: " c; echo "$c"`], { stdio: ["inherit", "pipe", "inherit"], encoding: "utf-8" });
            const answer = choice.stdout.trim().toLowerCase();
            console.log("");
            if (answer === "r") {
                continue;
            }
            if (answer === "s") {
                console.log(`    Opening System Settings > Privacy & Security > ${perm.settingsPanel}`);
                openSystemSettingsForPermission(perm.service);
                console.log(`    Grant the permission, then press Enter.`);
                waitForUser();
                const grantedViaSettings = await perm.check();
                if (grantedViaSettings) {
                    ok(`${prefix} ${perm.label} — granted`);
                }
                else {
                    warn(`${prefix} ${perm.label} — still not granted (you may need to restart Terminal)`);
                }
                break;
            }
            // Enter or anything else → skip
            warn(`${prefix} ${perm.label} — skipped`);
            break;
        }
    }
}
async function checkPermissions() {
    heading("macOS Permissions");
    // Step 0: Trigger permissions one-by-one with native dialogs
    await triggerPermissionDialogs();
    // Step 1: Check what's still missing after the interactive trigger round
    const missing = [];
    console.log("\n  Permission status after granting:\n");
    for (const perm of PERMISSION_CHECKS) {
        const granted = await perm.check();
        if (granted) {
            ok(`${perm.label} — granted`);
        }
        else {
            fail(`${perm.label} — NOT granted`);
            missing.push(perm);
        }
    }
    if (missing.length === 0) {
        console.log(`\n  All macOS permissions are granted.`);
        return;
    }
    // Step 2: Try auto-granting via TCC.db for anything still missing
    console.log(`\n  Attempting to auto-grant remaining permissions via TCC database...`);
    const nodePath = getNodeBinaryPath();
    if (nodePath) {
        console.log(`  Granting for Terminal and Node (${nodePath})`);
    }
    const results = await grantAllTerminalPermissions();
    const nodeResults = await grantAllNodePermissions();
    let autoGranted = 0;
    for (const perm of missing) {
        const key = perm.service;
        const terminalOk = results[key];
        const nodeOk = nodeResults[key];
        if (terminalOk || nodeOk) {
            const targets = [terminalOk ? "Terminal" : null, nodeOk ? "Node" : null].filter(Boolean).join(" + ");
            ok(`${perm.label} — auto-granted via TCC database (${targets})`);
            autoGranted++;
        }
    }
    if (autoGranted > 0) {
        console.log(`\n  ${autoGranted} permission(s) auto-granted.`);
        console.log("  Note: You may still need to restart Terminal for changes to take effect.\n");
    }
    // Step 3: Report anything still missing
    const stillMissing = [];
    for (const perm of missing) {
        const nowGranted = await perm.check();
        if (!nowGranted) {
            stillMissing.push(perm);
        }
    }
    if (stillMissing.length > 0) {
        console.log(`\n  ${stillMissing.length} permission(s) still need manual granting:\n`);
        for (const perm of stillMissing) {
            fail(`${perm.label} — System Settings > Privacy & Security > ${perm.settingsPanel}`);
            if (perm.service === "accessibility") {
                const cliclickPath = getCliclickBinaryPath();
                if (cliclickPath) {
                    console.log(`      Also add cliclick (${cliclickPath}) for Accessibility`);
                }
            }
        }
        const nodeHint = nodePath ? `, Node (${nodePath})` : "";
        const cliclickHint = getCliclickBinaryPath() ? `, and cliclick (${getCliclickBinaryPath()})` : "";
        console.log(`\n  After granting permissions in System Settings for Terminal${nodeHint}${cliclickHint}:`);
        console.log(`  1. Close and reopen Terminal`);
        console.log(`  2. Re-run: visionclaw prepare-mac\n`);
    }
}
async function printSummary() {
    heading("Summary");
    const checks = [
        { label: "cliclick", passed: await checkCliclickInstalled() },
        { label: "ffmpeg", passed: await checkFfmpegInstalled() },
        { label: "whisper-cli", passed: await checkWhisperCppInstalled() },
        { label: "Whisper model", passed: checkWhisperModelIntegrity() === "valid" },
        { label: "gh (GitHub CLI)", passed: await checkGhInstalled() },
        { label: "pm2", passed: await checkPm2Installed() },
        { label: "cloudflared", passed: isBinaryAvailable("cloudflared") },
        { label: "dockutil", passed: isBinaryAvailable("dockutil") },
        { label: "Google Chrome", passed: await checkChromeInstalled() },
        { label: "Google Chrome — default browser", passed: isChromeDefaultBrowser() },
        { label: "Playwriter extension", passed: isPlaywriterExtensionInstalled().installed },
        { label: "Playwriter CLI", passed: isBinaryAvailable("playwriter") },
        { label: "Screen Recording", passed: await checkScreenRecordingPermission() },
        { label: "Accessibility", passed: await checkAccessibilityPermission() },
        { label: "Automation (AppleScript)", passed: await checkAutomationPermission() },
        { label: "Full Disk Access", passed: await checkFullDiskAccessPermission() },
        { label: "Camera", passed: await checkCameraPermission() },
        { label: "Microphone", passed: await checkMicrophonePermission() },
        { label: "Contacts", passed: await checkContactsPermission() },
        { label: "Calendar", passed: await checkCalendarPermission() },
        { label: "Reminders", passed: await checkRemindersPermission() },
        { label: "Photos", passed: await checkPhotosPermission() },
        { label: "Speech Recognition", passed: await checkSpeechRecognitionPermission() },
        { label: "Input Monitoring", passed: await checkInputMonitoringPermission() },
        { label: "Desktop Folder", passed: await checkDesktopFolderPermission() },
        { label: "Documents Folder", passed: await checkDocumentsFolderPermission() },
        { label: "Downloads Folder", passed: await checkDownloadsFolderPermission() },
        { label: "Power & screen strategy", passed: isPowerStrategyConfigured() },
        { label: "Automatic software update — disabled", passed: isAutoUpdateDisabled() },
        { label: "Mac mini display resolution", passed: isMacMiniDisplayResolutionConfigured() },
        { label: "Dock configured", passed: isDockConfigured() },
        { label: "Passwordless sudo", passed: isPasswordlessSudoConfigured() },
        { label: "system.privilege.admin — grants allow", passed: isSystemPrivilegeAdminAllowed() },
    ];
    for (const c of checks) {
        if (c.passed) {
            ok(c.label);
        }
        else {
            fail(c.label);
        }
    }
    const allPassed = checks.every((c) => c.passed);
    if (allPassed) {
        console.log(`\n  ${GREEN}${BOLD}All checks passed!${RESET}`);
        console.log(`  Run ${DIM}visionclaw setup${RESET} to continue with manufacturer setup.\n`);
    }
    else {
        const failCount = checks.filter((c) => !c.passed).length;
        console.log(`\n  ${YELLOW}${failCount} check(s) still need attention.${RESET}`);
        console.log(`  Fix the issues above, then re-run ${DIM}visionclaw prepare-mac${RESET}.\n`);
    }
}
// ─── Main ────────────────────────────────────────────────────────────────────
function ensureMacPlatform() {
    if (process.platform !== "darwin") {
        console.log("This command is only supported on macOS.");
        process.exit(1);
    }
}
export function runPrepareMacPower() {
    ensureMacPlatform();
    console.log(`\n${BOLD}=== VisionClaw Mac Power Settings ===${RESET}`);
    configurePowerStrategy();
}
export async function runPrepareMac() {
    ensureMacPlatform();
    console.log(`\n${BOLD}=== VisionClaw Mac Preparation ===${RESET}`);
    guardClawSetupAccountContext();
    // Pre-create ~/.config as the current user before any sudo-elevated steps
    // can create it owned by root, which would cause tools like gh, npm, and
    // others to fail with "permission denied".
    mkdirSync(join(homedir(), ".config"), { recursive: true });
    // Configure passwordless sudo early — many subsequent steps (pmset,
    // sysadminctl, defaults write to /Library/Preferences, etc.) need sudo.
    configurePasswordlessSudo();
    // Set the system.privilege.admin authorization right to 'allow' so that
    // background utilities calling AuthorizationExecuteWithPrivileges() (cron
    // spool access, some Admin-framework code paths, etc.) don't trigger the
    // "would like to administer your computer" SecurityAgent dialog at runtime
    // when no user is at the keyboard.
    configureSystemPrivilegeAdmin();
    await installBrewPackages();
    await installNpmGlobals();
    downloadWhisperModel();
    configurePowerStrategy();
    configureAutoUpdate();
    configureDesktop();
    configureMacMiniDisplayResolution();
    await checkChrome();
    _checkPlaywriterExtension();
    await checkPermissions();
    // Record current binary paths so the agent can detect changes at startup
    saveTccPaths();
    configureDock();
    ensureConfigOwnership();
    await printSummary();
    console.log(`  ${BOLD}Next step:${RESET} ask your VisionClaw administrator for your setup code, then run ${DIM}visionclaw setup${RESET} in this Terminal to finish profile configuration.\n`);
}
//# sourceMappingURL=prepare-mac.js.map