# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
from abc import ABC, abstractmethod

from botbuilder.core import (TurnContext, NullTelemetryClient, BotTelemetryClient)
from .dialog_reason import DialogReason
from .dialog_turn_status import DialogTurnStatus
from .dialog_turn_result import DialogTurnResult

class Dialog(ABC):
    end_of_turn = DialogTurnResult(DialogTurnStatus.Waiting)

    def __init__(self, dialog_id: str):
        if dialog_id == None or not dialog_id.strip():
            raise TypeError('Dialog(): dialogId cannot be None.')

        self._telemetry_client = NullTelemetryClient()
        self._id = dialog_id

    @property
    def id(self) -> str:
        return self._id

    @property
    def telemetry_client(self) -> BotTelemetryClient:
        """
        Gets the telemetry client for logging events.
        """
        return self._telemetry_client

    @telemetry_client.setter
    def telemetry_client(self, value: BotTelemetryClient) -> None:
        """
        Sets the telemetry client for logging events.
        """
        if value is None:
            self._telemetry_client = NullTelemetryClient()
        else:
            self._telemetry_client = value

    @abstractmethod
    async def begin_dialog(self, dc, options: object = None):
        """
        Method called when a new dialog has been pushed onto the stack and is being activated.
        :param dc: The dialog context for the current turn of conversation.
        :param options: (Optional) additional argument(s) to pass to the dialog being started.
        """
        raise NotImplementedError()

    async def continue_dialog(self, dc):
        """
        Method called when an instance of the dialog is the "current" dialog and the
        user replies with a new activity. The dialog will generally continue to receive the user's
        replies until it calls either `end_dialog()` or `begin_dialog()`.
        If this method is NOT implemented then the dialog will automatically be ended when the user replies.
        :param dc: The dialog context for the current turn of conversation.
        :return:
        """
        # By default just end the current dialog.
        return await dc.end_dialog(None)

    async def resume_dialog(self, dc, reason: DialogReason, result: object):
        """
        Method called when an instance of the dialog is being returned to from another
        dialog that was started by the current instance using `begin_dialog()`.
        If this method is NOT implemented then the dialog will be automatically ended with a call
        to `end_dialog()`. Any result passed from the called dialog will be passed
        to the current dialog's parent.
        :param dc: The dialog context for the current turn of conversation.
        :param reason: Reason why the dialog resumed.
        :param result: (Optional) value returned from the dialog that was called. The type of the value returned is dependent on the dialog that was called.
        :return:
        """
        # By default just end the current dialog.
        return await dc.EndDialog(result)

    # TODO: instance is DialogInstance
    async def reprompt_dialog(self, context: TurnContext, instance):
        """
        :param context:
        :return:
        """
        # No-op by default
        return
    # TODO: instance is DialogInstance
    async def end_dialog(self, context: TurnContext, instance):
        """
        :param context:
        :return:
        """
        # No-op by default
        return


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

from pathlib import PurePosixPath
from typing import Tuple
from urllib.parse import ParseResult, parse_qs, unquote, urlparse, urlunparse
from uuid import UUID, uuid4

class LuisApplication(object):
    """
    Data describing a LUIS application.
    """

    def __init__(self, application_id: str, endpoint_key: str, endpoint: str):
        """Initializes a new instance of the <see cref="LuisApplication"/> class.

        :param application_id: LUIS application ID.
        :type application_id: str
        :param endpoint_key: LUIS subscription or endpoint key.
        :type endpoint_key: str
        :param endpoint: LUIS endpoint to use like https://westus.api.cognitive.microsoft.com.
        :type endpoint: str
        :raises ValueError:
        :raises ValueError:
        :raises ValueError:
        """

        _, valid = LuisApplication._try_parse_uuid4(application_id)
        if not valid:
            raise ValueError(f'"{application_id}" is not a valid LUIS application id.')

        _, valid = LuisApplication._try_parse_uuid4(endpoint_key)
        if not valid:
            raise ValueError(f'"{endpoint_key}" is not a valid LUIS subscription key.')

        if not endpoint or endpoint.isspace():
            endpoint = "https://westus.api.cognitive.microsoft.com"

        _, valid = LuisApplication._try_parse_url(endpoint)
        if not valid:
            raise ValueError(f'"{endpoint}" is not a valid LUIS endpoint.')

        self._application_id = application_id
        self._endpoint_key = endpoint_key
        self._endpoint = endpoint

    @classmethod
    def from_application_endpoint(cls, application_endpoint: str):
        """Initializes a new instance of the <see cref="LuisApplication"/> class.

        :param application_endpoint: LUIS application endpoint.
        :type application_endpoint: str
        :return:
        :rtype: LuisApplication
        """
        (application_id, endpoint_key, endpoint) = LuisApplication._parse(
            application_endpoint
        )
        return cls(application_id, endpoint_key, endpoint)

    @property
    def application_id(self) -> str:
        """Gets LUIS application ID.

        :return: LUIS application ID.
        :rtype: str
        """

        return self._application_id

    @application_id.setter
    def application_id(self, value: str) -> None:
        """Sets LUIS application ID.

        :param value: LUIS application ID.
        :type value: str
        :return:
        :rtype: None
        """

        self._application_id = value

    @property
    def endpoint_key(self) -> str:
        """Gets LUIS subscription or endpoint key.

        :return: LUIS subscription or endpoint key.
        :rtype: str
        """

        return self._endpoint_key

    @endpoint_key.setter
    def endpoint_key(self, value: str) -> None:
        """Sets LUIS subscription or endpoint key.

        :param value: LUIS subscription or endpoint key.
        :type value: str
        :return:
        :rtype: None
        """

        self._endpoint_key = value

    @property
    def endpoint(self) -> str:
        """Gets LUIS endpoint like https://westus.api.cognitive.microsoft.com.

        :return: LUIS endpoint where application is hosted.
        :rtype: str
        """

        return self._endpoint

    @endpoint.setter
    def endpoint(self, value: str) -> None:
        """Sets LUIS endpoint like https://westus.api.cognitive.microsoft.com.

        :param value: LUIS endpoint where application is hosted.
        :type value: str
        :return:
        :rtype: None
        """

        self._endpoint = value

    @staticmethod
    def _parse(application_endpoint: str) -> Tuple[str, str, str]:
        url, valid = LuisApplication._try_parse_url(application_endpoint)
        if not valid:
            raise ValueError(
                f"{application_endpoint} is not a valid LUIS application endpoint."
            )

        segments = PurePosixPath(unquote(url.path)).parts
        application_id = segments[-1] if segments else None
        qs_parsed_result = parse_qs(url.query)
        endpoint_key = qs_parsed_result.get("subscription-key", [None])[0]

        parts_for_base_url = url.scheme, url.netloc, "", None, None, None
        endpoint = urlunparse(parts_for_base_url)
        return (application_id, endpoint_key, endpoint)

    @staticmethod
    def _try_parse_uuid4(uuid_string: str) -> Tuple[uuid4, bool]:
        try:
            uuid = UUID(uuid_string, version=4)
        except (TypeError, ValueError):
            return None, False

        return uuid, True

    @staticmethod
    def _try_parse_url(url: str) -> Tuple[ParseResult, bool]:
        try:
            result = urlparse(url)
            return result, True
        except ValueError:
            return None, False


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

import json
from typing import Dict, List, Tuple, Union

from azure.cognitiveservices.language.luis.runtime import LUISRuntimeClient
from azure.cognitiveservices.language.luis.runtime.models import LuisResult
from msrest.authentication import CognitiveServicesCredentials

from botbuilder.core import (
    BotAssert,
    BotTelemetryClient,
    NullTelemetryClient,
    TurnContext,
)
from botbuilder.schema import Activity, ActivityTypes, ChannelAccount

from . import (
    IntentScore,
    LuisApplication,
    LuisPredictionOptions,
    LuisTelemetryConstants,
    RecognizerResult,
)
from .activity_util import ActivityUtil
from .luis_util import LuisUtil

