import datetime as dt
import logging
from enum import Enum

from dateutil.parser import parse

from .message import Recipients, Message
from .utils import AttachableMixin, TrackerSet
from .utils import Pagination, NEXT_LINK_KEYWORD, ApiComponent

GAL_MAIN_RESOURCE = 'users'

log = logging.getLogger(__name__)


class RecipientType(Enum):
    TO = 'to'
    CC = 'cc'
    BCC = 'bcc'


class Contact(ApiComponent, AttachableMixin):
    """ Contact manages lists of events on associated contact on office365. """

    _endpoints = {
        'contact': '/contacts',
        'root_contact': '/contacts/{id}',
        'child_contact': '/contactFolders/{folder_id}/contacts'
    }

    message_constructor = Message

    def __init__(self, *, parent=None, con=None, **kwargs):
        """ Create a contact API component

        :param parent: parent account for this folder
        :type parent: Account
        :param Connection con: connection to use if no parent specified
        :param Protocol protocol: protocol to use if no parent specified
         (kwargs)
        :param str main_resource: use this resource instead of parent resource
         (kwargs)
        """
        assert parent or con, 'Need a parent or a connection'
        self.con = parent.con if parent else con

        # Choose the main_resource passed in kwargs over parent main_resource
        main_resource = kwargs.pop('main_resource',
                                   None) or getattr(parent, 'main_resource',
                                                    None) if parent else None
        super().__init__(
            protocol=parent.protocol if parent else kwargs.get('protocol'),
            main_resource=main_resource)

        cloud_data = kwargs.get(self._cloud_data_key, {})
        cc = self._cc  # alias to shorten the code

        # internal to know which properties need to be updated on the server
        self._track_changes = TrackerSet(casing=cc)

        self.object_id = cloud_data.get(cc('id'), None)
        self.__created = cloud_data.get(cc('createdDateTime'), None)
        self.__modified = cloud_data.get(cc('lastModifiedDateTime'), None)

        local_tz = self.protocol.timezone
        self.__created = parse(self.created).astimezone(
            local_tz) if self.__created else None
        self.__modified = parse(self.modified).astimezone(
            local_tz) if self.__modified else None

        self.__display_name = cloud_data.get(cc('displayName'), '')
        self.__name = cloud_data.get(cc('givenName'), '')
        self.__surname = cloud_data.get(cc('surname'), '')

        self.__title = cloud_data.get(cc('title'), '')
        self.__job_title = cloud_data.get(cc('jobTitle'), '')
        self.__company_name = cloud_data.get(cc('companyName'), '')
        self.__department = cloud_data.get(cc('department'), '')
        self.__office_location = cloud_data.get(cc('officeLocation'), '')
        self.__business_phones = cloud_data.get(cc('businessPhones'), []) or []
        self.__mobile_phone = cloud_data.get(cc('mobilePhone'), '')
        self.__home_phones = cloud_data.get(cc('homePhones'), []) or []

        emails = cloud_data.get(cc('emailAddresses'), [])
        self.__emails = Recipients(
            recipients=[(rcp.get(cc('name'), ''), rcp.get(cc('address'), ''))
                        for rcp in emails],
            parent=self, field=cc('emailAddresses'))
        email = cloud_data.get(cc('email'))
        self.__emails.untrack = True
        if email and email not in self.__emails:
            # a Contact from OneDrive?
            self.__emails.add(email)
        self.__business_address = cloud_data.get(cc('businessAddress'), {})
        self.__home_address = cloud_data.get(cc('homesAddress'), {})
        self.__other_address = cloud_data.get(cc('otherAddress'), {})
        self.__preferred_language = cloud_data.get(cc('preferredLanguage'),
                                                   None)

        self.__categories = cloud_data.get(cc('categories'), [])
        self.__folder_id = cloud_data.get(cc('parentFolderId'), None)

        # When using Users endpoints (GAL)
        # Missing keys: ['mail', 'userPrincipalName']
        mail = cloud_data.get(cc('mail'), None)
        user_principal_name = cloud_data.get(cc('userPrincipalName'), None)
        if mail and mail not in self.emails:
            self.emails.add(mail)
        if user_principal_name and user_principal_name not in self.emails:
            self.emails.add(user_principal_name)
        self.__emails.untrack = False

    @property
    def created(self):
        """ Created Time

        :rtype: datetime
        """
        return self.__created

    @property
    def modified(self):
        """ Last Modified Time

        :rtype: datetime
        """
        return self.__modified

    @property
    def display_name(self):
        """ Display Name

        :getter: Get the display name of the contact
        :setter: Update the display name
        :type: str
        """
        return self.__display_name

    @display_name.setter
    def display_name(self, value):
        self.__display_name = value
        self._track_changes.add(self._cc('displayName'))

    @property
    def name(self):
        """ First Name

        :getter: Get the name of the contact
        :setter: Update the name
        :type: str
        """
        return self.__name

    @name.setter
    def name(self, value):
        self.__name = value
        self._track_changes.add(self._cc('givenName'))

    @property
    def surname(self):
        """ Surname of Contact

        :getter: Get the surname of the contact
        :setter: Update the surname
        :type: str
        """
        return self.__surname

    @surname.setter
    def surname(self, value):
        self.__surname = value
        self._track_changes.add(self._cc('surname'))

    @property
    def full_name(self):
        """ Full Name (Name + Surname)

        :rtype: str
        """
        return '{} {}'.format(self.name, self.surname).strip()

    @property
    def title(self):
        """ Title (Mr., Ms., etc..)

        :getter: Get the title of the contact
        :setter: Update the title
        :type: str
        """
        return self.__title

    @title.setter
    def title(self, value):
        self.__title = value
        self._track_changes.add(self._cc('title'))

    @property
    def job_title(self):
        """ Job Title

        :getter: Get the job title of contact
        :setter: Update the job title
        :type: str
        """
        return self.__job_title

    @job_title.setter
    def job_title(self, value):
        self.__job_title = value
        self._track_changes.add(self._cc('jobTitle'))

    @property
    def company_name(self):
        """ Name of the company

        :getter: Get the company name of contact
        :setter: Update the company name
        :type: str
        """
        return self.__company_name

    @company_name.setter
    def company_name(self, value):
        self.__company_name = value
        self._track_changes.add(self._cc('companyName'))

    @property
    def department(self):
        """ Department

        :getter: Get the department of contact
        :setter: Update the department
        :type: str
        """
        return self.__department

    @department.setter
    def department(self, value):
        self.__department = value
        self._track_changes.add(self._cc('department'))

    @property
    def office_location(self):
        """ Office Location

        :getter: Get the office location of contact
        :setter: Update the office location
        :type: str
        """
        return self.__office_location

    @office_location.setter
    def office_location(self, value):
        self.__office_location = value
        self._track_changes.add(self._cc('officeLocation'))

    @property
    def business_phones(self):
        """ Business Contact numbers

        :getter: Get the contact numbers of contact
        :setter: Update the contact numbers
        :type: list[str]
        """
        return self.__business_phones

    @business_phones.setter
    def business_phones(self, value):
        if isinstance(value, tuple):
            value = list(value)
        if not isinstance(value, list):
            value = [value]
        self.__business_phones = value
        self._track_changes.add(self._cc('businessPhones'))

    @property
    def mobile_phone(self):
        """ Personal Contact numbers

        :getter: Get the contact numbers of contact
        :setter: Update the contact numbers
        :type: list[str]
        """
        return self.__mobile_phone

    @mobile_phone.setter
    def mobile_phone(self, value):
        self.__mobile_phone = value
        self._track_changes.add(self._cc('mobilePhone'))

    @property
    def home_phones(self):
        """ Home Contact numbers

        :getter: Get the contact numbers of contact
        :setter: Update the contact numbers
        :type: list[str]
        """
        return self.__home_phones

    @home_phones.setter
    def home_phones(self, value):
        if isinstance(value, list):
            self.__home_phones = value
        elif isinstance(value, str):
            self.__home_phones = [value]
        elif isinstance(value, tuple):
            self.__home_phones = list(value)
        else:
            raise ValueError('home_phones must be a list')
        self._track_changes.add(self._cc('homePhones'))

    @property
    def emails(self):
        """ List of email ids of the Contact

        :rtype: Recipients
        """
        return self.__emails

    @property
    def main_email(self):
        """ Primary(First) email id of the Contact

        :rtype: str
        """
        if not self.emails:
            return None
        return self.emails[0].address

    @property
    def business_address(self):
        """ Business Address

        :getter: Get the address of contact
        :setter: Update the address
        :type: dict
        """
        return self.__business_address

    @business_address.setter
    def business_address(self, value):
        if not isinstance(value, dict):
            raise ValueError('"business_address" must be dict')
        self.__business_address = value
        self._track_changes.add(self._cc('businessAddress'))

    @property
    def home_address(self):
        """ Home Address

        :getter: Get the address of contact
        :setter: Update the address
        :type: dict
        """
        return self.__home_address

    @home_address.setter
    def home_address(self, value):
        if not isinstance(value, dict):
            raise ValueError('"home_address" must be dict')
        self.__home_address = value
        self._track_changes.add(self._cc('homesAddress'))

    @property
    def other_address(self):
        """ Other Address

        :getter: Get the address of contact
        :setter: Update the address
        :type: dict
        """
        return self.__other_address

    @other_address.setter
    def other_address(self, value):
        if not isinstance(value, dict):
            raise ValueError('"other_address" must be dict')
        self.__other_address = value
        self._track_changes.add(self._cc('otherAddress'))

    @property
    def preferred_language(self):
        """ Preferred Language

        :getter: Get the language of contact
        :setter: Update the language
        :type: str
        """
        return self.__preferred_language

    @preferred_language.setter
    def preferred_language(self, value):
        self.__preferred_language = value
        self._track_changes.add(self._cc('preferredLanguage'))

    @property
    def categories(self):
        """ Assigned Categories

        :getter: Get the categories
        :setter: Update the categories
        :type: list[str]
        """
        return self.__categories

    @categories.setter
    def categories(self, value):
        if isinstance(value, list):
            self.__categories = value
        elif isinstance(value, str):
            self.__categories = [value]
        elif isinstance(value, tuple):
            self.__categories = list(value)
        else:
            raise ValueError('categories must be a list')
        self._track_changes.add(self._cc('categories'))

    @property
    def folder_id(self):
        """ ID of the folder

        :rtype: str
        """
        return self.__folder_id

    def __str__(self):
        return self.__repr__()

    def __repr__(self):
        return self.display_name or self.full_name or 'Unknown Name'

    def to_api_data(self, restrict_keys=None):
        """ Returns a dictionary in cloud format

        :param restrict_keys: a set of keys to restrict the returned data to.
        """
        cc = self._cc  # alias

        data = {
            cc('displayName'): self.__display_name,
            cc('givenName'): self.__name,
            cc('surname'): self.__surname,
            cc('title'): self.__title,
            cc('jobTitle'): self.__job_title,
            cc('companyName'): self.__company_name,
            cc('department'): self.__department,
            cc('officeLocation'): self.__office_location,
            cc('businessPhones'): self.__business_phones,
            cc('mobilePhone'): self.__mobile_phone,
            cc('homePhones'): self.__home_phones,
            cc('emailAddresses'): [{self._cc('name'): recipient.name or '',
                                    self._cc('address'): recipient.address}
                                   for recipient in self.emails],
            cc('businessAddress'): self.__business_address,
            cc('homesAddress'): self.__home_address,
            cc('otherAddress'): self.__other_address,
            cc('categories'): self.__categories
        }

        if restrict_keys:
            restrict_keys.add(cc(
                'givenName'))  # GivenName is required by the api all the time.
            for key in list(data.keys()):
                if key not in restrict_keys:
                    del data[key]
        return data

    def delete(self):
        """ Deletes this contact

        :return: Success or Failure
        :rtype: bool
        :raises RuntimeError: if contact is not yet saved to cloud
        """
        if not self.object_id:
            raise RuntimeError('Attempting to delete an unsaved Contact')

        url = self.build_url(
            self._endpoints.get('root_contact').format(id=self.object_id))

        response = self.con.delete(url)

        return bool(response)

    def save(self):
        """ Saves this contact to the cloud (create or update existing one
        based on what values have changed)

        :return: Saved or Not
        :rtype: bool
        """
        if self.object_id:
            # Update Contact
            if not self._track_changes:
                return True  # there's nothing to update
            url = self.build_url(
                self._endpoints.get('root_contact').format(id=self.object_id))
            method = self.con.patch
            data = self.to_api_data(restrict_keys=self._track_changes)
        else:
            # Save new Contact
            if self.__folder_id:
                url = self.build_url(
                    self._endpoints.get('child_contact').format(
                        folder_id=self.__folder_id))
            else:
                url = self.build_url(self._endpoints.get('contact'))
            method = self.con.post
            data = self.to_api_data(restrict_keys=self._track_changes)
        response = method(url, data=data)

        if not response:
            return False

        if not self.object_id:
            # New Contact
            contact = response.json()

            self.object_id = contact.get(self._cc('id'), None)

            self.__created = contact.get(self._cc('createdDateTime'), None)
            self.__modified = contact.get(self._cc('lastModifiedDateTime'),
                                          None)

            local_tz = self.protocol.timezone
            self.__created = parse(self.created).astimezone(
                local_tz) if self.__created else None
            self.__modified = parse(self.modified).astimezone(
                local_tz) if self.__modified else None
        else:
            self.__modified = self.protocol.timezone.localize(dt.datetime.now())

        return True

    def new_message(self, recipient=None, *, recipient_type=RecipientType.TO):
        """ This method returns a new draft Message instance with
        contacts first email as a recipient

        :param Recipient recipient: a Recipient instance where to send this
         message. If None first email of this contact will be used
        :param RecipientType recipient_type: section to add recipient into
        :return: newly created message
        :rtype: Message or None
        """
        if self.main_resource == GAL_MAIN_RESOURCE:
            # preventing the contact lookup to explode for big organizations..
            raise RuntimeError('Sending a message to all users within an '
                               'Organization is not allowed')

        if isinstance(recipient_type, str):
            recipient_type = RecipientType(recipient_type)

        recipient = recipient or self.emails.get_first_recipient_with_address()
        if not recipient:
            return None

        new_message = self.message_constructor(parent=self, is_draft=True)

        target_recipients = getattr(new_message, str(recipient_type.value))
        target_recipients.add(recipient)

        return new_message


