// SPDX-FileCopyrightText: 2026 FocusFlow contributors
// SPDX-License-Identifier: AGPL-3.0-only

/// Provides shared FocusFlow logging primitives for scheduler code.
library;

/// Lazily creates a log message after level gating passes.
typedef FocusFlowLogMessage = Object? Function();

/// Receives one enabled log entry.
typedef FocusFlowLogSink = void Function(FocusFlowLogEntry entry);

/// Shared process logger used by scheduler core code.
final FocusFlowLogger logger = FocusFlowLogger.disabled();

/// Supported FocusFlow logging levels, ordered from most to least verbose.
enum FocusFlowLogLevel {
  /// Extremely granular diagnostics, data dumps, and caller details for every log.
  finest,

  /// Intra-method minor actions and small state changes.
  finer,

  /// Secondary details, possible issues, and rich warning/error context.
  fine,

  /// High-level debug flow and useful state values.
  debug,

  /// Very high-level operational checkpoints.
  info,

  /// Handled issues where the app recovered and kept going.
  warn,

  /// Invalid or unsafe states that need review.
  error;

  /// Parses [value] into a supported logging level.
  static FocusFlowLogLevel? tryParse(String? value) {
    final normalized = value?.trim().toLowerCase();
    if (normalized == null || normalized.isEmpty) {
      return null;
    }
    if (normalized == 'trace') {
      return finest;
    }
    for (final level in FocusFlowLogLevel.values) {
      if (level.name == normalized) {
        return level;
      }
    }
    return null;
  }

  /// Ordered severity used for threshold comparisons.
  int get severity {
    return switch (this) {
      FocusFlowLogLevel.finest => 0,
      FocusFlowLogLevel.finer => 1,
      FocusFlowLogLevel.fine => 2,
      FocusFlowLogLevel.debug => 3,
      FocusFlowLogLevel.info => 4,
      FocusFlowLogLevel.warn => 5,
      FocusFlowLogLevel.error => 6,
    };
  }

  /// Whether this level should write when [minimumLevel] is configured.
  bool writesAt(FocusFlowLogLevel minimumLevel) {
    return severity >= minimumLevel.severity;
  }
}

/// Structured log entry produced after level gating has passed.
final class FocusFlowLogEntry {
  /// Creates a structured log entry.
  const FocusFlowLogEntry({
    required this.timestamp,
    required this.level,
    required this.message,
    this.caller,
    this.error,
    this.stackTrace,
  });

  /// UTC timestamp for the log event.
  final DateTime timestamp;

  /// Severity level for the log event.
  final FocusFlowLogLevel level;

  /// Resolved log message.
  final String message;

  /// Optional caller frame captured by [FocusFlowLogger].
  final String? caller;

  /// Optional error object attached to the entry.
  final Object? error;

  /// Optional stack trace attached to the entry.
  final StackTrace? stackTrace;

  /// Formats this entry as one append-only log line.
  String toSingleLine() {
    final buffer = StringBuffer()
      ..write(timestamp.toUtc().toIso8601String())
      ..write(' ')
      ..write(level.name.toUpperCase())
      ..write(' ');
    final currentCaller = caller;
    if (currentCaller != null) {
      buffer
        ..write('caller=')
        ..write(_sanitizeForSingleLine(currentCaller))
        ..write(' ');
    }
    buffer.write(_sanitizeForSingleLine(message));
    final currentError = error;
    if (currentError != null) {
      buffer
        ..write(' | error=')
        ..write(_sanitizeForSingleLine(currentError.toString()));
    }
    final currentStackTrace = stackTrace;
    if (currentStackTrace != null) {
      buffer
        ..write(' | stackTrace=')
        ..write(_sanitizeForSingleLine(currentStackTrace.toString()));
    }
    return buffer.toString();
  }
}

/// Logger that handles level gating, lazy messages, and caller capture.
final class FocusFlowLogger {
  /// Creates a logger with optional runtime configuration.
  FocusFlowLogger({
    FocusFlowLogLevel? minimumLevel,
    FocusFlowLogSink? sink,
    DateTime Function()? now,
    Iterable<String> callerFrameIgnores = const <String>[],
  })  : _minimumLevel = minimumLevel,
        _sink = sink,
        _now = now ?? _utcNow,
        _callerFrameIgnores = _normalizedCallerFrameIgnores(
          callerFrameIgnores,
        );

  /// Creates a logger that drops every message.
  factory FocusFlowLogger.disabled({DateTime Function()? now}) {
    return FocusFlowLogger(now: now);
  }

  /// Minimum level currently configured for this logger.
  FocusFlowLogLevel? _minimumLevel;

  /// Destination receiving enabled entries.
  FocusFlowLogSink? _sink;

  /// Clock used to timestamp log entries.
  DateTime Function() _now;

  /// Stack-frame markers ignored when capturing the caller.
  List<String> _callerFrameIgnores;

  /// Minimum level that will be written, or null when disabled.
  FocusFlowLogLevel? get minimumLevel => _minimumLevel;

  /// Whether this logger has enough configuration to write entries.
  bool get isEnabled => _minimumLevel != null && _sink != null;

  /// Replaces this logger's runtime configuration.
  void configure({
    required FocusFlowLogLevel minimumLevel,
    required FocusFlowLogSink sink,
    DateTime Function()? now,
    Iterable<String> callerFrameIgnores = const <String>[],
  }) {
    _minimumLevel = minimumLevel;
    _sink = sink;
    _now = now ?? _utcNow;
    _callerFrameIgnores = _normalizedCallerFrameIgnores(callerFrameIgnores);
  }

