import 'dart:async';
import 'dart:convert';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import '../api/api_client.dart';
import '../api/api_client_provider.dart';
import '../auth/auth_bridge.dart';
import '../constants.dart';
import 'active_session_notifier.dart';

part 'chat_notifier.g.dart';

// States
sealed class ChatData {
  const ChatData();
}

class ChatInitialData extends ChatData {
  const ChatInitialData();
}

class ChatConnectingData extends ChatData {
  const ChatConnectingData();
}

class ChatConnectedData extends ChatData {
  final List<ChatMessage> messages;
  final bool isOtherTyping;
  final int? remainingSeconds;
  final bool sessionExpired;
  final bool sessionPaused;
  final bool sessionClosing;
  final bool goodbyeSubmitted;
  final Map<String, dynamic>? extensionResponse;

  const ChatConnectedData({
    required this.messages,
    this.isOtherTyping = false,
    this.remainingSeconds,
    this.sessionExpired = false,
    this.sessionPaused = false,
    this.sessionClosing = false,
    this.goodbyeSubmitted = false,
    this.extensionResponse,
  });

  ChatConnectedData copyWith({
    List<ChatMessage>? messages,
    bool? isOtherTyping,
    int? remainingSeconds,
    bool? sessionExpired,
    bool? sessionPaused,
    bool? sessionClosing,
    bool? goodbyeSubmitted,
    Map<String, dynamic>? extensionResponse,
  }) {
    return ChatConnectedData(
      messages: messages ?? this.messages,
      isOtherTyping: isOtherTyping ?? this.isOtherTyping,
      remainingSeconds: remainingSeconds ?? this.remainingSeconds,
      sessionExpired: sessionExpired ?? this.sessionExpired,
      sessionPaused: sessionPaused ?? this.sessionPaused,
      sessionClosing: sessionClosing ?? this.sessionClosing,
      goodbyeSubmitted: goodbyeSubmitted ?? this.goodbyeSubmitted,
      extensionResponse: extensionResponse ?? this.extensionResponse,
    );
  }
}

class ChatErrorData extends ChatData {
  final String message;
  const ChatErrorData(this.message);
}

// Message model
class ChatMessage {
  final String id;
  final String senderType;
  final String content;
  final String type;
  final String status;
  final DateTime createdAt;

  const ChatMessage({
    required this.id,
    required this.senderType,
    required this.content,
    this.type = MessageType.text,
    this.status = MessageStatus.sent,
    required this.createdAt,
  });

  ChatMessage copyWith({String? status}) {
    return ChatMessage(
      id: id,
      senderType: senderType,
      content: content,
      type: type,
      status: status ?? this.status,
      createdAt: createdAt,
    );
  }
}

@Riverpod(keepAlive: true)
class Chat extends _$Chat {
  WebSocketChannel? _channel;
  StreamSubscription? _wsSubscription;
  Timer? _typingTimer;
  String? _connectedSessionId;

  ApiClient get _apiClient => ref.read(apiClientProvider);

  @override
  ChatData build() => const ChatInitialData();

  /// Idempotent connect: if we're already connected to [sessionId], refresh
  /// the session status from the server (in case it transitioned to closing /
  /// expired while we weren't watching closely) but reuse the live WS.
  /// If we're connected to a different session, disconnect first.
  Future<void> connectIfNotConnected(String sessionId) async {
    // ignore: avoid_print
    print('[CONNECT_IF_NOT] target=$sessionId existing=$_connectedSessionId state=${state.runtimeType}');
    if (_connectedSessionId == sessionId &&
        (state is ChatConnectedData || state is ChatConnectingData)) {
      await refreshSessionStatus(sessionId);
      return;
    }
    if (_connectedSessionId != null && _connectedSessionId != sessionId) {
      _cleanup();
    }
    await connect(sessionId);
  }

