# This file is part of Gajim.
#
# SPDX-License-Identifier: GPL-3.0-only

from __future__ import annotations

from enum import Enum
from pathlib import Path

from gi.repository import GObject
from gi.repository import Gtk
from gi.repository import Pango
from nbxmpp.modules.dataforms import SimpleDataForm
from nbxmpp.protocol import InvalidJid
from nbxmpp.protocol import JID
from nbxmpp.protocol import validate_resourcepart

from gajim.common import app
from gajim.common import types
from gajim.common.const import SimpleClientState
from gajim.common.i18n import _
from gajim.common.modules.contacts import BareContact
from gajim.common.modules.contacts import GroupchatContact

from gajim.gtk.dataform import DataFormWidget
from gajim.gtk.file_transfer_selector import FileTransferSelector
from gajim.gtk.groupchat_inviter import GroupChatInviter


class FunctionMode(Enum):
    INVITE = 'invite'
    SEND_FILE = 'send-file'
    CHANGE_NICKNAME = 'change-nickname'
    KICK = 'kick'
    BAN = 'ban'
    PASSWORD_REQUEST = 'password-request'  # noqa: S105
    CAPTCHA_REQUEST = 'captcha-request'
    CAPTCHA_ERROR = 'captcha-error'
    JOIN_FAILED = 'join-failed'
    CREATION_FAILED = 'creation-failed'
    CONFIG_FAILED = 'config-failed'


