/**
 * Session Manager - Persist and resume conversations
 * Similar to Vigthoria's session persistence
 */
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { atomicWriteJson, secureFileMode } from './cli-state.js';
function createSessionLoadError(message, details) {
    return {
        code: 'SESSION_LOAD_FAILED',
        message,
        details,
        isCritical: true,
    };
}
function normalizeAuthState(raw) {
    if (!raw || typeof raw !== 'object') {
        return { token: null, expiresAt: null, isValid: false };
    }
    const source = raw.auth && typeof raw.auth === 'object' ? raw.auth : raw;
    const rawToken = source.token ?? source.accessToken ?? source.jwt ?? null;
    const rawExpiresAt = source.expiresAt ?? null;
    const token = typeof rawToken === 'string' && rawToken.trim().length > 0 ? rawToken.trim() : null;
    const expiresAt = typeof rawExpiresAt === 'number' && Number.isFinite(rawExpiresAt) ? rawExpiresAt : null;
    const session = { token, expiresAt, isValid: false };
    session.isValid = validateSession(session, { silent: true });
    return session;
}
/**
 * Validate persisted authentication state without assuming any field is present.
 *
 * Production hygiene: by default this helper is SILENT.  Earlier revisions
 * wrote ``console.warn`` for every missing-token branch, which polluted
 * stderr on PowerShell (NativeCommandError styling) and surfaced as noise
 * on first-run installs.  Pass ``{ silent: false }`` only when the caller
 * actually wants the warning visible.
 */
export function validateSession(session, options = {}) {
    const silent = options.silent !== false;
    if (!session || typeof session !== 'object') {
        if (!silent)
            console.warn('Invalid session: session state is missing.');
        return false;
    }
    if (typeof session.token !== 'string' || session.token.trim().length === 0) {
        if (!silent)
            console.warn('Invalid session: authentication token is missing.');
        return false;
    }
    if (typeof session.expiresAt !== 'number' || !Number.isFinite(session.expiresAt)) {
        if (!silent)
            console.warn('Invalid session: expiration timestamp is missing.');
        return false;
    }
    if (session.expiresAt <= Date.now()) {
        if (!silent)
            console.warn('Invalid session: authentication token has expired.');
        return false;
    }
    return true;
}
/**
 * Load persisted authentication state and normalize nullable token fields safely.
 *
 * Production behaviour: first-run users (no session file yet) get a clean
 * ``{ token: null, expiresAt: null, isValid: false }`` instead of an
 * exception.  Callers that need a hard failure can inspect ``isValid``.
 */
