/**
 * Session State Tracker
 *
 * Manages session-level state for tracking plugin installations
 * and other session data that needs to persist during a Claude Code session.
 *
 * The state is persisted to `.specweave/state/session-state.json` and is used
 * to detect when plugins were installed during the current session (requiring restart).
 *
 * @module core/session/session-state
 */
import * as fs from 'fs';
import * as path from 'path';
import { randomUUID } from 'crypto';
import { acquireFileLock } from '../../utils/platform-utils.js';
/**
 * Current version of the session state schema
 * Increment when making breaking changes to state structure
 */
const STATE_SCHEMA_VERSION = 1;
// ============================================================================
// Constants
// ============================================================================
/** Name of the state file */
const STATE_FILE_NAME = 'session-state.json';
/** Directory containing state files */
const STATE_DIR_NAME = 'state';
/** Lock file for session state operations */
const STATE_LOCK_NAME = 'session-state.lock';
/** Maximum attempts to acquire lock before giving up */
const LOCK_MAX_ATTEMPTS = 10;
/** Delay between lock acquisition attempts (ms) */
const LOCK_RETRY_DELAY_MS = 50;
// ============================================================================
// Path Utilities
// ============================================================================
/**
 * Get the full path to the session state file
 */
function getStateFilePath(projectRoot) {
    return path.join(projectRoot, '.specweave', STATE_DIR_NAME, STATE_FILE_NAME);
}
/**
 * Get the full path to the session state lock file
 */
function getLockFilePath(projectRoot) {
    return path.join(projectRoot, '.specweave', STATE_DIR_NAME, STATE_LOCK_NAME);
}
/**
 * Ensure the state directory exists
 */
function ensureStateDirectory(projectRoot) {
    const stateDir = path.join(projectRoot, '.specweave', STATE_DIR_NAME);
    if (!fs.existsSync(stateDir)) {
        fs.mkdirSync(stateDir, { recursive: true });
    }
}
/**
 * Release a file lock
 */
function releaseLock(lockPath) {
    try {
        if (fs.existsSync(lockPath)) {
            fs.rmdirSync(lockPath);
        }
    }
    catch {
        // Ignore errors during unlock
    }
}
/**
 * Acquire a lock with retry logic
 * Returns true if lock acquired, false otherwise
 */
function acquireLockWithRetry(lockPath) {
    for (let attempt = 0; attempt < LOCK_MAX_ATTEMPTS; attempt++) {
        if (acquireFileLock(lockPath)) {
            return true;
        }
        // Wait before retry (synchronous to keep API simple)
        const end = Date.now() + LOCK_RETRY_DELAY_MS;
        while (Date.now() < end) {
            // Busy wait - brief enough for session state operations
        }
    }
    return false;
}
// ============================================================================
// Type Guards
// ============================================================================
/**
 * Check if an object is a valid SessionState
 */
function isValidSessionState(obj) {
    if (typeof obj !== 'object' || obj === null) {
        return false;
    }
    const state = obj;
    return (typeof state.sessionId === 'string' &&
        typeof state.startTime === 'string' &&
        Array.isArray(state.pluginsInstalledDuringSession));
}
// ============================================================================
// Main Functions
// ============================================================================
/**
 * Initialize a new session state
 *
 * Creates a fresh session state with a unique ID and persists it to disk.
 *
 * @param projectRoot - Root directory of the project
 * @returns The newly created session state
 *
 * @example
 * ```typescript
 * const state = initSessionState('/path/to/project');
 * console.log('Session started:', state.sessionId);
 * ```
 */
export function initSessionState(projectRoot) {
    const state = {
        version: STATE_SCHEMA_VERSION,
        sessionId: randomUUID(),
        startTime: new Date().toISOString(),
        pluginsInstalledDuringSession: [],
    };
    // Persist to file
    ensureStateDirectory(projectRoot);
    const stateFile = getStateFilePath(projectRoot);
    fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf-8');
    return state;
}
/**
 * Record plugin installation in session state
 *
 * Adds the installed plugins to the session state and updates the
 * last installation event. Plugins are deduplicated.
 *
 * @param projectRoot - Root directory of the project
 * @param plugins - List of plugin names that were installed
 * @param options - Additional installation context
 * @returns Updated session state
 *
 * @example
 * ```typescript
 * const state = recordPluginInstallation('/path/to/project', ['sw', 'frontend'], {
 *   trigger: 'specweave-init',
 *   projectPath: '/new/project'
 * });
 * ```
 */
