import logging
from typing import Any

import boto3
import botocore
from typeguard import typechecked

from psi_pinpoint import get_logger


@typechecked
class PinpointClient:
    """Wrapper class around the Boto3 Pinpoint client to make interacting with
    the service a smidge easier"""

    def __init__(
        self, aws_session: boto3.Session = None, logger: logging.Logger = None
    ) -> None:
        self.__aws_session = aws_session or boto3.session.Session()
        self.__client = self.aws_session.client("pinpoint")
        self.__logger = logger or get_logger()

    @property
    def aws_session(self) -> boto3.Session:
        return self.__aws_session

    @property
    def client(self) -> botocore.client:
        return self.__client

    @property
    def logger(self) -> logging.Logger:
        return self.__logger

    @typechecked
    def create_application(self, app_name: str, tags: dict) -> dict:
        """Create a new Pinpoint Application (Projecet)

        Parameters
        ----------
        app_name : str
            Friendly name of an Application/Project

        tags : dict
            tag key/tag values to apply to the app e.g. {'t_AppID': 'SVC01234'}

        Returns
        -------
        dict : {
            'Arn': 'arn:aws:mobiletargeting:us-east-1:490126044637:apps/fc3e1678715c489fb6ac6e31306c627f',
            'Id': 'fc3e1678715c489fb6ac6e31306c627f',
            'Name': 'pp-utils-test',
            'tags': {
                'owner': 'jonathan.morris@pearson.com',
                't_AppID': 'SVC01234'
            },
            'CreationDate': '2022-06-06T19:42:15.824Z'
        }
        """
        return self._make_api_call(
            "create_app",
            **{"CreateApplicationRequest": {"Name": app_name, "tags": tags}},
        )["ApplicationResponse"]

    @typechecked
    def get_application(
        self, app_id: str = None, app_name: str = None
    ) -> dict:
        """Return a Pinpoint Application (Projecet) by ID or name

        Parameters
        ----------
        app_id : str
            An Application ID (Project ID in the console) for direct lookup

        app_name : str
            Friendly name of an Application/Project. Gets a list of all apps
            in an account and tries to match by name.

        Returns
        -------
        dict: {
            'Arn': 'arn:aws:mobiletargeting:us-east-1:490126044637:apps/fc3e1678715c489fb6ac6e31306c627f',
            'Id': 'fc3e1678715c489fb6ac6e31306c627f',
            'Name': 'pp-utils-test',
            'tags': {
                'owner': 'jonathan.morris@pearson.com',
                't_AppID': 'SVC01234'
            },
            'CreationDate': '2022-06-06T19:42:15.824Z'
        }
        """
        if app_id:
            self.logger.info(f"Fetching app {app_id}")
            result = self._make_api_call(ApplicationId=app_id, api="get_app")[
                "ApplicationResponse"
            ]

        elif app_name:
            self.logger.info(f"Fetching app {app_name}")
            result = self._get_from_list(
                filter_key="Name",
                filter_value=app_name,
                results_selector="ApplicationsResponse",
                api="get_apps",
            )

        else:
            raise TypeError(
                f"Must provide one of {list(locals().keys())[1:]}"
            )  # Kinda hacky. Should probably use the inspect module or just write out each param
        return result

    @typechecked
    def list_applications(self) -> list:
        """Return a list of all applications

        Returns
        -------
        list : [
            {
                'Arn': 'arn:aws:mobiletargeting:us-east-1:490126044637:apps/fc3e1678715c489fb6ac6e31306c627f',
                'Id': 'fc3e1678715c489fb6ac6e31306c627f',
                'Name': 'pp-utils-test',
                'tags': {
                    'owner': 'jonathan.morris@pearson.com',
                    't_AppID': 'SVC01234'
                },
                'CreationDate': '2022-06-06T19:42:15.824Z'
            },
            ...
        ]
        """
        return self._paginate("get_apps", "ApplicationsResponse")

    @typechecked
    def get_campaign(
        self,
        app_id: str = None,
        app_name: str = None,
        campaign_id: str = None,
        campaign_name: str = None,
    ) -> dict:
        app_id = app_id or self.get_application(app_name=app_name)["Id"]

        if campaign_id:
            self.logger.info(f"Fetching campaign {campaign_id}")
            result = self._make_api_call(
                ApplicationId=app_id,
                CampaignId=campaign_id,
                api="get_campaign",
            )["CampaignResponse"]

        elif campaign_name:
            self.logger.info(f"Fetching campaign {campaign_name}")
            result = self._get_from_list(
                filter_key="Name",
                filter_value=campaign_name,
                results_selector="CampaignsResponse",
                api="get_campaigns",
                ApplicationId=app_id,
            )

        else:
            raise TypeError("Must provide one of campaign_id, campaign_name")
        return result

    @typechecked
    def list_campaigns(self, app_id: str = None, app_name: str = None) -> list:
        app_id = app_id or self.get_application(app_name=app_name)["Id"]
        return self._paginate(
            "get_campaigns", "CampaignsResponse", ApplicationId=app_id
        )

    @typechecked
    def send_users_messages(
        self,
        app_id: str = None,
        app_name: str = None,
        send_users_message_request: dict = None,
    ) -> dict:
        """Send a message to a list of users
        https: //boto3.amazonaws.com/v1/documentation/api/latest/reference/services/pinpoint.html#Pinpoint.Client.send_users_messages

        Parameters
        ----------
        app_id : str
            An Application ID (Project ID in the console) for direct lookup

        app_name : str
            Friendly name of an Application/Project. Gets a list of all apps
            in an account and tries to match by name.

        send_users_message_request: dict
            Message configuration dict. See the doc linked above for details.
            Example Email message config: {
                "MessageConfiguration": {
                    "EmailMessage": {
                        "SimpleEmail": {
                            "Subject": {
                                "Data": "Email Subject"
                            },
                            "TextPart": {
                                "Data": "This is an example plain text email body."
                            }
                        }
                    }
                }
                        "Users": {
                    "john.doe@pearson.com": {},
                    "jane.doe@pearson.com": {}
                }
            }

        Returns
        -------
        dict : {
            "ApplicationId": "0670d277a2284ab89136e31a53716b38",
            "RequestId": "81ef986a-61ce-44d1-bd9c-5f41f112bc6b",
            "Result": {
                "john.doe@pearson.com": {
                    "john.doe@pearson.com": {
                        "Address": "john.doe@pearson.com",
                        "DeliveryStatus": "SUCCESSFUL",
                        "MessageId": "ggoj0rsveqptvo59idadahfq59ejvh8mnjsmq5g0",
                        "StatusCode": 200,
                        "StatusMessage": "02000000ggoj0rsv-eqptvo59-idad-ahfq-59ej-vh8mnjsmq5g0-000000"
                    }
                },
                "jane.doe@pearson.com": {
                    "jane.doe@pearson.com": {
                        "Address": "jane.doe@pearson.com",
                        "DeliveryStatus": "SUCCESSFUL",
                        "MessageId": "arrptporpcmj9pt7us4lrn33mf2acnsc4phveh80",
                        "StatusCode": 200,
                        "StatusMessage": "02000000arrptpor-pcmj9pt7-us4l-rn33-mf2a-cnsc4phveh80-000000"
                    }
                }
            }
        }
        """
        app_id = app_id or self.get_application(app_name=app_name)["Id"]
        return self._make_api_call(
            "send_users_messages",
            ApplicationId=app_id,
            SendUsersMessageRequest=send_users_message_request,
        )["SendUsersMessageResponse"]

    @typechecked
    def get_user_endpoints(
        self, app_id: str = None, app_name: str = None, user_id: str = None
    ) -> list:
        """Lookup all endpoints associeted with a given user_id
        https: //boto3.amazonaws.com/v1/documentation/api/latest/reference/services/pinpoint.html#Pinpoint.Client.get_user_endpoints

        Parameters
        ----------
        app_id : str
            An Application ID (Project ID in the console) for direct lookup

        app_name : str
            Friendly name of an Application/Project. Gets a list of all apps
            in an account and tries to match by name.

        user_id : str
            A user's unique ID. For Courier, this is just their email address.

        Returns
        -------
        list : [
            {
                'Address': 'jonathan.morris@pearson.com',
                'ApplicationId': '0670d277a2284ab89136e31a53716b38',
                'ChannelType': 'EMAIL',
                'CohortId': '45',
                'CreationDate': '2022-08-08T16: 26: 11.156Z',
                'EffectiveDate': '2022-08-08T16: 26: 11.156Z',
                'EndpointStatus': 'ACTIVE',
                'Id': 'jonathan.morris@pearson.com',
                'OptOut': 'NONE',
                'RequestId': 'df17fe05-9c6a-414e-ac60-be97f668577a',
                'User': {'UserId': 'jonathan.morris@pearson.com'}
            },
            ...
        ]
        """
        app_id = app_id or self.get_application(app_name=app_name)["Id"]
        return self._make_api_call(
            "get_user_endpoints",
            ApplicationId=app_id,
            UserId=user_id,
        )["EndpointsResponse"]["Item"]

    def _make_api_call(self, api: str, **kwargs) -> dict:
        """Return the result of an API call

        Parameters
        ----------
        api : str
            the Pinpoint client method to invoke, such as get_app or create_app

        kwargs : Any
            Any arguments needed by the given `api` parameter
        """
        return getattr(self.client, api)(**kwargs)

    def _get_from_list(
        self,
        filter_key: str,
        filter_value: Any,
        results_selector: str,
        api: str = None,
        **kwargs,
    ) -> dict:
        """Look up a resource by name or any other attribute and return the
        first match

        Parameters
        ----------
        filter_key : str
            The resource attribute to match on e.g. "Name"

        filter_value : Any
            The attribute value to match e.g. "psi-general-coms"

        results_selector : str
            Top-level key used to identify the collection of resources returned
            by a LIST call, e.g. `JourneysResponse` for the `list_journeys` API

        api : str
            the API to call e.g. `list_journeys`

        kwargs: Any
            Any additional arguments to pass to the API
        """
        results = self._paginate(api, results_selector, **kwargs)
        for result in results:
            if result.get(filter_key) == filter_value:
                return result
        return {}

    def _paginate(self, api: str, results_selector: str, **kwargs) -> list:
        """Paginate a list of results with a native Pinpoint paginator if
        available or manually with results that return `NextToken`

        Parameters
        ----------
        results_selector : str
            Top-level key used to identify the collection of resources returned
            by a LIST call, e.g. `JourneysResponse` for the `list_journeys` API

        api : str
            the API to call e.g. `list_journeys`

        kwargs: Any
            Any additional arguments to pass to the API"""
        results = []
        if self.client.can_paginate(api):
            paginator = self.client.get_paginator(api)
            iterator = paginator.paginate()
            results.extend(
                result.get(results_selector, []) for result in iterator
            )
        else:
            self.logger.warning(
                f"Unable to get paginator for {api}. Trying manually..."
            )
            manual = getattr(self.client, api)(**kwargs)
            results.extend(manual[results_selector].get("Item", []))
            while manual.get("NextToken"):
                manual = getattr(self.client, api)(
                    manual["NextToken"], **kwargs
                )
                results.extend(manual[results_selector].get("Item", []))
        return results


if __name__ == "__main__":
    print(
        PinpointClient().get_user_endpoints(
            "0670d277a2284ab89136e31a53716b38",
            user_id="stephen.davidson@pearson.com",
        )
    )