class LuisRecognizer(object):
    """
    A LUIS based implementation of <see cref="IRecognizer"/>.
    """

    # The value type for a LUIS trace activity.
    luis_trace_type: str = "https://www.luis.ai/schemas/trace"

    # The context label for a LUIS trace activity.
    luis_trace_label: str = "Luis Trace"

    def __init__(
        self,
        application: Union[LuisApplication, str],
        prediction_options: LuisPredictionOptions = None,
        include_api_results: bool = False,
    ):
        """Initializes a new instance of the <see cref="LuisRecognizer"/> class.

        :param application: The LUIS application to use to recognize text.
        :type application: LuisApplication
        :param prediction_options: The LUIS prediction options to use, defaults to None
        :param prediction_options: LuisPredictionOptions, optional
        :param include_api_results: True to include raw LUIS API response, defaults to False
        :param include_api_results: bool, optional
        :raises TypeError:
        """

        if isinstance(application, LuisApplication):
            self._application = application
        elif isinstance(application, str):
            self._application = LuisApplication.from_application_endpoint(application)
        else:
            raise TypeError(
                "LuisRecognizer.__init__(): application is not an instance of LuisApplication or str."
            )

        self._options = prediction_options or LuisPredictionOptions()

        self._include_api_results = include_api_results

        self._telemetry_client = self._options.telemetry_client
        self._log_personal_information = self._options.log_personal_information

        credentials = CognitiveServicesCredentials(self._application.endpoint_key)
        self._runtime = LUISRuntimeClient(self._application.endpoint, credentials)
        self._runtime.config.add_user_agent(LuisUtil.get_user_agent())
        self._runtime.config.connection.timeout = self._options.timeout // 1000

    @property
    def log_personal_information(self) -> bool:
        """Gets a value indicating whether to log personal information that came from the user to telemetry.

        :return: If True, personal information is logged to Telemetry; otherwise the properties will be filtered.
        :rtype: bool
        """

        return self._log_personal_information

    @log_personal_information.setter
    def log_personal_information(self, value: bool) -> None:
        """Sets a value indicating whether to log personal information that came from the user to telemetry.

        :param value: If True, personal information is logged to Telemetry; otherwise the properties will be filtered.
        :type value: bool
        :return:
        :rtype: None
        """

        self._log_personal_information = value

    @property
    def telemetry_client(self) -> BotTelemetryClient:
        """Gets the currently configured <see cref="BotTelemetryClient"/> that logs the LuisResult event.

        :return: The <see cref="BotTelemetryClient"/> being used to log events.
        :rtype: BotTelemetryClient
        """

        return self._telemetry_client

    @telemetry_client.setter
    def telemetry_client(self, value: BotTelemetryClient):
        """Gets the currently configured <see cref="BotTelemetryClient"/> that logs the LuisResult event.

        :param value: The <see cref="BotTelemetryClient"/> being used to log events.
        :type value: BotTelemetryClient
        """

        self._telemetry_client = value

    @staticmethod
    def top_intent(
        results: RecognizerResult, default_intent: str = "None", min_score: float = 0.0
    ) -> str:
        """Returns the name of the top scoring intent from a set of LUIS results.

        :param results: Result set to be searched.
        :type results: RecognizerResult
        :param default_intent: Intent name to return should a top intent be found, defaults to "None"
        :param default_intent: str, optional
        :param min_score: Minimum score needed for an intent to be considered as a top intent. If all intents in the set are below this threshold then the `defaultIntent` will be returned, defaults to 0.0
        :param min_score: float, optional
        :raises TypeError:
        :return: The top scoring intent name.
        :rtype: str
        """

        if results is None:
            raise TypeError("LuisRecognizer.top_intent(): results cannot be None.")

        top_intent: str = None
        top_score: float = -1.0
        if results.intents:
            for intent_name, intent_score in results.intents.items():
                score = intent_score.score
                if score > top_score and score >= min_score:
                    top_intent = intent_name
                    top_score = score

        return top_intent or default_intent

    async def recognize(
        self,
        turn_context: TurnContext,
        telemetry_properties: Dict[str, str] = None,
        telemetry_metrics: Dict[str, float] = None,
    ) -> RecognizerResult:
        """Return results of the analysis (Suggested actions and intents).

        :param turn_context: Context object containing information for a single turn of conversation with a user.
        :type turn_context: TurnContext
        :param telemetry_properties: Additional properties to be logged to telemetry with the LuisResult event, defaults to None
        :param telemetry_properties: Dict[str, str], optional
        :param telemetry_metrics: Additional metrics to be logged to telemetry with the LuisResult event, defaults to None
        :param telemetry_metrics: Dict[str, float], optional
        :return: The LUIS results of the analysis of the current message text in the current turn's context activity.
        :rtype: RecognizerResult
        """

        return await self._recognize_internal(
            turn_context, telemetry_properties, telemetry_metrics
        )

    def on_recognizer_result(
        self,
        recognizer_result: RecognizerResult,
        turn_context: TurnContext,
        telemetry_properties: Dict[str, str] = None,
        telemetry_metrics: Dict[str, float] = None,
    ):
        """Invoked prior to a LuisResult being logged.

        :param recognizer_result: The Luis Results for the call.
        :type recognizer_result: RecognizerResult
        :param turn_context: Context object containing information for a single turn of conversation with a user.
        :type turn_context: TurnContext
        :param telemetry_properties: Additional properties to be logged to telemetry with the LuisResult event, defaults to None
        :param telemetry_properties: Dict[str, str], optional
        :param telemetry_metrics: Additional metrics to be logged to telemetry with the LuisResult event, defaults to None
        :param telemetry_metrics: Dict[str, float], optional
        """

        properties = self.fill_luis_event_properties(
            recognizer_result, turn_context, telemetry_properties
        )

        # Track the event
        self.telemetry_client.track_event(
            LuisTelemetryConstants.luis_result, properties, telemetry_metrics
        )

    @staticmethod
    def _get_top_k_intent_score(
        intent_names: List[str], intents: Dict[str, IntentScore], index: int
    ) -> Tuple[str, str]:
        intent_name = ""
        intent_score = "0.00"
        if intent_names:
            intent_name = intent_names[0]
            if intents[intent_name] is not None:
                intent_score = "{:.2f}".format(intents[intent_name].score)

        return intent_name, intent_score

    def fill_luis_event_properties(
        self,
        recognizer_result: RecognizerResult,
        turn_context: TurnContext,
        telemetry_properties: Dict[str, str] = None,
    ) -> Dict[str, str]:
        """Fills the event properties for LuisResult event for telemetry.
        These properties are logged when the recognizer is called.

        :param recognizer_result: Last activity sent from user.
        :type recognizer_result: RecognizerResult
        :param turn_context: Context object containing information for a single turn of conversation with a user.
        :type turn_context: TurnContext
        :param telemetry_properties: Additional properties to be logged to telemetry with the LuisResult event, defaults to None
        :param telemetry_properties: Dict[str, str], optional
        :return: A dictionary that is sent as "Properties" to IBotTelemetryClient.TrackEvent method for the BotMessageSend event.
        :rtype: Dict[str, str]
        """

        intents = recognizer_result.intents
        top_two_intents = (
            sorted(intents.keys(), key=lambda k: intents[k].score, reverse=True)[:2]
            if intents
            else []
        )

        intent_name, intent_score = LuisRecognizer._get_top_k_intent_score(
            top_two_intents, intents, index=0
        )
        intent2_name, intent2_score = LuisRecognizer._get_top_k_intent_score(
            top_two_intents, intents, index=1
        )

        # Add the intent score and conversation id properties
        properties: Dict[str, str] = {
            LuisTelemetryConstants.application_id_property: self._application.application_id,
            LuisTelemetryConstants.intent_property: intent_name,
            LuisTelemetryConstants.intent_score_property: intent_score,
            LuisTelemetryConstants.intent2_property: intent2_name,
            LuisTelemetryConstants.intent_score2_property: intent2_score,
            LuisTelemetryConstants.from_id_property: turn_context.activity.from_property.id,
        }

        sentiment = recognizer_result.properties.get("sentiment")
        if sentiment is not None and isinstance(sentiment, Dict):
            label = sentiment.get("label")
            if label is not None:
                properties[LuisTelemetryConstants.sentiment_label_property] = str(label)

            score = sentiment.get("score")
            if score is not None:
                properties[LuisTelemetryConstants.sentiment_score_property] = str(score)

        entities = None
        if recognizer_result.entities is not None:
            entities = json.dumps(recognizer_result.entities)
        properties[LuisTelemetryConstants.entities_property] = entities

        # Use the LogPersonalInformation flag to toggle logging PII data, text is a common example
        if self.log_personal_information and turn_context.activity.text:
            properties[
                LuisTelemetryConstants.question_property
            ] = turn_context.activity.text

        # Additional Properties can override "stock" properties.
        if telemetry_properties is not None:
            for key in telemetry_properties:
                properties[key] = telemetry_properties[key]

        return properties

    async def _recognize_internal(
        self,
        turn_context: TurnContext,
        telemetry_properties: Dict[str, str],
        telemetry_metrics: Dict[str, float],
    ) -> RecognizerResult:

        BotAssert.context_not_null(turn_context)

        if turn_context.activity.type != ActivityTypes.message:
            return None

        utterance: str = turn_context.activity.text if turn_context.activity is not None else None
        recognizer_result: RecognizerResult = None
        luis_result: LuisResult = None

        if not utterance or utterance.isspace():
            recognizer_result = RecognizerResult(
                text=utterance, intents={"": IntentScore(score=1.0)}, entities={}
            )
        else:
            luis_result = self._runtime.prediction.resolve(
                self._application.application_id,
                utterance,
                timezone_offset=self._options.timezone_offset,
                verbose=self._options.include_all_intents,
                staging=self._options.staging,
                spell_check=self._options.spell_check,
                bing_spell_check_subscription_key=self._options.bing_spell_check_subscription_key,
                log=self._options.log if self._options.log is not None else True,
            )

            recognizer_result = RecognizerResult(
                text=utterance,
                altered_text=luis_result.altered_query,
                intents=LuisUtil.get_intents(luis_result),
                entities=LuisUtil.extract_entities_and_metadata(
                    luis_result.entities,
                    luis_result.composite_entities,
                    self._options.include_instance_data
                    if self._options.include_instance_data is not None
                    else True,
                ),
            )
            LuisUtil.add_properties(luis_result, recognizer_result)
            if self._include_api_results:
                recognizer_result.properties["luisResult"] = luis_result

        # Log telemetry
        self.on_recognizer_result(
            recognizer_result, turn_context, telemetry_properties, telemetry_metrics
        )

        trace_info: Dict[str, object] = {
            "recognizerResult": recognizer_result,
            "luisModel": {"ModelID": self._application.application_id},
            "luisOptions": self._options,
            "luisResult": luis_result,
        }

        trace_activity = ActivityUtil.create_trace(
            turn_context.activity,
            "LuisRecognizer",
            trace_info,
            LuisRecognizer.luis_trace_type,
            LuisRecognizer.luis_trace_label,
        )

        await turn_context.send_activity(trace_activity)

        return recognizer_result


