// Global structured logging bus for BadNote.
//
// Always-on (unlike the old PDF-only DiagnosticLogger opt-in). Writes NDJSON
// lines to a rotating session file under the app documents directory so a
// Surface user can export a diagnostic pack without attaching a debugger.

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

import 'package:path_provider/path_provider.dart';
import 'package:uuid/uuid.dart';

enum LogLevel { trace, debug, info, warn, error }

/// Known subsystems — keep the set small so filters stay useful.
abstract final class LogSubsystem {
  static const shell = 'shell';
  static const ink = 'ink';
  static const arbiter = 'arbiter';
  static const penNative = 'pen_native';
  static const pdf = 'pdf';
  static const office = 'office';
  static const board = 'board';
  static const sync = 'sync';
  static const diag = 'diag';
  static const frame = 'frame';
}

class BadNoteLog {
  BadNoteLog._();
  static final BadNoteLog instance = BadNoteLog._();

  final String sessionId = const Uuid().v4();
  final List<Map<String, Object?>> _ring = <Map<String, Object?>>[];
  static const int _ringCap = 4000;

  File? _file;
  Directory? _dir;
  Timer? _flushTimer;
  final List<String> _pending = <String>[];
  bool _started = false;
  LogLevel minLevel = LogLevel.debug;

  /// Absolute path of the current session log, once [start] succeeds.
  String? get path => _file?.path;

  Directory? get directory => _dir;

  Future<void> start() async {
    if (_started) return;
    _started = true;
    try {
      Directory base;
      try {
        base = await getApplicationDocumentsDirectory();
      } catch (_) {
        base = await getTemporaryDirectory();
      }
      _dir = Directory(
        '${base.path}${Platform.pathSeparator}badnote_diagnostics',
      );
      if (!await _dir!.exists()) {
        await _dir!.create(recursive: true);
      }
      final stamp = DateTime.now()
          .toIso8601String()
          .replaceAll(':', '-')
          .replaceAll('.', '-');
      _file = File(
        '${_dir!.path}${Platform.pathSeparator}session_$stamp.ndjson',
      );
      await _file!.writeAsString(
        '${jsonEncode({
          'ts': DateTime.now().toIso8601String(),
          'level': 'info',
          'subsystem': LogSubsystem.diag,
          'msg': 'session_start',
          'sessionId': sessionId,
          'platform': Platform.operatingSystem,
          'osVersion': Platform.operatingSystemVersion,
        })}\n',
        flush: true,
      );
      _flushTimer = Timer.periodic(const Duration(seconds: 1), (_) => _flush());
      info(LogSubsystem.diag, 'log file ready', fields: {'path': _file!.path});
    } catch (e) {
      // Logging must never crash the app.
      developer.log('BadNoteLog start failed: $e', name: 'badnote');
    }
  }

  void trace(String subsystem, String msg, {Map<String, Object?>? fields}) =>
      _emit(LogLevel.trace, subsystem, msg, fields);

  void debug(String subsystem, String msg, {Map<String, Object?>? fields}) =>
      _emit(LogLevel.debug, subsystem, msg, fields);

  void info(String subsystem, String msg, {Map<String, Object?>? fields}) =>
      _emit(LogLevel.info, subsystem, msg, fields);

  void warn(String subsystem, String msg, {Map<String, Object?>? fields}) =>
      _emit(LogLevel.warn, subsystem, msg, fields);

  void error(String subsystem, String msg, {Map<String, Object?>? fields}) =>
      _emit(LogLevel.error, subsystem, msg, fields);

  void _emit(
    LogLevel level,
    String subsystem,
    String msg,
    Map<String, Object?>? fields,
  ) {
    if (level.index < minLevel.index) return;
    final entry = <String, Object?>{
      'ts': DateTime.now().toIso8601String(),
      'level': level.name,
      'subsystem': subsystem,
      'msg': msg,
      'sessionId': sessionId,
      if (fields != null) ...fields,
    };
    _ring.add(entry);
    if (_ring.length > _ringCap) {
      _ring.removeRange(0, _ring.length - _ringCap);
    }
    final line = jsonEncode(entry);
    developer.log(line, name: 'badnote.$subsystem');
    if (_file != null) {
      _pending.add(line);
      if (_pending.length >= 200) {
        unawaited(_flush());
      }
    }
  }

  Future<void> _flush() async {
    final file = _file;
    if (file == null || _pending.isEmpty) return;
    final chunk = '${_pending.join('\n')}\n';
    _pending.clear();
    try {
      await file.writeAsString(chunk, mode: FileMode.append, flush: true);
    } catch (_) {}
  }

  /// Snapshot of the in-memory ring (newest last).
  List<Map<String, Object?>> snapshotRing() =>
      List<Map<String, Object?>>.unmodifiable(_ring);

  Future<void> flush() => _flush();

  Future<void> stop() async {
    _flushTimer?.cancel();
    _flushTimer = null;
    await _flush();
  }
}

/// Bridge for legacy call sites that still use plain strings.
void logLegacyInputLine(String line) {
  BadNoteLog.instance.debug(LogSubsystem.penNative, line);
}
