// lib/storage/sidecar_store.dart
//
// Atomic read/write for `<file>.badnote.json` sidecars (Phase 1 / §F.1 of
// docs/plans/2026-06-24-file-based-storage.md). Pure dart:io, NO UI.
//
// Write protocol (§F.1):
//   1. Serialize to pretty JSON, write to `<target>.tmp` with flush:true.
//   2. Before clobbering, copy the current good `<target>` to `<target>.bak`
//      (one-deep backup — cheap insurance against a corrupt write).
//   3. `rename` tmp → target. rename is atomic on the same filesystem (NTFS /
//      POSIX), so a reader never observes a half-written sidecar.
//
// Read protocol: parse `<target>`; if it is missing OR fails to parse, fall back
// to `<target>.bak`. If neither yields valid JSON, return null.

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

import 'badnote_sidecar.dart';

/// Stateless helper namespace for sidecar persistence.
class SidecarStore {
  const SidecarStore._();

  static const JsonEncoder _encoder = JsonEncoder.withIndent('  ');

  /// Suffix for the in-progress temp file.
  static const String tmpSuffix = '.tmp';

  /// Suffix for the one-deep backup of the last good sidecar.
  static const String bakSuffix = '.bak';

  /// Atomically writes [sidecar] to [target] (temp + rename), keeping a `.bak`
  /// of the previous good file. Never leaves a partial sidecar at [target]:
  /// either the previous content (on failure before rename) or the new content.
  static Future<void> writeAtomic(File target, BadnoteSidecar sidecar) async {
    final json = _encoder.convert(sidecar.toJson());
    await writeAtomicJson(target, json);
  }

  /// Lower-level variant for callers that already hold the JSON string.
  static Future<void> writeAtomicJson(File target, String json) async {
    await target.parent.create(recursive: true);

    final tmp = File('${target.path}$tmpSuffix');
    await tmp.writeAsString(json, flush: true);

    // Back up the previous good file before clobbering it.
    if (await target.exists()) {
      final bak = File('${target.path}$bakSuffix');
      try {
        await target.copy(bak.path);
      } catch (_) {
        // A failed backup must not block the write; the atomic rename below
        // still guarantees the new content lands intact.
      }
    }

    // Atomic on the same filesystem.
    await tmp.rename(target.path);
  }

  /// Reads and parses the sidecar at [target], falling back to `<target>.bak`
  /// if the primary is missing or corrupt. Returns null if neither is readable.
  static Future<BadnoteSidecar?> read(File target) async {
    final primary = await _tryRead(target);
    if (primary != null) return primary;

    final bak = File('${target.path}$bakSuffix');
    return _tryRead(bak);
  }

  static Future<BadnoteSidecar?> _tryRead(File file) async {
    try {
      if (!await file.exists()) return null;
      final raw = await file.readAsString();
      final decoded = jsonDecode(raw);
      if (decoded is! Map<String, dynamic>) return null;
      return BadnoteSidecar.fromJson(decoded);
    } catch (_) {
      return null;
    }
  }
}
