#!/usr/bin/env python3

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

import bridge_with_slack_config
from slack_sdk.web.client import WebClient

import zulip

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

StreamTopicT = Tuple[str, str]


def get_slack_channel_for_zulip_message(
    msg: Dict[str, Any], zulip_to_slack_map: Dict[StreamTopicT, Any], bot_email: str
) -> Optional[str]:
    is_a_stream = msg["type"] == "stream"
    if not is_a_stream:
        return None

    stream_name = msg["display_recipient"]
    topic_name = msg["subject"]
    stream_topic: StreamTopicT = (stream_name, topic_name)
    if stream_topic not in zulip_to_slack_map:
        return None

    # We do this to identify the messages generated from Slack -> Zulip
    # and we make sure we don't forward it again to the Slack.
    from_zulip_bot = msg["sender_email"] == bot_email
    if from_zulip_bot:
        return None
    return zulip_to_slack_map[stream_topic]


def check_token_access(token: str) -> None:
    if token.startswith("xoxp-"):
        print(
            "--- Warning! ---\n"
            "You entered a Slack user token, please copy the token under\n"
            "'Bot User OAuth Token' which starts with 'xoxb-...'."
        )
        sys.exit(1)
    elif token.startswith("xoxb-"):
        return


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

        self.zulip_to_slack_map: Dict[StreamTopicT, str] = {
            (z["channel"], z["topic"]): s for s, z in config["channel_mapping"].items()
        }

        # zulip-specific
        def zulip_client_constructor() -> zulip.Client:
            return zulip.Client(
                email=self.zulip_config["email"],
                api_key=self.zulip_config["api_key"],
                site=self.zulip_config["site"],
            )

        self.zulip_client = zulip_client_constructor()
        # Temporary workaround until
        # https://github.com/zulip/python-zulip-api/issues/761 is fixed.
        self.zulip_client_constructor = zulip_client_constructor

        # Spawn a non-websocket client for getting the users
        # list and for posting messages in Slack.
        self.slack_webclient = 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 is_message_from_slack(self, msg: Dict[str, Any]) -> bool:
        # Check whether or not this message is from Slack to prevent
        # them from being tossed back to Zulip.
        return msg["sender_email"] == self.zulip_config.get("email")

    def zulip_to_slack(self) -> Callable[[Dict[str, Any]], None]:
        def _zulip_to_slack(msg: Dict[str, Any]) -> None:
            slack_channel = get_slack_channel_for_zulip_message(
                msg, self.zulip_to_slack_map, self.zulip_config["email"]
            )

            if slack_channel is not None and not self.is_message_from_slack(msg):
                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=slack_channel,
                    text=slack_text,
                )

        return _zulip_to_slack


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)

    args = parser.parse_args()

    config: Dict[str, Any] = bridge_with_slack_config.config
    if "channel_mapping" not in config:
        print(
            'The key "channel_mapping" is not found in bridge_with_slack_config.py.\n'
            "Your config file may be outdated."
        )
        sys.exit(1)

    check_token_access(config["slack"]["token"])

    print("Starting slack mirroring bot")
    print("MAKE SURE THE BOT IS SUBSCRIBED TO THE RELEVANT ZULIP STREAM(S) & SLACK CHANNEL(S)!")

    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(),)
            )
            print("Starting message handler on Zulip client")
            zp.start()

            print(
                "Make sure your Slack Webhook integration is running\n"
                "to receive messages from Slack."
            )
            zp.join()
        except Exception:
            traceback.print_exc()
        backoff.fail()