# --------------------------------------------------------------------------
#
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the ""Software""), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#
# --------------------------------------------------------------------------

import asyncio
from collections.abc import AsyncIterator
import functools
import logging

from oauthlib import oauth2
import requests

from msrest.exceptions import (
    TokenExpiredError,
    ClientRequestError,
    raise_with_traceback,
)

_LOGGER = logging.getLogger(__name__)

class AsyncServiceClientMixin:

    async def async_send_formdata(self, request, headers=None, content=None, **config):
        """Send data as a multipart form-data request.
        We only deal with file-like objects or strings at this point.
        The requests is not yet streamed.
        :param ClientRequest request: The request object to be sent.
        :param dict headers: Any headers to add to the request.
        :param dict content: Dictionary of the fields of the formdata.
        :param config: Any specific config overrides.
        """
        files = self._prepare_send_formdata(request, headers, content)
        return await self.async_send(request, headers, files=files, **config)

    async def async_send(self, request, headers=None, content=None, **config):
        """Prepare and send request object according to configuration.
        :param ClientRequest request: The request object to be sent.
        :param dict headers: Any headers to add to the request.
        :param content: Any body data to add to the request.
        :param config: Any specific config overrides
        """
        loop = asyncio.get_event_loop()
        if self.config.keep_alive and self._session is None:
            self._session = requests.Session()
        try:
            session = self.creds.signed_session(self._session)
        except TypeError:  # Credentials does not support session injection
            session = self.creds.signed_session()
            if self._session is not None:
                _LOGGER.warning(
                    "Your credentials class does not support session injection. Performance will not be at the maximum.")

        kwargs = self._configure_session(session, **config)
        if headers:
            request.headers.update(headers)

        if not kwargs.get('files'):
            request.add_content(content)
        if request.data:
            kwargs['data'] = request.data
        kwargs['headers'].update(request.headers)

        response = None
        try:

            try:
                future = loop.run_in_executor(
                    None,
                    functools.partial(
                        session.request,
                        request.method,
                        request.url,
                        **kwargs
                    )
                )
                return await future

            except (oauth2.rfc6749.errors.InvalidGrantError,
                    oauth2.rfc6749.errors.TokenExpiredError) as err:
                error = "Token expired or is invalid. Attempting to refresh."
                _LOGGER.warning(error)

            try:
                session = self.creds.refresh_session()
                kwargs = self._configure_session(session)
                if request.data:
                    kwargs['data'] = request.data
                kwargs['headers'].update(request.headers)

                future = loop.run_in_executor(
                    None,
                    functools.partial(
                        session.request,
                        request.method,
                        request.url,
                        **kwargs
                    )
                )
                return await future
            except (oauth2.rfc6749.errors.InvalidGrantError,
                    oauth2.rfc6749.errors.TokenExpiredError) as err:
                msg = "Token expired or is invalid."
                raise_with_traceback(TokenExpiredError, msg, err)

        except (requests.RequestException,
                oauth2.rfc6749.errors.OAuth2Error) as err:
            msg = "Error occurred in request."
            raise_with_traceback(ClientRequestError, msg, err)
        finally:
            self._close_local_session_if_necessary(response, session, kwargs['stream'])

    def stream_download_async(self, response, user_callback):
        """Async Generator for streaming request body data.
        :param response: The initial response
        :param user_callback: Custom callback for monitoring progress.
        """
        block = self.config.connection.data_block_size
        return StreamDownloadGenerator(response, user_callback, block)

class _MsrestStopIteration(Exception):
    pass

def _msrest_next(iterator):
    """"To avoid:
    TypeError: StopIteration interacts badly with generators and cannot be raised into a Future
    """
    try:
        return next(iterator)
    except StopIteration:
        raise _MsrestStopIteration()

class StreamDownloadGenerator(AsyncIterator):

    def __init__(self, response, user_callback, block):
        self.response = response
        self.block = block
        self.user_callback = user_callback
        self.iter_content_func = self.response.iter_content(self.block)

    async def __anext__(self):
        loop = asyncio.get_event_loop()
        try:
            chunk = await loop.run_in_executor(
                None,
                _msrest_next,
                self.iter_content_func,
            )
            if not chunk:
                raise _MsrestStopIteration()
            if self.user_callback and callable(self.user_callback):
                self.user_callback(chunk, self.response)
            return chunk
        except _MsrestStopIteration:
            self.response.close()
            raise StopAsyncIteration()
        except Exception as err:
            _LOGGER.warning("Unable to stream download: %s", err)
            self.response.close()
            raise


"""CosmosDB Middleware for Python Bot Framework.

This is middleware to store items in CosmosDB.
Part of the Azure Bot Framework in Python.
"""

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

from typing import Dict, List
import json
from botbuilder.core.storage import Storage, StoreItem
import azure.cosmos.cosmos_client as cosmos_client
import azure.cosmos.errors as cosmos_errors

class CosmosDbConfig():
    """The class for CosmosDB configuration for the Azure Bot Framework."""

    def __init__(self, **kwargs):
        """Create the Config object.

        :param endpoint:
        :param masterkey:
        :param database:
        :param container:
        :param filename:
        :return CosmosDbConfig:
        """
        self.__config_file = kwargs.pop('filename', None)
        if self.__config_file:
            kwargs = json.load(open(self.__config_file))
        self.endpoint = kwargs.pop('endpoint')
        self.masterkey = kwargs.pop('masterkey')
        self.database = kwargs.pop('database', 'bot_db')
        self.container = kwargs.pop('container', 'bot_container')

class CosmosDbStorage(Storage):
    """The class for CosmosDB middleware for the Azure Bot Framework."""

    def __init__(self, config: CosmosDbConfig):
        """Create the storage object.

        :param config:
        """
        super(CosmosDbStorage, self).__init__()
        self.config = config
        self.client = cosmos_client.CosmosClient(
            self.config.endpoint,
            {'masterKey': self.config.masterkey}
            )
        # these are set by the functions that check
        # the presence of the db and container or creates them
        self.db = None
        self.container = None

    async def read(self, keys: List[str]) -> dict:
        """Read storeitems from storage.

        :param keys:
        :return dict:
        """
        try:
            # check if the database and container exists and if not create
            if not self.__container_exists:
                self.__create_db_and_container()
            if len(keys) > 0:
                # create the parameters object
                parameters = [
                    {'name': f'@id{i}', 'value': f'{self.__sanitize_key(key)}'}
                    for i, key in enumerate(keys)
                    ]
                # get the names of the params
                parameter_sequence = ','.join(param.get('name')
                                              for param in parameters)
                # create the query
                query = {
                    "query":
                    f"SELECT c.id, c.realId, c.document, c._etag \
FROM c WHERE c.id in ({parameter_sequence})",
                    "parameters": parameters
                    }
                options = {'enableCrossPartitionQuery': True}
                # run the query and store the results as a list
                results = list(
                    self.client.QueryItems(
                        self.__container_link, query, options)
                    )
                # return a dict with a key and a StoreItem
                return {
                    r.get('realId'): self.__create_si(r) for r in results
                    }
            else:
                raise Exception('cosmosdb_storage.read(): \
provide at least one key')
        except TypeError as e:
            raise e

    async def write(self, changes: Dict[str, StoreItem]):
        """Save storeitems to storage.

        :param changes:
        :return:
        """
        try:
            # check if the database and container exists and if not create
            if not self.__container_exists:
                self.__create_db_and_container()
                # iterate over the changes
            for (key, change) in changes.items():
                # store the e_tag
                e_tag = change.e_tag
                # create the new document
                doc = {'id': self.__sanitize_key(key),
                       'realId': key,
                       'document': self.__create_dict(change)
                       }
                # the e_tag will be * for new docs so do an insert
                if (e_tag == '*' or not e_tag):
                    self.client.UpsertItem(
                        database_or_Container_link=self.__container_link,
                        document=doc,
                        options={'disableAutomaticIdGeneration': True}
                        )
                # if we have an etag, do opt. concurrency replace
                elif(len(e_tag) > 0):
                    access_condition = {'type': 'IfMatch', 'condition': e_tag}
                    self.client.ReplaceItem(
                        document_link=self.__item_link(
                            self.__sanitize_key(key)),
                        new_document=doc,
                        options={'accessCondition': access_condition}
                        )
                # error when there is no e_tag
                else:
                    raise Exception('cosmosdb_storage.write(): etag missing')
        except Exception as e:
            raise e

    async def delete(self, keys: List[str]):
        """Remove storeitems from storage.

        :param keys:
        :return:
        """
        try:
            # check if the database and container exists and if not create
            if not self.__container_exists:
                self.__create_db_and_container()
            # call the function for each key
            for k in keys:
                self.client.DeleteItem(
                    document_link=self.__item_link(self.__sanitize_key(k)))
                # print(res)
        except cosmos_errors.HTTPFailure as h:
            # print(h.status_code)
            if h.status_code != 404:
                raise h
        except TypeError as e:
            raise e

    def __create_si(self, result) -> StoreItem:
        """Create a StoreItem from a result out of CosmosDB.

        :param result:
        :return StoreItem:
        """
        # get the document item from the result and turn into a dict
        doc = result.get('document')
        # readd the e_tag from Cosmos
        doc['e_tag'] = result.get('_etag')
        # create and return the StoreItem
        return StoreItem(**doc)

    def __create_dict(self, si: StoreItem) -> Dict:
        """Return the dict of a StoreItem.

        This eliminates non_magic attributes and the e_tag.

        :param si:
        :return dict:
        """
        # read the content
        non_magic_attr = ([attr for attr in dir(si)
                          if not attr.startswith('_') or attr.__eq__('e_tag')])
        # loop through attributes and write and return a dict
        return ({attr: getattr(si, attr)
                for attr in non_magic_attr})

    def __sanitize_key(self, key) -> str:
        """Return the sanitized key.

        Replace characters that are not allowed in keys in Cosmos.

        :param key:
        :return str:
        """
        # forbidden characters
        bad_chars = ['\\', '?', '/', '#', '\t', '\n', '\r']
        # replace those with with '*' and the
        # Unicode code point of the character and return the new string
        return ''.join(
            map(
                lambda x: '*'+str(ord(x)) if x in bad_chars else x, key
                )
            )

    def __item_link(self, id) -> str:
        """Return the item link of a item in the container.

        :param id:
        :return str:
        """
        return self.__container_link + '/docs/' + id

    @property
    def __container_link(self) -> str:
        """Return the container link in the database.

        :param:
        :return str:
        """
        return self.__database_link + '/colls/' + self.container

    @property
    def __database_link(self) -> str:
        """Return the database link.

        :return str:
        """
        return 'dbs/' + self.db

    @property
    def __container_exists(self) -> bool:
        """Return whether the database and container have been created.

        :return bool:
        """
        return self.db and self.container

    def __create_db_and_container(self):
        """Call the get or create methods."""
        db_id = self.config.database
        container_name = self.config.container
        self.db = self.__get_or_create_database(self.client, db_id)
        self.container = self.__get_or_create_container(
            self.client, container_name
            )

    def __get_or_create_database(self, doc_client, id) -> str:
        """Return the database link.

        Check if the database exists or create the db.

        :param doc_client:
        :param id:
        :return str:
        """
        # query CosmosDB for a database with that name/id
        dbs = list(doc_client.QueryDatabases({
            "query": "SELECT * FROM r WHERE r.id=@id",
            "parameters": [
                {"name": "@id", "value": id}
            ]
        }))
        # if there are results, return the first (db names are unique)
        if len(dbs) > 0:
            return dbs[0]['id']
        else:
            # create the database if it didn't exist
            res = doc_client.CreateDatabase({'id': id})
            return res['id']

    def __get_or_create_container(self, doc_client, container) -> str:
        """Return the container link.

        Check if the container exists or create the container.

        :param doc_client:
        :param container:
        :return str:
        """
        # query CosmosDB for a container in the database with that name
        containers = list(doc_client.QueryContainers(
            self.__database_link,
            {
                "query": "SELECT * FROM r WHERE r.id=@id",
                "parameters": [
                    {"name": "@id", "value": container}
                ]
            }
        ))
        # if there are results, return the first (container names are unique)
        if len(containers) > 0:
            return containers[0]['id']
        else:
            # Create a container if it didn't exist
            res = doc_client.CreateContainer(
                self.__database_link, {'id': container})
            return res['id']


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

