/**
 * Shared helpers used by both onboarding entry points:
 *   - `runSetup()` in `./index.ts`              (interactive CLI)
 *   - `runSetupStepCli()` in `./setup-steps.ts` (Electron GUI step runner)
 *
 * Anything deterministic (config object construction, identity-memory
 * formatting, profile-photo download, GitHub username resolution) lives
 * here so the two entry points can share a single source of truth.
 *
 * Anything that requires a user prompt or a real browser session stays in
 * the interactive `index.ts` flow; anything that emits structured JSON for
 * the GUI stays in `setup-steps.ts`.
 */
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { ensureConfigDir, getMemoriesDir, getProfilePhotosDir, } from "../config/index.js";
import { OBS_TUNNEL_DOMAIN, } from "../config/types.js";
import { sanitizeTunnelName } from "../obs/cloudflared-config.js";
import { setWorkspaceUserPhoto } from "./provision-workspace-account.js";
/**
 * Resolve the local GitHub username via `gh api user`. Returns undefined
 * when `gh` is not authenticated or unavailable.
 */
export function resolveGithubUsername() {
    const result = spawnSync("gh", ["api", "user", "-q", ".login"], {
        encoding: "utf-8",
        timeout: 15_000,
        windowsHide: true,
    });
    const login = result.stdout.trim();
    return result.status === 0 && login ? login : undefined;
}
export function authenticateGithubWithPat(pat) {
    const trimmedPat = pat.trim();
    if (!trimmedPat) {
        return { ok: false, error: "GitHub PAT is empty" };
    }
    const ghConfigDir = path.join(os.homedir(), ".config", "gh");
    fs.mkdirSync(ghConfigDir, { recursive: true });
    const login = spawnSync("gh", ["auth", "login", "--with-token"], {
        input: `${trimmedPat}\n`,
        encoding: "utf-8",
        timeout: 30_000,
        windowsHide: true,
        env: {
            ...process.env,
            GH_CONFIG_DIR: ghConfigDir,
        },
    });
    if (login.status !== 0) {
        const stderr = login.stderr.trim();
        const stdout = login.stdout.trim();
        return {
            ok: false,
            error: stderr || stdout || "gh auth login --with-token failed",
        };
    }
    const username = resolveGithubUsername();
    return { ok: true, username };
}
/**
 * Expand `~` / `~/...` to the user's home directory; pass other paths
 * through unchanged. The interactive CLI variant trims the input first;
 * callers that already trimmed (or never had whitespace) can pass the
 * raw value.
 */
export function resolveLocalPath(source) {
    const trimmed = source.trim();
    if (trimmed === "~")
        return os.homedir();
    if (trimmed.startsWith("~/"))
        return path.join(os.homedir(), trimmed.slice(2));
    return trimmed;
}
/**
 * Resolve the OBS tunnel name + hostname for the saved config.
 *
 * Precedence:
 *   1. `spec.cloudflared.tunnelName` / `tunnelHostname` — the onboarding
 *      service emits these, derived from the canonical `emailUsername`.
 *      They are the authoritative value because the server-issued runtime
 *      tunnel credential will use the same name.
 *   2. `sanitizeTunnelName(spec.agentName)` — local fallback that keeps
 *      legacy specs (which don't carry tunnel fields) working.
 *
 * Returns `{ undefined, undefined }` when cloudflared is disabled in the
 * spec, so callers can spread the result into the obs config without
 * special-casing.
 */
export function resolveObsTunnelIdentity(spec) {
    if (!spec.cloudflared.enabled) {
        return { tunnelName: undefined, tunnelHostname: undefined };
    }
    const specTunnelName = spec.cloudflared.tunnelName?.trim();
    const specTunnelHostname = spec.cloudflared.tunnelHostname?.trim();
    const tunnelName = specTunnelName && specTunnelName.length > 0
        ? specTunnelName
        : sanitizeTunnelName(spec.agentName);
    const tunnelHostname = specTunnelHostname && specTunnelHostname.length > 0
        ? specTunnelHostname
        : `${tunnelName}.${OBS_TUNNEL_DOMAIN}`;
    return { tunnelName, tunnelHostname };
}
/**
 * Build the persisted `VisionClawConfig` for a freshly-set-up agent.
 *
 * This is the single source of truth for the post-setup config layout —
 * both onboarding entry points (`runSetup` and the GUI `save-config`
 * step) call into here so the obs/tunnel/AWS/channels block can never
 * drift between them again.
 */
