import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

import 'supabase_provider.dart';

class TypingIndicatorState {
  const TypingIndicatorState({
    required this.userIds,
    required this.channelStatus,
    required this.lastPayload,
  });

  final Set<String> userIds;
  final String channelStatus;
  final Map<String, dynamic> lastPayload;

  TypingIndicatorState copyWith({
    Set<String>? userIds,
    String? channelStatus,
    Map<String, dynamic>? lastPayload,
  }) {
    return TypingIndicatorState(
      userIds: userIds ?? this.userIds,
      channelStatus: channelStatus ?? this.channelStatus,
      lastPayload: lastPayload ?? this.lastPayload,
    );
  }
}

final typingIndicatorProvider = StateNotifierProvider.autoDispose
    .family<TypingIndicatorController, TypingIndicatorState, String>((
      ref,
      ticketId,
    ) {
      final client = ref.watch(supabaseClientProvider);
      final controller = TypingIndicatorController(client, ticketId);
      ref.onDispose(controller.dispose);
      return controller;
    });

class TypingIndicatorController extends StateNotifier<TypingIndicatorState> {
  TypingIndicatorController(this._client, this._ticketId)
    : super(
        const TypingIndicatorState(
          userIds: {},
          channelStatus: 'init',
          lastPayload: {},
        ),
      ) {
    _initChannel();
  }

  final SupabaseClient _client;
  final String _ticketId;
  RealtimeChannel? _channel;
  Timer? _typingTimer;
  final Map<String, Timer> _remoteTimeouts = {};
  // Marked when dispose() starts to prevent late async callbacks mutating state.
  bool _disposed = false;

  void _initChannel() {
    final channel = _client.channel('typing:$_ticketId');
    channel.onBroadcast(
      event: 'typing',
      callback: (payload) {
        // Prevent any work if we're already disposing. Log stack for diagnostics.
        if (_disposed || !mounted) {
          if (kDebugMode) {
            debugPrint(
              'TypingIndicatorController: onBroadcast skipped (disposed|unmounted)',
            );
            debugPrint(StackTrace.current.toString());
          }
          return;
        }

        final Map<String, dynamic> data = _extractPayload(payload);
        final userId = data['user_id'] as String?;
        final rawType = data['type']?.toString();
        final currentUserId = _client.auth.currentUser?.id;
        if (_disposed || !mounted) {
          if (kDebugMode) {
            debugPrint(
              'TypingIndicatorController: payload received but controller disposed/unmounted',
            );
            debugPrint(StackTrace.current.toString());
          }
          return;
        }
        state = state.copyWith(lastPayload: data);
        if (userId == null || userId == currentUserId) {
          return;
        }
        if (rawType == 'stop') {
          _clearRemoteTyping(userId);
          return;
        }
        _markRemoteTyping(userId);
      },
    );
    channel.subscribe((status, error) {
      if (_disposed || !mounted) {
        if (kDebugMode) {
          debugPrint(
            'TypingIndicatorController: subscribe callback skipped (disposed|unmounted)',
          );
          debugPrint(StackTrace.current.toString());
        }
        return;
      }
      state = state.copyWith(channelStatus: status.name);
    });
    _channel = channel;
  }

  Map<String, dynamic> _extractPayload(dynamic payload) {
    if (payload is Map<String, dynamic>) {
      final inner = payload['payload'];
      if (inner is Map<String, dynamic>) {
        return inner;
      }
      return payload;
    }
    final dynamic inner = payload.payload;
    if (inner is Map<String, dynamic>) {
      return inner;
    }
    return <String, dynamic>{};
  }

  void userTyping() {
    if (_disposed || !mounted) {
      if (kDebugMode) {
        debugPrint(
          'TypingIndicatorController.userTyping() ignored after dispose',
        );
      }
      return;
    }
    if (_client.auth.currentUser?.id == null) return;
    _sendTypingEvent('start');
    _typingTimer?.cancel();
    _typingTimer = Timer(const Duration(milliseconds: 150), () {
      if (_disposed || !mounted) {
        if (kDebugMode) {
          debugPrint(
            'TypingIndicatorController._typingTimer callback ignored after dispose',
          );
        }
        return;
      }
      _sendTypingEvent('stop');
    });
  }

  void stopTyping() {
    if (_disposed || !mounted) {
      if (kDebugMode) {
        debugPrint(
          'TypingIndicatorController.stopTyping() ignored after dispose',
        );
      }
      return;
    }
    _typingTimer?.cancel();
    _sendTypingEvent('stop');
  }

  void _markRemoteTyping(String userId) {
    if (_disposed || !mounted) {
      if (kDebugMode) {
        debugPrint(
          'TypingIndicatorController._markRemoteTyping ignored after dispose for user: $userId',
        );
        debugPrint(StackTrace.current.toString());
      }
      return;
    }
    final updated = {...state.userIds, userId};
    if (_disposed || !mounted) return;
    state = state.copyWith(userIds: updated);
    _remoteTimeouts[userId]?.cancel();
    _remoteTimeouts[userId] = Timer(const Duration(milliseconds: 400), () {
      if (_disposed || !mounted) {
        if (kDebugMode) {
          debugPrint(
            'TypingIndicatorController.remote timeout callback ignored after dispose for user: $userId',
          );
        }
        return;
      }
      _clearRemoteTyping(userId);
    });
  }

  void _clearRemoteTyping(String userId) {
    if (_disposed || !mounted) {
      if (kDebugMode) {
        debugPrint(
          'TypingIndicatorController._clearRemoteTyping ignored after dispose for user: $userId',
        );
      }
      return;
    }
    final updated = {...state.userIds}..remove(userId);
    if (_disposed || !mounted) return;
    state = state.copyWith(userIds: updated);
    _remoteTimeouts[userId]?.cancel();
    _remoteTimeouts.remove(userId);
  }

  void _sendTypingEvent(String type) {
    if (_disposed || !mounted) return;
    final userId = _client.auth.currentUser?.id;
    if (userId == null || _channel == null) return;
    _channel!.sendBroadcastMessage(
      event: 'typing',
      payload: {'user_id': userId, 'type': type},
    );
  }

  // Exposed for tests only: simulate a remote typing broadcast.
  @visibleForTesting
  void debugSimulateRemoteTyping(String userId, {bool stop = false}) {
    if (_disposed || !mounted) return;
    final data = {'user_id': userId, 'type': stop ? 'stop' : 'start'};
    state = state.copyWith(lastPayload: data);
    if (stop) {
      _clearRemoteTyping(userId);
    } else {
      _markRemoteTyping(userId);
    }
  }

  @override
  void dispose() {
    // Mark disposed first so any late async callbacks will no-op.
    _disposed = true;

    // Cancel local timers and remote timeouts; do NOT send network events during
    // dispose (prevents broadcasts from re-entering callbacks after disposal).
    _typingTimer?.cancel();
    for (final timer in _remoteTimeouts.values) {
      timer.cancel();
    }
    _remoteTimeouts.clear();

    // Unsubscribe from realtime channel.
    _channel?.unsubscribe();
    _channel = null;

    super.dispose();
  }
}