  /// Disables this logger and drops subsequent messages.
  void disable({DateTime Function()? now}) {
    _minimumLevel = null;
    _sink = null;
    _now = now ?? _utcNow;
    _callerFrameIgnores = _normalizedCallerFrameIgnores(const <String>[]);
  }

  /// Whether [level] would currently be written.
  bool wouldWrite(FocusFlowLogLevel level) {
    final configured = _minimumLevel;
    return configured != null && _sink != null && level.writesAt(configured);
  }

  /// Writes [message] at `finest` when enabled.
  void finest(
    Object? message, {
    Object? error,
    StackTrace? stackTrace,
  }) {
    write(
      FocusFlowLogLevel.finest,
      message,
      error: error,
      stackTrace: stackTrace,
    );
  }

  /// Writes [message] at `finer` when enabled.
  void finer(
    Object? message, {
    Object? error,
    StackTrace? stackTrace,
  }) {
    write(
      FocusFlowLogLevel.finer,
      message,
      error: error,
      stackTrace: stackTrace,
    );
  }

  /// Writes [message] at `fine` when enabled.
  void fine(
    Object? message, {
    Object? error,
    StackTrace? stackTrace,
  }) {
    write(
      FocusFlowLogLevel.fine,
      message,
      error: error,
      stackTrace: stackTrace,
    );
  }

  /// Writes [message] at `debug` when enabled.
  void debug(
    Object? message, {
    Object? error,
    StackTrace? stackTrace,
  }) {
    write(
      FocusFlowLogLevel.debug,
      message,
      error: error,
      stackTrace: stackTrace,
    );
  }

  /// Writes [message] at `info` when enabled.
  void info(
    Object? message, {
    Object? error,
    StackTrace? stackTrace,
  }) {
    write(
      FocusFlowLogLevel.info,
      message,
      error: error,
      stackTrace: stackTrace,
    );
  }

  /// Writes [message] at `warn` when enabled.
  void warn(
    Object? message, {
    Object? error,
    StackTrace? stackTrace,
  }) {
    write(
      FocusFlowLogLevel.warn,
      message,
      error: error,
      stackTrace: stackTrace,
    );
  }

  /// Writes [message] at `error` when enabled.
  void error(
    Object? message, {
    Object? error,
    StackTrace? stackTrace,
  }) {
    write(
      FocusFlowLogLevel.error,
      message,
      error: error,
      stackTrace: stackTrace,
    );
  }

  /// Writes [message] at [level] when it passes the configured threshold.
  void write(
    FocusFlowLogLevel level,
    Object? message, {
    Object? error,
    StackTrace? stackTrace,
  }) {
    final configured = _minimumLevel;
    final currentSink = _sink;
    if (configured == null ||
        currentSink == null ||
        !level.writesAt(configured)) {
      return;
    }

    final entry = FocusFlowLogEntry(
      timestamp: _now().toUtc(),
      level: level,
      message: _resolveMessage(message),
      caller: _shouldCaptureCaller(level: level, minimumLevel: configured)
          ? _callerFrame()
          : null,
      error: error,
      stackTrace: stackTrace,
    );

    try {
      currentSink(entry);
    } on Object {
      return;
    }
  }

  /// Captures the first stack frame outside known logging internals.
  String _callerFrame() {
    for (final line in StackTrace.current.toString().split('\n')) {
      final trimmed = line.trim();
      final shouldIgnore = _callerFrameIgnores.any(trimmed.contains);
      if (trimmed.isEmpty || shouldIgnore) {
        continue;
      }
      return _sanitizeForSingleLine(trimmed);
    }
    return 'unknown';
  }
}

/// Returns whether caller capture should run for [level].
bool _shouldCaptureCaller({
  required FocusFlowLogLevel level,
  required FocusFlowLogLevel minimumLevel,
}) {
  if (minimumLevel == FocusFlowLogLevel.finest) {
    return true;
  }
  final capturesWarningContext =
      level == FocusFlowLogLevel.warn || level == FocusFlowLogLevel.error;
  return capturesWarningContext &&
      minimumLevel.severity <= FocusFlowLogLevel.fine.severity;
}

/// Resolves a plain or lazy [message] after level gating passes.
String _resolveMessage(Object? message) {
  try {
    final resolved = message is FocusFlowLogMessage ? message() : message;
    return resolved?.toString().trim() ?? '';
  } on Object catch (error) {
    return 'Log message evaluation failed: $error';
  }
}

/// Returns the current UTC timestamp.
DateTime _utcNow() => DateTime.now().toUtc();

/// Builds the complete set of caller frame markers to skip.
List<String> _normalizedCallerFrameIgnores(
  Iterable<String> callerFrameIgnores,
) {
  final ignores = <String>{'focus_flow_logger.dart'};
  for (final ignore in callerFrameIgnores) {
    final trimmed = ignore.trim();
    if (trimmed.isNotEmpty) {
      ignores.add(trimmed);
    }
  }
  return List<String>.unmodifiable(ignores);
}

/// Keeps diagnostic text on one physical log line.
String _sanitizeForSingleLine(String value) {
  return value.replaceAll('\n', r'\n');
}
