import 'dart:async';

import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/controllers/chat_message_controller.dart';
import 'package:surface/providers/channel.dart';
import 'package:surface/providers/chat_call.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/websocket.dart';
import 'package:surface/types/chat.dart';
import 'package:surface/widgets/chat/call/call_prejoin.dart';
import 'package:surface/widgets/chat/chat_message.dart';
import 'package:surface/widgets/chat/chat_message_input.dart';
import 'package:surface/widgets/chat/chat_typing_indicator.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';

import '../../providers/user_directory.dart';
import '../../providers/userinfo.dart';

class ChatRoomScreen extends StatefulWidget {
  final String scope;
  final String alias;

  const ChatRoomScreen({super.key, required this.scope, required this.alias});

  @override
  State<ChatRoomScreen> createState() => _ChatRoomScreenState();
}

class _ChatRoomScreenState extends State<ChatRoomScreen> {
  bool _isBusy = false;
  bool _isCalling = false;

  SnChannel? _channel;
  SnChannelMember? _otherMember;
  SnChatCall? _ongoingCall;

  final GlobalKey<ChatMessageInputState> _inputGlobalKey = GlobalKey();
  late final ChatMessageController _messageController;

  StreamSubscription? _wsSubscription;

  Future<void> _fetchChannel() async {
    setState(() => _isBusy = true);

    try {
      final chan = context.read<ChatChannelProvider>();
      _channel = await chan.getChannel('${widget.scope}:${widget.alias}');

      if (!mounted || _channel == null) return;
      final ud = context.read<UserDirectoryProvider>();
      final ua = context.read<UserProvider>();
      if (_channel!.type == 1) {
        await ud.listAccount(
          _channel!.members
                  ?.cast<SnChannelMember?>()
                  .map((ele) => ele?.accountId)
                  .where((ele) => ele != null && ele != ua.user?.id)
                  .toSet() ??
              {},
        );
        _otherMember = _channel!.members?.cast<SnChannelMember?>().firstWhere(
              (ele) => ele?.accountId != ua.user?.id,
              orElse: () => null,
            );
      }
    } catch (err) {
      if (!mounted) return;
      context.showErrorDialog(err);
    } finally {
      setState(() => _isBusy = false);
    }
  }

  Future<void> _fetchOngoingCall() async {
    setState(() => _isCalling = true);

    try {
      final sn = context.read<SnNetworkProvider>();
      final resp = await sn.client.get(
        '/cgi/im/channels/${_messageController.channel!.keyPath}/calls/ongoing',
        options: Options(
          validateStatus: (status) => status != null && status < 500,
          receiveTimeout: const Duration(seconds: 60),
          sendTimeout: const Duration(seconds: 60),
        ),
      );
      if (resp.statusCode == 200) {
        _ongoingCall = SnChatCall.fromJson(resp.data);
      }
    } catch (err) {
      if (!mounted) return;
      context.showErrorDialog(err);
    } finally {
      setState(() => _isCalling = false);
    }
  }

  Future<void> _makeCall() async {
    setState(() => _isCalling = true);

    try {
      final sn = context.read<SnNetworkProvider>();
      await sn.client.post(
        '/cgi/im/channels/${_messageController.channel!.keyPath}/calls',
        options: Options(
          sendTimeout: const Duration(seconds: 30),
          receiveTimeout: const Duration(seconds: 30),
        ),
      );
    } catch (err) {
      if (!mounted) return;
      if (_ongoingCall == null) {
        // ignore the error because the call is already ongoing
        context.showErrorDialog(err);
      }
    } finally {
      setState(() => _isCalling = false);
    }
  }

  Future<void> _endCall() async {
    setState(() => _isCalling = true);

    try {
      final sn = context.read<SnNetworkProvider>();
      await sn.client.delete(
        '/cgi/im/channels/${_messageController.channel!.keyPath}/calls/ongoing',
      );
    } catch (err) {
      if (!mounted) return;
      context.showErrorDialog(err);
    } finally {
      setState(() => _isCalling = false);
    }
  }

  Future<void> _onCallJoin() async {
    await showModalBottomSheet(
      context: context,
      builder: (context) => ChatCallPrejoinPopup(
        ongoingCall: _ongoingCall!,
        channel: _channel!,
        onJoin: _onCallResume,
      ),
    );
  }

  void _onCallResume() {
    GoRouter.of(context).pushNamed(
      'chatCallRoom',
      pathParameters: {
        'scope': _channel!.realm?.alias ?? 'global',
        'alias': _channel!.alias,
      },
    );
  }

  bool _checkMessageMergeable(SnChatMessage? a, SnChatMessage? b) {
    if (a == null || b == null) return false;
    if (a.sender.accountId != b.sender.accountId) return false;
    return a.createdAt.difference(b.createdAt).inMinutes <= 3;
  }

