# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import hashlib
import hmac
import json
from io import IOBase
from typing import List, Union

import aiohttp
from aiohttp.web_request import Request

from slack.web.client import WebClient
from slack.web.slack_response import SlackResponse

from botbuilder.schema import Activity
from botbuilder.adapters.slack.slack_client_options import SlackClientOptions
from botbuilder.adapters.slack.slack_message import SlackMessage

POST_MESSAGE_URL = "https://slack.com/api/chat.postMessage"
POST_EPHEMERAL_MESSAGE_URL = "https://slack.com/api/chat.postEphemeral"


class SlackClient(WebClient):
    """
    Slack client that extends https://github.com/slackapi/python-slackclient.
    """

    def __init__(self, options: SlackClientOptions):
        if not options or not options.slack_bot_token:
            raise Exception("SlackAdapterOptions and bot_token are required")

        if (
            not options.slack_verification_token
            and not options.slack_client_signing_secret
        ):
            warning = (
                "\n****************************************************************************************\n"
                "* WARNING: Your bot is operating without recommended security mechanisms in place.     *\n"
                "* Initialize your adapter with a clientSigningSecret parameter to enable               *\n"
                "* verification that all incoming webhooks originate with Slack:                        *\n"
                "*                                                                                      *\n"
                "* adapter = new SlackAdapter({clientSigningSecret: <my secret from slack>});           *\n"
                "*                                                                                      *\n"
                "****************************************************************************************\n"
                ">> Slack docs: https://api.slack.com/docs/verifying-requests-from-slack"
            )
            raise Exception(
                warning
                + "Required: include a verificationToken or clientSigningSecret to verify incoming Events API webhooks"
            )

        super().__init__(token=options.slack_bot_token, run_async=True)

        self.options = options
        self.identity = None

    async def login_with_slack(self):
        if self.options.slack_bot_token:
            self.identity = await self.test_auth()
        elif (
            not self.options.slack_client_id
            or not self.options.slack_client_secret
            or not self.options.slack_redirect_uri
            or not self.options.slack_scopes
        ):
            raise Exception(
                "Missing Slack API credentials! Provide SlackClientId, SlackClientSecret, scopes and SlackRedirectUri "
                "as part of the SlackAdapter options."
            )

    def is_logged_in(self):
        return self.identity is not None

    async def test_auth(self) -> str:
        auth = await self.auth_test()
        return auth.data["user_id"]

    async def channels_list_ex(self, exclude_archived: bool = True) -> SlackResponse:
        args = {"exclude_archived": "1" if exclude_archived else "0"}
        return await self.channels_list(**args)

    async def users_counts(self) -> SlackResponse:
        return await self.api_call("users.counts")

    async def im_history_ex(
        self,
        channel: str,
        latest_timestamp: str = None,
        oldest_timestamp: str = None,
        count: int = None,
        unreads: bool = None,
    ) -> SlackResponse:
        args = {}
        if latest_timestamp:
            args["latest"] = latest_timestamp
        if oldest_timestamp:
            args["oldest"] = oldest_timestamp
        if count:
            args["count"] = str(count)
        if unreads:
            args["unreads"] = "1" if unreads else "0"

        return await self.im_history(channel=channel, **args)

    async def files_info_ex(
        self, file_id: str, page: int = None, count: int = None
    ) -> SlackResponse:
        args = {"count": str(count), "page": str(page)}
        return await self.files_info(file=file_id, **args)

    async def files_list_ex(
        self,
        user_id: str = None,
        date_from: str = None,
        date_to: str = None,
        count: int = None,
        page: int = None,
        types: List[str] = None,
    ) -> SlackResponse:
        args = {}

        if user_id:
            args["user"] = user_id

        if date_from:
            args["ts_from"] = date_from
        if date_to:
            args["ts_to"] = date_to

        if count:
            args["count"] = str(count)
        if page:
            args["page"] = str(page)

        if types:
            args["types"] = ",".join(types)

        return await self.files_list(**args)

    async def groups_history_ex(
        self, channel: str, latest: str = None, oldest: str = None, count: int = None
    ) -> SlackResponse:
        args = {}

        if latest:
            args["latest"] = latest
        if oldest:
            args["oldest"] = oldest

        if count:
            args["count"] = count

        return await self.groups_history(channel=channel, **args)

    async def groups_list_ex(self, exclude_archived: bool = True) -> SlackResponse:
        args = {"exclude_archived": "1" if exclude_archived else "0"}
        return await self.groups_list(**args)

    async def get_preferences(self) -> SlackResponse:
        return await self.api_call("users.prefs.get", http_verb="GET")

    async def stars_list_ex(
        self, user: str = None, count: int = None, page: int = None
    ) -> SlackResponse:
        args = {}

        if user:
            args["user"] = user
        if count:
            args["count"] = str(count)
        if page:
            args["page"] = str(page)

        return await self.stars_list(**args)

    async def groups_close(self, channel: str) -> SlackResponse:
        args = {"channel": channel}
        return await self.api_call("groups.close", params=args)

    async def chat_post_ephemeral_ex(
        self,
        channel: str,
        text: str,
        target_user: str,
        parse: str = None,
        link_names: bool = False,
        attachments: List[str] = None,  # pylint: disable=unused-argument
        as_user: bool = False,
    ) -> SlackResponse:
        args = {
            "text": text,
            "link_names": "1" if link_names else "0",
            "as_user": "1" if as_user else "0",
        }

        if parse:
            args["parse"] = parse

        # TODO: attachments (see PostEphemeralMessageAsync)
        # See: https://api.slack.com/messaging/composing/layouts#attachments
        # See: https://github.com/Inumedia/SlackAPI/blob/master/SlackAPI/Attachment.cs

        return await self.chat_postEphemeral(channel=channel, user=target_user, **args)

    async def chat_post_message_ex(
        self,
        channel: str,
        text: str,
        bot_name: str = None,
        parse: str = None,
        link_names: bool = False,
        blocks: List[str] = None,  # pylint: disable=unused-argument
        attachments: List[str] = None,  # pylint: disable=unused-argument
        unfurl_links: bool = False,
        icon_url: str = None,
        icon_emoji: str = None,
        as_user: bool = False,
    ) -> SlackResponse:
        args = {
            "text": text,
            "link_names": "1" if link_names else "0",
            "as_user": "1" if as_user else "0",
        }

        if bot_name:
            args["username"] = bot_name

        if parse:
            args["parse"] = parse

        if unfurl_links:
            args["unfurl_links"] = "1" if unfurl_links else "0"

        if icon_url:
            args["icon_url"] = icon_url

        if icon_emoji:
            args["icon_emoji"] = icon_emoji

        # TODO: blocks and attachments (see PostMessageAsync)
        # the blocks and attachments are combined into a single dict
        # See: https://api.slack.com/messaging/composing/layouts#attachments
        # See: https://github.com/Inumedia/SlackAPI/blob/master/SlackAPI/Attachment.cs

        return await self.chat_postMessage(channel=channel, **args)

    async def search_all_ex(
        self,
        query: str,
        sorting: str = None,
        direction: str = None,
        enable_highlights: bool = False,
        count: int = None,
        page: int = None,
    ) -> SlackResponse:
        args = {"highlight": "1" if enable_highlights else "0"}

        if sorting:
            args["sort"] = sorting

        if direction:
            args["sort_dir"] = direction

        if count:
            args["count"] = str(count)

        if page:
            args["page"] = str(page)

        return await self.search_all(query=query, **args)

    async def search_files_ex(
        self,
        query: str,
        sorting: str = None,
        direction: str = None,
        enable_highlights: bool = False,
        count: int = None,
        page: int = None,
    ) -> SlackResponse:
        args = {"highlight": "1" if enable_highlights else "0"}

        if sorting:
            args["sort"] = sorting

        if direction:
            args["sort_dir"] = direction

        if count:
            args["count"] = str(count)

        if page:
            args["page"] = str(page)

        return await self.search_files(query=query, **args)

    async def search_messages_ex(
        self,
        query: str,
        sorting: str = None,
        direction: str = None,
        enable_highlights: bool = False,
        count: int = None,
        page: int = None,
    ) -> SlackResponse:
        args = {"highlight": "1" if enable_highlights else "0"}

        if sorting:
            args["sort"] = sorting

        if direction:
            args["sort_dir"] = direction

        if count:
            args["count"] = str(count)

        if page:
            args["page"] = str(page)

        return await self.search_messages(query=query, **args)

    async def chat_update_ex(
        self,
        timestamp: str,
        channel: str,
        text: str,
        bot_name: str = None,
        parse: str = None,
        link_names: bool = False,
        attachments: List[str] = None,  # pylint: disable=unused-argument
        as_user: bool = False,
    ):
        args = {
            "text": text,
            "link_names": "1" if link_names else "0",
            "as_user": "1" if as_user else "0",
        }

        if bot_name:
            args["username"] = bot_name

        if parse:
            args["parse"] = parse

        # TODO: attachments (see PostEphemeralMessageAsync)
        # See: https://api.slack.com/messaging/composing/layouts#attachments
        # See: https://github.com/Inumedia/SlackAPI/blob/master/SlackAPI/Attachment.cs

        return await self.chat_update(channel=channel, ts=timestamp)

    async def files_upload_ex(
        self,
        file: Union[str, IOBase] = None,
        content: str = None,
        channels: List[str] = None,
        title: str = None,
        initial_comment: str = None,
        file_type: str = None,
    ):
        args = {}

        if channels:
            args["channels"] = ",".join(channels)

        if title:
            args["title"] = title

        if initial_comment:
            args["initial_comment"] = initial_comment

        if file_type:
            args["filetype"] = file_type

        return await self.files_upload(file=file, content=content, **args)

    async def get_bot_user_identity(
        self, activity: Activity  # pylint: disable=unused-argument
    ) -> str:
        return self.identity

    def verify_signature(self, req: Request, body: str) -> bool:
        timestamp = req.headers["X-Slack-Request-Timestamp"]
        message = ":".join(["v0", timestamp, body])

        computed_signature = "V0=" + hmac.new(
            bytes(self.options.slack_client_signing_secret, "utf-8"),
            msg=bytes(message, "utf-8"),
            digestmod=hashlib.sha256,
        ).hexdigest().upper().replace("-", "")

        received_signature = req.headers["X-Slack-Signature"].upper()

        return computed_signature == received_signature

    async def post_message(self, message: SlackMessage) -> SlackResponse:
        if not message:
            return None

        request_content = {
            "token": self.options.slack_bot_token,
            "channel": message.channel,
            "text": message.text,
        }

        if message.thread_ts:
            request_content["thread_ts"] = message.thread_ts

        if message.blocks:
            request_content["blocks"] = json.dumps(message.blocks)

        session = aiohttp.ClientSession(
            timeout=aiohttp.ClientTimeout(total=30),
        )

        http_verb = "POST"
        api_url = POST_EPHEMERAL_MESSAGE_URL if message.ephemeral else POST_MESSAGE_URL
        req_args = {"data": request_content}

        async with session.request(http_verb, api_url, **req_args) as res:
            response_content = {}
            try:
                response_content = await res.json()
            except aiohttp.ContentTypeError:
                pass

            response_data = {
                "data": response_content,
                "headers": res.headers,
                "status_code": res.status,
            }

            data = {
                "client": self,
                "http_verb": http_verb,
                "api_url": api_url,
                "req_args": req_args,
            }
            response = SlackResponse(**{**data, **response_data}).validate()

        await session.close()

        return response
