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, name: str, tags: dict) -> dict:
        '''Create a new Pinpoint Application (Projecet)

        Parameters
        ----------
        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': name, 'tags': tags}}
        )['ApplicationResponse']

    @typechecked
    def get_application(
            self,
            app_id: str = None,
            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

        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 name:
            self.logger.info(f'Fetching app {name}')
            result = self._get_from_list(
                filter_key='Name',
                filter_value=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(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

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

    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_application(name='psi-general-coms'))
