#!/usr/bin/python3

# Copyright (C) 2019-2020
#
# * Felix Delattre <felix@delattre.de>
# * W. Martin Borgert <debacle@debian.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/>.

import re
import slixmpp


class GatewayXmpp(slixmpp.ComponentXMPP):

    PHONE_RE = re.compile(r"^\+?[0-9]+$")

    def __init__(self, config, loop, logger):
        self.logger = logger
        self.gateway = None

        self.xmpp_server = config.xmpp_server
        self.port = config.xmpp_server_port
        self.password = config.xmpp_component_password
        self.component_jid = config.xmpp_component_jid
        self.senders = config.xmpp_sender_jids  # was `allowed`
        self.recipients = config.xmpp_recipient_jids

        slixmpp.ComponentXMPP.__init__(
            self, self.component_jid, self.password, self.xmpp_server, self.port
        )
        self.register_plugin("xep_0004")  # Data Forms
        self.register_plugin("xep_0030")  # Service Discovery
        self.register_plugin("xep_0050")  # Ad-Hoc Commands

        self.register_plugin("xep_0054")  # vcard-temp
        self.remove_handler("VCardTemp")
        self.register_handler(
            slixmpp.xmlstream.handler.Callback(
                "VCardTemp",
                slixmpp.xmlstream.matcher.StanzaPath("iq/vcard_temp"),
                self.handle_get_vcard,
            )
        )

        self.register_plugin("xep_0092")  # Software Version
        self["xep_0092"].config["software_name"] = "sms4you"
        self["xep_0092"].config["version"] = config.version

        self.register_plugin("xep_0115")  # Entity Capabilities
        self["xep_0115"].caps_node = "xmpp:" + self.boundjid.full

        self.register_plugin("xep_0184")  # Message Delivery Receipts
        self.register_plugin("xep_0199")  # XMPP Ping
        self.register_plugin("xep_0202")  # Entity Time

        self.connect()

    def setup(self, gateway):
        self.gateway = gateway
        self.add_event_handler("message", self.message)
        self["xep_0030"].add_identity(
            category="gateway", itype="sms", name="sms4you", node=None, jid=self.boundjid.full
        )
        self.add_event_handler("session_start", self._prepare)

    async def _prepare(self, event):
        self["xep_0115"].session_bind(self.boundjid.full)
        await self["xep_0115"].update_caps(self.boundjid.full)
        for jid in set(self.senders + self.recipients):
            self.send_presence(pto=jid)

        self["xep_0050"].add_command(
            node="get_modem_information",
            name="Request Modem Information",
            handler=self._request_modem_information,
        )
        # writing the result in a variable is a workaround for handler
        # not being awaited in slixmpp
        self.modem_information = ""

        self["xep_0050"].add_command(
            node="modem_enable",
            name="Enable/Disable Modem",
            handler=self._enable_modem_options,
        )

    async def message(self, msg):
        # get_xmpp_stanza
        if msg.get_type() not in ["normal", "chat"]:
            return  # wrong message type

        from_ = msg.get_from()
        self.logger.log.debug("XMPP received from: %s", from_.bare)
        if from_.bare not in self.senders:
            self.logger.log.info("%s is not one of %s", from_.bare, ", ".join(self.senders))
            raise slixmpp.exceptions.XMPPError("forbidden", "Forbidden, sorry.")

        phone = msg.get_to().node
        if not self.PHONE_RE.match(phone):
            self.logger.log.debug("XMPP contains invalid phone number: %s", phone)
            raise slixmpp.exceptions.XMPPError("forbidden", "Invalid phone number.")

        body = msg.get("body")
        if len(body) > 160:  # SMS cannot exceed 160 characters
            self.logger.log.debug("XMPP message is too long")
            raise slixmpp.exceptions.XMPPError("forbidden", "Message too long.")

        self.logger.log.debug("XMPP passed on to SMS")
        await self.gateway.dispatch(phone, body)

    async def dispatch(self, number, smsc, text, timestamp):
        footer = "send from %s at %s via SMSC %s" % (number, timestamp, smsc)
        self.logger.log.debug("XMPP prepared")
        for recipient in self.recipients:
            self.send_message(
                mfrom=number + "@" + self.component_jid,
                mto=recipient,
                mbody=text + "\n" + footer,
                mtype="chat",
            )
            self.logger.log.debug("XMPP sent to: %s", recipient)

    def handle_get_vcard(self, iq):
        if iq["type"] != "get" or iq["from"].bare not in (self.senders + self.recipients):
            raise slixmpp.exceptions.XMPPError("forbidden", "Forbidden, sorry.")

        vcard = self["xep_0054"].stanza.VCardTemp()
        vcard["FN"] = "sms4you"
        vcard["NICKNAME"] = "sms4you"
        vcard["URL"] = "https://sms4you.xama.nu/"
        vcard["DESC"] = "\n".join(
            [  # keep lines < 70 chars for client UI
                "Personal gateway connecting SMS with XMPP or email.",
                "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.",
            ]
        )
        iq = iq.reply()
        iq.append(vcard)
        iq.send()

    async def get_modem_properties(self):
        try:
            (
                manufacturer,
                model,
                signalquality,
                ownnumbers,
            ) = await self.gateway.get_modem_properties()
            self.modem_information = "\n".join(
                [
                    "Modem Information",
                    "Manufacturer: %s" % manufacturer,
                    "Model: %s" % model,
                    "Signal Quality: %s %%" % signalquality[0],
                    "Own Numbers: %s" % ", ".join(ownnumbers),
                ]
            )
        except Exception as e:
            self.logger.log.warning(str(e))

    def _request_modem_information(self, iq, session):
        from_ = session["from"]

        if from_.bare not in (self.senders + self.recipients):
            self.logger.log.info(
                "%s is not one of %s", from_.bare, ", ".join(self.senders + self.recipients)
            )
            raise slixmpp.exceptions.XMPPError("forbidden", "Forbidden, sorry.")

        self.modem_information = "modem properties not available"
        self.loop.create_task(self.get_modem_properties())
        form = self["xep_0004"].make_form("form", "Request Modem Information")
        session["payload"] = form
        session["next"] = self._request_modem_information_finish
        session["has_next"] = True
        return session

    def _request_modem_information_finish(self, iq, session):
        form = self["xep_0004"].make_form("form", "Modem Information")
        form.add_field(
            var="modem_information",
            ftype="fixed",
            label="Modem Information",
            value=self.modem_information,
        )
        session["payload"] = form
        session["next"] = None
        session["has_next"] = False
        return session

    def _enable_modem_options(self, iq, session):
        from_ = session["from"]

        if from_.bare not in (self.senders + self.recipients):
            self.logger.log.info(
                "%s is not one of %s", from_.bare, ", ".join(self.senders + self.recipients)
            )
            raise slixmpp.exceptions.XMPPError("forbidden", "Forbidden, sorry.")

        form = self["xep_0004"].make_form("form", "Enable/Disable Modem")
        options = form.add_field(
            "enable", "list-single", label="Enable or disable modem", value="enable"
        )
        options.addOption("Enable", "enable")
        options.addOption("Disable", "disable")

        session["payload"] = form
        session["next"] = self._enable_modem_finish
        session["has_next"] = True
        return session

    def _enable_modem_finish(self, iq, session):
        enable = iq.get_values().get("enable")

        if enable not in ["enable", "disable"]:
            raise slixmpp.exceptions.XMPPError("undefined-condition", "Illegal value.")

        self.loop.create_task(self.gateway.modem_enable(enable == "enable"))
        session["payload"] = None
        session["next"] = None
        session["has_next"] = False
        return session