from botbuilder.schema import Activity
from botbuilder.core import BotTelemetryClient, NullTelemetryClient, TurnContext
from copy import copy
import json, requests
from typing import Dict, List, NamedTuple

from .metadata import Metadata
from .query_result import QueryResult
from .qnamaker_endpoint import QnAMakerEndpoint
from .qnamaker_options import QnAMakerOptions
from .qnamaker_telemetry_client import QnAMakerTelemetryClient
from .qna_telemetry_constants import QnATelemetryConstants
from .qnamaker_trace_info import QnAMakerTraceInfo

QNAMAKER_TRACE_NAME = 'QnAMaker'
QNAMAKER_TRACE_LABEL = 'QnAMaker Trace'
QNAMAKER_TRACE_TYPE = 'https://www.qnamaker.ai/schemas/trace'

class EventData(NamedTuple):
    properties: Dict[str, str]
    metrics: Dict[str, float]

class QnAMaker(QnAMakerTelemetryClient):
    """
    Class used to query a QnA Maker knowledge base for answers.
    """

    def __init__(
        self,
        endpoint: QnAMakerEndpoint,
        options: QnAMakerOptions = None,
        telemetry_client: BotTelemetryClient = None,
        log_personal_information: bool = None
    ):
        if not isinstance(endpoint, QnAMakerEndpoint):
            raise TypeError('QnAMaker.__init__(): endpoint is not an instance of QnAMakerEndpoint')

        if endpoint.host.endswith('v2.0'):
            raise ValueError('v2.0 of QnA Maker service is no longer supported in the Bot Framework. Please upgrade your QnA Maker service at www.qnamaker.ai.')

        self._endpoint = endpoint
        self._is_legacy_protocol: bool = self._endpoint.host.endswith('v3.0')

        self._options: QnAMakerOptions = options or QnAMakerOptions()
        self._validate_options(self._options)

        self._telemetry_client = telemetry_client or NullTelemetryClient()
        self._log_personal_information = log_personal_information or False

    @property
    def log_personal_information(self) -> bool:
        """Gets a value indicating whether to log personal information that came from the user to telemetry.

        :return: If True, personal information is logged to Telemetry; otherwise the properties will be filtered.
        :rtype: bool
        """

        return self._log_personal_information

    @log_personal_information.setter
    def log_personal_information(self, value: bool) -> None:
        """Sets a value indicating whether to log personal information that came from the user to telemetry.

        :param value: If True, personal information is logged to Telemetry; otherwise the properties will be filtered.
        :type value: bool
        :return:
        :rtype: None
        """

        self._log_personal_information = value

    @property
    def telemetry_client(self) -> BotTelemetryClient:
        """Gets the currently configured BotTelemetryClient that logs the event.

        :return: The BotTelemetryClient being used to log events.
        :rtype: BotTelemetryClient
        """

        return self._telemetry_client

    @telemetry_client.setter
    def telemetry_client(self, value: BotTelemetryClient):
        """Sets the currently configured BotTelemetryClient that logs the event.

        :param value: The BotTelemetryClient being used to log events.
        :type value: BotTelemetryClient
        """

        self._telemetry_client = value

    async def on_qna_result(
        self,
        query_results: [QueryResult],
        turn_context: TurnContext,
        telemetry_properties: Dict[str, str] = None,
        telemetry_metrics: Dict[str, float] = None
    ):
        event_data = await self.fill_qna_event(query_results, turn_context, telemetry_properties, telemetry_metrics)

        self.telemetry_client.track_event(
            name = QnATelemetryConstants.qna_message_event,
            properties = event_data.properties,
            measurements = event_data.metrics
        )

    def fill_qna_event(
        self,
        query_results: [QueryResult],
        turn_context: TurnContext,
        telemetry_properties: Dict[str,str] = None,
        telemetry_metrics: Dict[str,float] = None
    ) -> EventData:
        """
        Fills the event properties and metrics for the QnaMessage event for telemetry.

        :return: A tuple of event data properties and metrics that will be sent to the BotTelemetryClient.track_event() method for the QnAMessage event. The properties and metrics returned the standard properties logged with any properties passed from the get_answers() method.

        :rtype: EventData
        """

        properties: Dict[str,str] = dict()
        metrics: Dict[str, float] = dict()

        properties[QnATelemetryConstants.knowledge_base_id_property] = self._endpoint.knowledge_base_id

        text: str = turn_context.activity.text
        userName: str = turn_context.activity.from_property.name

        # Use the LogPersonalInformation flag to toggle logging PII data; text and username are common examples.
        if self.log_personal_information:
            if text:
                properties[QnATelemetryConstants.question_property] = text

            if userName:
                properties[QnATelemetryConstants.username_property] = userName

        # Fill in Qna Results (found or not).
        if len(query_results) > 0:
            query_result = query_results[0]

            result_properties = {
                QnATelemetryConstants.matched_question_property: json.dumps(query_result.questions),
                QnATelemetryConstants.question_id_property: str(query_result.id),
                QnATelemetryConstants.answer_property: query_result.answer,
                QnATelemetryConstants.score_metric: query_result.score,
                QnATelemetryConstants.article_found_property: 'true'
            }

            properties.update(result_properties)
        else:
            no_match_properties = {
                QnATelemetryConstants.matched_question_property : 'No Qna Question matched',
                QnATelemetryConstants.question_id_property : 'No Qna Question Id matched',
                QnATelemetryConstants.answer_property : 'No Qna Answer matched',
                QnATelemetryConstants.article_found_property : 'false'
            }

            properties.update(no_match_properties)

        # Additional Properties can override "stock" properties.
        if telemetry_properties:
            properties.update(telemetry_properties)

        # Additional Metrics can override "stock" metrics.
        if telemetry_metrics:
            metrics.update(telemetry_metrics)

        return EventData(properties=properties, metrics=metrics)

    async def get_answers(
        self,
        context: TurnContext,
        options: QnAMakerOptions = None,
        telemetry_properties: Dict[str,str] = None,
        telemetry_metrics: Dict[str,int] = None
    ) -> [QueryResult]:
        """
        Generates answers from the knowledge base.

        :return: A list of answers for the user's query, sorted in decreasing order of ranking score.

        :rtype: [QueryResult]
        """

        hydrated_options = self._hydrate_options(options)
        self._validate_options(hydrated_options)

        result = self._query_qna_service(context.activity, hydrated_options)

        await self._emit_trace_info(context, result, hydrated_options)

        return result

    def _validate_options(self, options: QnAMakerOptions):
        if not options.score_threshold:
            options.score_threshold = 0.3

        if not options.top:
            options.top = 1

        if options.score_threshold < 0 or options.score_threshold > 1:
            raise ValueError('Score threshold should be a value between 0 and 1')

        if options.top < 1:
            raise ValueError('QnAMakerOptions.top should be an integer greater than 0')

        if not options.strict_filters:
            options.strict_filters = []

    def _hydrate_options(self, query_options: QnAMakerOptions) -> QnAMakerOptions:
        """
        Combines QnAMakerOptions passed into the QnAMaker constructor with the options passed as arguments into get_answers().

        :return: QnAMakerOptions with options passed into constructor overwritten by new options passed into get_answers()

        :rtype: QnAMakerOptions
        """

        hydrated_options = copy(self._options)

        if query_options:
            if (
                query_options.score_threshold != hydrated_options.score_threshold
                and query_options.score_threshold
            ):
                hydrated_options.score_threshold = query_options.score_threshold

            if (query_options.top != hydrated_options.top and query_options.top != 0):
                hydrated_options.top = query_options.top

            if (len(query_options.strict_filters) > 0):
                hydrated_options.strict_filters = query_options.strict_filters

        return hydrated_options

    def _query_qna_service(self, message_activity: Activity, options: QnAMakerOptions) -> [QueryResult]:
        url = f'{ self._endpoint.host }/knowledgebases/{ self._endpoint.knowledge_base_id }/generateAnswer'

        question = {
            'question': message_activity.text,
            'top': options.top,
            'scoreThreshold': options.score_threshold,
            'strictFilters': options.strict_filters
        }

        serialized_content = json.dumps(question)

        headers = self._get_headers()

        response = requests.post(url, data=serialized_content, headers=headers)

        result = self._format_qna_result(response, options)

        return result

    async def _emit_trace_info(self, turn_context: TurnContext, result: [QueryResult], options: QnAMakerOptions):
        trace_info = QnAMakerTraceInfo(
            message = turn_context.activity,
            query_results = result,
            knowledge_base_id = self._endpoint.knowledge_base_id,
            score_threshold = options.score_threshold,
            top = options.top,
            strict_filters = options.strict_filters
        )

        trace_activity = Activity(
            label = QNAMAKER_TRACE_LABEL,
            name = QNAMAKER_TRACE_NAME,
            type = 'trace',
            value = trace_info,
            value_type = QNAMAKER_TRACE_TYPE
        )

        await turn_context.send_activity(trace_activity)

    def _format_qna_result(self, qna_result: requests.Response, options: QnAMakerOptions) -> [QueryResult]:
        result = qna_result.json()

        answers_within_threshold = [
            { **answer,'score': answer['score']/100 } for answer in result['answers']
            if answer['score']/100 > options.score_threshold
        ]
        sorted_answers = sorted(answers_within_threshold, key = lambda ans: ans['score'], reverse = True)

        # The old version of the protocol returns the id in a field called qnaId
        # The following translates this old structure to the new
        if self._is_legacy_protocol:
            for answer in answers_within_threshold:
                answer['id'] = answer.pop('qnaId', None)

        answers_as_query_results = list(map(lambda answer: QueryResult(**answer), sorted_answers))

        return answers_as_query_results

    def _get_headers(self):
        headers = { 'Content-Type': 'application/json' }

        if self._is_legacy_protocol:
            headers['Ocp-Apim-Subscription-Key'] = self._endpoint.endpoint_key
        else:
            headers['Authorization'] = f'EndpointKey {self._endpoint.endpoint_key}'

        # need user-agent header

        return headers


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