class BaseContactFolder(ApiComponent):
    """ Base Contact Folder Grouping Functionality """

    _endpoints = {
        'gal': '',
        'root_contacts': '/contacts',
        'folder_contacts': '/contactFolders/{id}/contacts',
        'get_folder': '/contactFolders/{id}',
        'root_folders': '/contactFolders',
        'child_folders': '/contactFolders/{id}/childFolders'
    }

    contact_constructor = Contact
    message_constructor = Message

    def __init__(self, *, parent=None, con=None, **kwargs):
        """ Create a contact folder component

        :param parent: parent folder/account for this folder
        :type parent: BaseContactFolder or Account
        :param Connection con: connection to use if no parent specified
        :param Protocol protocol: protocol to use if no parent specified
         (kwargs)
        :param str main_resource: use this resource instead of parent resource
         (kwargs)
        """
        assert parent or con, 'Need a parent or a connection'
        self.con = parent.con if parent else con

        # Choose the main_resource passed in kwargs over parent main_resource
        main_resource = (kwargs.pop('main_resource', None) or
                         getattr(parent, 'main_resource',
                                 None) if parent else None)
        super().__init__(
            protocol=parent.protocol if parent else kwargs.get('protocol'),
            main_resource=main_resource)

        # This folder has no parents if root = True.
        self.root = kwargs.pop('root', False)

        cloud_data = kwargs.get(self._cloud_data_key, {})

        # Fallback to manual folder if nothing available on cloud data
        self.name = cloud_data.get(self._cc('displayName'),
                                   kwargs.get('name',
                                              ''))
        # TODO: Most of above code is same as mailbox.Folder __init__

        self.folder_id = cloud_data.get(self._cc('id'), None)
        self.parent_id = cloud_data.get(self._cc('parentFolderId'), None)

    def __str__(self):
        return self.__repr__()

    def __repr__(self):
        return 'Contact Folder: {}'.format(self.name)

    def get_contacts(self, limit=100, *, query=None, order_by=None, batch=None):
        """ Gets a list of contacts from this address book

        When querying the Global Address List the Users endpoint will be used.
        Only a limited set of information will be available unless you have
        access to scope 'User.Read.All' which requires App Administration
        Consent.

        Also using endpoints has some limitations on the querying capabilities.

        To use query an order_by check the OData specification here:
        http://docs.oasis-open.org/odata/odata/v4.0/errata03/os/complete/
        part2-url-conventions/odata-v4.0-errata03-os-part2-url-conventions
        -complete.html

        :param limit: max no. of contacts to get. Over 999 uses batch.
        :type limit: int or None
        :param query: applies a OData filter to the request
        :type query: Query or str
        :param order_by: orders the result set based on this condition
        :type order_by: Query or str
        :param int batch: batch size, retrieves items in
         batches allowing to retrieve more items than the limit.
        :return: list of contacts
        :rtype: list[Contact] or Pagination
        """

        if self.main_resource == GAL_MAIN_RESOURCE:
            # using Users endpoint to access the Global Address List
            url = self.build_url(self._endpoints.get('gal'))
        else:
            if self.root:
                url = self.build_url(self._endpoints.get('root_contacts'))
            else:
                url = self.build_url(
                    self._endpoints.get('folder_contacts').format(
                        id=self.folder_id))

        if limit is None or limit > self.protocol.max_top_value:
            batch = self.protocol.max_top_value

        params = {'$top': batch if batch else limit}

        if order_by:
            params['$orderby'] = order_by

        if query:
            if isinstance(query, str):
                params['$filter'] = query
            else:
                params.update(query.as_params())

        response = self.con.get(url, params=params)
        if not response:
            return []

        data = response.json()

        # Everything received from cloud must be passed as self._cloud_data_key
        contacts = [self.contact_constructor(parent=self,
                                             **{self._cloud_data_key: contact})
                    for contact in data.get('value', [])]

        next_link = data.get(NEXT_LINK_KEYWORD, None)

        if batch and next_link:
            return Pagination(parent=self, data=contacts,
                              constructor=self.contact_constructor,
                              next_link=next_link, limit=limit)
        else:
            return contacts

    def get_contact_by_email(self, email):
        """ Returns a Contact by it's email

        :param email: email to get contact for
        :return: Contact for specified email
        :rtype: Contact
        """
        if not email:
            return None

        email = email.strip()
        query = self.q().any(collection='email_addresses', attribute='address',
                             word=email, operation='eq')
        contacts = self.get_contacts(limit=1, query=query)
        return contacts[0] if contacts else None