export function recordPluginInstallation(projectRoot, plugins, options = {}) {
    ensureStateDirectory(projectRoot);
    const lockPath = getLockFilePath(projectRoot);
    // SECURITY: Acquire lock to prevent race conditions from concurrent hooks
    if (!acquireLockWithRetry(lockPath)) {
        // Failed to acquire lock after retries - proceed anyway but log warning
        // This is better than crashing, and the worst case is a lost update
        console.error('[session-state] Warning: Could not acquire lock, proceeding without');
    }
    try {
        // Load existing state or create new one
        let state = loadSessionState(projectRoot);
        if (!state) {
            state = initSessionState(projectRoot);
        }
        // Add new plugins (deduplicate using Set)
        const existingPlugins = new Set(state.pluginsInstalledDuringSession);
        for (const plugin of plugins) {
            existingPlugins.add(plugin);
        }
        state.pluginsInstalledDuringSession = Array.from(existingPlugins);
        // Update last installation event
        state.lastInstallationEvent = {
            timestamp: new Date().toISOString(),
            plugins,
            trigger: options.trigger || 'manual',
            projectPath: options.projectPath,
        };
        // Update project path if provided
        if (options.projectPath) {
            state.projectPath = options.projectPath;
        }
        // Persist updated state
        const stateFile = getStateFilePath(projectRoot);
        fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf-8');
        return state;
    }
    finally {
        // Always release lock
        releaseLock(lockPath);
    }
}
/**
 * Load session state from file
 *
 * Reads and parses the session state from disk. Returns null if the file
 * doesn't exist or contains invalid data.
 *
 * @param projectRoot - Root directory of the project
 * @returns Session state or null if not found/invalid
 *
 * @example
 * ```typescript
 * const state = loadSessionState('/path/to/project');
 * if (state && state.pluginsInstalledDuringSession.length > 0) {
 *   console.log('Plugins installed this session:', state.pluginsInstalledDuringSession);
 * }
 * ```
 */
export function loadSessionState(projectRoot) {
    const stateFile = getStateFilePath(projectRoot);
    if (!fs.existsSync(stateFile)) {
        return null;
    }
    try {
        const content = fs.readFileSync(stateFile, 'utf-8');
        const parsed = JSON.parse(content);
        // Validate the parsed data
        if (!isValidSessionState(parsed)) {
            // Invalid state structure - treat as missing
            return null;
        }
        // Handle schema migration if needed
        if (!parsed.version || parsed.version < STATE_SCHEMA_VERSION) {
            // Future: Add migration logic here
            parsed.version = STATE_SCHEMA_VERSION;
        }
        return parsed;
    }
    catch {
        // JSON parsing error - treat as missing
        return null;
    }
}
/**
 * Clear session state
 *
 * Removes the session state file from disk. Safe to call even if
 * the file doesn't exist.
 *
 * @param projectRoot - Root directory of the project
 *
 * @example
 * ```typescript
 * clearSessionState('/path/to/project');
 * ```
 */
export function clearSessionState(projectRoot) {
    const stateFile = getStateFilePath(projectRoot);
    try {
        if (fs.existsSync(stateFile)) {
            fs.unlinkSync(stateFile);
        }
    }
    catch {
        // Ignore errors during cleanup
    }
}
/**
 * Check if plugins were installed during current session
 *
 * Convenience function to quickly check if the session restart warning
 * should be shown.
 *
 * @param projectRoot - Root directory of the project
 * @returns True if plugins were installed this session
 */
export function hasPluginsInstalledThisSession(projectRoot) {
    const state = loadSessionState(projectRoot);
    return state !== null && state.pluginsInstalledDuringSession.length > 0;
}
//# sourceMappingURL=session-state.js.map