import fs from "fs";
import fsp from "fs/promises";
import os from "os";
import path from "path";
import readline from "readline";
import { logger } from "../logger.js";
const TRANSCRIPT_RETENTION_DAYS = 180;
async function writeFileAtomic(filePath, content) {
    const dir = path.dirname(filePath);
    const tmp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
    await fsp.writeFile(tmp, content, "utf8");
    await fsp.rename(tmp, filePath);
}
/**
 * Derive the archive path from a transcript path by inserting
 * `.archive.<date>` before the final `.jsonl` extension.
 *
 * The date defaults to today (local time) if not provided.  Date-stamped
 * archive files enable date-based filtering at collection time and prevent
 * a single archive file from growing unboundedly.
 *
 * Examples:
 *   /path/to/abc123.jsonl       →  /path/to/abc123.archive.2026-03-25.jsonl
 *   /path/to/abc123             →  /path/to/abc123.archive.2026-03-25.jsonl
 *   deriveArchivePath(p, "2026-01-01")  →  *.archive.2026-01-01.jsonl
 */
export function deriveArchivePath(transcriptPath, date) {
    const d = date ?? formatLocalDate(new Date());
    const dir = path.dirname(transcriptPath);
    const base = path.basename(transcriptPath);
    if (base.endsWith(".jsonl")) {
        return path.join(dir, base.slice(0, -".jsonl".length) + `.archive.${d}.jsonl`);
    }
    return path.join(dir, base + `.archive.${d}.jsonl`);
}
function formatLocalDate(d) {
    const year = d.getFullYear();
    const month = String(d.getMonth() + 1).padStart(2, "0");
    const day = String(d.getDate()).padStart(2, "0");
    return `${year}-${month}-${day}`;
}
/**
 * After a compaction event, trim the JSONL transcript to remove all content
 * BEFORE the last compaction boundary line. Claude transcripts use a
 * `system`/`compact_boundary` record; Pi Coding transcripts use a top-level
 * `compaction` record. The removed (pre-compaction) content is appended to an
 * archive file for history preservation.
 *
 * This keeps the live transcript small so that:
 *  - `pruneSessionImages()` only scans post-compaction lines
 *  - The SDK resumes from a much smaller file (both with and without
 *    CLAUDE_CODE_SKIP_PRECOMPACT_LOAD)
 *
 * Safety: the trim is only performed when a supported compaction boundary is
 * found at index > 0, meaning there is actually pre-compaction content to
 * remove.
 */
export async function trimTranscriptAfterCompaction(options) {
    const { transcriptPath, archivePath } = options;
    logger.debug(`[session-trimmer] start file=${transcriptPath}`);
    if (!fs.existsSync(transcriptPath)) {
        logger.debug("[session-trimmer] skip (file missing)");
        return { trimmedLines: 0, keptLines: 0, archivePath: null, fileChanged: false };
    }
    // Read all lines, tracking the index of the last supported compaction boundary.
    const input = fs.createReadStream(transcriptPath, { encoding: "utf8" });
    const rl = readline.createInterface({ input, crlfDelay: Infinity });
    const lines = [];
    let lastBoundaryIndex = -1;
    let lineIndex = 0;
    for await (const line of rl) {
        lines.push(line);
        if (line.trim()) {
            try {
                const parsed = JSON.parse(line);
                const isClaudeCompactBoundary = parsed.type === "system" && parsed.subtype === "compact_boundary";
                const isPiCodingCompaction = parsed.type === "compaction";
                if (isClaudeCompactBoundary || isPiCodingCompaction) {
                    lastBoundaryIndex = lineIndex;
                }
            }
            catch {
                // Malformed JSON — skip
            }
        }
        lineIndex++;
    }
    rl.close();
    input.close();
    // Nothing to trim: no boundary found, or boundary is already the first line.
    if (lastBoundaryIndex <= 0) {
        logger.debug(`[session-trimmer] skip (boundary index=${lastBoundaryIndex}, nothing to trim)`);
        return {
            trimmedLines: 0,
            keptLines: lines.length,
            archivePath: null,
            fileChanged: false,
        };
    }
    const toArchive = lines.slice(0, lastBoundaryIndex);
    const toKeep = lines.slice(lastBoundaryIndex);
    // Append pre-compaction lines to the rolling archive.
    const archiveContent = toArchive.join(os.EOL) + os.EOL;
    await fsp.appendFile(archivePath, archiveContent, "utf8");
    logger.debug(`[session-trimmer] archived ${toArchive.length} lines to ${archivePath}`);
    // Atomically replace transcript with only post-compaction content.
    const keptContent = toKeep.join(os.EOL) + os.EOL;
    await writeFileAtomic(transcriptPath, keptContent);
    logger.system(`Trimmed transcript after compaction: removed=${toArchive.length} kept=${toKeep.length} archive=${archivePath}`);
    return {
        trimmedLines: toArchive.length,
        keptLines: toKeep.length,
        archivePath,
        fileChanged: true,
    };
}
// ---------------------------------------------------------------------------
// Transcript retention cleanup
// ---------------------------------------------------------------------------
/**
 * Delete old transcript archive files and sub-agent transcripts that exceed
 * the retention period.
 *
 * What gets cleaned:
 *  - Date-stamped archives: `*.archive.YYYY-MM-DD.jsonl`, `*.image-archive.YYYY-MM-DD.jsonl`
 *    → deleted if embedded date < cutoff
 *  - Legacy undated archives: `*.archive.jsonl`, `*.image-archive.jsonl`
 *    → deleted if file mtime < cutoff
 *  - Sub-agent transcripts: `<sessionId>/subagents/agent-*.jsonl`
 *    → deleted if file mtime < cutoff; empty directories pruned
 *  - Live transcripts (`<sessionId>.jsonl`): NOT deleted (managed by SDK)
 */