import asyncio
from copy import copy
from uuid import uuid4
from typing import List, Callable, Union, Dict
from botbuilder.schema import (
                            Activity,
                            ConversationReference,
                            ResourceResponse
                            )
from .assertions import BotAssert

class TurnContext(object):
    def __init__(self, adapter_or_context, request: Activity=None):
        """
        Creates a new TurnContext instance.
        :param adapter_or_context:
        :param request:
        """
        if isinstance(adapter_or_context, TurnContext):
            adapter_or_context.copy_to(self)
        else:
            self.adapter = adapter_or_context
            self._activity = request
            self.responses: List[Activity] = []
            self._services: dict = {}
            self._on_send_activities: Callable[[]] = []
            self._on_update_activity: Callable[[]] = []
            self._on_delete_activity: Callable[[]] = []
            self._responded : bool = False

        if self.adapter is None:
            raise TypeError('TurnContext must be instantiated with an adapter.')
        if self.activity is None:
            raise TypeError('TurnContext must be instantiated with a request parameter of type Activity.')

        self._turn_state = {}

    @property
    def turn_state(self) -> Dict[str, object]:
        return self._turn_state

    def copy_to(self, context: 'TurnContext') -> None:
        """
        Called when this TurnContext instance is passed into the constructor of a new TurnContext
        instance. Can be overridden in derived classes.
        :param context:
        :return:
        """
        for attribute in ['adapter', 'activity', '_responded', '_services',
                          '_on_send_activities', '_on_update_activity', '_on_delete_activity']:
            setattr(context, attribute, getattr(self, attribute))

    @property
    def activity(self):
        """
        The received activity.
        :return:
        """
        return self._activity

    @activity.setter
    def activity(self, value):
        """
        Used to set TurnContext._activity when a context object is created. Only takes instances of Activities.
        :param value:
        :return:
        """
        if not isinstance(value, Activity):
            raise TypeError('TurnContext: cannot set `activity` to a type other than Activity.')
        else:
            self._activity = value

    @property
    def responded(self) -> bool:
        """
        If `true` at least one response has been sent for the current turn of conversation.
        :return:
        """
        return self._responded

    @responded.setter
    def responded(self, value: bool):
        if value == False:
            raise ValueError('TurnContext: cannot set TurnContext.responded to False.')
        else:
            self._responded = True

    @property
    def services(self):
        """
        Map of services and other values cached for the lifetime of the turn.
        :return:
        """
        return self._services

    def get(self, key: str) -> object:
        if not key or not isinstance(key, str):
            raise TypeError('"key" must be a valid string.')
        try:
            return self._services[key]
        except KeyError:
            raise KeyError('%s not found in TurnContext._services.' % key)

    def has(self, key: str) -> bool:
        """
        Returns True is set() has been called for a key. The cached value may be of type 'None'.
        :param key:
        :return:
        """
        if key in self._services:
            return True
        return False

    def set(self, key: str, value: object) -> None:
        """
        Caches a value for the lifetime of the current turn.
        :param key:
        :param value:
        :return:
        """
        if not key or not isinstance(key, str):
            raise KeyError('"key" must be a valid string.')

        self._services[key] = value

    async def send_activity(self, *activity_or_text: Union[Activity, str]) -> ResourceResponse:
        """
        Sends a single activity or message to the user.
        :param activity_or_text:
        :return:
        """
        reference = TurnContext.get_conversation_reference(self.activity)

        output = [TurnContext.apply_conversation_reference(
            Activity(text=a, type='message') if isinstance(a, str) else a, reference)
            for a in activity_or_text]
        for activity in output:
            activity.input_hint = 'acceptingInput'

        async def callback(context: 'TurnContext', output):
            responses = await context.adapter.send_activities(context, output)
            context._responded = True
            return responses

        await self._emit(self._on_send_activities, output, callback(self, output))

    async def update_activity(self, activity: Activity):
        """
        Replaces an existing activity.
        :param activity:
        :return:
        """
        return await self._emit(self._on_update_activity, activity, self.adapter.update_activity(self, activity))

    async def delete_activity(self, id_or_reference: Union[str, ConversationReference]):
        """
        Deletes an existing activity.
        :param id_or_reference:
        :return:
        """
        if type(id_or_reference) == str:
            reference = TurnContext.get_conversation_reference(self.activity)
            reference.activity_id = id_or_reference
        else:
            reference = id_or_reference
        return await self._emit(self._on_delete_activity, reference, self.adapter.delete_activity(self, reference))

    def on_send_activities(self, handler) -> 'TurnContext':
        """
        Registers a handler to be notified of and potentially intercept the sending of activities.
        :param handler:
        :return:
        """
        self._on_send_activities.append(handler)
        return self

    def on_update_activity(self, handler) -> 'TurnContext':
        """
        Registers a handler to be notified of and potentially intercept an activity being updated.
        :param handler:
        :return:
        """
        self._on_update_activity.append(handler)
        return self

    def on_delete_activity(self, handler) -> 'TurnContext':
        """
        Registers a handler to be notified of and potentially intercept an activity being deleted.
        :param handler:
        :return:
        """
        self._on_delete_activity.append(handler)
        return self

    async def _emit(self, plugins, arg, logic):
        handlers = copy(plugins)

        async def emit_next(i: int):
            context = self
            try:
                if i < len(handlers):
                    async def next_handler():
                        await emit_next(i + 1)
                    await handlers[i](context, arg, next_handler)

            except Exception as e:
                raise e
        await emit_next(0)
        # This should be changed to `return await logic()`
        return await logic

    @staticmethod
    def get_conversation_reference(activity: Activity) -> ConversationReference:
        """
        Returns the conversation reference for an activity. This can be saved as a plain old JSON
        object and then later used to message the user proactively.

        Usage Example:
        reference = TurnContext.get_conversation_reference(context.request)
        :param activity:
        :return:
        """
        return ConversationReference(activity_id=activity.id,
                                     user=copy(activity.from_property),
                                     bot=copy(activity.recipient),
                                     conversation=copy(activity.conversation),
                                     channel_id=activity.channel_id,
                                     service_url=activity.service_url)

    @staticmethod
    def apply_conversation_reference(activity: Activity,
                                     reference: ConversationReference,
                                     is_incoming: bool=False) -> Activity:
        """
        Updates an activity with the delivery information from a conversation reference. Calling
        this after get_conversation_reference on an incoming activity
        will properly address the reply to a received activity.
        :param activity:
        :param reference:
        :param is_incoming:
        :return:
        """
        activity.channel_id = reference.channel_id
        activity.service_url = reference.service_url
        activity.conversation = reference.conversation
        if is_incoming:
            activity.from_property = reference.user
            activity.recipient = reference.bot
            if reference.activity_id:
                activity.id = reference.activity_id
        else:
            activity.from_property = reference.bot
            activity.recipient = reference.user
            if reference.activity_id:
                activity.reply_to_id = reference.activity_id

        return activity


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

