import { existsSync, statSync } from "fs";
import { resolve } from "path";
import { normalizePath, resolvePath } from "../utils/paths.js";
import { createBackupSnapshot, createBranchedSessionState, forkSessionFromFile, } from "./session-manager-archive.js";
import { createBranchSummaryEntry, createContextCompactionEntry, createContextWindowChangeEntry, createCustomEntry, createCustomMessageEntry, createLabelEntry, getEntriesWithoutHeader, createMessageEntry, createModelChangeEntry, createSessionFilePath, createSessionHeader, createSessionInfoEntry, createThinkingLevelChangeEntry, getLatestSessionName, } from "./session-manager-entries.js";
import { buildSessionContext, buildSessionIndex, buildSessionTree, getBranchPath } from "./session-manager-history.js";
import { listAllSessions, listProjectSessions } from "./session-manager-list.js";
import { migrateToCurrentVersion } from "./session-manager-migrations.js";
import { getDefaultSessionDir, getDefaultSessionDirPath } from "./session-manager-paths.js";
import { appendSessionEntries, appendSessionEntry, ensureDirectory, findMostRecentSession, hasAssistantMessage, loadEntriesFromFile, writeSessionEntries, } from "./session-manager-storage.js";
import { assertValidSessionId, createSessionId } from "./session-manager-validation.js";
/** Manages conversation sessions as append-only trees stored in JSONL files.  Each session entry has an id and parentId forming a tree structure. The "leaf" pointer tracks the current position. Appending creates a child of the current leaf. Branching moves the leaf to an earlier entry, allowing new branches without modifying history.  Use buildSessionContext() to get the resolved message list for the LLM, which applies context-deletion filtering and follows the path from root to current leaf. */
export class SessionManager {
    constructor(cwd, sessionDir, sessionFile, persist, newSessionOptions) {
        this.sessionId = "";
        this.flushed = false;
        this.fileEntries = [];
        this.byId = new Map();
        this.labelsById = new Map();
        this.labelTimestampsById = new Map();
        this.leafId = null;
        this.cwd = resolvePath(cwd);
        this.sessionDir = normalizePath(sessionDir);
        this.persist = persist;
        if (persist && this.sessionDir) {
            ensureDirectory(this.sessionDir);
        }
        if (sessionFile) {
            this.setSessionFile(sessionFile);
        }
        else {
            this.newSession(newSessionOptions);
        }
    }
    /** Switch to a different session file (used for resume and branching) */
    setSessionFile(sessionFile) {
        this.sessionFile = resolvePath(sessionFile);
        if (existsSync(this.sessionFile)) {
            this.fileEntries = loadEntriesFromFile(this.sessionFile);
            // If file was empty, initialize it with a valid session header. If it was non-empty but did not parse as a pi session, fail without modifying it.
            if (this.fileEntries.length === 0) {
                const explicitPath = this.sessionFile;
                if (statSync(explicitPath).size > 0) {
                    throw new Error(`Session file is not a valid pi session: ${explicitPath}`);
                }
                this.newSession();
                this.sessionFile = explicitPath;
                this._rewriteFile();
                this.flushed = true;
                return;
            }
            const header = this.fileEntries.find((entry) => entry.type === "session");
            this.sessionId = header?.id ?? createSessionId();
            if (migrateToCurrentVersion(this.fileEntries)) {
                this._rewriteFile();
            }
            this._buildIndex();
            this.flushed = true;
        }
        else {
            const explicitPath = this.sessionFile;
            this.newSession();
            this.sessionFile = explicitPath; // preserve explicit path from --session flag
        }
    }
    newSession(options) {
        if (options?.id !== undefined) {
            assertValidSessionId(options.id);
        }
        this.sessionId = options?.id ?? createSessionId();
        const timestamp = new Date().toISOString();
        const header = createSessionHeader(this.sessionId, this.cwd, timestamp, options?.parentSession, options?.internal, options?.workflow);
        this.fileEntries = [header];
        this.byId.clear();
        this.labelsById.clear();
        this.labelTimestampsById.clear();
        this.leafId = null;
        this.flushed = false;
        if (this.persist) {
            this.sessionFile = createSessionFilePath(this.getSessionDir(), timestamp, this.sessionId);
        }
        return this.sessionFile;
    }
    /** Mark the session header as internal (e.g. workflow stage). Preserves an existing full marker on reattach. */
    markSessionInternal(workflow) {
        const header = this.fileEntries.find((entry) => entry.type === "session");
        if (!header || (header.internal && header.workflow))
            return;
        header.internal = true;
        if (workflow)
            header.workflow = workflow;
        if (this.flushed)
            this._rewriteFile();
    }
    _buildIndex() {
        const index = buildSessionIndex(this.fileEntries);
        this.byId = index.byId;
        this.labelsById = index.labelsById;
        this.labelTimestampsById = index.labelTimestampsById;
        this.leafId = index.leafId;
    }
    _rewriteFile() {
        if (!this.persist || !this.sessionFile)
            return;
        writeSessionEntries(this.sessionFile, this.fileEntries);
    }
    isPersisted() {
        return this.persist;
    }
    getCwd() {
        return this.cwd;
    }
    getSessionDir() {
        return this.sessionDir;
    }
    usesDefaultSessionDir() {
        return this.sessionDir === getDefaultSessionDirPath(this.cwd);
    }
    getSessionId() {
        return this.sessionId;
    }
    getSessionFile() {
        return this.sessionFile;
    }
    _persist(entry) {
        if (!this.persist || !this.sessionFile)
            return;
        if (!hasAssistantMessage(this.fileEntries)) {
            // Mark as not flushed so when assistant arrives, all entries get written
            this.flushed = false;
            return;
        }
        if (!this.flushed) {
            appendSessionEntries(this.sessionFile, this.fileEntries);
            this.flushed = true;
        }
        else {
            appendSessionEntry(this.sessionFile, entry);
        }
    }
    _appendEntry(entry) {
        this.fileEntries.push(entry);
        this.byId.set(entry.id, entry);
        this.leafId = entry.id;
        this._persist(entry);
    }
    /** Append a message as child of current leaf, then advance leaf. Returns entry id. */
    appendMessage(message) {
        const entry = createMessageEntry(message, this.byId, this.leafId);
        this._appendEntry(entry);
        return entry.id;
    }
    /** Append a thinking level change as child of current leaf, then advance leaf. Returns entry id. */
    appendThinkingLevelChange(thinkingLevel) {
        const entry = createThinkingLevelChangeEntry(thinkingLevel, this.byId, this.leafId);
        this._appendEntry(entry);
        return entry.id;
    }
    /** Append a context window change as child of current leaf, then advance leaf. Returns entry id. */
    appendContextWindowChange(contextWindow) {
        const entry = createContextWindowChangeEntry(contextWindow, this.byId, this.leafId);
        this._appendEntry(entry);
        return entry.id;
    }
    /** Append a model change as child of current leaf, then advance leaf. Returns entry id. */
    appendModelChange(provider, modelId) {
        const entry = createModelChangeEntry(provider, modelId, this.byId, this.leafId);
        this._appendEntry(entry);
        return entry.id;
    }
    /** Append logical deletion metadata for deletion-only context compaction. */
    appendContextCompaction(deletedTargets, protectedEntryIds, stats, backupPath) {
        const entry = createContextCompactionEntry(deletedTargets, protectedEntryIds, stats, backupPath, this.byId, this.leafId);
        this._appendEntry(entry);
        return entry.id;
    }
    /** Write a recoverable snapshot of the current session entries without mutating the active JSONL. */
    writeBackupSnapshot(label = "compact") {
        if (!this.persist)
            return undefined;
        return createBackupSnapshot(this.sessionFile, this.fileEntries, label);
    }
    /** Append a custom entry (for extensions) as child of current leaf, then advance leaf. Returns entry id. */
    appendCustomEntry(customType, data) {
        const entry = createCustomEntry(customType, data, this.byId, this.leafId);
        this._appendEntry(entry);
        return entry.id;
    }
    /** Append a session info entry (e.g., display name). Returns entry id. */
    appendSessionInfo(name) {
        const entry = createSessionInfoEntry(name, this.byId, this.leafId);
        this._appendEntry(entry);
        return entry.id;
    }
    /** Get the current session name from the latest session_info entry, if any. */
    getSessionName() {
        return getLatestSessionName(this.getEntries());
    }
    /** Append a custom message entry (for extensions) that participates in LLM context unless excluded. */
    appendCustomMessageEntry(customType, content, display, details, excludeFromContext) {
        const entry = createCustomMessageEntry(customType, content, display, details, excludeFromContext, this.byId, this.leafId);
        this._appendEntry(entry);
        return entry.id;
    }
    getLeafId() {
        return this.leafId;
    }
    getLeafEntry() {
        return this.leafId ? this.byId.get(this.leafId) : undefined;
    }
    getEntry(id) {
        return this.byId.get(id);
    }
    /** Get all direct children of an entry. */
    getChildren(parentId) {
        const children = [];
        for (const entry of this.byId.values()) {
            if (entry.parentId === parentId) {
                children.push(entry);
            }
        }
        return children;
    }
    /** Get the label for an entry, if any. */
    getLabel(id) {
        return this.labelsById.get(id);
    }
    /** Set or clear a label on an entry. */
    appendLabelChange(targetId, label) {
        if (!this.byId.has(targetId)) {
            throw new Error(`Entry ${targetId} not found`);
        }
        const entry = createLabelEntry(targetId, label, this.byId, this.leafId);
        this._appendEntry(entry);
        if (label) {
            this.labelsById.set(targetId, label);
            this.labelTimestampsById.set(targetId, entry.timestamp);
        }
        else {
            this.labelsById.delete(targetId);
            this.labelTimestampsById.delete(targetId);
        }
        return entry.id;
    }
    /** Walk from entry to root, returning all entries in path order. */
    getBranch(fromId) {
        return getBranchPath(fromId ?? this.leafId, this.byId);
    }
    /** Build the session context (what gets sent to the LLM). */
    buildSessionContext() {
        return buildSessionContext(this.getEntries(), this.leafId, this.byId);
    }
    /** Get session header. */
    getHeader() {
        const header = this.fileEntries.find((entry) => entry.type === "session");
        return header ? header : null;
    }
    /** Get all session entries (excludes header). Returns a shallow copy. */
    getEntries() {
        return getEntriesWithoutHeader(this.fileEntries);
    }
    /** Get the session as a tree structure. Returns a shallow defensive copy of all entries. */
    getTree() {
        return buildSessionTree(this.getEntries(), this.labelsById, this.labelTimestampsById);
    }
    /** Start a new branch from an earlier entry. */
    branch(branchFromId) {
        if (!this.byId.has(branchFromId)) {
            throw new Error(`Entry ${branchFromId} not found`);
        }
        this.leafId = branchFromId;
    }
    /** Reset the leaf pointer to null (before any entries). */
    resetLeaf() {
        this.leafId = null;
    }
    /** Start a new branch with a summary of the abandoned path. */
    branchWithSummary(branchFromId, summary, details, fromHook) {
        if (branchFromId !== null && !this.byId.has(branchFromId)) {
            throw new Error(`Entry ${branchFromId} not found`);
        }
        this.leafId = branchFromId;
        const entry = createBranchSummaryEntry(branchFromId, summary, details, fromHook, this.byId);
        this._appendEntry(entry);
        return entry.id;
    }
    /** Create a new session file containing only the path from root to the specified leaf. */
    createBranchedSession(leafId) {
        const result = createBranchedSessionState({
            leafId,
            persist: this.persist,
            sessionDir: this.getSessionDir(),
            cwd: this.cwd,
            previousSessionFile: this.sessionFile,
            path: this.getBranch(leafId),
            labelsById: this.labelsById,
            labelTimestampsById: this.labelTimestampsById,
        });
        this.fileEntries = result.fileEntries;
        this.sessionId = result.sessionId;
        if (result.sessionFile)
            this.sessionFile = result.sessionFile;
        this._buildIndex();
        if (this.persist) {
            if (result.shouldRewriteFile)
                this._rewriteFile();
            this.flushed = result.flushed ?? false;
            return result.sessionFile;
        }
        return undefined;
    }
    /** Create a new session. */
    static create(cwd, sessionDir, options) {
        const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
        return new SessionManager(cwd, dir, undefined, true, options);
    }
    /** Open a specific session file. */
    static open(path, sessionDir, cwdOverride) {
        const resolvedPath = resolvePath(path);
        const entries = loadEntriesFromFile(resolvedPath);
        const header = entries.find((entry) => entry.type === "session");
        const cwd = cwdOverride ?? header?.cwd ?? process.cwd();
        const dir = sessionDir ? normalizePath(sessionDir) : resolve(resolvedPath, "..");
        return new SessionManager(cwd, dir, resolvedPath, true);
    }
    /** Continue the most recent session (skips internal workflow sessions unless `includeInternal: true`). */
    static continueRecent(cwd, sessionDir, options) {
        const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
        const filterCwd = sessionDir !== undefined && dir !== getDefaultSessionDirPath(cwd);
        const mostRecent = findMostRecentSession(dir, filterCwd ? cwd : undefined, options?.includeInternal === true);
        return new SessionManager(cwd, dir, mostRecent ?? undefined, true);
    }
    /** Create an in-memory session (no file persistence) */
    static inMemory(cwd = process.cwd(), options) {
        return new SessionManager(cwd, "", undefined, false, options);
    }
    /** Fork a session from another project directory into the current project. */
    static forkFrom(sourcePath, targetCwd, sessionDir, options) {
        const forked = forkSessionFromFile(sourcePath, targetCwd, sessionDir, options);
        return new SessionManager(forked.cwd, forked.sessionDir, forked.sessionFile, true);
    }
    /** List sessions for a directory. Internal (workflow) sessions are excluded unless `includeInternal: true`. */
    static async list(cwd, sessionDir, onProgress, options) {
        return listProjectSessions(cwd, sessionDir, onProgress, options?.includeInternal === true);
    }
    static async listAll(sessionDirOrOnProgress, onProgress, options) {
        return listAllSessions(sessionDirOrOnProgress, onProgress, options?.includeInternal === true);
    }
}
//# sourceMappingURL=session-manager-core.js.map