import 'dart:async';
import 'dart:convert';
import 'package:dio/dio.dart';
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';

part 'mitra_chat_notifier.g.dart';

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

class MitraChatInitialData extends MitraChatData {
  const MitraChatInitialData();
}

class MitraChatConnectingData extends MitraChatData {
  const MitraChatConnectingData();
}

class MitraChatConnectedData extends MitraChatData {
  final List<MitraChatMessage> messages;
  final bool isOtherTyping;
  final int? remainingSeconds;
  final bool sessionExpired;
  final bool sessionClosing;
  final bool goodbyeSubmitted;
  final Map<String, dynamic>? extensionRequest;
  final TopicSensitivity topicSensitivity;

  const MitraChatConnectedData({
    required this.messages,
    this.isOtherTyping = false,
    this.remainingSeconds,
    this.sessionExpired = false,
    this.sessionClosing = false,
    this.goodbyeSubmitted = false,
    this.extensionRequest,
    this.topicSensitivity = TopicSensitivity.regular,
  });

  MitraChatConnectedData copyWith({
    List<MitraChatMessage>? messages,
    bool? isOtherTyping,
    int? remainingSeconds,
    bool? sessionExpired,
    bool? sessionClosing,
    bool? goodbyeSubmitted,
    Map<String, dynamic>? extensionRequest,
    bool clearExtensionRequest = false,
    TopicSensitivity? topicSensitivity,
  }) {
    return MitraChatConnectedData(
      messages: messages ?? this.messages,
      isOtherTyping: isOtherTyping ?? this.isOtherTyping,
      remainingSeconds: remainingSeconds ?? this.remainingSeconds,
      sessionExpired: sessionExpired ?? this.sessionExpired,
      sessionClosing: sessionClosing ?? this.sessionClosing,
      goodbyeSubmitted: goodbyeSubmitted ?? this.goodbyeSubmitted,
      extensionRequest: clearExtensionRequest ? null : (extensionRequest ?? this.extensionRequest),
      topicSensitivity: topicSensitivity ?? this.topicSensitivity,
    );
  }
}

class MitraChatErrorData extends MitraChatData {
  final String message;
  const MitraChatErrorData(this.message);
}

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

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

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

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

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

  @override
  MitraChatData build() => const MitraChatInitialData();

  Future<void> connect(String sessionId) async {
    state = const MitraChatConnectingData();
    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 MitraChatErrorData('Sesi sudah berakhir.');
        return;
      }

      final isClosing = sessionStatus == SessionStatus.closing;
      final goodbyeSubmittedByMe = sessionData?['goodbye_submitted_by_me'] as bool? ?? false;
      final sessionTopic = TopicSensitivity.fromString(sessionData?['topic_sensitivity'] as String?);

      final response = await _apiClient.get('/api/shared/chat/$sessionId/messages');
      final messagesData = response['data'] as List<dynamic>;
      final messages = messagesData.map((m) => MitraChatMessage(
        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 MitraChatErrorData('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 = MitraChatConnectedData(
        messages: messages,
        sessionClosing: isClosing,
        goodbyeSubmitted: goodbyeSubmittedByMe,
        topicSensitivity: sessionTopic,
      );
    } catch (e) {
      state = const MitraChatErrorData('Gagal terhubung ke chat.');
    }
  }

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

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

    final tempId = 'temp_${DateTime.now().millisecondsSinceEpoch}';
    final msg = MitraChatMessage(
      id: tempId,
      senderType: UserType.mitra,
      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! MitraChatConnectedData) return;
    final current = state as MitraChatConnectedData;
    final type = data['type'] as String?;

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

      case WsMessage.message:
        final msg = MitraChatMessage(
          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.mitra);
        if (idx >= 0) {
          final old = updatedMessages[idx];
          updatedMessages[idx] = MitraChatMessage(
            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 MitraChatConnectedData) {
            state = (state as MitraChatConnectedData).copyWith(isOtherTyping: false);
          }
        });
        break;

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

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

      case WsMessage.extensionRequest:
        state = current.copyWith(extensionRequest: data);
        break;

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

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

      case WsMessage.sessionCompleted:
        _cleanup();
        break;

      case WsMessage.sessionTopicUpdated:
        final newValue = TopicSensitivity.fromString(data['topic_sensitivity'] as String?);
        state = current.copyWith(topicSensitivity: newValue);
        break;
    }
  }

  /// Flip the session's topic sensitivity. Returns error code on failure, null on success.
  Future<String?> flipTopic(String sessionId, TopicSensitivity toValue) async {
    try {
      final response = await _apiClient.patch(
        '/api/shared/chat/sessions/$sessionId/topic',
        data: {'topic_sensitivity': toValue.value},
      );
      final updated = TopicSensitivity.fromString(response['data']?['topic_sensitivity'] as String?);
      final current = state;
      if (current is MitraChatConnectedData) {
        state = current.copyWith(topicSensitivity: updated);
      }
      return null;
    } on DioException catch (e) {
      return e.response?.data?['error']?['code'] as String? ?? 'FLIP_FAILED';
    } catch (_) {
      return 'FLIP_FAILED';
    }
  }

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