// src/core/session/SessionManager.ts
//
// Vision 27 §11.11: Session Isolation
// Vision 27 §1.5: Topic structure requires {root}/{session}/{mount}...
//
// Butler Lampson (Turing Award, Capability Systems):
//   "The namespace IS the security boundary. Per-session namespaces are the minimum."
//
// Mark Miller (Capability Security, E Language):
//   "Session ID is first step toward capability-secure isolation."

/**
 * Session configuration options.
 */
export interface ISessionConfig {
  /**
   * Root identifier - organization or user namespace.
   * @example 'anonymous', 'user.alice', 'corp.acme.com'
   * @default 'anonymous'
   */
  root?: string;
  /** @deprecated Use `root`. */
  realm?: string;

  /**
   * Session ID - unique per browser tab.
   * If not provided, auto-generates and persists to sessionStorage.
   */
  sessionId?: string;

  /**
   * Storage key for persisting session ID.
   * @default 'matrix-session-id'
   */
  storageKey?: string;
}

/**
 * Session information returned by SessionManager.
 */
export interface ISessionInfo {
  /** Root identifier */
  root: string;
  /** @deprecated Use `root`. */
  realm?: string;
  /** Session ID (unique per browser tab) */
  sessionId: string;
  /** Full namespace for topics: {root}/{sessionId} */
  namespace: string;
  /** Whether this is a new session (just created) */
  isNew: boolean;
}

/**
 * Shared session configuration for collaboration.
 */
export interface ISharedSessionConfig {
  /**
   * Root identifier.
   * @default 'anonymous'
   */
  root?: string;
  /** @deprecated Use `root`. */
  realm?: string;

  /**
   * Shared room ID - all participants use the same room ID.
   * This should be known/shared between collaborators.
   * @example 'meeting-123', 'project-alpha'
   */
  roomId: string;
}

/**
 * Shared session information.
 */
export interface ISharedSessionInfo {
  /** Root identifier */
  root: string;
  /** @deprecated Use `root`. */
  realm?: string;
  /** Shared room ID */
  roomId: string;
  /** Full namespace for topics: {root}/shared/{roomId} */
  namespace: string;
}

// Storage for session IDs in non-browser environments (supports multiple keys)
const fallbackStorage = new Map<string, string>();

/**
 * Default storage key for session ID persistence.
 * Extracted as constant per Uncle Bob's Clean Code principles.
 */
export const DEFAULT_SESSION_STORAGE_KEY = 'matrix-session-id';

/**
 * Production mode flag - when true, error messages are sanitized.
 * Set via environment variable or direct assignment.
 *
 * Peter Sommerlad (Security Patterns):
 *   "For production public APIs, sanitize errors to prevent information leakage."
 */
export let PRODUCTION_MODE = false;

/**
 * Enable production mode (sanitized error messages).
 * Call this in production deployments to prevent information leakage.
 */
export function enableProductionMode(): void {
  PRODUCTION_MODE = true;
}

/**
 * Disable production mode (detailed error messages for debugging).
 */
export function disableProductionMode(): void {
  PRODUCTION_MODE = false;
}

/**
 * Create a validation error with appropriate detail level.
 *
 * In production: Generic "invalid input" message (no information leakage)
 * In development: Detailed message (developer visibility)
 *
 * @param name - Parameter name for the error
 * @param devMessage - Detailed message for development
 * @returns Error with appropriate message
 */
function createValidationError(name: string, devMessage: string): Error {
  if (PRODUCTION_MODE) {
    // Production: Generic message prevents information leakage
    return new Error(`Invalid ${name}`);
  }
  // Development: Detailed message for debugging
  return new Error(`${name} ${devMessage}`);
}

/**
 * Validate a namespace segment to prevent path traversal attacks.
 *
 * Butler Lampson (Turing Award, Capability Systems):
 *   "Namespace segments must be validated to prevent path traversal."
 *
 * Peter Sommerlad (Security Patterns):
 *   "Error messages should not reveal implementation details in production."
 *
 * @param segment - The segment to validate
 * @param name - Name of the parameter for error messages
 * @throws Error if segment contains invalid characters
 */
