/**
 * Data collector: packages agent logs, session transcripts, and workspace
 * data into tar.gz archives and uploads them to TOS.
 *
 * Triggered as a `collect_data` command via the command dispatch framework.
 */
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import { execFileSync } from "node:child_process";
import { getConfigDir, getConfigFile, getMemoriesDir, getSessionFile, getSkillsDir, getOwnerFile, getDynamicMcpServersFile, } from "../config/index.js";
import { getClaudeTranscriptDirForProfileDir, getReferencedClaudeTranscriptIds, } from "../utils/claude-transcripts.js";
import { logger } from "../logger.js";
import { resolveTosCredentials } from "../tos-credentials.js";
import { formatTosError, uploadTosObject } from "../tos-storage.js";
const DATA_COLLECTOR_BUCKET = "visionclaw-data-collector";
const MULTIPART_THRESHOLD = 64 * 1024 * 1024; // 64 MB
const PART_SIZE = 20 * 1024 * 1024; // 20 MB
const UPLOAD_MAX_ATTEMPTS = 4;
const UPLOAD_RETRY_BASE_DELAY_MS = 1_000;
function getTosCredentials(config) {
    const creds = resolveTosCredentials(config);
    if (!creds)
        return null;
    return {
        accessKeyId: creds.accessKeyId,
        accessKeySecret: creds.accessKeySecret,
        region: creds.region,
        endpoint: creds.endpoint,
    };
}
function getAgentSlug(config) {
    return config.agentName
        .trim()
        .toLowerCase()
        .replace(/\s+/g, "-");
}
// ---------------------------------------------------------------------------
// Tar.gz helper — uses system `tar` (always available on macOS)
// ---------------------------------------------------------------------------
function createTarGz(cwd, outputPath, paths) {
    if (paths.length === 0)
        return;
    execFileSync("tar", ["czf", outputPath, ...paths], { cwd, stdio: "pipe" });
}
// ---------------------------------------------------------------------------
// TOS upload
// ---------------------------------------------------------------------------
async function uploadToTos(filePath, key, creds, contentType = "application/gzip") {
    await uploadTosObject({
        filePath,
        bucket: DATA_COLLECTOR_BUCKET,
        key,
        credentials: creds,
        contentType,
        maxAttempts: UPLOAD_MAX_ATTEMPTS,
        retryBaseDelayMs: UPLOAD_RETRY_BASE_DELAY_MS,
        multipartThreshold: MULTIPART_THRESHOLD,
        partSize: PART_SIZE,
        taskNum: 1,
        checkpointDir: path.join(os.tmpdir(), ".tos-data-collector-checkpoints"),
        logPrefix: "[data-collector]",
        onRetry: (message) => logger.system(message),
    });
}
function formatError(err) {
    return formatTosError(err);
}
// ---------------------------------------------------------------------------
// Collection stages
// ---------------------------------------------------------------------------
function collectLogs(since, tmpDir) {
    const logsDir = path.join(getConfigDir(), "logs");
    if (!fs.existsSync(logsDir)) {
        logger.system(`[data-collector] Logs dir not found: ${logsDir}`);
        return null;
    }
    const allFiles = fs.readdirSync(logsDir).filter((f) => f.endsWith(".jsonl") || f.endsWith(".jsonl.gz"));
    const filtered = allFiles.filter((f) => {
        // Date-prefixed log files (YYYY-MM-DD.jsonl, api-calls-YYYY-MM-DD.jsonl, etc.)
        const match = /(\d{4}-\d{2}-\d{2})/.exec(f);
        return match ? match[1] >= since : false;
    });
    // Fallback: if no date-prefixed files matched, pack everything in the folder
    const toPack = filtered.length > 0 ? filtered : allFiles;
    if (toPack.length === 0) {
        logger.system(`[data-collector] No log files found in dir`);
        return null;
    }
    if (filtered.length === 0 && allFiles.length > 0) {
        logger.system(`[data-collector] No dated log files matched since=${since} — falling back to all ${allFiles.length} file(s)`);
    }
    // Copy to tmp for concurrent safety
    const logsTmpDir = path.join(tmpDir, "logs-src");
    fs.mkdirSync(logsTmpDir, { recursive: true });
    let totalBytes = 0;
    for (const f of toPack) {
        fs.copyFileSync(path.join(logsDir, f), path.join(logsTmpDir, f));
        totalBytes += fs.statSync(path.join(logsTmpDir, f)).size;
    }
    const archivePath = path.join(tmpDir, "logs.tar.gz");
    createTarGz(logsTmpDir, archivePath, toPack);
    return { archivePath, fileCount: toPack.length, totalBytes };
}
function copyPiCodingTranscripts(tmpRoot, filesToPack) {
    const piAgentDir = path.join(getConfigDir(), "pi-agent");
    if (!fs.existsSync(piAgentDir))
        return { fileCount: 0, totalBytes: 0 };
    const targetRoot = path.join(tmpRoot, "pi-coding-agent");
    let fileCount = 0;
    let totalBytes = 0;
    const copySessionsOnly = (dir) => {
        for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
            const full = path.join(dir, entry.name);
            if (entry.isDirectory()) {
                copySessionsOnly(full);
                continue;
            }
            if (!entry.isFile() || !full.endsWith(".jsonl") || path.basename(path.dirname(full)) !== "sessions")
                continue;
            const relative = path.relative(piAgentDir, full);
            const dest = path.join(targetRoot, relative);
            fs.mkdirSync(path.dirname(dest), { recursive: true });
            fs.copyFileSync(full, dest);
            const size = fs.statSync(dest).size;
            fileCount++;
            totalBytes += size;
            filesToPack.push(path.relative(tmpRoot, dest));
        }
    };
    copySessionsOnly(piAgentDir);
    return { fileCount, totalBytes };
}
function collectTranscripts(since, tmpDir) {
    const configDir = getConfigDir();
    const transcriptDir = getClaudeTranscriptDirForProfileDir(configDir);
    const sessionFilePath = getSessionFile();
    logger.system(`[data-collector] Transcript dir: ${transcriptDir}`);
    logger.system(`[data-collector] Session file: ${sessionFilePath}`);
    logger.system(`[data-collector] Transcript since: ${since}`);
    const transcriptTmpDir = path.join(tmpDir, "transcripts-src");
    fs.mkdirSync(transcriptTmpDir, { recursive: true });
    const filesToPack = [];
    let totalBytes = 0;
    const hasClaudeTranscriptDir = fs.existsSync(transcriptDir);
    if (!hasClaudeTranscriptDir) {
        logger.system(`[data-collector] Claude transcript dir does not exist — skipping Claude transcripts`);
    }
    const activeSessionIds = getReferencedClaudeTranscriptIds(sessionFilePath);
    if (activeSessionIds.length === 0) {
        logger.system(`[data-collector] No active Claude session IDs found in session.json — skipping Claude transcripts`);
    }
    logger.system(`[data-collector] Active session IDs: ${activeSessionIds.join(", ")}`);
    const sinceMs = new Date(since + "T00:00:00").getTime();
    const copyFile = (src, destRelative) => {
        if (!fs.existsSync(src))
            return;
        const destPath = path.join(transcriptTmpDir, destRelative);
        const destDir = path.dirname(destPath);
        if (!fs.existsSync(destDir))
            fs.mkdirSync(destDir, { recursive: true });
        const srcSize = fs.statSync(src).size;
        logger.system(`[data-collector] Copying transcript file ${destRelative} (${formatBytes(srcSize)})`);
        fs.copyFileSync(src, destPath);
        totalBytes += srcSize;
        filesToPack.push(destRelative);
    };
    // List all files in transcript dir once
    const allDirEntries = hasClaudeTranscriptDir ? fs.readdirSync(transcriptDir) : [];
    if (hasClaudeTranscriptDir)
        for (const sessionId of activeSessionIds) {
            const countBefore = filesToPack.length;
            // 1. Always copy live transcript (current active data)
            copyFile(path.join(transcriptDir, `${sessionId}.jsonl`), `${sessionId}.jsonl`);
            // Collect all files belonging to this session for date-filtered pass
            const sessionFiles = allDirEntries.filter((f) => f.startsWith(sessionId));
            let datedFilesFound = false;
            // 2. Date-stamped archives: *.archive.YYYY-MM-DD.jsonl
            //    and image archives: *.image-archive.YYYY-MM-DD.jsonl
            for (const f of sessionFiles) {
                // Date-stamped archive
                const archiveDate = /\.archive\.(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(f);
                if (archiveDate) {
                    datedFilesFound = true;
                    if (archiveDate[1] >= since) {
                        copyFile(path.join(transcriptDir, f), f);
                    }
                    continue;
                }
                // Date-stamped image archive
                const imgArchiveDate = /\.image-archive\.(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(f);
                if (imgArchiveDate) {
                    datedFilesFound = true;
                    if (imgArchiveDate[1] >= since) {
                        copyFile(path.join(transcriptDir, f), f);
                    }
                    continue;
                }
            }
            if (!datedFilesFound) {
                // Fallback: no date-stamped files found for this session — collect all
                // legacy files (undated archives) regardless of mtime so we don't miss data.
                for (const f of sessionFiles) {
                    // Skip the live .jsonl (already copied above)
                    if (f === `${sessionId}.jsonl`)
                        continue;
                    if (!f.endsWith(".jsonl"))
                        continue;
                    copyFile(path.join(transcriptDir, f), f);
                }
                logger.system(`[data-collector] No dated archives for session ${sessionId.slice(0, 8)} — falling back to all files`);
            }
            else {
                // Date-stamped files exist; also collect legacy undated archives if mtime matches
                for (const f of sessionFiles) {
                    if (f === `${sessionId}.archive.jsonl` || f === `${sessionId}.image-archive.jsonl`) {
                        const src = path.join(transcriptDir, f);
                        try {
                            const stat = fs.statSync(src);
                            if (stat.mtimeMs >= sinceMs) {
                                copyFile(src, f);
                            }
                        }
                        catch { /* ignore */ }
                    }
                }
            }
            // 3. Sub-agent transcripts by mtime (fallback: all if nothing matches)
            const subagentsDir = path.join(transcriptDir, sessionId, "subagents");
            if (fs.existsSync(subagentsDir)) {
                try {
                    const subEntries = fs.readdirSync(subagentsDir).filter((s) => s.endsWith(".jsonl"));
                    const matchedSubs = [];
                    for (const sub of subEntries) {
                        const subPath = path.join(subagentsDir, sub);
                        try {
                            const stat = fs.statSync(subPath);
                            if (stat.mtimeMs >= sinceMs) {
                                matchedSubs.push(sub);
                            }
                        }
                        catch { /* ignore */ }
                    }
                    // Fallback: if no sub-agent files matched by mtime, pack all of them
                    const subsToPack = matchedSubs.length > 0 ? matchedSubs : subEntries;
                    if (matchedSubs.length === 0 && subEntries.length > 0) {
                        logger.system(`[data-collector] No sub-agent files matched since=${since} for ${sessionId.slice(0, 8)} — falling back to all ${subEntries.length} file(s)`);
                    }
                    for (const sub of subsToPack) {
                        const relPath = path.join(sessionId, "subagents", sub);
                        copyFile(path.join(subagentsDir, sub), relPath);
                    }
                }
                catch { /* ignore */ }
            }
            if (filesToPack.length === countBefore) {
                logger.system(`[data-collector] No transcript files collected for session ${sessionId.slice(0, 8)}`);
            }
        }
    const piCoding = copyPiCodingTranscripts(transcriptTmpDir, filesToPack);
    totalBytes += piCoding.totalBytes;
    if (piCoding.fileCount > 0) {
        logger.system(`[data-collector] Included ${piCoding.fileCount} Pi coding transcript file(s)`);
    }
    if (filesToPack.length === 0)
        return null;
    const archivePath = path.join(tmpDir, "transcripts.tar.gz");
    createTarGz(transcriptTmpDir, archivePath, filesToPack);
    return { archivePath, fileCount: filesToPack.length, totalBytes };
}
function collectWorkspace(tmpDir) {
    const memoriesDir = getMemoriesDir();
    const skillsDir = getSkillsDir();
    const configFile = getConfigFile();
    const mcpServersFile = getDynamicMcpServersFile();
    const ownerFile = getOwnerFile();
    const sessionFile = getSessionFile();
    // Stage everything into a single tmp directory preserving structure
    const wsTmpDir = path.join(tmpDir, "workspace-src");
    fs.mkdirSync(wsTmpDir, { recursive: true });
    let fileCount = 0;
    let totalBytes = 0;
    const packPaths = [];
    // Copy directories recursively
    const copyDir = (src, destName) => {
        if (!fs.existsSync(src))
            return;
        const destDir = path.join(wsTmpDir, destName);
        fs.cpSync(src, destDir, { recursive: true });
        packPaths.push(destName);
        const countFiles = (dir) => {
            for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
                const full = path.join(dir, entry.name);
                if (entry.isDirectory())
                    countFiles(full);
                else if (entry.isFile()) {
                    fileCount++;
                    totalBytes += fs.statSync(full).size;
                }
            }
        };
        countFiles(destDir);
    };
    copyDir(memoriesDir, "memories");
    copyDir(skillsDir, "skills");
    // Copy single files
    const copyFile = (src, name) => {
        if (!fs.existsSync(src))
            return;
        fs.copyFileSync(src, path.join(wsTmpDir, name));
        fileCount++;
        totalBytes += fs.statSync(path.join(wsTmpDir, name)).size;
        packPaths.push(name);
    };
    copyFile(configFile, "config.json");
    copyFile(mcpServersFile, "mcp-servers.json");
    copyFile(ownerFile, "owner.json");
    copyFile(sessionFile, "session.json");
    // Capture the current user's crontab
    try {
        const crontab = execFileSync("crontab", ["-l"], { stdio: "pipe", encoding: "utf-8" });
        if (crontab.trim().length > 0) {
            const crontabPath = path.join(wsTmpDir, "crontab.txt");
            fs.writeFileSync(crontabPath, crontab, "utf-8");
            fileCount++;
            totalBytes += fs.statSync(crontabPath).size;
            packPaths.push("crontab.txt");
        }
    }
    catch {
        // No crontab or crontab command failed — skip
    }
    if (packPaths.length === 0) {
        logger.system("[data-collector] No workspace files found to collect — skipping");
        return null;
    }
    const archivePath = path.join(tmpDir, "workspace.tar.gz");
    createTarGz(wsTmpDir, archivePath, packPaths);
    return { archivePath, fileCount, totalBytes };
}
// ---------------------------------------------------------------------------
// Main collect_data handler
// ---------------------------------------------------------------------------
export function createCollectDataHandler(config) {
    return async (_commandId, payload) => {
        logger.warn("[data-collector] collect_data is deprecated for billing/analytics — " +
            "usage metering via heartbeat now handles WU tracking. " +
            "This command remains available for raw log/transcript collection only.");
        const startTime = Date.now();
        const p = (payload ?? {});
        const since = p.since ?? new Date(Date.now() - 7 * 86400000).toISOString().split("T")[0];
        const agentId = getAgentSlug(config);
        const timestamp = new Date()
            .toISOString()
            .replace(/[-:]/g, "")
            .replace("T", "T")
            .slice(0, 15);
        // Cross-midnight safety: subtract 1 day so trajectories spanning midnight
        // are fully captured (the extra day of overlap is cheap and safe).
        const sinceDate = new Date(since);
        sinceDate.setDate(sinceDate.getDate() - 1);
        const effectiveSince = sinceDate.toISOString().slice(0, 10);
        const creds = getTosCredentials(config);
        if (!creds) {
            return { status: "failed", result: "TOS credentials not available. They should be provided via the onboarding service heartbeat." };
        }
        const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "vc-collect-"));
        let uploadedFiles = 0;
        let totalUploadedBytes = 0;
        try {
            // 1. Logs
            try {
                logger.system(`[data-collector] Collecting logs since ${effectiveSince} (requested: ${since})...`);
                const logs = collectLogs(effectiveSince, tmpDir);
                if (logs) {
                    const key = `${agentId}/${timestamp}/logs/logs.tar.gz`;
                    logger.system(`[data-collector] Uploading logs (${logs.fileCount} files, ${formatBytes(logs.totalBytes)})...`);
                    await uploadToTos(logs.archivePath, key, creds);
                    uploadedFiles++;
                    totalUploadedBytes += fs.statSync(logs.archivePath).size;
                }
            }
            catch (err) {
                logger.err(`[data-collector] Logs stage failed: ${formatError(err)}`);
            }
            // 2. Transcripts
            try {
                logger.system(`[data-collector] Collecting transcripts since ${effectiveSince}...`);
                const transcripts = collectTranscripts(effectiveSince, tmpDir);
                if (transcripts) {
                    const key = `${agentId}/${timestamp}/transcripts/transcripts.tar.gz`;
                    logger.system(`[data-collector] Uploading transcripts (${transcripts.fileCount} files, ${formatBytes(transcripts.totalBytes)} raw)...`);
                    await uploadToTos(transcripts.archivePath, key, creds);
                    uploadedFiles++;
                    totalUploadedBytes += fs.statSync(transcripts.archivePath).size;
                }
            }
            catch (err) {
                logger.err(`[data-collector] Transcripts stage failed: ${formatError(err)}`);
            }
            // 3. Workspace
            try {
                logger.system("[data-collector] Collecting workspace snapshot...");
                const workspace = collectWorkspace(tmpDir);
                if (workspace) {
                    const key = `${agentId}/${timestamp}/workspace/workspace.tar.gz`;
                    logger.system(`[data-collector] Uploading workspace (${workspace.fileCount} files, ${formatBytes(workspace.totalBytes)})...`);
                    await uploadToTos(workspace.archivePath, key, creds);
                    uploadedFiles++;
                    totalUploadedBytes += fs.statSync(workspace.archivePath).size;
                }
            }
            catch (err) {
                logger.err(`[data-collector] Workspace stage failed: ${formatError(err)}`);
            }
            const durationMs = Date.now() - startTime;
            const resultMsg = `Uploaded ${uploadedFiles} archive(s), ${formatBytes(totalUploadedBytes)} total, in ${(durationMs / 1000).toFixed(1)}s`;
            logger.system(`[data-collector] ${resultMsg}`);
            return {
                status: "completed",
                result: JSON.stringify({
                    uploadedFiles,
                    totalUploadedBytes,
                    durationMs,
                    message: resultMsg,
                }),
            };
        }
        catch (err) {
            const errMsg = formatError(err);
            logger.err(`[data-collector] Failed: ${errMsg}`);
            return {
                status: "failed",
                result: JSON.stringify({ error: errMsg }),
            };
        }
        finally {
            try {
                fs.rmSync(tmpDir, { recursive: true, force: true });
            }
            catch {
                // best-effort
            }
        }
    };
}
function formatBytes(bytes) {
    if (bytes < 1024)
        return `${bytes}B`;
    if (bytes < 1024 * 1024)
        return `${(bytes / 1024).toFixed(1)}KB`;
    if (bytes < 1024 * 1024 * 1024)
        return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
    return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)}GB`;
}
//# sourceMappingURL=data-collector.js.map