  @override
  void initState() {
    super.initState();
    _messageController = ChatMessageController(context);
    _fetchChannel().then((_) async {
      await _messageController.initialize(_channel!);
      await _messageController.checkUpdate();
      await _fetchOngoingCall();
    });

    final ws = context.read<WebSocketProvider>();
    _wsSubscription = ws.stream.stream.listen((event) {
      switch (event.method) {
        case 'calls.new':
          final payload = SnChatCall.fromJson(event.payload!);
          if (payload.channelId == _channel?.id) {
            setState(() => _ongoingCall = payload);
          }
          break;
        case 'calls.end':
          final payload = SnChatCall.fromJson(event.payload!);
          if (payload.channelId == _channel?.id) {
            setState(() => _ongoingCall = null);
          }
          break;
      }
    });
  }

  @override
  void dispose() {
    _wsSubscription?.cancel();
    _messageController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final call = context.watch<ChatCallProvider>();
    final ud = context.read<UserDirectoryProvider>();

    return Scaffold(
      appBar: AppBar(
        title: Text(
          _channel?.type == 1
              ? ud.getAccountFromCache(_otherMember?.accountId)?.nick ?? _channel!.name
              : _channel?.name ?? 'loading'.tr(),
        ),
        actions: [
          IconButton(
            icon: _ongoingCall == null ? const Icon(Symbols.call) : const Icon(Symbols.call_end),
            onPressed: _isCalling
                ? null
                : _ongoingCall == null
                    ? _makeCall
                    : _endCall,
          ),
          IconButton(
            icon: const Icon(Symbols.more_vert),
            onPressed: () {
              GoRouter.of(context).pushNamed('channelDetail', pathParameters: {
                'scope': widget.scope,
                'alias': widget.alias,
              }).then((value) {
                if (value == false && context.mounted) {
                  Navigator.pop(context, true);
                } else if (value != null && context.mounted) {
                  _fetchChannel();
                }
              });
            },
          ),
          const Gap(8),
        ],
      ),
      body: ListenableBuilder(
        listenable: _messageController,
        builder: (context, _) {
          return Column(
            children: [
              LoadingIndicator(isActive: _isBusy),
              SingleChildScrollView(
                physics: const NeverScrollableScrollPhysics(),
                child: MaterialBanner(
                  dividerColor: Colors.transparent,
                  leading: const Icon(Symbols.call_received),
                  content: Text('callOngoingNotice').tr().padding(top: 2),
                  actions: [
                    if (call.current == null)
                      TextButton(
                        onPressed: _onCallJoin,
                        child: Text('callJoin').tr(),
                      )
                    else if (call.current?.channelId == _channel?.id)
                      TextButton(
                        onPressed: _onCallResume,
                        child: Text('callResume').tr(),
                      )
                  ],
                ),
              )
                  .height(_ongoingCall != null ? 54 : 0, animate: true)
                  .animate(const Duration(milliseconds: 300), Curves.fastLinearToSlowEaseIn),
              if (_messageController.isPending)
                Expanded(
                  child: const CircularProgressIndicator().center(),
                ),
              if (!_messageController.isPending)
                Expanded(
                  child: InfiniteList(
                    reverse: true,
                    padding: const EdgeInsets.only(top: 12),
                    hasReachedMax: _messageController.isAllLoaded,
                    itemCount: _messageController.messages.length,
                    isLoading: _messageController.isLoading,
                    onFetchData: () {
                      _messageController.loadMessages();
                    },
                    itemBuilder: (context, idx) {
                      final message = _messageController.messages[idx];

                      bool canMerge = false, canMergePrevious = false;
                      if (idx > 0) {
                        canMergePrevious = _checkMessageMergeable(
                          _messageController.messages[idx - 1],
                          _messageController.messages[idx],
                        );
                      }
                      if (idx + 1 < _messageController.messages.length) {
                        canMerge = _checkMessageMergeable(
                          _messageController.messages[idx],
                          _messageController.messages[idx + 1],
                        );
                      }

                      return Align(
                        alignment: Alignment.centerLeft,
                        child: ChatMessage(
                          data: message,
                          isMerged: canMerge,
                          hasMerged: canMergePrevious,
                          isPending: _messageController.unconfirmedMessages.contains(message.uuid),
                          onReply: (value) {
                            _inputGlobalKey.currentState?.setReply(value);
                          },
                          onEdit: (value) {
                            _inputGlobalKey.currentState?.setEdit(value);
                          },
                          onDelete: (value) {
                            _inputGlobalKey.currentState?.deleteMessage(value);
                          },
                        ),
                      );
                    },
                  ),
                ),
              if (!_messageController.isPending)
                Material(
                  elevation: 2,
                  child: Column(
                    children: [
                      ChatTypingIndicator(controller: _messageController),
                      ChatMessageInput(
                        key: _inputGlobalKey,
                        otherMember: _otherMember,
                        controller: _messageController,
                      ),
                      Gap(MediaQuery.of(context).padding.bottom),
                    ],
                  ),
                ),
            ],
          );
        },
      ),
    );
  }
}