class ContactFolder(BaseContactFolder):
    """ A Contact Folder representation """

    def get_folder(self, folder_id=None, folder_name=None):
        """ Returns a Contact Folder by it's id or child folders by name

        :param folder_id: the folder_id to be retrieved.
         Can be any folder Id (child or not)
        :param folder_name: the folder name to be retrieved.
         Must be a child of this folder
        :return: a single contact folder
        :rtype: ContactFolder
        """

        if folder_id and folder_name:
            raise RuntimeError('Provide only one of the options')

        if not folder_id and not folder_name:
            raise RuntimeError('Provide one of the options')

        if folder_id:
            # get folder by it's id, independent of the parent of this folder_id
            url = self.build_url(
                self._endpoints.get('get_folder').format(id=folder_id))
            params = None
        else:
            # get folder by name. Only looks up in child folders.
            if self.root:
                url = self.build_url(self._endpoints.get('root_folders'))
            else:
                url = self.build_url(
                    self._endpoints.get('child_folders').format(
                        id=self.folder_id))

            params = {'$filter': "{} eq '{}'".format(self._cc('displayName'),
                                                     folder_name), '$top': 1}

        response = self.con.get(url, params=params)
        if not response:
            return None

        if folder_id:
            folder = response.json()
        else:
            folder = response.json().get('value')
            folder = folder[0] if folder else None
            if folder is None:
                return None

        # Everything received from cloud must be passed as self._cloud_data_key
        # we don't pass parent, as this folder may not be a child of self.
        return ContactFolder(con=self.con, protocol=self.protocol,
                             main_resource=self.main_resource,
                             **{self._cloud_data_key: folder})

    def get_folders(self, limit=None, *, query=None, order_by=None):
        """ Returns a list of child folders

        :param int limit: max no. of folders to get. Over 999 uses batch.
        :param query: applies a OData filter to the request
        :type query: Query or str
        :param order_by: orders the result set based on this condition
        :type order_by: Query or str
        :return: list of folders
        :rtype: list[ContactFolder]
        """
        if self.root:
            url = self.build_url(self._endpoints.get('root_folders'))
        else:
            url = self.build_url(
                self._endpoints.get('child_folders').format(id=self.folder_id))

        params = {}

        if limit:
            params['$top'] = limit

        if order_by:
            params['$orderby'] = order_by

        if query:
            if isinstance(query, str):
                params['$filter'] = query
            else:
                params.update(query.as_params())

        response = self.con.get(url, params=params or None)
        if not response:
            return []

        data = response.json()

        return [ContactFolder(parent=self, **{self._cloud_data_key: folder})
                for folder in data.get('value', [])]

    def create_child_folder(self, folder_name):
        """ Creates a new child folder

        :param str folder_name: name of the new folder to create
        :return: newly created folder
        :rtype: ContactFolder or None
        """

        if not folder_name:
            return None

        if self.root:
            url = self.build_url(self._endpoints.get('root_folders'))
        else:
            url = self.build_url(
                self._endpoints.get('child_folders').format(id=self.folder_id))

        response = self.con.post(url,
                                 data={self._cc('displayName'): folder_name})
        if not response:
            return None

        folder = response.json()

        # Everything received from cloud must be passed as self._cloud_data_key
        return ContactFolder(parent=self, **{self._cloud_data_key: folder})

    def update_folder_name(self, name):
        """ Change this folder name

        :param str name: new name to change to
        :return: Updated or Not
        :rtype: bool
        """
        if self.root:
            return False
        if not name:
            return False

        url = self.build_url(
            self._endpoints.get('get_folder').format(id=self.folder_id))

        response = self.con.patch(url, data={self._cc('displayName'): name})
        if not response:
            return False

        folder = response.json()

        self.name = folder.get(self._cc('displayName'), '')
        self.parent_id = folder.get(self._cc('parentFolderId'), None)

        return True

    def move_folder(self, to_folder):
        """ Change this folder name

        :param to_folder: folder_id/ContactFolder to move into
        :type to_folder: str or ContactFolder
        :return: Moved or Not
        :rtype: bool
        """
        if self.root:
            return False
        if not to_folder:
            return False

        url = self.build_url(
            self._endpoints.get('get_folder').format(id=self.folder_id))

        if isinstance(to_folder, ContactFolder):
            folder_id = to_folder.folder_id
        elif isinstance(to_folder, str):
            folder_id = to_folder
        else:
            return False

        response = self.con.patch(url,
                                  data={self._cc('parentFolderId'): folder_id})
        if not response:
            return False

        folder = response.json()

        self.name = folder.get(self._cc('displayName'), '')
        self.parent_id = folder.get(self._cc('parentFolderId'), None)

        return True

    def delete(self):
        """ Deletes this folder

        :return: Deleted or Not
        :rtype: bool
        """

        if self.root or not self.folder_id:
            return False

        url = self.build_url(
            self._endpoints.get('get_folder').format(id=self.folder_id))

        response = self.con.delete(url)
        if not response:
            return False

        self.folder_id = None

        return True

    def new_contact(self):
        """ Creates a new contact to be saved into it's parent folder

        :return: newly created contact
        :rtype: Contact
        """
        contact = self.contact_constructor(parent=self)
        if not self.root:
            contact.__folder_id = self.folder_id
        return contact

    def new_message(self, recipient_type=RecipientType.TO, *, query=None):
        """ This method returns a new draft Message instance with all the
        contacts first email as a recipient

        :param RecipientType recipient_type: section to add recipient into
        :param query: applies a OData filter to the request
        :type query: Query or str
        :return: newly created message
        :rtype: Message or None
        """

        if isinstance(recipient_type, str):
            recipient_type = RecipientType(recipient_type)

        recipients = [contact.emails[0]
                      for contact in self.get_contacts(limit=None, query=query)
                      if contact.emails and contact.emails[0].address]

        if not recipients:
            return None

        new_message = self.message_constructor(parent=self, is_draft=True)
        target_recipients = getattr(new_message, str(recipient_type.value))
        target_recipients.add(recipients)

        return new_message


