#!/usr/bin/env python3


# SAT plugin for managing gateways (xep-0100)
# Copyright (C) 2009-2026 Jérôme Poisson (goffi@goffi.org)

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

from typing import cast
from pydantic import BaseModel, Field
from twisted.internet import defer, reactor
from twisted.words.protocols.jabber import jid
from tx_xmpp import disco

from libervia.backend.core import exceptions
from libervia.backend.core.constants import Const as C
from libervia.backend.core.core_types import SatXMPPEntity
from libervia.backend.core.i18n import D_, _
from libervia.backend.core.log import getLogger
from libervia.backend.models.core import DiscoIdentity
from libervia.backend.models.types import StrictJIDType
from libervia.backend.tools import xml_tools

log = getLogger(__name__)


PLUGIN_INFO = {
    C.PI_NAME: "Gateways Plugin",
    C.PI_IMPORT_NAME: "XEP-0100",
    C.PI_TYPE: "XEP",
    C.PI_PROTOCOLS: ["XEP-0100"],
    C.PI_DEPENDENCIES: ["XEP-0077"],
    C.PI_MAIN: "XEP_0100",
    C.PI_DESCRIPTION: _("""Implementation of Gateways protocol"""),
}

WARNING_MSG = D_(
    "Please exercise caution. Gateways facilitate the use of external instant messaging "
    "platforms (legacy IM), enabling you to view your contacts as XMPP contacts. "
    "However, this process routes all messages through the external legacy IM server, "
    "raising significant privacy concerns. Specifically, it is possible for the external "
    "server, often operated by a private company, to monitor, record, and analyze all "
    "messages that traverse the gateway."
)

GATEWAY_TIMEOUT = 10  # time to wait before cancelling a gateway disco info, in seconds

TYPE_DESCRIPTIONS = {
    "irc": D_("Internet Relay Chat"),
    "xmpp": D_("XMPP"),
    "qq": D_("Tencent QQ"),
    "simple": D_("SIP/SIMPLE"),
    "icq": D_("ICQ"),
    "yahoo": D_("Yahoo! Messenger"),
    "gadu-gadu": D_("Gadu-Gadu"),
    "aim": D_("AOL Instant Messenger"),
    "msn": D_("Windows Live Messenger"),
    "smtp": D_("Email"),
}


class GatewayData(BaseModel):
    entity: StrictJIDType
    identities: list[DiscoIdentity]


class FoundGateways(BaseModel):
    available: list[GatewayData]
    unavailable: list[StrictJIDType] = Field(
        description="Gateways registered but not answering."
    )