import uuid
from typing import (
                    Dict,
                    Coroutine,
                    List)
from .dialog_reason import DialogReason
from .dialog import Dialog
from .dialog_turn_result import DialogTurnResult
from .dialog_context import DialogContext
from .dialog_instance import DialogInstance
from .waterfall_step_context import WaterfallStepContext
from botbuilder.core import TurnContext
from botbuilder.schema import ActivityTypes
from typing import Coroutine, List

class WaterfallDialog(Dialog):
    PersistedOptions = "options"
    StepIndex = "stepIndex"
    PersistedValues = "values"
    PersistedInstanceId = "instanceId"

    def __init__(self, dialog_id: str, steps: [Coroutine] = None):
        super(WaterfallDialog, self).__init__(dialog_id)
        if not steps:
            self._steps = []
        else:
            if not isinstance(steps, list):
                raise TypeError('WaterfallDialog(): steps must be list of steps')
            self._steps = steps

    def add_step(self, step):
        """
        Adds a new step to the waterfall.
        :param step: Step to add

        :return: Waterfall dialog for fluent calls to `add_step()`.
        """
        if not step:
            raise TypeError('WaterfallDialog.add_step(): step cannot be None.')

        self._steps.append(step)
        return self

    async def begin_dialog(self, dc: DialogContext, options: object = None) -> DialogTurnResult:

        if not dc:
            raise TypeError('WaterfallDialog.begin_dialog(): dc cannot be None.')

        # Initialize waterfall state
        state = dc.active_dialog.state

        instance_id = uuid.uuid1().__str__()
        state[self.PersistedOptions] = options
        state[self.PersistedValues] = {}
        state[self.PersistedInstanceId] = instance_id

        properties = {}
        properties['DialogId'] = self.id
        properties['InstanceId'] = instance_id
        self.telemetry_client.track_event("WaterfallStart", properties)
        # Run first stepkinds
        return await self.run_step(dc, 0, DialogReason.BeginCalled, None)

    async def continue_dialog(self, dc: DialogContext = None, reason: DialogReason = None, result: object = NotImplementedError) -> DialogTurnResult:
        if not dc:
            raise TypeError('WaterfallDialog.continue_dialog(): dc cannot be None.')

        if dc.context.activity.type != ActivityTypes.message:
            return Dialog.end_of_turn

        return await self.resume_dialog(dc, DialogReason.ContinueCalled, dc.context.activity.text)

    async def resume_dialog(self, dc: DialogContext, reason: DialogReason, result: object):
        if dc is None:
            raise TypeError('WaterfallDialog.resume_dialog(): dc cannot be None.')

        # Increment step index and run step
        state = dc.active_dialog.state

        # Future Me:
        # If issues with CosmosDB, see https://github.com/Microsoft/botbuilder-dotnet/issues/871
        # for hints.
        return await self.run_step(dc, state[self.StepIndex] + 1, reason, result)

    async def end_dialog(self, turn_context: TurnContext, instance: DialogInstance, reason: DialogReason) -> None:
        if reason is DialogReason.CancelCalled:
            index = instance.state[self.StepIndex]
            step_name = self.get_step_name(index)
            instance_id = str(instance.state[self.PersistedInstanceId])
            properties = {
                "DialogId": self.id,
                "StepName" : step_name,
                "InstanceId" : instance_id
            }
            self.telemetry_client.track_event("WaterfallCancel", properties)
        else:
            if reason is DialogReason.EndCalled:

                instance_id = str(instance.state[self.PersistedInstanceId])
                properties = {
                    "DialogId": self.id,
                    "InstanceId": instance_id
                }
                self.telemetry_client.track_event("WaterfallComplete", properties)

        return

    async def on_step(self, step_context: WaterfallStepContext) -> DialogTurnResult:
        step_name = self.get_step_name(step_context.index)
        instance_id = str(step_context.active_dialog.state[self.PersistedInstanceId])
        properties = {
                "DialogId": self.id,
                "StepName": step_name,
                "InstanceId": instance_id
            }
        self.telemetry_client.track_event("WaterfallStep", properties)
        return await self._steps[step_context.index](step_context)

    async def run_step(self, dc: DialogContext, index: int, reason: DialogReason, result: object) -> DialogTurnResult:
        if not dc:
            raise TypeError('WaterfallDialog.run_steps(): dc cannot be None.')
        if index < len(self._steps):
            # Update persisted step index
            state = dc.active_dialog.state
            state[self.StepIndex] = index

            # Create step context
            options = state[self.PersistedOptions]
            values = state[self.PersistedValues]
            step_context = WaterfallStepContext(self, dc, options, values, index, reason, result)
            return await self.on_step(step_context)
        else:
            # End of waterfall so just return any result to parent
            return await dc.end_dialog(result)

    def get_step_name(self, index: int) -> str:
        """
        Give the waterfall step a unique name
        """
        step_name = self._steps[index].__qualname__

        if not step_name or ">" in step_name :
            step_name = f"Step{index + 1}of{len(self._steps)}"

        return step_name


# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
import sys
import traceback
from .bot_telemetry_client import BotTelemetryClient, TelemetryDataPointType
from typing import Dict

class NullTelemetryClient(BotTelemetryClient):

    def __init__(self):
        pass

    def track_pageview(self, name: str, url, duration: int = 0, properties : Dict[str, object]=None,
                        measurements: Dict[str, object]=None) -> None:
        """
        Send information about the page viewed in the application (a web page for instance).
        :param name: the name of the page that was viewed.
        :param url: the URL of the page that was viewed.
        :param duration: the duration of the page view in milliseconds. (defaults to: 0)
        :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
        :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None)
        """
        pass

    def track_exception(self, type: type = None, value : Exception =None, tb : traceback =None,
                        properties: Dict[str, object]=None, measurements: Dict[str, object]=None) -> None:
        """
        Send information about a single exception that occurred in the application.
        :param type: the type of the exception that was thrown.
        :param value: the exception that the client wants to send.
        :param tb: the traceback information as returned by :func:`sys.exc_info`.
        :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
        :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None)
        """
        pass

    def track_event(self, name: str, properties: Dict[str, object] = None,
                    measurements: Dict[str, object] = None) -> None:
        """
        Send information about a single event that has occurred in the context of the application.
        :param name: the data to associate to this event.
        :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
        :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None)
        """
        pass

    def track_metric(self, name: str, value: float, type: TelemetryDataPointType =None,
                    count: int =None, min: float=None, max: float=None, std_dev: float=None,
                    properties: Dict[str, object]=None) -> NotImplemented:
        """
        Send information about a single metric data point that was captured for the application.
        :param name: The name of the metric that was captured.
        :param value: The value of the metric that was captured.
        :param type: The type of the metric. (defaults to: TelemetryDataPointType.aggregation`)
        :param count: the number of metrics that were aggregated into this data point. (defaults to: None)
        :param min: the minimum of all metrics collected that were aggregated into this data point. (defaults to: None)
        :param max: the maximum of all metrics collected that were aggregated into this data point. (defaults to: None)
        :param std_dev: the standard deviation of all metrics collected that were aggregated into this data point. (defaults to: None)
        :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
        """
        pass

    def track_trace(self, name, properties=None, severity=None):
        """
        Sends a single trace statement.
        :param name: the trace statement.\n
        :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)\n
        :param severity: the severity level of this trace, one of DEBUG, INFO, WARNING, ERROR, CRITICAL
        """
        pass

    def track_request(self, name: str, url: str, success: bool, start_time: str=None,
                    duration: int=None, response_code: str =None, http_method: str=None,
                    properties: Dict[str, object]=None, measurements: Dict[str, object]=None,
                    request_id: str=None):
        """
        Sends a single request that was captured for the application.
        :param name: The name for this request. All requests with the same name will be grouped together.
        :param url: The actual URL for this request (to show in individual request instances).
        :param success: True if the request ended in success, False otherwise.
        :param start_time: the start time of the request. The value should look the same as the one returned by :func:`datetime.isoformat()` (defaults to: None)
        :param duration: the number of milliseconds that this request lasted. (defaults to: None)
        :param response_code: the response code that this request returned. (defaults to: None)
        :param http_method: the HTTP method that triggered this request. (defaults to: None)
        :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
        :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None)
        :param request_id: the id for this request. If None, a new uuid will be generated. (defaults to: None)
        """
        pass

    def track_dependency(self, name:str, data:str, type:str=None, target:str=None, duration:int=None,
                        success:bool=None, result_code:str=None, properties:Dict[str, object]=None,
                        measurements:Dict[str, object]=None, dependency_id:str=None):
        """
        Sends a single dependency telemetry that was captured for the application.
        :param name: the name of the command initiated with this dependency call. Low cardinality value. Examples are stored procedure name and URL path template.
        :param data: the command initiated by this dependency call. Examples are SQL statement and HTTP URL with all query parameters.
        :param type: the dependency type name. Low cardinality value for logical grouping of dependencies and interpretation of other fields like commandName and resultCode. Examples are SQL, Azure table, and HTTP. (default to: None)
        :param target: the target site of a dependency call. Examples are server name, host address. (default to: None)
        :param duration: the number of milliseconds that this dependency call lasted. (defaults to: None)
        :param success: true if the dependency call ended in success, false otherwise. (defaults to: None)
        :param result_code: the result code of a dependency call. Examples are SQL error code and HTTP status code. (defaults to: None)
        :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
        :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None)
        :param id: the id for this dependency call. If None, a new uuid will be generated. (defaults to: None)
        """
        pass


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