  /// Re-pull session status from the server and reconcile local flags. Used
  /// when re-entering the chat screen for a session we're already connected
  /// to — covers the case where a `sessionClosing` / `sessionExpired` WS event
  /// was missed (e.g. socket dropped, app backgrounded, status changed before
  /// we connected).
  Future<void> refreshSessionStatus(String sessionId) async {
    final current = state;
    // ignore: avoid_print
    print('[REFRESH_STATUS] called sessionId=$sessionId currentState=${current.runtimeType}');
    if (current is! ChatConnectedData) return;
    try {
      final info = await _apiClient.get('/api/shared/chat/$sessionId/info');
      final data = info['data'] as Map<String, dynamic>?;
      final status = data?['status'] as String?;
      // ignore: avoid_print
      print('[REFRESH_STATUS] backend status=$status');
      if (status == SessionStatus.completed ||
          status == SessionStatus.cancelled ||
          status == SessionStatus.expired) {
        state = current.copyWith(sessionExpired: true);
        return;
      }
      final goodbyeSubmittedByMe = data?['goodbye_submitted_by_me'] as bool? ?? false;
      state = current.copyWith(
        sessionClosing: status == SessionStatus.closing,
        sessionPaused: status == SessionStatus.extending,
        sessionExpired: false,
        goodbyeSubmitted: goodbyeSubmittedByMe,
      );
    } catch (e) {
      // ignore: avoid_print
      print('[REFRESH_STATUS] error=$e');
    }
  }

  Future<void> connect(String sessionId) async {
    _connectedSessionId = sessionId;
    state = const ChatConnectingData();

    try {
      final sessionInfo = await _apiClient.get('/api/shared/chat/$sessionId/info');
      final sessionData = sessionInfo['data'] as Map<String, dynamic>?;
      final sessionStatus = sessionData?['status'] as String?;
      if (sessionStatus == SessionStatus.completed ||
          sessionStatus == SessionStatus.cancelled ||
          sessionStatus == SessionStatus.expired) {
        state = const ChatErrorData('Sesi sudah berakhir.');
        return;
      }

      final isClosing = sessionStatus == SessionStatus.closing;
      final goodbyeSubmittedByMe = sessionData?['goodbye_submitted_by_me'] as bool? ?? false;

      final response = await _apiClient.get('/api/shared/chat/$sessionId/messages');
      final messagesData = response['data'] as List<dynamic>;
      final messages = messagesData.map((m) => ChatMessage(
        id: m['id'] as String,
        senderType: m['sender_type'] as String,
        content: m['content'] as String,
        type: m['type'] as String? ?? MessageType.text,
        status: m['status'] as String? ?? MessageStatus.sent,
        createdAt: DateTime.parse(m['created_at'] as String),
      )).toList();

      final token = ref.read(authBridgeProvider).accessToken;
      if (token == null) {
        state = const ChatErrorData('Sesi berakhir. Silakan login ulang.');
        return;
      }
      final wsUrl = ApiClient.baseUrl
          .replaceFirst('https://', 'wss://')
          .replaceFirst('http://', 'ws://');
      _channel = WebSocketChannel.connect(Uri.parse('$wsUrl/api/shared/ws'));

      _wsSubscription = _channel!.stream.listen(
        (raw) {
          final data = jsonDecode(raw as String) as Map<String, dynamic>;
          _onMessageReceived(data);
        },
        onError: (_) {},
        onDone: () {},
      );

      _channel!.sink.add(jsonEncode({
        'type': WsMessage.auth,
        'token': token,
        'session_id': sessionId,
      }));

      state = ChatConnectedData(
        messages: messages,
        sessionClosing: isClosing,
        goodbyeSubmitted: goodbyeSubmittedByMe,
      );
    } catch (e) {
      state = const ChatErrorData('Gagal terhubung ke chat.');
    }
  }

  void disconnect() {
    _cleanup();
    state = const ChatInitialData();
  }

  String? get connectedSessionId => _connectedSessionId;

  void sendMessage(String content) {
    if (state is! ChatConnectedData || _channel == null) return;
    final current = state as ChatConnectedData;

    final tempId = 'temp_${DateTime.now().millisecondsSinceEpoch}';
    final msg = ChatMessage(
      id: tempId,
      senderType: UserType.customer,
      content: content,
      status: 'sending',
      createdAt: DateTime.now(),
    );

    state = current.copyWith(messages: [...current.messages, msg]);

    _channel!.sink.add(jsonEncode({
      'type': WsMessage.message,
      'content': content,
      '_temp_id': tempId,
    }));
  }