class XEP_0100(object):
    def __init__(self, host):
        log.info(_("Gateways plugin initialization"))
        self.host = host
        self.__gateways = (
            {}
        )  # dict used to construct the answer to gateways_find. Key = target jid
        host.bridge.add_method(
            "gateways_find",
            ".plugin",
            in_sign="ss",
            out_sign="s",
            method=self._gateways_find,
        )
        host.bridge.add_method(
            "gateways_find_xmlui",
            ".plugin",
            in_sign="ss",
            out_sign="s",
            method=self._gateways_find_xmlui,
        )
        host.bridge.add_method(
            "gateway_register",
            ".plugin",
            in_sign="ss",
            out_sign="s",
            method=self._gateway_register,
            async_=True,
        )
        self.__menu_id = host.register_callback(self._gateways_menu, with_data=True)
        self.__selected_id = host.register_callback(
            self._gateway_selected_cb, with_data=True
        )
        host.import_menu(
            (D_("Service"), D_("Gateways")),
            self._gateways_menu,
            security_limit=1,
            help_string=D_("Find gateways"),
        )

    def _gateways_menu(self, data, profile):
        """XMLUI activated by menu: return Gateways UI

        @param profile: %(doc_profile)s
        """
        client = self.host.get_client(profile)
        try:
            jid_ = jid.JID(
                data.get(xml_tools.form_escape("external_jid"), client.jid.host)
            )
        except RuntimeError:
            raise exceptions.DataError(_("Invalid JID"))
        d = self.gateways_find_raw(jid_, profile)
        d.addCallback(self._gateways_result_2_xmlui, jid_)
        d.addCallback(lambda xmlui: {"xmlui": xmlui.toXml()})
        return d

    def _gateways_result_2_xmlui(self, result, entity):
        xmlui = xml_tools.XMLUI(title=_("Gateways manager (%s)") % entity.full())
        xmlui.addText(_(WARNING_MSG))
        xmlui.addDivider("dash")
        adv_list = xmlui.change_container(
            "advanced_list",
            columns=3,
            selectable="single",
            callback_id=self.__selected_id,
        )
        for success, gateway_data in result:
            if not success:
                fail_cond, disco_item = gateway_data
                xmlui.addJid(disco_item.entity)
                xmlui.addText(_("Failed (%s)") % fail_cond)
                xmlui.addEmpty()
            else:
                jid_, data = gateway_data
                for datum in data:
                    identity, name = datum
                    adv_list.set_row_index(jid_.full())
                    xmlui.addJid(jid_)
                    xmlui.addText(name)
                    xmlui.addText(self._get_identity_desc(identity))
        adv_list.end()
        xmlui.addDivider("blank")
        xmlui.change_container("advanced_list", columns=3)
        xmlui.addLabel(_("Use external XMPP server"))
        xmlui.addString("external_jid")
        xmlui.addButton(self.__menu_id, _("Go !"), fields_back=("external_jid",))
        return xmlui

    def _gateway_selected_cb(self, data, profile):
        try:
            target_jid = jid.JID(data["index"])
        except (KeyError, RuntimeError):
            log.warning(_("No gateway index selected"))
            return {}

        d = self.gateway_register(target_jid, profile)
        d.addCallback(lambda xmlui: {"xmlui": xmlui.toXml()})
        return d

    def _get_identity_desc(self, identity):
        """Return a human readable description of identity
        @param identity: tuple as returned by Disco identities (category, type)

        """
        category, type_ = identity
        if category != "gateway":
            log.error(
                _(
                    'INTERNAL ERROR: identity category should always be "gateway" in '
                    '_getTypeString, got "%s"'
                )
                % category
            )
        try:
            return _(TYPE_DESCRIPTIONS[type_])
        except KeyError:
            return _("Unknown IM")

    def _registration_successful(self, jid_, profile):
        """Called when in_band registration is ok, we must now follow the rest of procedure"""
        log.debug(_("Registration successful, doing the rest"))
        self.host.contact_add(jid_, profile_key=profile)
        self.host.presence_set(jid_, profile_key=profile)

    def _gateway_register(self, target_jid_s, profile_key=C.PROF_KEY_NONE):
        client = self.host.get_client(profile_key)
        d = self.gateway_register(client, jid.JID(target_jid_s))
        d.addCallback(lambda xmlui: xmlui.toXml())
        return d

    def gateway_register(
        self, client: SatXMPPEntity, target_jid: jid.JID
    ) -> defer.Deferred:
        """Register gateway using in-band registration, then log-in to gateway"""
        return defer.ensureDeferred(
            self.host.plugins["XEP-0077"].in_band_register(
                client, target_jid, self._registration_successful
            )
        )

    def _gateways_find(self, target_jid_s: str, profile_key: str) -> defer.Deferred[str]:
        client = self.host.get_client(profile_key)
        target_jid = jid.JID(target_jid_s) if target_jid_s else client.server_jid
        d = defer.ensureDeferred(self.gateways_find(client, target_jid))
        d.addCallback(lambda found_gateways: found_gateways.model_dump_json())
        # The Deferred will actually return a str due to `model_dump_json`, but type
        # checker doesn't get that.
        d = cast(defer.Deferred[str], d)
        return d

    async def gateways_find(
        self, client: SatXMPPEntity, target: jid.JID
    ) -> FoundGateways:
        """Find gateways and convert FoundGateways instance."""
        gateways_data = await self.gateways_find_raw(client, target)
        available = []
        unavailable = []
        for gw_available, data in gateways_data:
            if gw_available:
                data = cast(tuple[jid.JID, list[tuple[tuple[str, str], str]]], data)
                identities = []
                for (category, type_), name in data[1]:
                    identities.append(
                        DiscoIdentity(name=name, category=category, type=type_)
                    )
                available.append(GatewayData(entity=data[0], identities=identities))
            else:
                disco_item = cast(disco.DiscoItem, data[1])
                unavailable.append(disco_item.entity)
        return FoundGateways(available=available, unavailable=unavailable)

    def _gateways_find_xmlui(
        self, target_jid_s: str, profile_key: str
    ) -> defer.Deferred[str]:
        target_jid = jid.JID(target_jid_s)
        client = self.host.get_client(profile_key)
        d = defer.ensureDeferred(self.gateways_find_raw(client, target_jid))
        d.addCallback(self._gateways_result_2_xmlui, target_jid)
        d.addCallback(lambda xmlui: xmlui.toXml())
        d = cast(defer.Deferred[str], d)
        return d

    async def gateways_find_raw(self, client: SatXMPPEntity, target: jid.JID) -> list:
        """Find gateways in the target JID, using discovery protocol"""
        log.debug(
            _("find gateways (target = {target}, profile = {profile})").format(
                target=target.full(), profile=client.profile
            )
        )
        disco = await client.disco.requestItems(target)
        if len(disco._items) == 0:
            log.debug(_("No gateway found"))
            return []

        defers_ = []
        for item in disco._items:
            log.debug(_("item found: {}").format(item.entity))
            defers_.append(client.disco.requestInfo(item.entity))
        deferred_list = defer.DeferredList(defers_)
        dl_result = await deferred_list
        reactor.callLater(GATEWAY_TIMEOUT, deferred_list.cancel)
        items = disco._items
        ret = []
        for idx, (success, result) in enumerate(dl_result):
            if not success:
                if isinstance(result.value, defer.CancelledError):
                    msg = _("Timeout")
                else:
                    try:
                        msg = result.value.condition
                    except AttributeError:
                        msg = str(result)
                ret.append((success, (msg, items[idx])))
            else:
                entity = items[idx].entity
                gateways = [
                    (identity, list(result.identities[identity].values())[0])
                    for identity in result.identities
                    if identity[0] == "gateway"
                ]
                if gateways:
                    log.debug(
                        _("Found gateway [{jid}]: {identity_name}").format(
                            jid=entity.full(),
                            identity_name=" - ".join(
                                [gateway[1].name for gateway in gateways]
                            ),
                        )
                    )
                    ret.append((success, (entity, gateways)))
                else:
                    log.debug(
                        _("Skipping [{jid}] which is not a gateway").format(
                            jid=entity.full()
                        )
                    )

        return ret