export async function loadSession() {
    const configDir = path.join(os.homedir(), '.vigthoria');
    const candidateFiles = [
        path.join(configDir, 'auth.json'),
        path.join(configDir, 'session.json'),
        path.join(configDir, 'config.json'),
    ];
    const sessionFile = candidateFiles.find((filePath) => {
        try {
            return fs.existsSync(filePath);
        }
        catch {
            return false;
        }
    });
    if (!sessionFile) {
        return { token: null, expiresAt: null, isValid: false };
    }
    try {
        const content = fs.readFileSync(sessionFile, 'utf-8');
        const parsed = JSON.parse(content);
        return normalizeAuthState(parsed);
    }
    catch (error) {
        if (process.env.VIGTHORIA_DEBUG === '1') {
            console.error('[vigthoria] Could not read session file:', error?.message ?? error);
        }
        return { token: null, expiresAt: null, isValid: false };
    }
}
export class SessionManager {
    sessionsDir;
    compactThreshold = 40;
    retainRecentMessages = 18;
    /** Keep at most this many session files on disk per CLI install. */
    maxRetainedSessions = 200;
    /** Sessions older than this are pruned (90 days). */
    retentionWindowMs = 90 * 24 * 60 * 60 * 1000;
    /** Avoid running prune on every save — at most once per CLI process. */
    prunedThisProcess = false;
    constructor() {
        this.sessionsDir = path.join(os.homedir(), '.vigthoria', 'sessions');
        this.ensureDir();
    }
    ensureDir() {
        try {
            if (!fs.existsSync(this.sessionsDir)) {
                fs.mkdirSync(this.sessionsDir, { recursive: true, mode: 0o700 });
            }
            if (process.platform !== 'win32') {
                try {
                    fs.chmodSync(this.sessionsDir, 0o700);
                }
                catch { /* best-effort */ }
            }
        }
        catch (error) {
            if (error?.code === 'EACCES' || error?.code === 'EPERM') {
                this.sessionsDir = path.join(os.tmpdir(), 'vigthoria-sessions');
                try {
                    if (!fs.existsSync(this.sessionsDir)) {
                        fs.mkdirSync(this.sessionsDir, { recursive: true });
                    }
                }
                catch {
                    // If even tmpdir is unwritable we'll fail at write time with a useful error.
                }
            }
        }
    }
    /**
     * Prune sessions older than the retention window AND cap the number of
     * retained files.  This is best-effort: any unreadable file is treated
     * as stale and removed.  Pruning is bounded to once per process.
     */
    pruneIfNeeded() {
        if (this.prunedThisProcess)
            return;
        this.prunedThisProcess = true;
        try {
            const now = Date.now();
            const files = fs.readdirSync(this.sessionsDir)
                .filter((f) => f.endsWith('.json'))
                .map((f) => {
                const full = path.join(this.sessionsDir, f);
                let mtimeMs = 0;
                try {
                    mtimeMs = fs.statSync(full).mtimeMs;
                }
                catch { /* treat as stale */ }
                return { file: f, full, mtimeMs };
            });
            // Drop everything older than the retention window.
            for (const entry of files) {
                if (entry.mtimeMs === 0 || now - entry.mtimeMs > this.retentionWindowMs) {
                    try {
                        fs.unlinkSync(entry.full);
                    }
                    catch { /* ignore */ }
                }
            }
            // Cap retained-session count (keep most recent).
            const remaining = files
                .filter((e) => e.mtimeMs > 0 && now - e.mtimeMs <= this.retentionWindowMs)
                .sort((a, b) => b.mtimeMs - a.mtimeMs);
            if (remaining.length > this.maxRetainedSessions) {
                for (const entry of remaining.slice(this.maxRetainedSessions)) {
                    try {
                        fs.unlinkSync(entry.full);
                    }
                    catch { /* ignore */ }
                }
            }
        }
        catch {
            // Pruning is purely a hygiene step — never block on failure.
        }
    }
    /**
     * Generate unique session ID
     */
    generateId() {
        const timestamp = Date.now().toString(36);
        const random = Math.random().toString(36).substring(2, 8);
        return `${timestamp}-${random}`;
    }
    /**
     * Create a new session
     */
    create(project, model, agentMode = false, operatorMode = false) {
        const session = {
            id: this.generateId(),
            name: `Session ${new Date().toLocaleString()}`,
            project,
            model,
            messages: [],
            memorySummary: '',
            compactedAt: null,
            createdAt: new Date().toISOString(),
            updatedAt: new Date().toISOString(),
            agentMode,
            operatorMode,
        };
        this.save(session);
        return session;
    }
    /**
     * Save session to disk — atomic + 0600 mode so transcripts cannot be
     * read by other local users on a shared POSIX host.
     */
    save(session) {
        const compacted = this.compactSession(session);
        const targetSession = compacted.session;
        targetSession.updatedAt = new Date().toISOString();
        const filePath = path.join(this.sessionsDir, `${session.id}.json`);
        try {
            atomicWriteJson(filePath, targetSession, 0o600);
            secureFileMode(filePath);
        }
        catch (error) {
            // Surface a best-effort hint rather than crashing the active chat.
            if (process.env.VIGTHORIA_DEBUG === '1') {
                console.error('[vigthoria] session save failed:', error?.message ?? error);
            }
        }
        this.pruneIfNeeded();
    }
    /**
     * Load session by ID
     */
    load(id) {
        const filePath = path.join(this.sessionsDir, `${id}.json`);
        if (!fs.existsSync(filePath)) {
            return null;
        }
        try {
            const content = fs.readFileSync(filePath, 'utf-8');
            return JSON.parse(content);
        }
        catch (error) {
            console.warn(`Failed to load session ${id}:`, error);
            return null;
        }
    }
    normalizeProjectKey(projectPath) {
        const raw = String(projectPath || '').trim();
        if (!raw) {
            return '';
        }
        // Normalize separators and collapse trailing slashes.
        let normalized = raw.replace(/\\/g, '/').replace(/\/+$/g, '');
        // Canonicalize obvious Windows drive prefix casing (C:/... === c:/...).
        const driveMatch = normalized.match(/^([a-zA-Z]):\//);
        if (driveMatch) {
            normalized = `${driveMatch[1].toUpperCase()}:${normalized.slice(2)}`;
        }
        // Windows paths are case-insensitive in practice for this CLI matching purpose.
        if (/^[a-zA-Z]:\//.test(normalized) || normalized.startsWith('//')) {
            return normalized.toLowerCase();
        }
        return normalized;
    }
    /**
     * Get the most recent session for a project
     */
    getLatest(project) {
        const sessions = this.list();
        const projectKey = this.normalizeProjectKey(project);
        const projectBase = path.basename(projectKey || '').toLowerCase();
        const exactMatches = sessions
            .filter((s) => this.normalizeProjectKey(s.project) === projectKey)
            .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
        if (exactMatches.length > 0) {
            return this.load(exactMatches[0].id);
        }
        // Fallback: if old sessions stored inconsistent slash/case forms, allow
        // latest same-folder match (e.g., C:\Proj and C:/proj) to preserve /retry UX.
        if (projectBase) {
            const softMatches = sessions
                .filter((s) => path.basename(this.normalizeProjectKey(s.project) || '').toLowerCase() === projectBase)
                .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
            if (softMatches.length > 0) {
                return this.load(softMatches[0].id);
            }
        }
        return null;
    }
    /**
     * List all sessions (metadata only) — corrupt or unreadable files are
     * skipped silently unless VIGTHORIA_DEBUG=1.
     */
    list() {
        let files = [];
        try {
            files = fs.readdirSync(this.sessionsDir).filter(f => f.endsWith('.json'));
        }
        catch {
            return [];
        }
        return files.map(f => {
            try {
                const content = fs.readFileSync(path.join(this.sessionsDir, f), 'utf-8');
                const session = JSON.parse(content);
                const { messages, ...metadata } = session;
                return { ...metadata, messages: [] };
            }
            catch (error) {
                if (process.env.VIGTHORIA_DEBUG === '1') {
                    console.warn(`Failed to read session metadata from ${f}:`, error);
                }
                return null;
            }
        }).filter(Boolean);
    }
    /**
     * Delete a session
     */
    delete(id) {
        const filePath = path.join(this.sessionsDir, `${id}.json`);
        if (fs.existsSync(filePath)) {
            fs.unlinkSync(filePath);
            return true;
        }
        return false;
    }
    /**
     * Clear all sessions
     */
    clearAll() {
        const files = fs.readdirSync(this.sessionsDir)
            .filter(f => f.endsWith('.json'));
        files.forEach(f => fs.unlinkSync(path.join(this.sessionsDir, f)));
        return files.length;
    }
    /**
     * Add message to session
     */
    addMessage(session, message) {
        session.messages.push(message);
        this.save(session);
    }
    compactInMemory(session) {
        return this.compactSession(session).session;
    }
    buildMemoryContext(session) {
        if (!session?.memorySummary || !session.memorySummary.trim()) {
            return '';
        }
        return [
            'Session memory summary from earlier conversation turns.',
            'Use this as compressed context for follow-up questions and resume behavior.',
            session.memorySummary.trim(),
        ].join('\n');
    }
    compactSession(session) {
        if (session.messages.length <= this.compactThreshold) {
            return { session, compacted: false };
        }
        const systemMessages = session.messages.filter((message) => message.role === 'system' && this.isPersistentSystemMessage(message.content));
        const recentMessages = session.messages.slice(-this.retainRecentMessages);
        const olderMessages = session.messages.slice(0, -this.retainRecentMessages);
        const nextSummary = this.mergeSummaries(session.memorySummary || '', this.summarizeMessages(olderMessages));
        session.memorySummary = nextSummary;
        session.compactedAt = new Date().toISOString();
        session.messages = this.mergePersistentAndRecent(systemMessages, recentMessages);
        return { session, compacted: true };
    }
    isPersistentSystemMessage(content) {
        return content.includes('Vigthoria CLI agent operating contract')
            || content.includes('Session memory summary from earlier conversation turns.');
    }
    mergePersistentAndRecent(systemMessages, recentMessages) {
        const merged = [];
        const seen = new Set();
        for (const message of [...systemMessages, ...recentMessages]) {
            const key = `${message.role}:${message.content}`;
            if (seen.has(key)) {
                continue;
            }
            seen.add(key);
            merged.push(message);
        }
        return merged;
    }
    mergeSummaries(existingSummary, newSummary) {
        const blocks = [existingSummary.trim(), newSummary.trim()].filter(Boolean);
        if (blocks.length === 0) {
            return '';
        }
        const mergedLines = new Set();
        for (const block of blocks) {
            for (const line of block.split('\n')) {
                const trimmed = line.trim();
                if (trimmed) {
                    mergedLines.add(trimmed);
                }
            }
        }
        return Array.from(mergedLines).slice(-18).join('\n');
    }
    summarizeMessages(messages) {
        const summaryLines = [];
        let userCount = 0;
        let assistantCount = 0;
        for (const message of messages) {
            const normalized = this.normalizeMessageContent(message.content);
            if (!normalized) {
                continue;
            }
            if (message.role === 'user' && userCount < 8) {
                summaryLines.push(`- User intent: ${normalized}`);
                userCount += 1;
                continue;
            }
            if (message.role === 'assistant' && assistantCount < 6) {
                summaryLines.push(`- Assistant outcome: ${normalized}`);
                assistantCount += 1;
                continue;
            }
            if (message.role === 'system' && /Tool .* (succeeded|failed)\./.test(normalized) && assistantCount < 10) {
                summaryLines.push(`- Tool evidence: ${normalized}`);
                assistantCount += 1;
            }
        }
        return summaryLines.slice(-14).join('\n');
    }
    normalizeMessageContent(content) {
        const collapsed = String(content || '')
            .replace(/<tool_call>[\s\S]*?<\/tool_call>/g, ' ')
            .replace(/\s+/g, ' ')
            .trim();
        if (!collapsed) {
            return '';
        }
        return collapsed.length > 220 ? `${collapsed.slice(0, 220)}...` : collapsed;
    }
    /**
     * Get session summary for display
     */
    getSummary(session) {
        const userMessages = session.messages.filter(m => m.role === 'user');
        const preview = userMessages.length > 0
            ? userMessages[0].content.substring(0, 50) + '...'
            : 'Empty session';
        return `[${session.id}] ${preview} (${userMessages.length} messages)`;
    }
}
