import 'package:drift/drift.dart' as drift;
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../api/endpoints/likes.dart';
import '../auth/auth_provider.dart';
import '../cache/audio_cache_manager.dart' show appDbProvider;
import '../cache/cache_first.dart';
import '../cache/connectivity_provider.dart';
import '../cache/db.dart';
import '../cache/mutation_queue.dart';
import '../library/library_providers.dart';

final likesApiProvider = FutureProvider<LikesApi>((ref) async {
  return LikesApi(await ref.watch(dioProvider.future));
});

class LikedIds {
  const LikedIds({
    required this.artists,
    required this.albums,
    required this.tracks,
  });
  final Set<String> artists;
  final Set<String> albums;
  final Set<String> tracks;

  bool has(LikeKind kind, String id) => switch (kind) {
        LikeKind.artist => artists.contains(id),
        LikeKind.album => albums.contains(id),
        LikeKind.track => tracks.contains(id),
      };

  static const empty = LikedIds(artists: {}, albums: {}, tracks: {});
}

/// Drift-first per #357 plan C. Reads from cached_likes (populated by
/// SyncController). Reactive — sync writes propagate via watch().
/// Empty + online triggers REST cold-cache fallback that re-populates.
final likedIdsProvider = StreamProvider<LikedIds>((ref) {
  final db = ref.watch(appDbProvider);
  return cacheFirst<CachedLike, LikedIds>(
    driftStream: db.select(db.cachedLikes).watch(),
    fetchAndPopulate: () async {
      final api = await ref.read(likesApiProvider.future);
      final fresh = await api.ids();
      // Need a userId for the composite primary key. The auth controller
      // exposes the current user.
      final user = ref.read(authControllerProvider).value;
      if (user == null) return; // not logged in; skip
      await db.batch((b) {
        for (final id in fresh.tracks) {
          b.insert(
            db.cachedLikes,
            CachedLikesCompanion.insert(
              userId: user.id,
              entityType: 'track',
              entityId: id,
            ),
            mode: drift.InsertMode.insertOrIgnore,
          );
        }
        for (final id in fresh.albums) {
          b.insert(
            db.cachedLikes,
            CachedLikesCompanion.insert(
              userId: user.id,
              entityType: 'album',
              entityId: id,
            ),
            mode: drift.InsertMode.insertOrIgnore,
          );
        }
        for (final id in fresh.artists) {
          b.insert(
            db.cachedLikes,
            CachedLikesCompanion.insert(
              userId: user.id,
              entityType: 'artist',
              entityId: id,
            ),
            mode: drift.InsertMode.insertOrIgnore,
          );
        }
      });
    },
    toResult: (rows) => LikedIds(
      tracks: rows
          .where((r) => r.entityType == 'track')
          .map((r) => r.entityId)
          .toSet(),
      albums: rows
          .where((r) => r.entityType == 'album')
          .map((r) => r.entityId)
          .toSet(),
      artists: rows
          .where((r) => r.entityType == 'artist')
          .map((r) => r.entityId)
          .toSet(),
    ),
    isOnline: () async => (await ref.read(connectivityProvider.future)),
  );
});

/// Mutation controller. Writes optimistically to drift first (so the
/// likedIdsProvider stream re-emits immediately for snappy UI), then to
/// REST. Rolls back drift on REST failure.
class LikesController {
  LikesController(this._ref);
  final Ref _ref;

  Future<void> toggle(LikeKind kind, String id) async {
    final user = _ref.read(authControllerProvider).value;
    if (user == null) return;
    final db = _ref.read(appDbProvider);
    final entityType = _entityType(kind);

    final existing = await (db.select(db.cachedLikes)
          ..where((t) =>
              t.userId.equals(user.id) &
              t.entityType.equals(entityType) &
              t.entityId.equals(id)))
        .getSingleOrNull();
    final wasLiked = existing != null;

    // Optimistic mutation — drift watch() re-emits immediately.
    if (wasLiked) {
      await (db.delete(db.cachedLikes)
            ..where((t) =>
                t.userId.equals(user.id) &
                t.entityType.equals(entityType) &
                t.entityId.equals(id)))
          .go();
    } else {
      await db.into(db.cachedLikes).insert(
            CachedLikesCompanion.insert(
              userId: user.id,
              entityType: entityType,
              entityId: id,
            ),
            mode: drift.InsertMode.insertOrIgnore,
          );
    }

    try {
      final api = await _ref.read(likesApiProvider.future);
      if (wasLiked) {
        await api.unlike(kind, id);
      } else {
        await api.like(kind, id);
      }
    } catch (_) {
      // REST failed (network or HTTP). Don't roll back drift — the
      // user's intent is to like/unlike, and we want that visible to
      // them even when offline. Queue the call for replay; the
      // MutationReplayer will retry on next connectivity transition.
      // If retries exhaust (5 attempts), the row drops and the next
      // SyncController.sync brings drift back in line with the
      // server's authoritative state.
      await _ref.read(mutationQueueProvider).enqueue(
        wasLiked ? MutationKinds.likeRemove : MutationKinds.likeAdd,
        {'kind': entityType, 'id': id},
      );
    }
  }

  String _entityType(LikeKind kind) => switch (kind) {
        LikeKind.artist => 'artist',
        LikeKind.album => 'album',
        LikeKind.track => 'track',
      };
}

final likesControllerProvider =
    Provider<LikesController>((ref) => LikesController(ref));
