import 'dart:async';

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

import '../api/endpoints/me.dart';
import '../api/endpoints/quarantine.dart';
import '../cache/audio_cache_manager.dart' show appDbProvider;
import '../cache/connectivity_provider.dart';
import '../cache/db.dart';
import '../cache/mutation_queue.dart';
import '../library/library_providers.dart' show dioProvider;
import '../models/quarantine_mine.dart';
import '../models/track.dart';

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

/// Drift-first ("hidden") tab controller. Reads from cached_quarantine_
/// mine via a drift watch() subscription set up in build(), so any drift
/// mutation (incoming sync, optimistic flag/unflag, SSE-triggered
/// refresh) re-emits the list automatically.
///
/// API surface unchanged from the prior FutureProvider-backed controller
/// — call sites still do `ref.read(myQuarantineProvider.notifier).flag()`
/// / `.unflag()` / `.isHidden()`. Internal storage moved from in-memory
/// AsyncNotifier state to drift so the Hidden tab paints from disk on
/// cold open and the quarantine list is queryable offline.
class MyQuarantineController extends AsyncNotifier<List<QuarantineMineRow>> {
  @override
  Future<List<QuarantineMineRow>> build() async {
    final db = ref.watch(appDbProvider);

    // Subscribe to drift first so any mutation (incoming sync, flag/
    // unflag, etc.) propagates without needing a manual invalidate.
    // Disposed when the provider rebuilds or the listener detaches.
    final sub = db.select(db.cachedQuarantineMine).watch().listen((rows) {
      state = AsyncData(rows.map(_rowToModel).toList());
    });
    ref.onDispose(sub.cancel);

    // SWR refresh on every build so the freshest server-side state
    // overtakes drift in the background. Don't await — UI gets the
    // cached snapshot first, freshness lands later via the watch().
    unawaited(_refreshFromServer());

    final initial = await db.select(db.cachedQuarantineMine).get();
    return initial.map(_rowToModel).toList();
  }

  /// Hits /api/quarantine/mine and replaces the local table with the
  /// authoritative server set. Best-effort: offline / 5xx / unresolved
  /// dependencies (e.g. uninitialised serverUrl in tests) all collapse
  /// to a no-op — drift stays on the prior cached state.
  Future<void> _refreshFromServer() async {
    try {
      final online = await ref
          .read(connectivityProvider.future)
          .timeout(const Duration(seconds: 3), onTimeout: () => true);
      if (!online) return;
      final dio = await ref.read(dioProvider.future);
      final fresh = await MeApi(dio).quarantineMine();
      final db = ref.read(appDbProvider);
      await db.transaction(() async {
        // Full replace — quarantine list is small and we want server
        // unflags from other devices to take effect. Doing this in a
        // transaction means the watch() sees exactly one emission with
        // the merged state, not a delete-then-insert flicker.
        await db.delete(db.cachedQuarantineMine).go();
        for (final r in fresh) {
          await db
              .into(db.cachedQuarantineMine)
              .insertOnConflictUpdate(_modelToCompanion(r));
        }
      });
    } catch (_) {
      // Swallow — cached state stays visible.
    }
  }

  /// True when this track is in the caller's quarantine list.
  bool isHidden(String trackId) =>
      (state.value ?? const []).any((r) => r.trackId == trackId);

  /// Optimistic flag: inserts the synthetic row into drift (watch fires
  /// immediately so the UI updates), calls server, rolls back on error.
  Future<void> flag(TrackRef track, String reason, String notes) async {
    final db = ref.read(appDbProvider);
    final existed = await (db.select(db.cachedQuarantineMine)
          ..where((t) => t.trackId.equals(track.id)))
        .getSingleOrNull();
    if (existed != null) return; // already hidden

    final companion = CachedQuarantineMineCompanion.insert(
      trackId: track.id,
      reason: reason,
      notes: notes.isEmpty
          ? const drift.Value.absent()
          : drift.Value(notes),
      createdAt: DateTime.now().toUtc().toIso8601String(),
      trackTitle: track.title,
      trackDurationMs: drift.Value(track.durationSec * 1000),
      albumId: track.albumId,
      albumTitle: track.albumTitle,
      artistId: track.artistId,
      artistName: track.artistName,
    );
    await db.into(db.cachedQuarantineMine).insertOnConflictUpdate(companion);

    try {
      final api = await ref.read(quarantineApiProvider.future);
      await api.flag(track.id, reason, notes: notes);
    } catch (_) {
      // REST failed — don't roll back; queue for replay so the user's
      // "hide this track" intent persists offline.
      await ref.read(mutationQueueProvider).enqueue(
        MutationKinds.quarantineFlag,
        {'trackId': track.id, 'reason': reason, 'notes': notes},
      );
    }
  }

  /// Optimistic unflag: removes the drift row, calls server, queues
  /// the call on failure so the unhide intent persists offline.
  Future<void> unflag(String trackId) async {
    final db = ref.read(appDbProvider);
    final existing = await (db.select(db.cachedQuarantineMine)
          ..where((t) => t.trackId.equals(trackId)))
        .getSingleOrNull();
    if (existing == null) return;

    await (db.delete(db.cachedQuarantineMine)
          ..where((t) => t.trackId.equals(trackId)))
        .go();

    try {
      final api = await ref.read(quarantineApiProvider.future);
      await api.unflag(trackId);
    } catch (_) {
      // Don't restore the drift row; queue the unflag for replay.
      await ref
          .read(mutationQueueProvider)
          .enqueue(MutationKinds.quarantineUnflag, {'trackId': trackId});
    }
  }

  static QuarantineMineRow _rowToModel(CachedQuarantineMineData r) =>
      QuarantineMineRow(
        trackId: r.trackId,
        reason: r.reason,
        notes: r.notes,
        createdAt: r.createdAt,
        trackTitle: r.trackTitle,
        trackDurationMs: r.trackDurationMs,
        albumId: r.albumId,
        albumTitle: r.albumTitle,
        albumCoverArtPath: r.albumCoverArtPath,
        artistId: r.artistId,
        artistName: r.artistName,
      );

  static CachedQuarantineMineCompanion _modelToCompanion(QuarantineMineRow r) =>
      CachedQuarantineMineCompanion.insert(
        trackId: r.trackId,
        reason: r.reason,
        notes: r.notes == null ? const drift.Value.absent() : drift.Value(r.notes),
        createdAt: r.createdAt,
        trackTitle: r.trackTitle,
        trackDurationMs: drift.Value(r.trackDurationMs),
        albumId: r.albumId,
        albumTitle: r.albumTitle,
        albumCoverArtPath: r.albumCoverArtPath == null
            ? const drift.Value.absent()
            : drift.Value(r.albumCoverArtPath),
        artistId: r.artistId,
        artistName: r.artistName,
      );
}

final myQuarantineProvider =
    AsyncNotifierProvider<MyQuarantineController, List<QuarantineMineRow>>(
  MyQuarantineController.new,
);
