// Resume-on-launch (#54). The #52 teardown tears the audio_service
// session down (and clears mediaItem) when idle/dismissed, so the
// headset / lock-screen play button otherwise has nothing to resume.
// This controller persists the live queue + index + position + #415
// source to a single-row drift snapshot on track change / pause / app
// teardown, and on construction restores the last snapshot into the
// handler PAUSED (the user sees their last track and continues with
// play / a media button — we never auto-blast on launch).
//
// Persist guard: when the handler clears (teardown → mediaItem null /
// empty queue) we deliberately do NOT write, so the last good snapshot
// survives for the next launch — that survival is the whole point.

import 'dart:async';
import 'dart:convert';

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

import '../auth/auth_provider.dart';
import '../models/track.dart';
import '../player/player_provider.dart';
import 'audio_cache_manager.dart' show appDbProvider;
import 'db.dart';

class ResumeController with WidgetsBindingObserver {
  ResumeController(this._ref);
  final Ref _ref;

  final _subs = <StreamSubscription<dynamic>>[];
  Timer? _debounce;
  bool _disposed = false;

  Future<void> start() async {
    try {
      // audioHandlerProvider throws until main() overrides it (the real
      // app always does). In tests / no-audio environments there's
      // nothing to resume — stay inert.
      _ref.read(audioHandlerProvider);
    } catch (_) {
      return;
    }
    if (_disposed) return;
    await _restore();
    if (_disposed) return;
    final h = _ref.read(audioHandlerProvider);
    // Media-button-when-torn-down hook (#448): play() invokes this when
    // mediaItem is null so a headset/watch press resumes the snapshot.
    h.setResumeHook(resumeFromMediaButton);
    _subs.add(h.mediaItem.listen((_) => _schedulePersist()));
    _subs.add(h.playbackState.listen((_) => _schedulePersist()));
    WidgetsBinding.instance.addObserver(this);
  }

  void _schedulePersist() {
    if (_disposed) return;
    _debounce?.cancel();
    _debounce = Timer(const Duration(seconds: 3), () {
      unawaited(_persist());
    });
  }

  Future<void> _persist() async {
    if (_disposed) return;
    try {
      final h = _ref.read(audioHandlerProvider);
      final tracks = h.queuedTracks;
      final mi = h.mediaItem.value;
      // Cleared by teardown — keep the last good snapshot for next
      // launch rather than wiping it with an empty queue.
      if (tracks.isEmpty || mi == null) return;
      var idx = tracks.indexWhere((t) => t.id == mi.id);
      if (idx < 0) idx = 0;
      final blob = jsonEncode({
        'v': 1,
        'source': h.queueSource,
        'index': idx,
        'position_ms': h.position.inMilliseconds,
        'tracks': tracks.map((t) => t.toJson()).toList(),
      });
      final db = _ref.read(appDbProvider);
      await db.into(db.cachedResumeState).insertOnConflictUpdate(
            CachedResumeStateCompanion.insert(
              json: blob,
              updatedAt: drift.Value(DateTime.now()),
            ),
          );
    } catch (e, st) {
      debugPrint('resume_controller: persist failed: $e\n$st');
    }
  }

  /// Launch path: restore the last session PAUSED (no auto-blast).
  Future<void> _restore() async {
    try {
      await _loadAndRestore();
    } catch (e, st) {
      debugPrint('resume_controller: restore failed: $e\n$st');
    }
  }

  /// Media-button path (#448): the user pressed play on the headset /
  /// watch / lock screen while the session was fully torn down (#52) and
  /// nothing is loaded. Restore the snapshot, then START playback (they
  /// asked to play). Registered as the handler's resume hook in start().
  /// No-op if there's nothing to resume.
  Future<void> resumeFromMediaButton() async {
    try {
      if (await _loadAndRestore()) {
        await _ref.read(audioHandlerProvider).play();
      }
    } catch (e, st) {
      debugPrint('resume_controller: media-button resume failed: $e\n$st');
    }
  }

  /// Shared loader. Restores the last persisted session PAUSED and
  /// returns whether it actually restored a queue. Guards: an already-
  /// active session (don't stomp), missing auth, no/empty snapshot.
  Future<bool> _loadAndRestore() async {
    final h = _ref.read(audioHandlerProvider);
    if (h.mediaItem.value != null) return false;
    final url = await _ref.read(serverUrlProvider.future);
    final token =
        await _ref.read(secureStorageProvider).read(key: 'session_token');
    if (url == null || url.isEmpty || token == null || token.isEmpty) {
      return false;
    }
    final db = _ref.read(appDbProvider);
    final row = await db.select(db.cachedResumeState).getSingleOrNull();
    if (row == null) return false;
    final m = jsonDecode(row.json) as Map<String, dynamic>;
    final rawTracks = (m['tracks'] as List?) ?? const [];
    final tracks = rawTracks
        .map((e) => TrackRef.fromJson(e as Map<String, dynamic>))
        .toList();
    if (tracks.isEmpty) return false;
    final idx = (m['index'] as num?)?.toInt() ?? 0;
    final posMs = (m['position_ms'] as num?)?.toInt() ?? 0;
    final source = m['source'] as String?;
    await _ref.read(playerActionsProvider).restoreQueue(
          tracks,
          initialIndex: idx,
          position: Duration(milliseconds: posMs),
          source: source,
        );
    return true;
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    // App backgrounded / killed: persist durably right now (the debounce
    // may not fire before teardown).
    if (state == AppLifecycleState.paused ||
        state == AppLifecycleState.detached) {
      _debounce?.cancel();
      unawaited(_persist());
    }
  }

  void dispose() {
    _disposed = true;
    _debounce?.cancel();
    WidgetsBinding.instance.removeObserver(this);
    for (final s in _subs) {
      s.cancel();
    }
    _subs.clear();
  }
}

/// Read once at app start (app.dart postFrame). On construction it
/// restores the last persisted session (paused) then persists
/// queue/index/position on track change, pause, and app teardown.
/// Disposed via ref.onDispose when the scope tears down.
final resumeControllerProvider = Provider<ResumeController>((ref) {
  final c = ResumeController(ref);
  ref.onDispose(c.dispose);
  // ignore: unawaited_futures
  c.start();
  return c;
});
