#!/usr/bin/env python3


# Libervia plugin for Stanza Forwarding (XEP-0297)
# Copyright (C) 2009-2026 Jérôme Poisson (goffi@goffi.org)
# Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.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 datetime import datetime
from typing import cast

from dateutil import tz
from twisted.internet import defer
from twisted.words.protocols.jabber import jid
from twisted.words.protocols.jabber.xmlstream import XMPPHandler
from twisted.words.xish import domish
from tx_xmpp import disco, itx_xmpp
from zope.interface import implementer

from libervia.backend import G
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.models.core import MessageData
from libervia.backend.core.i18n import D_, _
from libervia.backend.core.log import getLogger
from libervia.backend.memory.sqla_mapping import History
from libervia.backend.plugins.plugin_xep_0428 import XEP_0428
from libervia.backend.tools.common.date_utils import date_fmt

log = getLogger(__name__)


PLUGIN_INFO = {
    C.PI_NAME: "Stanza Forwarding",
    C.PI_IMPORT_NAME: "XEP-0297",
    C.PI_TYPE: "XEP",
    C.PI_MODES: [C.PLUG_MODE_COMPONENT],
    C.PI_PROTOCOLS: ["XEP-0297"],
    C.PI_DEPENDENCIES: ["XEP-0428"],
    C.PI_MAIN: "XEP_0297",
    C.PI_HANDLER: "yes",
    C.PI_DESCRIPTION: D_("""Implementation of Stanza Forwarding"""),
}

NS_FORWARD = "urn:xmpp:forward:0"


class XEP_0297:
    # TODO: Handle forwarded message reception, for now the fallback will be used.
    # TODO: Add a method to forward Pubsub Item, and notably blog content.

    def __init__(self, host):
        log.info(_("Stanza Forwarding plugin initialization"))
        self.host = host
        self._fallback = cast(XEP_0428, host.plugins["XEP-0428"])
        host.register_namespace("forward", NS_FORWARD)
        host.bridge.add_method(
            "message_forward",
            ".plugin",
            in_sign="sss",
            out_sign="",
            method=self._forward,
            async_=True,
        )

    def get_handler(self, client):
        return XEP_0297_handler(self, client.profile)

    @classmethod
    def update_uri(cls, element: domish.Element, uri: str) -> None:
        """Update recursively the element URI.

        @param element: element to update
        @param uri: new URI
        """
        # XXX: we need this because changing the URI of an existing element
        # containing children doesn't update the children's blank URI.
        element.uri = uri
        element.defaultUri = uri
        for child in element.children:
            if isinstance(child, domish.Element) and not child.uri:
                XEP_0297.update_uri(child, uri)

    @staticmethod
    def build_forwarded(
        stanza: domish.Element, timestamp: datetime | None = None
    ) -> domish.Element:
        """Build a <forwarded/> element containing a stanza.

        @param stanza: The stanza to wrap in forwarded element.
        @param timestamp: Timestamp for delay element (XEP-0203).
        @return: The <forwarded/> element with the stanza as child.
        """
        forwarded_elt = domish.Element((C.NS_FORWARD, "forwarded"))
        if timestamp:
            delay_elt = G.host.plugins["XEP-0203"].delay(timestamp)
            forwarded_elt.addChild(delay_elt)
        if not stanza.uri:
            XEP_0297.update_uri(stanza, "jabber:client")
        forwarded_elt.addChild(stanza)
        return forwarded_elt

    def _forward(
        self, message_id: str, recipient_jid_s: str, profile_key: str
    ) -> defer.Deferred[None]:
        client = self.host.get_client(profile_key)
        recipient_jid = jid.JID(recipient_jid_s)
        return defer.ensureDeferred(self.forward_by_id(client, message_id, recipient_jid))

    async def forward_by_id(
        self,
        client: SatXMPPEntity,
        message_id: str,
        recipient_jid: jid.JID,
    ) -> None:
        history = cast(
            History,
            await G.storage.get(
                client,
                History,
                History.uid,
                message_id,
                joined_loads=[History.messages, History.subjects, History.thread],
            ),
        )
        if not history:
            raise exceptions.NotFound(f"No history found with message {message_id!r}.")
        # FIXME: Q&D way to get MessageData from History. History should have a proper
        #     way to do that.
        serialised_history = history.serialise()
        serialised_history["from"] = jid.JID(serialised_history["from"])
        serialised_history["to"] = jid.JID(serialised_history["to"])
        mess_data = MessageData(serialised_history)
        client.generate_message_xml(mess_data)
        message_elt = cast(domish.Element, mess_data["xml"])
        timestamp_float = float(history.timestamp or history.received_timestamp)
        timestamp = datetime.fromtimestamp(timestamp_float, tz.tzutc())

        fallback_lines = [
            f"{client.jid.userhost()} is forwarding this message to you:",
            f"[{date_fmt(timestamp_float)}]",
            f"From: {history.source_jid.full()}",
            f"To: {history.dest_jid.full()}",
        ]

        for subject in history.subjects:
            if subject.language:
                fallback_lines.append(f"Subject [{subject.language}]: {subject.subject}")
            else:
                fallback_lines.append(f"Subject: {subject.subject}")

        for message in history.messages:
            if message.language:
                fallback_lines.append(f"Message [{message.language}]: {message.message}")
            else:
                fallback_lines.append(f"Message: {message.message}")

        fallback_msg = "\n".join(fallback_lines)

        await self.forward(client, message_elt, recipient_jid, timestamp, fallback_msg)

    async def forward(
        self,
        client: SatXMPPEntity,
        stanza: domish.Element,
        to_jid: jid.JID,
        timestamp: datetime | None,
        fallback_msg: str | None = None,
    ) -> defer.Deferred:
        """Forward a message to the given JID.

        @param client: client instance.
        @param stanza: original stanza to be forwarded.
        @param to_jid: recipient JID.
        @param timestamp: offset-aware timestamp of the original reception.
        @param body: optional description.
        @return: a Deferred when the message has been sent
        """
        message_elt = domish.Element((None, "message"))
        message_elt["to"] = to_jid.full()
        message_elt["type"] = stanza["type"]

        forwarded_elt = XEP_0297.build_forwarded(stanza, timestamp)

        message_elt.addChild(domish.Element((None, "body")))
        message_elt.addChild(forwarded_elt)
        self._fallback.add_fallback_elt(message_elt, NS_FORWARD, fallback_msg)
        return await client.send_message_data(
            MessageData({"xml": message_elt, "extra": {}})
        )


@implementer(itx_xmpp.IDisco)
class XEP_0297_handler(XMPPHandler):

    def __init__(self, plugin_parent: XEP_0297, profile: str) -> None:
        self.plugin_parent = plugin_parent
        self.host = plugin_parent.host
        self.profile = profile

    def getDiscoInfo(
        self, requestor: jid.JID, target: jid.JID, nodeIdentifier: str = ""
    ) -> list[disco.DiscoFeature]:
        """Get disco info for pubsub extended discovery

        @param requestor: JID of the requesting entity
        @param target: JID of the target entity
        @param nodeIdentifier: optional node identifier
        @return: list of disco features
        """
        return [disco.DiscoFeature(C.NS_FORWARD)]

    def getDiscoItems(
        self, requestor: jid.JID, target: jid.JID, nodeIdentifier: str = ""
    ) -> list[disco.DiscoItem]:
        """Get disco items with extended discovery support

        @param requestor: JID of the requesting entity
        @param target: JID of the target entity
        @param nodeIdentifier: optional node identifier
        @return: list of disco items
        """
        return []