from copy import copy
from abc import ABC, abstractmethod
from typing import Callable, List

from .turn_context import TurnContext

class Storage(ABC):
    @abstractmethod
    async def read(self, keys: List[str]):
        """
        Loads store items from storage.
        :param keys:
        :return:
        """
        raise NotImplementedError()

    @abstractmethod
    async def write(self, changes):
        """
        Saves store items to storage.
        :param changes:
        :return:
        """
        raise NotImplementedError()

    @abstractmethod
    async def delete(self, keys: List[str]):
        """
        Removes store items from storage.
        :param keys:
        :return:
        """
        raise NotImplementedError()

class StoreItem(ABC):
    """
    Object which is stored in Storage with an optional eTag.
    """
    def __init__(self, **kwargs):
        # If e_tag is passed in as a kwarg use that value, otherwise assign the wildcard value to the new e_tag
        self.e_tag = kwargs.get('e_tag', '*')
        for key, value in kwargs.items():
            setattr(self, key, value)

    def __str__(self):
        non_magic_attributes = [attr for attr in dir(self) if not attr.startswith('_')]
        output = '{' + ','.join(
            [f" '{attr}': '{getattr(self, attr)}'" for attr in non_magic_attributes]) + ' }'
        return output

StorageKeyFactory = Callable[[TurnContext], str]

def calculate_change_hash(item: StoreItem) -> str:
    """
    Utility function to calculate a change hash for a `StoreItem`.
    :param item:
    :return:
    """
    cpy = copy(item)
    if cpy.e_tag is not None:
        del cpy.e_tag
    return str(cpy)


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

from .dialog_state import DialogState
from .dialog_turn_status import DialogTurnStatus
from .dialog_turn_result import DialogTurnResult
from .dialog_reason import DialogReason
from .dialog_instance import DialogInstance
from .dialog import Dialog
from botbuilder.core.turn_context import TurnContext

class DialogContext():
    def __init__(self, dialog_set: object, turn_context: TurnContext, state: DialogState):
        if dialog_set is None:
            raise TypeError('DialogContext(): dialog_set cannot be None.')
        # TODO: Circular dependency with dialog_set: Check type.
        if turn_context is None:
            raise TypeError('DialogContext(): turn_context cannot be None.')
        self._turn_context = turn_context
        self._dialogs = dialog_set
        # self._id = dialog_id;
        self._stack = state.dialog_stack
        self.parent = None

    @property
    def dialogs(self):
        """Gets the set of dialogs that can be called from this context.

        :param:
        :return str:
        """
        return self._dialogs

    @property
    def context(self) -> TurnContext:
        """Gets the context for the current turn of conversation.

        :param:
        :return str:
        """
        return self._turn_context

    @property
    def stack(self):
        """Gets the current dialog stack.

        :param:
        :return str:
        """
        return self._stack

    @property
    def parent(self) -> 'DialogContext':
        """
        Gets the parent DialogContext if any. Used when searching for dialogs to start.

        :param:
        :return The parent DialogContext:
        """
        return self._parent

    @parent.setter
    def parent(self, parent_dialog_context: object):
        """
        Sets the parent DialogContext if any. Used when searching for dialogs to start.

        :param parent_dialog_context: The parent dialog context
        :return str:
        """
        self._parent = parent_dialog_context

    @property
    def active_dialog(self):
        """Return the container link in the database.

        :param:
        :return str:
        """
        if self._stack != None and len(self._stack) > 0:
            return self._stack[0]
        return None

    async def begin_dialog(self, dialog_id: str, options: object = None):
        """
        Pushes a new dialog onto the dialog stack.
        :param dialog_id: ID of the dialog to start..
        :param options: (Optional) additional argument(s) to pass to the dialog being started.
        """
        if (not dialog_id):
            raise TypeError('Dialog(): dialogId cannot be None.')
        # Look up dialog
        dialog = await self.find_dialog(dialog_id)
        if dialog is None:
            raise Exception("'DialogContext.begin_dialog(): A dialog with an id of '%s' wasn't found."
                    " The dialog must be included in the current or parent DialogSet."
                    " For example, if subclassing a ComponentDialog you can call add_dialog() within your constructor." % dialog_id)
        # Push new instance onto stack
        instance = DialogInstance()
        instance.id = dialog_id
        instance.state = {}

        self._stack.append(instance)

        # Call dialog's begin_dialog() method
        return await dialog.begin_dialog(self, options)

    # TODO: Fix options: PromptOptions instead of object
    async def prompt(self, dialog_id: str, options) -> DialogTurnResult:
        """
        Helper function to simplify formatting the options for calling a prompt dialog. This helper will
        take a `PromptOptions` argument and then call.
        :param dialog_id: ID of the prompt to start.
        :param options: Contains a Prompt, potentially a RetryPrompt and if using ChoicePrompt, Choices.
        :return:
        """
        if (not dialog_id):
            raise TypeError('DialogContext.prompt(): dialogId cannot be None.')

        if (not options):
            raise TypeError('DialogContext.prompt(): options cannot be None.')

        return await self.begin_dialog(dialog_id, options)

    async def continue_dialog(self):
        """
        Continues execution of the active dialog, if there is one, by passing the context object to
        its `Dialog.continue_dialog()` method. You can check `turn_context.responded` after the call completes
        to determine if a dialog was run and a reply was sent to the user.
        :return:
        """
        # Check for a dialog on the stack
        if self.active_dialog != None:
            # Look up dialog
            dialog = await self.find_dialog(self.active_dialog.id)
            if not dialog:
                raise Exception("DialogContext.continue_dialog(): Can't continue dialog. A dialog with an id of '%s' wasn't found." % active_dialog.id)

            # Continue execution of dialog
            return await dialog.continue_dialog(self)
        else:
            return DialogTurnResult(DialogTurnStatus.Empty)

    # TODO: instance is DialogInstance
    async def end_dialog(self, result: object = None):
        """
        Ends a dialog by popping it off the stack and returns an optional result to the dialog's
        parent. The parent dialog is the dialog that started the dialog being ended via a call to
        either "begin_dialog" or "prompt".
        The parent dialog will have its `Dialog.resume_dialog()` method invoked with any returned
        result. If the parent dialog hasn't implemented a `resume_dialog()` method then it will be
        automatically ended as well and the result passed to its parent. If there are no more
        parent dialogs on the stack then processing of the turn will end.
        :param result: (Optional) result to pass to the parent dialogs.
        :return:
        """
        await self.end_active_dialog(DialogReason.EndCalled)

        # Resume previous dialog
        if self.active_dialog != None:
            # Look up dialog
            dialog = await self.find_dialog(self.active_dialog.id)
            if not dialog:
                raise Exception("DialogContext.EndDialogAsync(): Can't resume previous dialog. A dialog with an id of '%s' wasn't found." % self.active_dialog.id)

            # Return result to previous dialog
            return await dialog.resume_dialog(self, DialogReason.EndCalled, result)
        else:
            return DialogTurnResult(DialogTurnStatus.Complete, result)

    async def cancel_all_dialogs(self):
        """
        Deletes any existing dialog stack thus cancelling all dialogs on the stack.
        :param result: (Optional) result to pass to the parent dialogs.
        :return:
        """
        if (len(self.stack) > 0):
            while (len(self.stack) > 0):
                await self.end_active_dialog(DialogReason.CancelCalled)
            return DialogTurnResult(DialogTurnStatus.Cancelled)
        else:
            return DialogTurnResult(DialogTurnStatus.Empty)

    async def find_dialog(self, dialog_id: str) -> Dialog:
        """
        If the dialog cannot be found within the current `DialogSet`, the parent `DialogContext`
        will be searched if there is one.
        :param dialog_id: ID of the dialog to search for.
        :return:
        """
        dialog = await self.dialogs.find(dialog_id)

        if (dialog == None and self.parent != None):
            dialog = self.parent.find_dialog(dialog_id)
        return dialog

    async def replace_dialog(self, dialog_id: str, options: object = None) -> DialogTurnResult:
        """
        Ends the active dialog and starts a new dialog in its place. This is particularly useful
        for creating loops or redirecting to another dialog.
        :param dialog_id: ID of the dialog to search for.
        :param options: (Optional) additional argument(s) to pass to the new dialog.
        :return:
        """
        # End the current dialog and giving the reason.
        await self.end_active_dialog(DialogReason.ReplaceCalled)

        # Start replacement dialog
        return await self.begin_dialog(dialog_id, options)

    async def reprompt_dialog(self):
        """
        Calls reprompt on the currently active dialog, if there is one. Used with Prompts that have a reprompt behavior.
        :return:
        """
        # Check for a dialog on the stack
        if self.active_dialog != None:
            # Look up dialog
            dialog = await self.find_dialog(self.active_dialog.id)
            if not dialog:
                raise Exception("DialogSet.reprompt_dialog(): Can't find A dialog with an id of '%s'." % self.active_dialog.id)

            # Ask dialog to re-prompt if supported
            await dialog.reprompt_dialog(self.context, self.active_dialog)

    async def end_active_dialog(self, reason: DialogReason):
        instance = self.active_dialog
        if instance != None:
            # Look up dialog
            dialog = await self.find_dialog(instance.id)
            if dialog != None:
                # Notify dialog of end
                await dialog.end_dialog(self.context, instance, reason)

            # Pop dialog off stack
            self._stack.pop()


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

