// lib/editor/persistence/editor_repository.dart
//
// MF3 diff-write contract: per-host diff of stroke ids against the last
// persisted set.  Only changed/new rows are upserted; only removed rows are
// deleted.  All mutations run in ONE transaction per saveHost call.
// The in-memory _persistedIds map is updated only after the transaction
// commits successfully.

import 'dart:convert';

import 'package:sqflite_common_ffi/sqflite_ffi.dart';

import '../engine/stroke_model.dart';
import '../../services/database_service.dart';

/// Repository for persisting [EditorStroke]s to the `ink` table.
///
/// Host-id scheme: `"page:<pageIndex>"` where pageIndex is the zero-based
/// index of the page within its document.  For example, page 0 of a document
/// uses host_id `"page:0"`.
///
/// [loadDocument] uses a single batched query over all ink rows whose
/// host_id begins with `"page:"` for the document, grouped by host_id.
/// [saveHost] implements the MF3 diff-write contract.
class EditorRepository {
  EditorRepository(this._db);

  final Database _db;

  /// Per host_id, the set of stroke ids that were last persisted to the DB.
  /// Updated only after a successful transaction commit.
  final Map<String, Set<String>> _persistedIds = {};

  // ── Factory ────────────────────────────────────────────────────────────

  /// Convenience constructor that initialises from [DatabaseService].
  static Future<EditorRepository> fromService(DatabaseService service) async {
    return EditorRepository(service.database);
  }

  // ── Load ───────────────────────────────────────────────────────────────

  /// Load all ink rows for [documentId] in a single batched query.
  ///
  /// Returns a map keyed by host_id (e.g. `"page:0"`) whose values are
  /// the strokes for that host in ascending ordinal order.
  ///
  /// The host_id scheme is: host_kind = `"page"`, host_id = `"page:<i>"`.
  Future<Map<String, List<EditorStroke>>> loadDocument(
    String documentId,
  ) async {
    // All page hosts for a document share the prefix "page:" inside host_id.
    // We tag them with document_id via the host_id prefix convention:
    // host_id = "doc:<documentId>:page:<pageIndex>"
    final rows = await _db.query(
      'ink',
      where: 'host_kind = ? AND host_id LIKE ?',
      whereArgs: ['page', 'doc:$documentId:page:%'],
      orderBy: 'host_id ASC, ordinal ASC',
    );

    final result = <String, List<EditorStroke>>{};

    for (final row in rows) {
      final hostId = row['host_id'] as String;
      final strokeJson =
          jsonDecode(row['stroke_json'] as String) as Map<String, dynamic>;
      final stroke = EditorStroke.fromJson(strokeJson);

      result.putIfAbsent(hostId, () => []).add(stroke);
    }

    // Populate _persistedIds from what we just read so that subsequent
    // saveHost calls can diff correctly even on a fresh repository instance.
    for (final entry in result.entries) {
      _persistedIds[entry.key] = entry.value.map((s) => s.id).toSet();
    }

    return result;
  }

  // ── Save (MF3 diff-write contract) ────────────────────────────────────

  /// Persist [strokes] for the given host ([hostKind], [hostId]).
  ///
  /// Diff against the last-known persisted id-set:
  /// - NEW / CHANGED rows → INSERT OR REPLACE (upsert)
  /// - REMOVED rows → DELETE
  ///
  /// All mutations execute in a single transaction.  [_persistedIds] is
  /// updated only after the transaction commits.
  Future<void> saveHost(
    String hostKind,
    String hostId,
    List<EditorStroke> strokes,
  ) async {
    final incoming = strokes;
    final incomingIds = incoming.map((s) => s.id).toSet();
    final persisted = _persistedIds[hostId] ?? {};

    final toDelete = persisted.difference(incomingIds);
    final toUpsert =
        incoming.where((s) => !persisted.contains(s.id)).toList();

    // Fast path: nothing to do.
    if (toDelete.isEmpty && toUpsert.isEmpty) return;

    final now = DateTime.now().millisecondsSinceEpoch;

    await _db.transaction((txn) async {
      // Upsert new/changed rows.
      for (var i = 0; i < incoming.length; i++) {
        final stroke = incoming[i];
        if (!persisted.contains(stroke.id)) {
          await txn.rawInsert(
            '''INSERT INTO ink (id, host_kind, host_id, stroke_json, ordinal, updated_at)
               VALUES (?, ?, ?, ?, ?, ?)
               ON CONFLICT(id) DO UPDATE SET
                 stroke_json = excluded.stroke_json,
                 ordinal     = excluded.ordinal,
                 updated_at  = excluded.updated_at''',
            [
              stroke.id,
              hostKind,
              hostId,
              jsonEncode(stroke.toJson()),
              i,
              now,
            ],
          );
        }
      }

      // Delete removed rows.
      for (final id in toDelete) {
        await txn.delete('ink', where: 'id = ?', whereArgs: [id]);
      }
    });

    // Update persisted id-set only after successful commit.
    _persistedIds[hostId] = Set<String>.from(incomingIds);
  }

  // ── Host-id helpers ───────────────────────────────────────────────────

  /// Build the canonical host_id for a document page.
  static String pageHostId(String documentId, int pageIndex) =>
      'doc:$documentId:page:$pageIndex';
}
