// Central error log for Studio. Every error surfaced to the
// operator (SnackBar, error sheet, recoverable dialog) is also
// appended here so `chain doctor` and "Open log" can show what
// went wrong without the operator having to reproduce the
// failure.
//
// Path: `~/.chain/logs/studio-errors.log` (cross-platform via
// HOME / USERPROFILE). One line per event, JSON-shaped so
// `chain doctor` can parse it; humans can still read it because
// the JSON is single-line and short.
//
// Best-effort by design: a failed write to the log file must
// never break the UI layer that asked us to log. We swallow
// IO errors silently — the worst case is the operator loses
// the trail of one failure, not the failure itself.
//
// Rotation: when the file passes 256 KiB we move it to
// `studio-errors.log.1` and start fresh. One rotation level is
// enough — anything older is noise, and a Studio session
// rarely produces more than a few KiB of error text.

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

import 'package:meta/meta.dart';
import 'package:path/path.dart' as p;

class FaiLog {
  FaiLog._();

  static final FaiLog instance = FaiLog._();

  // Serialise concurrent writes — Future-based mutex so a burst
  // of errors (rare but happens during failed install retries)
  // doesn't interleave half-lines on disk.
  Future<void> _tail = Future.value();

  static const int _rotateBytes = 256 * 1024;

  /// Test seam. Set this in `setUp` to redirect the singleton at
  /// a per-test temp file; reset to `null` in `tearDown`. Production
  /// code never touches it — the default getter computes from HOME.
  @visibleForTesting
  static String? testPathOverride;

  String get _path {
    final override = testPathOverride;
    if (override != null) return override;
    final home =
        Platform.environment['HOME'] ??
        Platform.environment['USERPROFILE'] ??
        '.';
    return p.join(home, '.fai', 'logs', 'studio-errors.log');
  }

  /// Absolute path of the studio-errors log. Public so the
  /// Doctor page and the in-app log viewer can read it without
  /// hard-coding the layout twice.
  String get path => _path;

  /// Append a single error event. The [source] is a short tag
  /// (`flows.run`, `daemon.logs`, …) that lets a reader group
  /// related failures. [error] can be any thrown object — gRPC
  /// codes, plain strings, exceptions; we serialise it via
  /// [error.toString()] so the on-disk record matches what the
  /// operator saw on screen.
  Future<void> error(String source, Object error, {Object? context}) {
    final entry = <String, Object?>{
      'ts': DateTime.now().toUtc().toIso8601String(),
      'level': 'error',
      'source': source,
      'error': error.toString(),
      if (context != null) 'context': context.toString(),
    };
    return _append(entry);
  }

  /// Read the last [maxLines] lines back. Used by `chain doctor`
  /// + Settings → "Recent errors" view. Returns oldest first so
  /// the caller can render in chronological order.
  Future<List<String>> tail({int maxLines = 50}) async {
    try {
      final f = File(_path);
      if (!await f.exists()) return const <String>[];
      final raw = await f.readAsString();
      final lines = raw.split('\n').where((l) => l.isNotEmpty).toList();
      if (lines.length <= maxLines) return lines;
      return lines.sublist(lines.length - maxLines);
    } catch (_) {
      return const <String>[];
    }
  }

  Future<void> _append(Map<String, Object?> entry) {
    final next = _tail.then((_) async {
      try {
        final f = File(_path);
        final dir = Directory(p.dirname(_path));
        if (!await dir.exists()) {
          await dir.create(recursive: true);
        }
        // Rotate before write so a hot-loop never balloons past
        // the cap. Single-level rotation: keep the previous file
        // as `.1`, anything older drops on the floor.
        if (await f.exists()) {
          final size = await f.length();
          if (size >= _rotateBytes) {
            final rotated = File('$_path.1');
            if (await rotated.exists()) {
              await rotated.delete();
            }
            await f.rename(rotated.path);
          }
        }
        await f.writeAsString(
          '${jsonEncode(entry)}\n',
          mode: FileMode.append,
          flush: false,
        );
      } catch (_) {
        // Best-effort. A failed log write must not crash the UI.
      }
    });
    _tail = next.catchError((_) {});
    return next;
  }
}