class ChatFunctionPage(Gtk.Box):

    __gsignals__ = {
        'finish': (GObject.SignalFlags.RUN_LAST, None, (bool,)),
        'message': (GObject.SignalFlags.RUN_LAST, None, (str,)),
    }

    def __init__(self) -> None:
        Gtk.Box.__init__(self,
                         orientation=Gtk.Orientation.VERTICAL,
                         spacing=18)
        self.set_halign(Gtk.Align.CENTER)
        self.set_valign(Gtk.Align.CENTER)
        self.get_style_context().add_class('padding-18')

        self._client: types.Client | None = None
        self._contact: types.ChatContactT | None = None
        self._mode: FunctionMode | None = None
        self._data: str | None = None
        self._ready_state = True

        self._widget: Gtk.Widget | None = None

        self._heading = Gtk.Label()
        self._heading.set_max_width_chars(30)
        self._heading.set_ellipsize(Pango.EllipsizeMode.END)
        self._heading.get_style_context().add_class('large-header')
        self.add(self._heading)

        self._content_box = Gtk.Box()
        self._content_box.set_halign(Gtk.Align.CENTER)
        self.add(self._content_box)

        cancel_button = Gtk.Button(label=_('Cancel'))
        cancel_button.connect('clicked', self._on_cancel_clicked)

        self._forget_button = Gtk.Button(label=_('Forget Group Chat'))
        self._forget_button.set_no_show_all(True)
        self._forget_button.get_style_context().add_class(
            'destructive-action')
        self._forget_button.connect('clicked', self._on_forget_clicked)

        self._confirm_button = Gtk.Button()
        self._confirm_button.set_can_default(True)
        self._confirm_button.connect('clicked', self._on_confirm_clicked)

        button_box = Gtk.Box(spacing=18)
        button_box.pack_start(cancel_button, False, True, 0)
        button_box.pack_end(self._forget_button, False, True, 0)
        button_box.pack_end(self._confirm_button, False, True, 0)

        self.add(button_box)

    def process_escape(self) -> None:
        close_control = self._mode in (
            FunctionMode.CREATION_FAILED,
            FunctionMode.CONFIG_FAILED,
            FunctionMode.CAPTCHA_ERROR)
        self.emit('finish', close_control)

    def _reset(self) -> None:
        if self._client is not None:
            self._client.disconnect_all_from_obj(self)

        for child in self._content_box.get_children():
            child.destroy()

        if self._widget is not None:
            self._widget.destroy()

        self._confirm_button.get_style_context().remove_class(
            'destructive-action')
        self._confirm_button.get_style_context().remove_class(
            'suggested-action')
        self._confirm_button.set_sensitive(False)
        self._confirm_button.grab_default()

        self._forget_button.set_sensitive(True)
        self._forget_button.hide()

        self._ready_state = True

    def set_mode(self,
                 contact: types.ChatContactT,
                 mode: FunctionMode,
                 data: str | None = None,
                 files: list[str] | None = None
                 ) -> None:

        self._reset()

        self._contact = contact
        self._client = app.get_client(contact.account)
        self._client.connect_signal(
            'state-changed', self._on_client_state_changed)

        self._mode = mode
        self._data = data

        self._heading.set_text(self._contact.name)

        if mode == FunctionMode.INVITE:
            self._confirm_button.set_label(_('Invite'))
            self._confirm_button.get_style_context().add_class(
                'suggested-action')
            self._widget = GroupChatInviter(str(contact.jid))
            self._widget.set_size_request(-1, 500)
            self._widget.connect('listbox-changed', self._on_ready)
            self._widget.load_contacts()

        elif mode == FunctionMode.SEND_FILE:
            self._confirm_button.set_label(_('Send Files'))
            self._confirm_button.get_style_context().add_class(
                'suggested-action')
            self._widget = FileTransferSelector(self._contact, data)
            self._widget.connect('changed', self._on_ready)
            if files is not None:
                self._widget.add_files(files)

        elif mode == FunctionMode.CHANGE_NICKNAME:
            self._confirm_button.set_label(_('Change'))
            self._confirm_button.get_style_context().add_class(
                'suggested-action')
            self._widget = InputWidget(self._contact, mode)
            self._widget.connect('changed', self._on_ready)

        elif mode == FunctionMode.KICK:
            self._confirm_button.set_label(_('Kick'))
            self._confirm_button.set_sensitive(True)
            self._confirm_button.get_style_context().add_class(
                'destructive-action')
            self._widget = InputWidget(self._contact, mode, data)

        elif mode == FunctionMode.BAN:
            self._confirm_button.set_label(_('Ban'))
            self._confirm_button.set_sensitive(True)
            self._confirm_button.get_style_context().add_class(
                'destructive-action')
            self._widget = InputWidget(self._contact, mode, data)

        elif mode == FunctionMode.PASSWORD_REQUEST:
            self._confirm_button.set_label(_('Join'))
            self._confirm_button.get_style_context().add_class(
                'suggested-action')
            self._widget = InputWidget(self._contact, mode)
            self._widget.connect('changed', self._on_ready)

        elif mode == FunctionMode.CAPTCHA_REQUEST:
            self._confirm_button.set_label(_('Join'))
            self._confirm_button.get_style_context().add_class(
                'suggested-action')
            muc_data = self._client.get_module('MUC').get_muc_data(
                self._contact.jid)
            assert muc_data is not None
            form = muc_data.captcha_form
            assert form is not None
            options = {'no-scrolling': True,
                       'entry-activates-default': True}
            self._widget = DataFormWidget(form, options=options)
            self._widget.set_valign(Gtk.Align.START)
            self._widget.show_all()
            self._widget.connect('is-valid', self._on_ready)

        elif mode == FunctionMode.CAPTCHA_ERROR:
            self._confirm_button.set_label(_('Try Again'))
            self._confirm_button.set_sensitive(True)
            self._confirm_button.get_style_context().add_class(
                'suggested-action')
            self._widget = ErrorWidget(error_text=data)

        elif mode in (FunctionMode.JOIN_FAILED,
                      FunctionMode.CREATION_FAILED,
                      FunctionMode.CONFIG_FAILED):
            self._confirm_button.set_label(_('Try Again'))
            self._confirm_button.set_sensitive(True)
            self._confirm_button.get_style_context().add_class(
                'suggested-action')
            if mode == FunctionMode.JOIN_FAILED:
                is_bookmark = self._client.get_module('Bookmarks').is_bookmark(
                    self._contact.jid)
                self._forget_button.set_visible(is_bookmark)

            error_text = str(self._contact.jid)
            if data is not None:
                error_text = f'{error_text}\n{data}'
            self._widget = ErrorWidget(mode=mode, error_text=error_text)

        assert self._widget is not None
        self._content_box.add(self._widget)
        if isinstance(self._widget, InputWidget):
            self._widget.focus()
        elif isinstance(self._widget, DataFormWidget):
            self._widget.focus_first_entry()
        elif mode == FunctionMode.SEND_FILE:
            self._confirm_button.grab_focus()

    def _on_client_state_changed(self,
                                 _client: types.Client,
                                 _signal_name: str,
                                 _state: SimpleClientState
                                 ) -> None:

        self._update_button_state()

    def _on_ready(self,
                  _widget: Gtk.Widget,
                  state: bool
                  ) -> None:

        self._ready_state = state
        self._update_button_state()

    def _update_button_state(self) -> None:
        assert self._contact is not None
        if app.account_is_connected(self._contact.account):
            self._confirm_button.set_sensitive(self._ready_state)
            self._forget_button.set_sensitive(True)
            return

        self._confirm_button.set_sensitive(False)
        self._forget_button.set_sensitive(False)

    def _on_confirm_clicked(self, _button: Gtk.Button) -> None:
        assert self._client is not None
        assert self._contact is not None

        if self._mode == FunctionMode.INVITE:
            assert isinstance(self._widget, GroupChatInviter)
            invitees = self._widget.get_invitees()
            for jid in invitees:
                self._invite(JID.from_string(jid))

        elif self._mode == FunctionMode.SEND_FILE:
            assert isinstance(self._widget, FileTransferSelector)
            if self._widget.transfer_resource_required():
                return

            self._send_files(self._widget.get_catalog())

        elif self._mode == FunctionMode.CHANGE_NICKNAME:
            assert isinstance(self._widget, InputWidget)
            nickname = self._widget.get_text()
            self._client.get_module('MUC').change_nick(
                self._contact.jid, nickname)

        elif self._mode == FunctionMode.KICK:
            assert isinstance(self._widget, InputWidget)
            reason = self._widget.get_text()
            self._client.get_module('MUC').set_role(
                self._contact.jid, self._data, 'none', reason)

        elif self._mode == FunctionMode.BAN:
            assert isinstance(self._widget, InputWidget)
            reason = self._widget.get_text()
            self._client.get_module('MUC').set_affiliation(
                self._contact.jid,
                {self._data: {'affiliation': 'outcast', 'reason': reason}})

        elif self._mode == FunctionMode.PASSWORD_REQUEST:
            assert isinstance(self._widget, InputWidget)
            password = self._widget.get_text()
            self._client.get_module('MUC').set_password(
                self._contact.jid, password)
            self._client.get_module('MUC').join(self._contact.jid)

        elif self._mode == FunctionMode.CAPTCHA_REQUEST:
            assert isinstance(self._widget, DataFormWidget)
            form_node = self._widget.get_submit_form()
            assert isinstance(form_node, SimpleDataForm)
            self._client.get_module('MUC').send_captcha(
                self._contact.jid, form_node)

        elif self._mode in (FunctionMode.JOIN_FAILED,
                            FunctionMode.CAPTCHA_ERROR):
            self._client.get_module('MUC').join(self._contact.jid)

        self.emit('finish', False)

    def _on_cancel_clicked(self, _button: Gtk.Button) -> None:
        assert self._client is not None
        assert self._contact is not None

        connected = app.account_is_connected(self._contact.account)
        close_control = False

        if self._mode == FunctionMode.CAPTCHA_REQUEST:
            if connected:
                self._client.get_module('MUC').cancel_captcha(
                    self._contact.jid)
            close_control = True

        elif self._mode == FunctionMode.PASSWORD_REQUEST:
            if connected:
                self._client.get_module('MUC').cancel_password_request(
                    self._contact.jid)
            close_control = True

        elif self._mode in (
                FunctionMode.CREATION_FAILED,
                FunctionMode.CONFIG_FAILED,
                FunctionMode.CAPTCHA_ERROR):
            close_control = True

        self.emit('finish', close_control)

    def _on_forget_clicked(self, _button: Gtk.Button) -> None:
        assert self._client is not None
        assert self._contact is not None
        self._client.get_module('Bookmarks').remove(self._contact.jid)
        self.emit('finish', True)

    def _invite(self, invited_jid: JID) -> None:
        assert self._contact is not None
        client = app.get_client(self._contact.account)
        client.get_module('MUC').invite(
            self._contact.jid, invited_jid)
        invited_contact = client.get_module('Contacts').get_contact(
            invited_jid)
        assert isinstance(invited_contact, BareContact)
        self.emit(
            'message',
            _('%s has been invited to this group chat') % invited_contact.name)

    def _send_files(self, catalog: list[tuple[Path, str, JID]]) -> None:
        assert self._contact is not None
        client = app.get_client(self._contact.account)

        # catalog: list[(file Path, transfer method, recipient JID)]
        for path, method, jid in catalog:
            if method == 'httpupload':
                client.get_module('HTTPUpload').send_file(
                    self._contact, path)
                continue

            # Send using Jingle
            app.interface.instances['file_transfers'].send_file(
                self._contact.account,
                self._contact,
                jid,
                str(path))


