import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import http from "node:http";
import https from "node:https";
import { execFile, spawn } from "node:child_process";
import { promisify } from "node:util";
import { getConfigDir, getProfile } from "./config/index.js";
import { logger } from "./logger.js";
import { getClaudeTranscriptDirForProfileDir, getReferencedClaudeTranscriptIds, } from "./utils/claude-transcripts.js";
const CLOUDFLARED_DIR = path.join(os.homedir(), ".cloudflared");
const GH_CLI_DIR = path.join(os.homedir(), ".config", "gh");
const DESKTOP_DIR = path.join(os.homedir(), "Desktop");
const GH_CLI_TOKEN_FILE = "oauth-token.txt";
const execFileAsync = promisify(execFile);
function parseRestoreArgs(args) {
    let source;
    let force = false;
    for (let i = 0; i < args.length; i += 1) {
        const arg = args[i];
        if (arg === "--force") {
            force = true;
            continue;
        }
        if (arg === "--profile") {
            i += 1;
            continue;
        }
        if (arg === "-d" ||
            arg === "--debug" ||
            arg === "-h" ||
            arg === "--help" ||
            arg === "-v" ||
            arg === "--version") {
            continue;
        }
        if (arg.startsWith("-")) {
            throw new Error(`Unknown restore option: ${arg}`);
        }
        if (source) {
            throw new Error(`Restore accepts a single source.\nUsage: visionclaw restore <local-zip-or-http-url> [--force]`);
        }
        source = arg;
    }
    if (!source) {
        throw new Error("Usage: visionclaw restore <local-zip-or-http-url> [--force]");
    }
    return { source, force };
}
function formatBytes(bytes) {
    if (bytes < 1024)
        return `${bytes} B`;
    const units = ["KB", "MB", "GB", "TB"];
    let value = bytes / 1024;
    let unitIndex = 0;
    while (value >= 1024 && unitIndex < units.length - 1) {
        value /= 1024;
        unitIndex += 1;
    }
    return `${value.toFixed(value >= 10 ? 1 : 2)} ${units[unitIndex]}`;
}
function formatDuration(ms) {
    if (ms < 1_000)
        return `${Math.max(0, Math.round(ms))}ms`;
    const totalSeconds = Math.max(1, Math.round(ms / 1_000));
    const minutes = Math.floor(totalSeconds / 60);
    const seconds = totalSeconds % 60;
    if (minutes === 0)
        return `${seconds}s`;
    return `${minutes}m ${seconds}s`;
}
function isHttpUrl(source) {
    try {
        const url = new URL(source);
        return url.protocol === "http:" || url.protocol === "https:";
    }
    catch {
        return false;
    }
}
function escapePowerShellLiteral(value) {
    return value.replace(/'/g, "''");
}
export async function resolveRestoreSource(source, tmpDir) {
    if (source.startsWith("tos://")) {
        throw new Error("Restore does not support tos:// sources. Use the signed HTTPS download URL from backup output instead.");
    }
    if (isHttpUrl(source)) {
        const zipPath = path.join(tmpDir, "source.zip");
        await downloadRestoreSource(source, zipPath);
        return { zipPath, displaySource: "signed URL" };
    }
    const zipPath = path.resolve(source);
    if (!fs.existsSync(zipPath)) {
        throw new Error(`Backup ZIP not found: ${zipPath}`);
    }
    if (!zipPath.toLowerCase().endsWith(".zip")) {
        throw new Error(`Restore expects a .zip file: ${zipPath}`);
    }
    return { zipPath, displaySource: zipPath };
}
async function downloadRestoreSource(source, destPath) {
    logger.system(`Downloading backup from URL: ${source}`);
    await downloadUrlToFile(source, destPath, 0);
    const sizeBytes = fs.statSync(destPath).size;
    if (!destPath.toLowerCase().endsWith(".zip")) {
        throw new Error(`Downloaded backup destination is not a .zip file: ${destPath}`);
    }
    logger.system(`Downloaded backup: ${destPath} (${formatBytes(sizeBytes)})`);
}
async function downloadUrlToFile(source, destPath, redirectCount) {
    if (redirectCount > 3) {
        throw new Error("Too many redirects while downloading backup.");
    }
    const url = new URL(source);
    const client = url.protocol === "https:" ? https : http;
    await new Promise((resolve, reject) => {
        const request = client.get(url, (response) => {
            const statusCode = response.statusCode ?? 0;
            const location = response.headers.location;
            if (statusCode >= 300 && statusCode < 400 && location) {
                response.resume();
                const nextUrl = new URL(location, url).toString();
                downloadUrlToFile(nextUrl, destPath, redirectCount + 1).then(resolve, reject);
                return;
            }
            if (statusCode === 403) {
                response.resume();
                reject(new Error("Backup download failed with HTTP 403. The signed URL may have expired."));
                return;
            }
            if (statusCode < 200 || statusCode >= 300) {
                response.resume();
                reject(new Error(`Backup download failed with HTTP ${statusCode}.`));
                return;
            }
            fs.mkdirSync(path.dirname(destPath), { recursive: true });
            const output = fs.createWriteStream(destPath);
            const totalBytes = Number(response.headers["content-length"] ?? 0);
            const startMs = Date.now();
            let downloadedBytes = 0;
            let lastLogMs = 0;
            response.on("data", (chunk) => {
                downloadedBytes += chunk.length;
                const nowMs = Date.now();
                if (nowMs - lastLogMs < 5_000 && downloadedBytes < totalBytes)
                    return;
                lastLogMs = nowMs;
                const elapsedMs = Math.max(1, nowMs - startMs);
                const bytesPerSecond = downloadedBytes / (elapsedMs / 1_000);
                if (totalBytes > 0) {
                    const remainingBytes = Math.max(0, totalBytes - downloadedBytes);
                    const etaMs = bytesPerSecond > 0 ? (remainingBytes / bytesPerSecond) * 1_000 : 0;
                    logger.system(`Download progress: ${formatBytes(downloadedBytes)}/${formatBytes(totalBytes)} (${Math.min(100, (downloadedBytes / totalBytes) * 100).toFixed(1)}%), ${formatBytes(bytesPerSecond)}/s, ETA ${formatDuration(etaMs)}.`);
                }
                else {
                    logger.system(`Download progress: ${formatBytes(downloadedBytes)}, ${formatBytes(bytesPerSecond)}/s.`);
                }
            });
            response.pipe(output);
            output.on("finish", () => {
                output.close(() => resolve());
            });
            output.on("error", reject);
            response.on("error", reject);
        });
        request.setTimeout(120_000, () => {
            request.destroy(new Error("Backup download timed out."));
        });
        request.on("error", reject);
    });
}
async function extractZipArchive(zipPath, destDir) {
    if (process.platform === "win32") {
        await execFileAsync("powershell", [
            "-NoProfile",
            "-Command",
            `Expand-Archive -LiteralPath '${escapePowerShellLiteral(zipPath)}' -DestinationPath '${escapePowerShellLiteral(destDir)}' -Force`,
        ], { timeout: 10 * 60 * 1000 });
        return;
    }
    await execFileAsync("unzip", ["-oq", zipPath, "-d", destDir], {
        timeout: 10 * 60 * 1000,
    });
}
function getSourceProfileDir(extractedRoot) {
    const profilesDir = path.join(extractedRoot, ".visionclaw", "profiles");
    if (!fs.existsSync(profilesDir)) {
        throw new Error("Backup does not contain a .visionclaw/profiles directory.");
    }
    const profileDirs = fs
        .readdirSync(profilesDir, { withFileTypes: true })
        .filter((entry) => entry.isDirectory());
    if (profileDirs.length === 0) {
        throw new Error("Backup does not contain a VisionClaw profile directory.");
    }
    if (profileDirs.length > 1) {
        throw new Error("Backup contains multiple VisionClaw profiles. Restore one profile per archive.");
    }
    const sourceProfile = profileDirs[0].name;
    return {
        sourceProfile,
        sourceProfileDir: path.join(profilesDir, sourceProfile),
    };
}
function buildTranscriptRestoreEntries(extractedRoot, sourceProfileDir) {
    const transcriptIds = getReferencedClaudeTranscriptIds(path.join(sourceProfileDir, "session.json"));
    if (transcriptIds.length === 0) {
        return { entries: [], warnings: [] };
    }
    const projectsDir = path.join(extractedRoot, ".claude", "projects");
    if (!fs.existsSync(projectsDir)) {
        return {
            entries: [],
            warnings: [
                `Backup does not contain ${transcriptIds.length} Claude transcript file${transcriptIds.length === 1 ? "" : "s"} referenced by session.json.`,
            ],
        };
    }
    const transcriptSourceDirs = fs
        .readdirSync(projectsDir, { withFileTypes: true })
        .filter((entry) => entry.isDirectory())
        .map((entry) => path.join(projectsDir, entry.name));
    const targetTranscriptDir = getClaudeTranscriptDirForProfileDir(getConfigDir());
    const entries = [];
    const missingIds = [];
    for (const transcriptId of transcriptIds) {
        const fileName = `${transcriptId}.jsonl`;
        let sourcePath;
        for (const sourceDir of transcriptSourceDirs) {
            const candidate = path.join(sourceDir, fileName);
            if (fs.existsSync(candidate)) {
                sourcePath = candidate;
                break;
            }
        }
        if (!sourcePath) {
            missingIds.push(transcriptId);
            continue;
        }
        entries.push({
            source: sourcePath,
            target: path.join(targetTranscriptDir, fileName),
            label: `Claude transcript ${fileName}`,
            kind: "file",
        });
    }
    return {
        entries,
        warnings: missingIds.length > 0
            ? [
                `Backup does not contain ${missingIds.length} Claude transcript file${missingIds.length === 1 ? "" : "s"} referenced by session.json.`,
            ]
            : [],
    };
}
function buildDesktopRestoreEntries(extractedRoot) {
    const sourceDir = path.join(extractedRoot, ".visionclaw", "user-data", "Desktop");
    if (!fs.existsSync(sourceDir))
        return [];
    return [{
            source: sourceDir,
            target: DESKTOP_DIR,
            label: "Desktop/",
            kind: "directory",
        }];
}
function buildCloudflaredRestoreEntries(extractedRoot) {
    const sourceDir = path.join(extractedRoot, ".cloudflared");
    if (!fs.existsSync(sourceDir))
        return [];
    const entries = [];
    try {
        for (const fileName of fs.readdirSync(sourceDir)) {
            const sourcePath = path.join(sourceDir, fileName);
            if (!fs.lstatSync(sourcePath).isFile())
                continue;
            entries.push({
                source: sourcePath,
                target: path.join(CLOUDFLARED_DIR, fileName),
                label: `cloudflared ${fileName}`,
                kind: "file",
            });
        }
    }
    catch {
        return [];
    }
    return entries;
}
function buildGhCliRestoreEntries(extractedRoot) {
    const sourceDir = path.join(extractedRoot, ".config", "gh");
    if (!fs.existsSync(sourceDir))
        return [];
    const entries = [];
    try {
        for (const fileName of fs.readdirSync(sourceDir)) {
            // Skip the token file — it's handled separately via `gh auth login`
            if (fileName === GH_CLI_TOKEN_FILE)
                continue;
            const sourcePath = path.join(sourceDir, fileName);
            if (!fs.lstatSync(sourcePath).isFile())
                continue;
            entries.push({
                source: sourcePath,
                target: path.join(GH_CLI_DIR, fileName),
                label: `gh-cli ${fileName}`,
                kind: "file",
            });
        }
    }
    catch {
        return [];
    }
    return entries;
}
export function buildRestorePlan(extractedRoot) {
    const { sourceProfile, sourceProfileDir } = getSourceProfileDir(extractedRoot);
    const warnings = [];
    const entries = [
        {
            source: sourceProfileDir,
            target: getConfigDir(),
            label: `${sourceProfile}/`,
            kind: "directory",
        },
    ];
    if (!fs.existsSync(path.join(sourceProfileDir, "google-tokens.json"))) {
        warnings.push("Backup does not contain google-tokens.json. Google integrations may require re-authentication after restore.");
    }
    const transcriptEntries = buildTranscriptRestoreEntries(extractedRoot, sourceProfileDir);
    entries.push(...transcriptEntries.entries);
    warnings.push(...transcriptEntries.warnings);
    const desktopEntries = buildDesktopRestoreEntries(extractedRoot);
    entries.push(...desktopEntries);
    const cloudflaredEntries = buildCloudflaredRestoreEntries(extractedRoot);
    entries.push(...cloudflaredEntries);
    const ghCliEntries = buildGhCliRestoreEntries(extractedRoot);
    entries.push(...ghCliEntries);
    return {
        sourceProfile,
        entries,
        warnings,
    };
}
function removePath(target, kind) {
    if (kind === "directory") {
        fs.rmSync(target, { recursive: true, force: true });
        return;
    }
    fs.rmSync(target, { force: true });
}
function copyRestoreEntry(entry) {
    if (entry.kind === "directory") {
        fs.mkdirSync(path.dirname(entry.target), { recursive: true });
        fs.cpSync(entry.source, entry.target, { recursive: true });
        return;
    }
    fs.mkdirSync(path.dirname(entry.target), { recursive: true });
    fs.copyFileSync(entry.source, entry.target);
}
function backupExistingEntry(entry, backupPath) {
    const hadOriginal = fs.existsSync(entry.target);
    if (hadOriginal) {
        fs.mkdirSync(path.dirname(backupPath), { recursive: true });
        if (entry.kind === "directory") {
            fs.cpSync(entry.target, backupPath, { recursive: true });
        }
        else {
            fs.copyFileSync(entry.target, backupPath);
        }
    }
    return {
        entry,
        backupPath,
        hadOriginal,
    };
}
function restoreRollbackEntries(entries) {
    for (let i = entries.length - 1; i >= 0; i -= 1) {
        const rollback = entries[i];
        removePath(rollback.entry.target, rollback.entry.kind);
        if (!rollback.hadOriginal)
            continue;
        if (rollback.entry.kind === "directory") {
            fs.mkdirSync(path.dirname(rollback.entry.target), { recursive: true });
            fs.cpSync(rollback.backupPath, rollback.entry.target, { recursive: true });
        }
        else {
            fs.mkdirSync(path.dirname(rollback.entry.target), { recursive: true });
            fs.copyFileSync(rollback.backupPath, rollback.entry.target);
        }
    }
}
function preflightRestore(entries, force) {
    if (force)
        return;
    const existingTargets = entries
        .filter((entry) => fs.existsSync(entry.target))
        .map((entry) => `${entry.label} -> ${entry.target}`);
    if (existingTargets.length > 0) {
        throw new Error(`Restore would overwrite existing data:\n${existingTargets.join("\n")}\nRe-run with --force to replace those paths.`);
    }
}
export async function runRestoreCli(args = process.argv.slice(2)) {
    const parsed = parseRestoreArgs(args);
    const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "vc-restore-"));
    const resolved = await resolveRestoreSource(parsed.source, tmpDir);
    const extractDir = path.join(tmpDir, "extract");
    const rollbackDir = path.join(tmpDir, "rollback");
    try {
        fs.mkdirSync(extractDir, { recursive: true });
        logger.system(`Restoring VisionClaw profile "${getProfile()}" from ${resolved.displaySource}`);
        await extractZipArchive(resolved.zipPath, extractDir);
        const plan = buildRestorePlan(extractDir);
        preflightRestore(plan.entries, parsed.force);
        logger.system(`Backup source profile: ${plan.sourceProfile}; restoring ${plan.entries.length} item(s) into "${getProfile()}"`);
        for (const warning of plan.warnings) {
            logger.warn(warning);
        }
        const applied = [];
        try {
            plan.entries.forEach((entry, index) => {
                const rollbackEntry = backupExistingEntry(entry, path.join(rollbackDir, String(index)));
                applied.push(rollbackEntry);
                removePath(entry.target, entry.kind);
                copyRestoreEntry(entry);
            });
        }
        catch (error) {
            restoreRollbackEntries(applied);
            throw error;
        }
        // Restore GitHub CLI OAuth token if present in the backup.
        const ghTokenFile = path.join(extractDir, ".config", "gh", GH_CLI_TOKEN_FILE);
        if (fs.existsSync(ghTokenFile)) {
            const token = fs.readFileSync(ghTokenFile, "utf-8").trim();
            if (token) {
                try {
                    await new Promise((resolve, reject) => {
                        const proc = spawn("gh", ["auth", "login", "--with-token"], {
                            stdio: ["pipe", "ignore", "pipe"],
                            timeout: 10_000,
                        });
                        let stderr = "";
                        proc.stderr.on("data", (chunk) => {
                            stderr += chunk.toString();
                        });
                        proc.on("close", (code) => {
                            if (code === 0)
                                resolve();
                            else
                                reject(new Error(stderr || `gh exited with code ${code}`));
                        });
                        proc.on("error", reject);
                        proc.stdin.end(token);
                    });
                    logger.system("GitHub CLI OAuth token restored.");
                }
                catch {
                    logger.warn("Failed to restore GitHub CLI OAuth token. Run `gh auth login` manually.");
                }
            }
        }
        logger.system(`Restore complete for profile "${getProfile()}".`);
    }
    finally {
        fs.rmSync(tmpDir, { recursive: true, force: true });
    }
}
//# sourceMappingURL=restore.js.map