// Version: 1.2.0 | Created: 2026-04-01 | Updated: 2026-04-02
// Drift database — rooms and messages tables.
// Uses driftDatabase() from drift_flutter for cross-platform support:
//   - Mobile/desktop: SQLite file via sqlite3_flutter_libs
//   - Web: OPFS (Origin Private File System) via IndexedDB fallback

import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'database.g.dart';

// ---------------------------------------------------------------------------
// Table definitions
// ---------------------------------------------------------------------------

/// Persisted room state. Upserted on every sync.
class RoomsTable extends Table {
  @override
  String get tableName => 'rooms';

  TextColumn get id => text()();
  TextColumn get name => text()();
  TextColumn get avatarUrl => text().nullable()();
  TextColumn get lastMessage => text().nullable()();
  IntColumn get lastActivityAt =>
      integer().nullable()(); // milliseconds since epoch
  IntColumn get unreadCount => integer().withDefault(const Constant(0))();
  BoolColumn get isDm => boolean().withDefault(const Constant(false))();

  @override
  Set<Column<Object>> get primaryKey => {id};
}

/// Persisted message events. Upserted when received from sync.
@TableIndex(name: 'idx_messages_room_id', columns: {#roomId})
class MessagesTable extends Table {
  @override
  String get tableName => 'messages';

  TextColumn get id => text()(); // eventId
  TextColumn get roomId => text()();
  TextColumn get senderId => text()();
  TextColumn get body => text().nullable()();
  TextColumn get type => text()(); // MessageType name
  IntColumn get timestamp => integer()(); // milliseconds since epoch

  @override
  Set<Column<Object>> get primaryKey => {id};
}

// ---------------------------------------------------------------------------
// Database class
// ---------------------------------------------------------------------------

@DriftDatabase(tables: [RoomsTable, MessagesTable])
class AppDatabase extends _$AppDatabase {
  AppDatabase() : super(driftDatabase(name: 'm8chat'));

  // Separate constructor for testing — accepts a custom executor.
  AppDatabase.forTesting(super.executor);

  @override
  int get schemaVersion => 2;

  @override
  MigrationStrategy get migration => MigrationStrategy(
    onUpgrade: (migrator, from, to) async {
      if (from < 2) {
        // v2: drop rawJson column, add index on roomId.
        // Drift cannot drop columns — recreate the table.
        await customStatement(
          'CREATE TABLE IF NOT EXISTS messages_new ('
          '  id TEXT NOT NULL PRIMARY KEY, '
          '  room_id TEXT NOT NULL, '
          '  sender_id TEXT NOT NULL, '
          '  body TEXT, '
          '  type TEXT NOT NULL, '
          '  timestamp INTEGER NOT NULL'
          ')',
        );
        await customStatement(
          'INSERT OR IGNORE INTO messages_new '
          '(id, room_id, sender_id, body, type, timestamp) '
          'SELECT id, room_id, sender_id, body, type, timestamp FROM messages',
        );
        await customStatement('DROP TABLE IF EXISTS messages');
        await customStatement('ALTER TABLE messages_new RENAME TO messages');
        await customStatement(
          'CREATE INDEX IF NOT EXISTS idx_messages_room_id '
          'ON messages (room_id)',
        );
      }
    },
    onCreate: (migrator) async {
      await migrator.createAll();
    },
  );
}

// ---------------------------------------------------------------------------
// DAOs
// ---------------------------------------------------------------------------

/// Data access for rooms.
extension RoomsDao on AppDatabase {
  /// Insert or replace a room row.
  Future<void> upsertRoom(RoomsTableCompanion room) =>
      into(roomsTable).insertOnConflictUpdate(room);

  /// Watch all rooms ordered by unread count desc then last activity desc.
  Stream<List<RoomsTableData>> watchAllRooms() {
    return (select(roomsTable)..orderBy([
          (t) =>
              OrderingTerm(expression: t.unreadCount, mode: OrderingMode.desc),
          (t) => OrderingTerm(
            expression: t.lastActivityAt,
            mode: OrderingMode.desc,
            nulls: NullsOrder.last,
          ),
        ]))
        .watch();
  }

  /// Get a single room by id, or null if not found.
  Future<RoomsTableData?> getRoomById(String id) =>
      (select(roomsTable)..where((t) => t.id.equals(id))).getSingleOrNull();
}

/// Data access for messages.
extension MessagesDao on AppDatabase {
  /// Insert or replace a message row.
  Future<void> upsertMessage(MessagesTableCompanion message) =>
      into(messagesTable).insertOnConflictUpdate(message);

  /// Insert or replace multiple message rows in a single batch transaction.
  Future<void> upsertMessages(List<MessagesTableCompanion> messages) async {
    await batch((b) {
      for (final msg in messages) {
        b.insert(messagesTable, msg, onConflict: DoUpdate((_) => msg));
      }
    });
  }

  /// Watch all messages for [roomId] ordered oldest-first.
  Stream<List<MessagesTableData>> watchByRoom(String roomId) {
    return (select(messagesTable)
          ..where((t) => t.roomId.equals(roomId))
          ..orderBy([
            (t) =>
                OrderingTerm(expression: t.timestamp, mode: OrderingMode.asc),
          ]))
        .watch();
  }

  /// Load one page of messages for [roomId], most recent first.
  Future<List<MessagesTableData>> getPage(
    String roomId, {
    int limit = 50,
    int offset = 0,
  }) {
    return (select(messagesTable)
          ..where((t) => t.roomId.equals(roomId))
          ..orderBy([
            (t) =>
                OrderingTerm(expression: t.timestamp, mode: OrderingMode.desc),
          ])
          ..limit(limit, offset: offset))
        .get();
  }
}

// ---------------------------------------------------------------------------
// Riverpod provider
// ---------------------------------------------------------------------------

/// Provides the singleton [AppDatabase]. keepAlive: true — opened once for
/// the app lifetime.
@Riverpod(keepAlive: true)
AppDatabase appDatabase(Ref ref) {
  final db = AppDatabase();
  // Close the database when the provider is finally disposed (app shutdown).
  ref.onDispose(db.close);
  return db;
}
