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

from abc import ABC
from typing import List, Callable, Awaitable

from aiohttp.web_request import Request
from aiohttp.web_response import Response
from botframework.connector.auth import ClaimsIdentity
from botbuilder.core import conversation_reference_extension
from botbuilder.core import BotAdapter, TurnContext
from botbuilder.schema import (
    Activity,
    ResourceResponse,
    ActivityTypes,
    ConversationAccount,
    ConversationReference,
)

from .activity_resourceresponse import ActivityResourceResponse
from .slack_client import SlackClient
from .slack_helper import SlackHelper
from .slack_adatper_options import SlackAdapterOptions


class SlackAdapter(BotAdapter, ABC):
    """
    BotAdapter that can handle incoming Slack events. Incoming Slack events are deserialized to an Activity that is
     dispatched through the middleware and bot pipeline.
    """

    def __init__(
        self,
        client: SlackClient,
        on_turn_error: Callable[[TurnContext, Exception], Awaitable] = None,
        options: SlackAdapterOptions = None,
    ):
        super().__init__(on_turn_error)
        self.slack_client = client
        self.slack_logged_in = False
        self.options = options if options else SlackAdapterOptions()

    async def send_activities(
        self, context: TurnContext, activities: List[Activity]
    ) -> List[ResourceResponse]:
        """
        Send a message from the bot to the messaging API.

        :param context: A TurnContext representing the current incoming message and environment.
        :type context: :class:`botbuilder.core.TurnContext`
        :param activities: An array of outgoing activities to be sent back to the messaging API.
        :type activities: :class:`typing.List[Activity]`
        :return: An array of ResourceResponse objects containing the IDs that Slack assigned to the sent messages.
        :rtype: :class:`typing.List[ResourceResponse]`
        """

        if not context:
            raise Exception("TurnContext is required")
        if not activities:
            raise Exception("List[Activity] is required")

        responses = []

        for activity in activities:
            if activity.type == ActivityTypes.message:
                message = SlackHelper.activity_to_slack(activity)

                slack_response = await self.slack_client.post_message(message)

                if slack_response and slack_response.status_code / 100 == 2:
                    resource_response = ActivityResourceResponse(
                        id=slack_response.data["ts"],
                        activity_id=slack_response.data["ts"],
                        conversation=ConversationAccount(
                            id=slack_response.data["channel"]
                        ),
                    )

                    responses.append(resource_response)

        return responses

    async def update_activity(self, context: TurnContext, activity: Activity):
        """
        Update a previous message with new content.

        :param context: A TurnContext representing the current incoming message and environment.
        :type context: :class:`botbuilder.core.TurnContext`
        :param activity: The updated activity in the form '{id: `id of activity to update`, ...}'.
        :type activity: :class:`botbuilder.schema.Activity`
        :return: A resource response with the ID of the updated activity.
        :rtype: :class:`botbuilder.schema.ResourceResponse`
        """

        if not context:
            raise Exception("TurnContext is required")
        if not activity:
            raise Exception("Activity is required")
        if not activity.id:
            raise Exception("Activity.id is required")
        if not activity.conversation:
            raise Exception("Activity.conversation is required")

        message = SlackHelper.activity_to_slack(activity)
        results = await self.slack_client.chat_update(
            ts=message.ts,
            channel=message.channel,
            text=message.text,
        )

        if results.status_code / 100 != 2:
            raise Exception(f"Error updating activity on slack: {results}")

        return ResourceResponse(id=activity.id)

    async def delete_activity(
        self, context: TurnContext, reference: ConversationReference
    ):
        """
        Delete a previous message.

        :param context: A TurnContext representing the current incoming message and environment.
        :type context: :class:`botbuilder.core.TurnContext`
        :param reference: An object in the form "{activityId: `id of message to delete`,conversation: { id: `id of Slack
         channel`}}".
        :type reference: :class:`botbuilder.schema.ConversationReference`
        """

        if not context:
            raise Exception("TurnContext is required")
        if not reference:
            raise Exception("ConversationReference is required")
        if not reference.channel_id:
            raise Exception("ConversationReference.channel_id is required")
        if not context.activity.timestamp:
            raise Exception("Activity.timestamp is required")

        await self.slack_client.chat_delete(
            channel=reference.conversation.id, ts=reference.activity_id
        )

    async def continue_conversation(
        self,
        reference: ConversationReference,
        callback: Callable,
        bot_id: str = None,  # pylint: disable=unused-argument
        claims_identity: ClaimsIdentity = None,
        audience: str = None,  # pylint: disable=unused-argument
    ):
        """
        Send a proactive message to a conversation.

        .. remarks::

            Most channels require a user to initiate a conversation with a bot before the bot can send activities to the
             user.

        :param reference: A reference to the conversation to continue.
        :type reference: :class:`botbuilder.schema.ConversationReference`
        :param callback: The method to call for the resulting bot turn.
        :type callback: :class:`typing.Callable`
        :param bot_id: Unused for this override.
        :type bot_id: str
        :param claims_identity: A ClaimsIdentity for the conversation.
        :type claims_identity: :class:`botframework.connector.auth.ClaimsIdentity`
        :param audience: Unused for this override.
        :type audience: str
        """

        if not reference:
            raise Exception("ConversationReference is required")
        if not callback:
            raise Exception("callback is required")

        if claims_identity:
            request = conversation_reference_extension.get_continuation_activity(
                reference
            )
            context = TurnContext(self, request)
            context.turn_state[BotAdapter.BOT_IDENTITY_KEY] = claims_identity
            context.turn_state[BotAdapter.BOT_CALLBACK_HANDLER_KEY] = callback
        else:
            request = TurnContext.apply_conversation_reference(
                conversation_reference_extension.get_continuation_activity(reference),
                reference,
            )
            context = TurnContext(self, request)

        return await self.run_pipeline(context, callback)

    async def process(self, req: Request, logic: Callable) -> Response:
        """
        Accept an incoming webhook request and convert it into a TurnContext which can be processed by the bot's logic.

        :param req: The aiohttp Request object.
        :type req: :class:`aiohttp.web_request.Request`
        :param logic: The method to call for the resulting bot turn.
        :type logic: :class:`tying.Callable`
        :return: The aiohttp Response.
        :rtype: :class:`aiohttp.web_response.Response`
        """

        if not req:
            raise Exception("Request is required")

        if not self.slack_logged_in:
            await self.slack_client.login_with_slack()
            self.slack_logged_in = True

        body = await req.text()

        if (
            self.options.verify_incoming_requests
            and not self.slack_client.verify_signature(req, body)
        ):
            return SlackHelper.response(
                req, 401, "Rejected due to mismatched header signature"
            )

        slack_body = SlackHelper.deserialize_body(req.content_type, body)

        if slack_body.type == "url_verification":
            return SlackHelper.response(req, 200, slack_body.challenge)

        if (
            not self.slack_client.options.slack_verification_token
            and slack_body.token != self.slack_client.options.slack_verification_token
        ):
            text = f"Rejected due to mismatched verificationToken:{body}"
            return SlackHelper.response(req, 403, text)

        if slack_body.payload:
            # handle interactive_message callbacks and block_actions
            activity = SlackHelper.payload_to_activity(slack_body.payload)
        elif slack_body.type == "event_callback":
            activity = await SlackHelper.event_to_activity(
                slack_body.event, self.slack_client
            )
        elif slack_body.command:
            activity = await SlackHelper.command_to_activity(
                slack_body, self.slack_client
            )
        else:
            return SlackHelper.response(
                req, 200, f"Unknown Slack event type {slack_body.type}"
            )

        context = TurnContext(self, activity)
        await self.run_pipeline(context, logic)

        return SlackHelper.response(req, 200)