class AddressBook(ContactFolder):
    """ A class representing an address book """

    def __init__(self, *, parent=None, con=None, **kwargs):
        # Set instance to be a root instance
        super().__init__(parent=parent, con=con, root=True, **kwargs)

    def __repr__(self):
        return 'Address Book resource: {}'.format(self.main_resource)


class GlobalAddressList(BaseContactFolder):
    """ A class representing the Global Address List (Users API) """

    def __init__(self, *, parent=None, con=None, **kwargs):
        # Set instance to root instance and main_resource to GAL_MAIN_RESOURCE
        super().__init__(parent=parent, con=con, root=True,
                         main_resource=GAL_MAIN_RESOURCE,
                         name='Global Address List', **kwargs)

    def __repr__(self):
        return 'Global Address List'

    def get_contact_by_email(self, email):
        """ Returns a Contact by it's email

        :param email: email to get contact for
        :return: Contact for specified email
        :rtype: Contact
        """
        if not email:
            return None

        email = email.strip()

        url = self.build_url('{}/{}'.format(self._endpoints.get('gal'), email))

        response = self.con.get(url)
        if not response:
            return []

        data = response.json()

        # Everything received from cloud must be passed as self._cloud_data_key
        return self.contact_constructor(parent=self,
                                        **{self._cloud_data_key: data})