export function buildVisionClawConfig(input) {
    const { spec, state, gmail, gmailPassword, model } = input;
    const { tunnelName, tunnelHostname } = resolveObsTunnelIdentity(spec);
    return {
        agentName: spec.agentName.trim(),
        gmail,
        gmailPassword: gmailPassword ?? state.gmailPassword ?? spec.gmailPassword ?? undefined,
        systemPassword: spec.systemPassword?.trim() ?? undefined,
        googleClientId: spec.googleOAuth.clientId?.trim() ?? undefined,
        googleClientSecret: spec.googleOAuth.clientSecret?.trim() ?? undefined,
        model,
        provider: "bedrock",
        vertexRegion: "global",
        ninerouterBaseUrl: "https://9r.gru.ai",
        awsRegion: spec.aws.region.trim(),
        awsBearerToken: spec.aws.bearerToken?.trim() ? spec.aws.bearerToken.trim() : undefined,
        awsAccessKeyId: spec.aws.accessKeyId.trim(),
        awsSecretAccessKey: spec.aws.secretAccessKey.trim(),
        awsSessionToken: spec.aws.sessionToken?.trim() ?? undefined,
        serpApiKey: spec.serpApiKey.trim(),
        channels: {
            telegram: { enabled: true, botToken: spec.telegram.botToken.trim() },
            discord: { enabled: false },
        },
        obs: {
            enabled: true,
            host: "127.0.0.1",
            port: 3101,
            bufferSize: 1000,
            tunnel: spec.cloudflared.enabled ? "cloudflared" : "off",
            tunnelName,
            tunnelHostname,
        },
        launchAtLoginEnabled: true,
        wallpaperEnabled: true,
        onboardingServiceUrl: spec.onboardingService?.url,
        visionclawServiceApiKey: spec.onboardingService?.apiKey,
        backupProvider: "gdrive",
        backupTosBucket: "visionclaw-backup",
        backupTosRegion: "cn-beijing",
        backupTosEndpoint: "tos-accelerate.volces.com",
        backupTosPublicEndpoint: "tos-accelerate.volces.com",
        backupTosSignedUrlExpiresSeconds: 7 * 24 * 60 * 60,
        backupDesktopEnabled: true,
        backupDesktopMaxBytes: 1024 * 1024 * 1024,
        browserBackend: "playwright",
        geminiApiKey: spec.geminiApiKey?.trim() ?? undefined,
    };
}
/**
 * Write the post-setup `identity.md` memory file. Both entry points
 * produce the same file layout; centralising it here keeps the two
 * onboarding paths in lockstep when fields are added.
 */
export function writeIdentityMemory(input) {
    ensureConfigDir();
    const memoriesDir = getMemoriesDir();
    fs.mkdirSync(memoriesDir, { recursive: true });
    const memoryPath = path.join(memoriesDir, "identity.md");
    const photosDir = getProfilePhotosDir();
    const photoLines = input.profilePhotos.length > 0
        ? [
            `- Current: ${input.profilePhotos[0]}`,
            `- Available: ${input.profilePhotos.join(", ")}`,
            `- Location: ${photosDir}`,
        ].join("\n")
        : "- No profile photos available";
    const systemPasswordLine = input.systemPassword
        ? `\n\n## Computer System Password\n- Password: ${input.systemPassword}`
        : "";
    const githubLine = input.githubUsername
        ? `\n\n## GitHub\n- Username: ${input.githubUsername}\n- Profile: https://github.com/${input.githubUsername}`
        : "";
    const content = `# VisionClaw Identity

## Gmail Account
- Email: ${input.gmail}
- Password: ${input.gmailPassword ?? "N/A"}${systemPasswordLine}${githubLine}

## ETH Wallet
- Address: ${input.wallet.address || "N/A"}
- Private Key: ${input.wallet.privateKey || "N/A"}
- Mnemonic: ${input.wallet.mnemonic || "N/A"}

## Profile Photos
${photoLines}
`;
    fs.writeFileSync(memoryPath, content, "utf-8");
}
/**
 * Download/copy each `profilePhotoSources` entry into the profile
 * photos directory. URLs are fetched with a 30s timeout; local paths
 * support `~` expansion via {@link resolveLocalPath}. Per-photo errors
 * are reported and skipped rather than aborting the batch — a single
 * dead URL must not stop onboarding.
 *
 * Returns the list of saved filenames (relative to
 * `getProfilePhotosDir()`), in the order they were processed.
 */
export async function downloadProfilePhotos(sources, report = () => {
    /* silent */
}) {
    const dir = getProfilePhotosDir();
    fs.mkdirSync(dir, { recursive: true });
    const saved = [];
    for (const source of sources) {
        const trimmed = source.trim();
        if (!trimmed)
            continue;
        try {
            let buffer;
            let filename;
            if (/^https?:\/\//i.test(trimmed)) {
                const res = await fetch(trimmed, {
                    signal: AbortSignal.timeout(30_000),
                });
                if (!res.ok) {
                    report(`  Warning: failed to download ${trimmed}: HTTP ${res.status}`);
                    continue;
                }
                buffer = Buffer.from(await res.arrayBuffer());
                const urlPath = new URL(trimmed).pathname;
                filename = path.basename(urlPath) || `photo_${saved.length + 1}.png`;
            }
            else {
                const localPath = resolveLocalPath(trimmed);
                if (!fs.existsSync(localPath)) {
                    report(`  Warning: profile photo not found: ${trimmed}`);
                    continue;
                }
                buffer = fs.readFileSync(localPath);
                filename = path.basename(localPath);
            }
            const destPath = path.join(dir, filename);
            fs.writeFileSync(destPath, buffer);
            saved.push(filename);
            report(`  Saved: ${filename} (${buffer.length} bytes)`);
        }
        catch (err) {
            report(`  Warning: failed to process ${trimmed}: ${err instanceof Error ? err.message : String(err)}`);
        }
    }
    return saved;
}
/**
 * Upload the first downloaded profile photo to the Google Workspace
 * user record. No-op when there are no photos. Failures are reported
 * (or swallowed when no reporter is provided) — they must not block
 * onboarding because the photo upload is cosmetic.
 */
export async function applyWorkspaceProfilePhoto(spec, gmail, savedPhotos, report = () => {
    /* silent */
}) {
    if (savedPhotos.length === 0)
        return;
    if (!spec.googleWorkspace) {
        report("Google Workspace profile photo skipped: Admin SDK config is not present in the provisioning spec");
        return;
    }
    try {
        const initialPhoto = path.join(getProfilePhotosDir(), savedPhotos[0]);
        const photoBuffer = fs.readFileSync(initialPhoto);
        await setWorkspaceUserPhoto(spec, gmail, photoBuffer);
        report("Google Workspace profile photo updated");
    }
    catch (err) {
        report(`Warning: failed to set Google Workspace photo: ${err instanceof Error ? err.message : String(err)}`);
    }
}
//# sourceMappingURL=setup-shared.js.map