export function cleanupOldTranscripts(transcriptDir, retentionDays = TRANSCRIPT_RETENTION_DAYS) {
    if (!fs.existsSync(transcriptDir))
        return;
    const cutoff = new Date();
    cutoff.setDate(cutoff.getDate() - retentionDays);
    const cutoffDate = formatLocalDate(cutoff);
    const cutoffMs = cutoff.getTime();
    let deletedFiles = 0;
    let deletedDirs = 0;
    try {
        const entries = fs.readdirSync(transcriptDir, { withFileTypes: true });
        for (const entry of entries) {
            const fullPath = path.join(transcriptDir, entry.name);
            if (entry.isFile()) {
                // Date-stamped archives: *.archive.YYYY-MM-DD.jsonl or *.image-archive.YYYY-MM-DD.jsonl
                const dateMatch = /\.(?:archive|image-archive)\.(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(entry.name);
                if (dateMatch) {
                    if (dateMatch[1] < cutoffDate) {
                        try {
                            fs.unlinkSync(fullPath);
                            deletedFiles++;
                        }
                        catch { /* ignore */ }
                    }
                    continue;
                }
                // Legacy undated archives: *.archive.jsonl or *.image-archive.jsonl
                if (/\.(archive|image-archive)\.jsonl$/.test(entry.name)) {
                    try {
                        const stat = fs.statSync(fullPath);
                        if (stat.mtimeMs < cutoffMs) {
                            fs.unlinkSync(fullPath);
                            deletedFiles++;
                        }
                    }
                    catch { /* ignore */ }
                    continue;
                }
                // Live transcripts (*.jsonl without archive suffix) — skip
            }
            if (entry.isDirectory()) {
                // Sub-agent transcripts in <sessionId>/subagents/
                const subagentsDir = path.join(fullPath, "subagents");
                if (fs.existsSync(subagentsDir)) {
                    try {
                        const subEntries = fs.readdirSync(subagentsDir);
                        for (const sub of subEntries) {
                            const subPath = path.join(subagentsDir, sub);
                            try {
                                const stat = fs.statSync(subPath);
                                if (stat.isFile() && stat.mtimeMs < cutoffMs) {
                                    fs.unlinkSync(subPath);
                                    deletedFiles++;
                                }
                            }
                            catch { /* ignore */ }
                        }
                        // Prune empty subagents/ directory
                        try {
                            const remaining = fs.readdirSync(subagentsDir);
                            if (remaining.length === 0) {
                                fs.rmdirSync(subagentsDir);
                                deletedDirs++;
                            }
                        }
                        catch { /* ignore */ }
                    }
                    catch { /* ignore */ }
                }
            }
        }
        if (deletedFiles > 0 || deletedDirs > 0) {
            logger.system(`[transcript-cleanup] deleted ${deletedFiles} file(s) and ${deletedDirs} empty dir(s) older than ${retentionDays} days`);
        }
    }
    catch (err) {
        logger.warn(`[transcript-cleanup] failed: ${err instanceof Error ? err.message : String(err)}`);
    }
}
//# sourceMappingURL=session-trimmer.js.map