// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:convert';
import 'dart:io' as io;

import 'package:analysis_server/src/session_logger/log_entry.dart';
import 'package:analysis_server/src/session_logger/log_normalizer.dart';
import 'package:analysis_server/src/session_logger/process_id.dart';
import 'package:language_server_protocol/protocol_special.dart' show Either2;

/// A sink for a session logger that will write entries to a file.
final class SessionLoggerFileSink extends SessionLoggerSink {
  /// The sink used to write to the file.
  late final io.IOSink _sink;

  @override
  final LogNormalizer _normalizer;

  /// Initializes a newly created sink to write to the file at the given
  /// [filePath].
  new(String filePath, {required this._normalizer}) {
    _sink = io.File(filePath).openWrite();
  }

  @override
  Future<void> close() => _sink.close();

  @override
  void writeLogEntry(JsonMap entry) {
    var jsonString = _normalizer.normalize(entry);
    _sink.writeln(jsonString);
  }
}

/// A sink for a session logger that will write entries to an in-memory buffer.
///
/// This class has been designed to allow a user to enable the capturing of log
/// entries at an arbitrary time, and then retrieve the captured entries at some
/// future time. To support this, the sink caches entries related to
/// - server initialization,
/// - workspace configuration, and
/// - text documents
/// until capturing is enabled.
final class SessionLoggerInMemorySink extends SessionLoggerSink {
  /// The maximum number of entries stored in the [_sessionBuffer].
  int maxBufferLength;

  /// Whether entries should be captured in the buffer.
  bool _capturingEntries = false;

  /// A session logger to which requests should be forwarded, or `null` if there
  /// is no logger to forward requests to.
  final SessionLoggerSink? _nextLogger;

  /// The buffer in which initialization related entries are stored.
  final List<LogEntry> _initializationBuffer = [];

  /// The buffer in which workspace configuration related entries are stored.
  ///
  /// This buffer is cleared every time a new workspace configuration is
  /// requested.
  final List<LogEntry> _configurationBuffer = [];

  /// The buffer in which text document related entries are stored.
  final List<LogEntry> _textDocumentBuffer = [];

  /// The buffer in which normal entries are stored.
  final List<LogEntry> _sessionBuffer = [];

  @override
  final LogNormalizer _normalizer;

  /// Initialize a newly created sink to store up to [maxBufferLength] entries.
  new({
    required this.maxBufferLength,
    required LogNormalizer normalizer,
    String? sessionLogFilePath,
  }) : _normalizer = normalizer,
       _nextLogger = sessionLogFilePath == null
           ? null
           : SessionLoggerFileSink(sessionLogFilePath, normalizer: normalizer);

  /// Returns a list of the entries that have been captured.
  ///
  /// The list includes necessary initialization entries that might have
  /// occurred before the capture was started.
  List<LogEntry> get capturedEntries {
    return [
      ..._initializationBuffer,
      ..._configurationBuffer,
      ..._textDocumentBuffer,
      ..._sessionBuffer,
    ].map((entry) => LogEntry(_normalize(entry.map))).toList();
  }

  /// Whether entries are currently being captured in the buffer.
  bool get isCapturingEntries => _capturingEntries;

  /// Returns the session logger to which requests should be forwarded.
  SessionLoggerSink? get nextLogger => _nextLogger;

  /// Stops the capturing of entries.
  void startCapture() {
    _sessionBuffer.clear();
    _capturingEntries = true;
  }

  /// Stops the capturing of entries.
  void stopCapture() {
    _capturingEntries = false;
  }

  @override
  void writeLogEntry(JsonMap entry) {
    _nextLogger?.writeLogEntry(entry);
    var logEntry = LogEntry(entry);
    if (_isInitializationEntry(logEntry)) {
      _initializationBuffer.add(logEntry);
      return;
    }
    if (_capturingEntries) {
      if (_sessionBuffer.length > maxBufferLength) {
        _sessionBuffer.removeAt(0);
      }
      _sessionBuffer.add(logEntry);
    } else {
      if (_isConfigurationEntry(logEntry)) {
        _configurationBuffer.add(logEntry);
      } else if (_isTextDocumentEntry(logEntry)) {
        _textDocumentBuffer.add(logEntry);
      }
    }
  }

  /// Returns the request ids of the messages in the given list of [entries].
  ///
  /// This assumes that the entries are all from the same process. If that isn't
  /// true, then the returned list may contain duplicate ids.
  List<Either2<int, String>> _getRequestIds(List<LogEntry> entries) {
    return entries
        .where((entry) => entry.isMessage)
        .map((entry) => entry.message.id)
        .nonNulls
        .toList();
  }

  /// Returns whether the [entry] is a configuration entry.
  ///
  /// A configuration entry is defined as an entry that records the
  /// configuration of the workspace.
  ///
  /// Configuration entries are captured even when [_capturingEntries] is
  /// `false`.
  bool _isConfigurationEntry(LogEntry entry) {
    // TODO(brianwilkerson): Make this method support the legacy protocol.
    if (entry.isMessage) {
      var message = entry.message;
      if (message.isWorkspaceConfiguration) {
        return true;
      }
      for (var requestId in _getRequestIds(_configurationBuffer)) {
        if (message.isResponseTo(requestId)) {
          return true;
        }
      }
    }
    return false;
  }

  /// Returns whether the [entry] is an initialization entry.
  ///
  /// An initialization entry is defined as an entry that records the
  /// initialization process.
  ///
  /// Initialization entries are captured even when [_capturingEntries] is
  /// `false`.
  bool _isInitializationEntry(LogEntry entry) {
    // TODO(brianwilkerson): Make this method support the legacy protocol.
    if (entry.isCommandLine) return true;
    if (entry.isMessage) {
      var message = entry.message;
      if (message.isInitializeRequest || message.isInitialized) {
        return true;
      }
      for (var requestId in _getRequestIds(_initializationBuffer)) {
        if (message.isResponseTo(requestId)) {
          return true;
        }
      }
    }
    return false;
  }

  /// Returns whether the [entry] is a text document entry.
  ///
  /// A text document entry is defined as an entry that records an operation
  /// on a text document.
  ///
  /// Text document entries are captured even when [_capturingEntries] is
  /// `false`.
  bool _isTextDocumentEntry(LogEntry entry) {
    if (entry.isMessage) {
      var message = entry.message;
      return message.isDidChange || message.isDidClose || message.isDidOpen;
    }
    return false;
  }
}

/// Used to write information about a session to a log.
sealed class SessionLoggerSink {
  /// The normalizer used to normalize paths in log entries.
  LogNormalizer get _normalizer;

  Future<void> close() async {}

  /// Writes the given log [entry] to this sink.
  void writeLogEntry(JsonMap entry);

  /// Returns the given [entry] after normalizing any paths in it.
  JsonMap _normalize(JsonMap entry) {
    var normalizedJsonString = _normalizer.normalize(entry);
    return json.decode(normalizedJsonString) as JsonMap;
  }
}