class InputWidget(Gtk.Box):

    __gsignals__ = {
        'changed': (GObject.SignalFlags.RUN_LAST, None, (bool,)),
    }

    def __init__(self,
                 contact: types.ChatContactT,
                 mode: FunctionMode,
                 data: str | None = None
                 ) -> None:

        Gtk.Box.__init__(self,
                         orientation=Gtk.Orientation.VERTICAL,
                         spacing=12)
        self._contact = contact
        self._mode = mode

        heading_label = Gtk.Label()
        heading_label.set_xalign(0)
        heading_label.get_style_context().add_class('bold16')
        self.add(heading_label)

        sub_label = Gtk.Label()
        sub_label.set_xalign(0)
        sub_label.get_style_context().add_class('dim-label')
        self.add(sub_label)

        self._entry = Gtk.Entry()
        self._entry.set_activates_default(True)
        self._entry.set_size_request(300, -1)
        self._entry.connect('changed', self._on_entry_changed)
        self.add(self._entry)

        if mode == FunctionMode.CHANGE_NICKNAME:
            heading_label.set_text(_('Change Nickname'))
            sub_label.set_text(_('Enter your new nickname'))
            assert isinstance(self._contact, GroupchatContact)
            self._entry.set_text(self._contact.nickname or '')

        elif mode == FunctionMode.KICK:
            heading_label.set_text(_('Kick %s') % data)
            sub_label.set_text(_('Reason (optional)'))

        elif mode == FunctionMode.BAN:
            heading_label.set_text(_('Ban %s') % data)
            sub_label.set_text(_('Reason (optional)'))

        elif mode == FunctionMode.PASSWORD_REQUEST:
            heading_label.set_text(_('Password Required'))
            sub_label.set_text(_('Enter a password to join this chat'))
            self._entry.set_input_purpose(Gtk.InputPurpose.PASSWORD)
            self._entry.set_visibility(False)

        self.connect('destroy', self._on_destroy)
        self.show_all()

    def _on_destroy(self, _widget: InputWidget) -> None:
        app.check_finalize(self)

    def _on_entry_changed(self, entry: Gtk.Entry) -> None:
        text = entry.get_text()

        if self._mode == FunctionMode.CHANGE_NICKNAME:
            assert isinstance(self._contact, GroupchatContact)
            if not text or text == self._contact.nickname:
                self.emit('changed', False)
                return
            try:
                validate_resourcepart(text)
            except InvalidJid:
                self.emit('changed', False)
                return

        self.emit('changed', bool(text))

    def focus(self) -> None:
        self._entry.grab_focus()

    def get_text(self) -> str:
        return self._entry.get_text()


class ErrorWidget(Gtk.Box):
    def __init__(self,
                 mode: FunctionMode | None = None,
                 error_text: str | None = None
                 ) -> None:

        Gtk.Box.__init__(self,
                         orientation=Gtk.Orientation.VERTICAL,
                         spacing=12)
        image = Gtk.Image.new_from_icon_name(
            'dialog-error-symbolic', Gtk.IconSize.DIALOG)
        image.get_style_context().add_class('error-color')

        heading = Gtk.Label()
        heading.get_style_context().add_class('bold16')
        heading_text = _('An Error Occurred')
        if mode == FunctionMode.JOIN_FAILED:
            heading_text = _('Failed to Join Group Chat')
        elif mode == FunctionMode.CREATION_FAILED:
            heading_text = _('Failed to Create Group Chat')
        elif mode == FunctionMode.CONFIG_FAILED:
            heading_text = _('Failed to Configure Group Chat')
        heading.set_text(heading_text)

        label = Gtk.Label()
        label.set_max_width_chars(40)
        if error_text is not None:
            label.set_text(error_text)

        self.add(image)
        self.add(heading)
        self.add(label)
        self.show_all()
