#!/usr/bin/env python3

import argparse
import os
import sys
import threading
import traceback
from typing import Any, Callable, Dict

import bridge_with_slack_config
import slack_sdk
from slack_sdk.rtm_v2 import RTMClient

import zulip

# change these templates to change the format of displayed message
ZULIP_MESSAGE_TEMPLATE = "**{username}**: {message}"
SLACK_MESSAGE_TEMPLATE = "<{username}> {message}"


def check_zulip_message_validity(msg: Dict[str, Any], config: Dict[str, Any]) -> bool:
    is_a_stream = msg["type"] == "stream"
    in_the_specified_stream = msg["display_recipient"] == config["stream"]
    at_the_specified_subject = msg["subject"] == config["topic"]

    # We do this to identify the messages generated from Matrix -> Zulip
    # and we make sure we don't forward it again to the Matrix.
    not_from_zulip_bot = msg["sender_email"] != config["email"]
    if is_a_stream and not_from_zulip_bot and in_the_specified_stream and at_the_specified_subject:
        return True
    return False


class SlackBridge:
    def __init__(self, config: Dict[str, Any]) -> None:
        self.config = config
        self.zulip_config = config["zulip"]
        self.slack_config = config["slack"]

        # zulip-specific
        self.zulip_client = zulip.Client(
            email=self.zulip_config["email"],
            api_key=self.zulip_config["api_key"],
            site=self.zulip_config["site"],
        )
        self.zulip_stream = self.zulip_config["stream"]
        self.zulip_subject = self.zulip_config["topic"]

        # slack-specific
        self.channel = self.slack_config["channel"]
        self.slack_client = rtm
        # Spawn a non-websocket client for getting the users
        # list and for posting messages in Slack.
        self.slack_webclient = slack_sdk.WebClient(token=self.slack_config["token"])

    def wrap_slack_mention_with_bracket(self, zulip_msg: Dict[str, Any]) -> None:
        words = zulip_msg["content"].split(" ")
        for w in words:
            if w.startswith("@"):
                zulip_msg["content"] = zulip_msg["content"].replace(w, "<" + w + ">")

    def replace_slack_id_with_name(self, msg: Dict[str, Any]) -> None:
        words = msg["text"].split(" ")
        for w in words:
            if w.startswith("<@") and w.endswith(">"):
                _id = w[2:-1]
                msg["text"] = msg["text"].replace(_id, self.slack_id_to_name[_id])

    def zulip_to_slack(self) -> Callable[[Dict[str, Any]], None]:
        def _zulip_to_slack(msg: Dict[str, Any]) -> None:
            message_valid = check_zulip_message_validity(msg, self.zulip_config)
            if message_valid:
                self.wrap_slack_mention_with_bracket(msg)
                slack_text = SLACK_MESSAGE_TEMPLATE.format(
                    username=msg["sender_full_name"], message=msg["content"]
                )
                self.slack_webclient.chat_postMessage(
                    channel=self.channel,
                    text=slack_text,
                )

        return _zulip_to_slack

    def run_slack_listener(self) -> None:
        members = self.slack_webclient.users_list()["members"]
        # See also https://api.slack.com/changelog/2017-09-the-one-about-usernames
        self.slack_id_to_name: Dict[str, str] = {
            u["id"]: u["profile"].get("display_name", u["profile"]["real_name"]) for u in members
        }
        self.slack_name_to_id = {v: k for k, v in self.slack_id_to_name.items()}

        @rtm.on("message")
        def slack_to_zulip(client: RTMClient, event: Dict[str, Any]) -> None:
            if event["channel"] != self.channel:
                return
            user_id = event["user"]
            user = self.slack_id_to_name[user_id]
            from_bot = user == self.slack_config["username"]
            if from_bot:
                return
            self.replace_slack_id_with_name(event)
            content = ZULIP_MESSAGE_TEMPLATE.format(username=user, message=event["text"])
            msg_data = dict(
                type="stream", to=self.zulip_stream, subject=self.zulip_subject, content=content
            )
            self.zulip_client.send_message(msg_data)

        self.slack_client.start()


if __name__ == "__main__":
    usage = """run-slack-bridge

    Relay each message received at a specified subject in a specified stream from
    the first realm to a channel in a Slack workspace.
    """

    sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
    parser = argparse.ArgumentParser(usage=usage)

    print("Starting slack mirroring bot")
    print("MAKE SURE THE BOT IS SUBSCRIBED TO THE RELEVANT ZULIP STREAM")

    config = bridge_with_slack_config.config

    # We have to define rtm outside of SlackBridge because the rtm variable is used as a method decorator.
    rtm = RTMClient(token=config["slack"]["token"])

    backoff = zulip.RandomExponentialBackoff(timeout_success_equivalent=300)
    while backoff.keep_going():
        try:
            sb = SlackBridge(config)

            zp = threading.Thread(
                target=sb.zulip_client.call_on_each_message, args=(sb.zulip_to_slack(),)
            )
            sp = threading.Thread(target=sb.run_slack_listener, args=())
            print("Starting message handler on Zulip client")
            zp.start()
            print("Starting message handler on Slack client")
            sp.start()

            zp.join()
            sp.join()
        except Exception:
            traceback.print_exc()
        backoff.fail()