function validateNamespaceSegment(segment: string, name: string): void {
  if (!segment || typeof segment !== 'string') {
    throw createValidationError(name, 'must be a non-empty string');
  }

  // Prevent path traversal
  if (segment.includes('/') || segment.includes('\\')) {
    throw createValidationError(name, 'cannot contain path separators (/ or \\)');
  }

  if (segment.includes('..')) {
    throw createValidationError(name, 'cannot contain path traversal (..)');
  }

  // Prevent wildcard injection
  if (segment.includes('#') || segment.includes('+')) {
    throw createValidationError(name, 'cannot contain wildcards (# or +)');
  }

  // Prevent null bytes
  if (segment.includes('\0')) {
    throw createValidationError(name, 'cannot contain null bytes');
  }
}

/**
 * Check if sessionStorage is available (browser environment).
 */
function hasSessionStorage(): boolean {
  try {
    return typeof sessionStorage !== 'undefined' && sessionStorage !== null;
  } catch {
    return false;
  }
}

/**
 * Generate cryptographically secure random bytes.
 *
 * Mark Miller (Capability Security, E Language):
 *   "Session IDs must be cryptographically unpredictable."
 *
 * Works in both browser (crypto.getRandomValues) and Node.js (crypto.randomUUID).
 */
function getCryptoRandom(): string {
  // Try crypto.randomUUID first (Node 19+, all modern browsers)
  if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
    return crypto.randomUUID();
  }

  // Fallback to crypto.getRandomValues (browser)
  if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
    const array = new Uint8Array(16);
    crypto.getRandomValues(array);
    return Array.from(array, (b) => b.toString(16).padStart(2, '0')).join('');
  }

  // Last resort fallback for older Node.js without global crypto.
  // Use process.getBuiltinModule when available so browser bundlers do not
  // pull node:crypto into web bundles through the core barrel exports.
  try {
    const builtinLoader = typeof process !== 'undefined'
      && process !== null
      && typeof (process as typeof process & { getBuiltinModule?: (name: string) => unknown }).getBuiltinModule === 'function'
      ? (process as typeof process & { getBuiltinModule: (name: string) => unknown }).getBuiltinModule.bind(process)
      : null;
    const nodeCrypto = builtinLoader?.('node:crypto') as { randomUUID?: () => string } | undefined
      ?? builtinLoader?.('crypto') as { randomUUID?: () => string } | undefined;
    if (typeof nodeCrypto?.randomUUID === 'function') {
      return nodeCrypto.randomUUID();
    }
  } catch {
    // Fall through to insecure fallback below.
  }

  // Ultimate fallback - log warning and use less secure method
  // eslint-disable-next-line no-console -- last-resort entropy warning in non-actor kernel utility
  console.warn(
    '[SessionManager] SECURITY WARNING: crypto APIs not available. ' +
      'Using less secure random generation. This is NOT suitable for production.'
  );
  return `fallback-${Date.now()}-${Math.random().toString(36).slice(2, 14)}`;
}

/**
 * Generate a unique session ID using cryptographic randomness.
 *
 * Format: session-{uuid}
 * Example: session-550e8400-e29b-41d4-a716-446655440000
 *
 * Provides 128 bits of entropy (production-grade security).
 *
 * Joe Armstrong (Erlang, "Programming Erlang"):
 *   "PIDs are unforgeable. Session IDs should be similarly hard to guess."
 *
 * Mark Miller (Capability Security, E Language):
 *   "Session ID is first step toward capability-secure isolation."
 */
export function generateSessionId(): string {
  const prefix = 'session';
  const uuid = getCryptoRandom();
  return `${prefix}-${uuid}`;
}

/**
 * Get the current session ID.
 * Creates one if needed and persists to sessionStorage (browser) or memory (Node).
 *
 * In browser: persists to sessionStorage so refresh maintains session.
 * In Node: stores in memory (new session per process).
 *
 * @param storageKey - Key for sessionStorage persistence
 * @returns The session ID
 */