from botbuilder.core import TurnContext
from botframework.connector import Channels

class Channel(object):
    """
    Methods for determining channel specific functionality.
    """

    @staticmethod
    def supports_suggested_actions(channel_id: str, button_cnt: int = 100) -> bool:
        """Determine if a number of Suggested Actions are supported by a Channel.

        Args:
            channel_id (str): The Channel to check the if Suggested Actions are supported in.
            button_cnt (int, optional): Defaults to 100. The number of Suggested Actions to check for the Channel.

        Returns:
            bool: True if the Channel supports the button_cnt total Suggested Actions, False if the Channel does not support that number of Suggested Actions.
        """

        max_actions = {
            # https://developers.facebook.com/docs/messenger-platform/send-messages/quick-replies
            Channels.facebook: 10,
            Channels.skype: 10,
            # https://developers.line.biz/en/reference/messaging-api/#items-object
            Channels.line: 13,
            # https://dev.kik.com/#/docs/messaging#text-response-object
            Channels.kik: 20,
            Channels.telegram: 100,
            Channels.slack: 100,
            Channels.emulator: 100,
            Channels.direct_line: 100,
            Channels.webchat: 100,
        }
        return button_cnt <= max_actions[channel_id] if channel_id in max_actions else False

    @staticmethod
    def supports_card_actions(channel_id: str, button_cnt: int = 100) -> bool:
        """Determine if a number of Card Actions are supported by a Channel.

        Args:
            channel_id (str): The Channel to check if the Card Actions are supported in.
            button_cnt (int, optional): Defaults to 100. The number of Card Actions to check for the Channel.

        Returns:
            bool: True if the Channel supports the button_cnt total Card Actions, False if the Channel does not support that number of Card Actions.
        """

        max_actions = {
            Channels.facebook: 3,
            Channels.skype: 3,
            Channels.ms_teams: 3,
            Channels.line: 99,
            Channels.slack: 100,
            Channels.emulator: 100,
            Channels.direct_line: 100,
            Channels.webchat: 100,
            Channels.cortana: 100,
        }
        return button_cnt <= max_actions[channel_id] if channel_id in max_actions else False

    @staticmethod
    def has_message_feed(channel_id: str) -> bool:
        """Determine if a Channel has a Message Feed.

        Args:
            channel_id (str): The Channel to check for Message Feed.

        Returns:
            bool: True if the Channel has a Message Feed, False if it does not.
        """

        return False if channel_id == Channels.cortana else True

    @staticmethod
    def max_action_title_length(channel_id: str) -> int:
        """Maximum length allowed for Action Titles.

        Args:
            channel_id (str): The Channel to determine Maximum Action Title Length.

        Returns:
            int: The total number of characters allowed for an Action Title on a specific Channel.
        """

        return 20

    @staticmethod
    def get_channel_id(turn_context: TurnContext) -> str:
        """Get the Channel Id from the current Activity on the Turn Context.

        Args:
            turn_context (TurnContext): The Turn Context to retrieve the Activity's Channel Id from.

        Returns:
            str: The Channel Id from the Turn Context's Activity.
        """

        if turn_context.activity.channel_id is None:
            return ""
        else:
            return turn_context.activity.channel_id


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

import datetime
import asyncio
import warnings
from typing import List, Callable

from botbuilder.schema import (Activity, ActivityTypes,
                               ChannelAccount, ConversationAccount,
                               ResourceResponse, ConversationReference)
from botbuilder.core.turn_context import TurnContext
from botbuilder.core.bot_adapter import BotAdapter

class ConsoleAdapter(BotAdapter):
    """
    Lets a user communicate with a bot from a console window.

    :Example:
    import asyncio
    from botbuilder.core import ConsoleAdapter

    async def logic(context):
        await context.send_activity('Hello World!')

    adapter = ConsoleAdapter()
    loop = asyncio.get_event_loop()
    if __name__ == "__main__":
        try:
            loop.run_until_complete(adapter.process_activity(logic))
        except KeyboardInterrupt:
            pass
        finally:
            loop.stop()
            loop.close()
    """
    def __init__(self, reference: ConversationReference = None):
        super(ConsoleAdapter, self).__init__()

        self.reference = ConversationReference(channel_id='console',
                                               user=ChannelAccount(id='user', name='User1'),
                                               bot=ChannelAccount(id='bot', name='Bot'),
                                               conversation=ConversationAccount(id='convo1', name='', is_group=False),
                                               service_url='')

        # Warn users to pass in an instance of a ConversationReference, otherwise the parameter will be ignored.
        if reference is not None and not isinstance(reference, ConversationReference):
            warnings.warn('ConsoleAdapter: `reference` argument is not an instance of ConversationReference and will '
                          'be ignored.')
        else:
            self.reference.channel_id = getattr(reference, 'channel_id', self.reference.channel_id)
            self.reference.user = getattr(reference, 'user', self.reference.user)
            self.reference.bot = getattr(reference, 'bot', self.reference.bot)
            self.reference.conversation = getattr(reference, 'conversation', self.reference.conversation)
            self.reference.service_url = getattr(reference, 'service_url', self.reference.service_url)
            # The only attribute on self.reference without an initial value is activity_id, so if reference does not
            # have a value for activity_id, default self.reference.activity_id to None
            self.reference.activity_id = getattr(reference, 'activity_id', None)

        self._next_id = 0

    async def process_activity(self, logic: Callable):
        """
        Begins listening to console input.
        :param logic:
        :return:
        """
        while True:
            msg = input()
            if msg is None:
                pass
            else:
                self._next_id += 1
                activity = Activity(text=msg,
                                    channel_id='console',
                                    from_property=ChannelAccount(id='user', name='User1'),
                                    recipient=ChannelAccount(id='bot', name='Bot'),
                                    conversation=ConversationAccount(id='Convo1'),
                                    type=ActivityTypes.message,
                                    timestamp=datetime.datetime.now(),
                                    id=str(self._next_id))

                activity = TurnContext.apply_conversation_reference(activity, self.reference, True)
                context = TurnContext(self, activity)
                await self.run_middleware(context, logic)

    async def send_activities(self, context: TurnContext, activities: List[Activity]):
        """
        Logs a series of activities to the console.
        :param context:
        :param activities:
        :return:
        """
        if context is None:
            raise TypeError('ConsoleAdapter.send_activities(): `context` argument cannot be None.')
        if type(activities) != list:
            raise TypeError('ConsoleAdapter.send_activities(): `activities` argument must be a list.')
        if len(activities) == 0:
            raise ValueError('ConsoleAdapter.send_activities(): `activities` argument cannot have a length of 0.')

        async def next_activity(i: int):
            responses = []

            if i < len(activities):
                responses.append(ResourceResponse())
                a = activities[i]

                if a.type == 'delay':
                    await asyncio.sleep(a.delay)
                    await next_activity(i + 1)
                elif a.type == ActivityTypes.message:
                    if a.attachments is not None and len(a.attachments) > 0:
                        append = '(1 attachment)' if len(a.attachments) == 1 else f'({len(a.attachments)} attachments)'
                        print(f'{a.text} {append}')
                    else:
                        print(a.text)
                    await next_activity(i + 1)
                else:
                    print(f'[{a.type}]')
                    await next_activity(i + 1)
            else:
                return responses

        await next_activity(0)

    async def delete_activity(self, context: TurnContext, reference: ConversationReference):
        """
        Not supported for the ConsoleAdapter. Calling this method or `TurnContext.delete_activity()`
        will result an error being returned.
        :param context:
        :param reference:
        :return:
        """
        raise NotImplementedError('ConsoleAdapter.delete_activity(): not supported.')

    async def update_activity(self, context: TurnContext, activity: Activity):
        """
        Not supported for the ConsoleAdapter. Calling this method or `TurnContext.update_activity()`
        will result an error being returned.
        :param context:
        :param activity:
        :return:
        """
        raise NotImplementedError('ConsoleAdapter.update_activity(): not supported.')