  void sendTyping() {
    if (_channel == null) return;
    _channel!.sink.add(jsonEncode({'type': WsMessage.typing}));
  }

  void markDelivered(List<String> messageIds) {
    if (_channel == null) return;
    _channel!.sink.add(jsonEncode({
      'type': WsMessage.delivered,
      'message_ids': messageIds,
    }));
  }

  void markRead(List<String> messageIds) {
    if (_channel == null) return;
    _channel!.sink.add(jsonEncode({
      'type': WsMessage.read,
      'message_ids': messageIds,
    }));
  }

  void _onMessageReceived(Map<String, dynamic> data) {
    if (state is! ChatConnectedData) return;
    final current = state as ChatConnectedData;
    final type = data['type'] as String?;

    switch (type) {
      case WsMessage.authOk:
        break;

      case WsMessage.message:
        final msg = ChatMessage(
          id: data['message_id'] as String,
          senderType: data['sender_type'] as String,
          content: data['content'] as String,
          type: data['message_type'] as String? ?? MessageType.text,
          status: MessageStatus.sent,
          createdAt: DateTime.parse(data['created_at'] as String),
        );
        state = current.copyWith(messages: [...current.messages, msg]);
        markDelivered([msg.id]);
        break;

      case WsMessage.messageAck:
        final messageId = data['message_id'] as String;
        final status = data['status'] as String;
        final updatedMessages = current.messages.map((m) {
          if (m.status == 'sending') {
            return m.copyWith(status: status);
          }
          return m;
        }).toList();
        final idx = updatedMessages.indexWhere((m) => m.status == status && m.senderType == UserType.customer);
        if (idx >= 0) {
          final old = updatedMessages[idx];
          updatedMessages[idx] = ChatMessage(
            id: messageId,
            senderType: old.senderType,
            content: old.content,
            type: old.type,
            status: status,
            createdAt: old.createdAt,
          );
        }
        state = current.copyWith(messages: updatedMessages);
        break;

      case WsMessage.messageStatus:
        final messageIds = (data['message_ids'] as List<dynamic>).cast<String>();
        final status = data['status'] as String;
        final updatedMessages = current.messages.map((m) {
          if (messageIds.contains(m.id)) {
            return m.copyWith(status: status);
          }
          return m;
        }).toList();
        state = current.copyWith(messages: updatedMessages);
        break;

      case WsMessage.typing:
        state = current.copyWith(isOtherTyping: true);
        _typingTimer?.cancel();
        _typingTimer = Timer(const Duration(seconds: 3), () {
          if (state is ChatConnectedData) {
            state = (state as ChatConnectedData).copyWith(isOtherTyping: false);
          }
        });
        break;

      case WsMessage.sessionTimer:
        final remaining = data['remaining_seconds'] as int?;
        state = current.copyWith(remainingSeconds: remaining);
        break;

      case WsMessage.sessionExpired:
        state = current.copyWith(sessionExpired: true);
        break;

      case WsMessage.sessionPaused:
        state = current.copyWith(sessionPaused: true);
        break;

      case WsMessage.sessionResumed:
        state = current.copyWith(sessionPaused: false, sessionExpired: false, sessionClosing: false);
        break;

      case WsMessage.sessionClosing:
        state = current.copyWith(sessionClosing: true);
        break;

      case WsMessage.extensionResponse:
        final accepted = data['accepted'] as bool? ?? false;
        state = current.copyWith(
          extensionResponse: data,
          sessionPaused: accepted ? false : current.sessionPaused,
          sessionExpired: accepted ? false : current.sessionExpired,
        );
        break;

      case WsMessage.sessionCompleted:
        _cleanup();
        // Home CTA must clear immediately — backend just told us the session
        // is gone, so refresh the shared snapshot.
        ref.invalidate(activeSessionProvider);
        break;

      case WsMessage.sessionTopicUpdated:
        // Customer is never given a visual cue about topic sensitivity;
        // message is received for future-proofing but intentionally a no-op.
        break;

      case WsMessage.error:
        break;
    }
  }

  void _cleanup() {
    _wsSubscription?.cancel();
    _wsSubscription = null;
    _channel?.sink.close();
    _channel = null;
    _typingTimer?.cancel();
    _typingTimer = null;
    _connectedSessionId = null;
  }
}