export function getSessionId(storageKey = DEFAULT_SESSION_STORAGE_KEY): string {
  // Browser environment - use sessionStorage
  if (hasSessionStorage()) {
    let sessionId = sessionStorage.getItem(storageKey);
    if (!sessionId) {
      sessionId = generateSessionId();
      sessionStorage.setItem(storageKey, sessionId);
    }
    return sessionId;
  }

  // Non-browser environment - use in-memory fallback (per key)
  let sessionId = fallbackStorage.get(storageKey);
  if (!sessionId) {
    sessionId = generateSessionId();
    fallbackStorage.set(storageKey, sessionId);
  }
  return sessionId;
}

/**
 * Clear the current session ID.
 * Useful for testing or explicit session reset.
 *
 * @param storageKey - Key for sessionStorage persistence. If not provided, clears the default key.
 */
export function clearSessionId(storageKey = DEFAULT_SESSION_STORAGE_KEY): void {
  if (hasSessionStorage()) {
    sessionStorage.removeItem(storageKey);
  }
  // Clear the specific key from fallback storage
  fallbackStorage.delete(storageKey);
}

/**
 * Get session information for isolated (per-tab) sessions.
 *
 * Vision 27 §1.5 wire format:
 *   {root}/{session}/{mount.path}.{direction}.{operation}
 *
 * @param config - Session configuration
 * @returns Session information including namespace
 */
export function getSession(config: ISessionConfig = {}): ISessionInfo {
  const root = config.root ?? config.realm ?? 'anonymous';
  const storageKey = config.storageKey ?? DEFAULT_SESSION_STORAGE_KEY;

  // Validate root to prevent path traversal (Butler Lampson)
  validateNamespaceSegment(root, 'root');

  // Validate explicit sessionId if provided
  if (config.sessionId) {
    validateNamespaceSegment(config.sessionId, 'sessionId');
  }

  // Check if this is a new session
  let isNew = false;
  if (hasSessionStorage()) {
    isNew = !sessionStorage.getItem(storageKey);
  } else {
    isNew = !fallbackStorage.has(storageKey);
  }

  const sessionId = config.sessionId ?? getSessionId(storageKey);
  const namespace = `${root}/${sessionId}`;

  return {
    root,
    realm: root, // backward compat
    sessionId,
    namespace,
    isNew,
  };
}

/**
 * Get session information for shared (collaborative) sessions.
 *
 * All participants using the same roomId will share the same topics.
 * This is an EXPLICIT opt-in for collaboration.
 *
 * Vision 27 §11.11 Session Types:
 *   - Isolated: {root}/{sessionId}/... (default)
 *   - Shared: {root}/shared/{roomId}/... (explicit collaboration)
 *
 * @param config - Shared session configuration
 * @returns Shared session information including namespace
 */
export function getSharedSession(config: ISharedSessionConfig): ISharedSessionInfo {
  const root = config.root ?? config.realm ?? 'anonymous';
  const { roomId } = config;

  if (!roomId || typeof roomId !== 'string') {
    throw new Error('roomId is required for shared sessions');
  }

  // Validate inputs to prevent path traversal and injection (Butler Lampson)
  validateNamespaceSegment(root, 'root');
  validateNamespaceSegment(roomId, 'roomId');

  const namespace = `${root}/shared/${roomId}`;

  return {
    root,
    realm: root, // backward compat
    roomId,
    namespace,
  };
}

// ═══════════════════════════════════════════════════════════════════════════
// NO SINGLETON HERE
//
// Robert C. Martin ("Clean Architecture"):
//   "Singletons are global state masquerading as a class. They create hidden
//    dependencies and make testing difficult. Use pure functions instead."
//
// The pure functions above (getSession, getSharedSession, etc.) are the
// proper API. They are stateless, testable, and have explicit dependencies.
// ═══════════════════════════════════════════════════════════════════════════
