from posixpath import join as urljoin

import requests

from ._meta import __project_link__, __project_name__, __version__
from .models import Category, Page, Post, PostRevision, PostStatus, Tag


class WordPress(object):

    def __init__(self, url, verify_ssl=True):
        """
        WordPress Library.

        Arguments
        ---------

        url : str
            The WordPress URL (ex https://example.org/).
        verify_ssl : bool
            Should we verify that the WordPress site is using a good SSL cert.
        """
        self.url = self._get_wp_api_url(url)
        self.version = 'v2'

        self.headers = {
            'User-Agent': '{0}/{1} +{2}'.format(
                __project_name__,
                __version__,
                __project_link__
            )
        }

    # Private Methods

    def _get_wp_api_url(self, url):
        """
        Private function for finding the WP-API URL.

        Arguments
        ---------

        url : str
            WordPress instance URL.
        """
        resp = requests.head(url)

        # Search the Links for rel="https://api.w.org/".
        wp_api_rel = resp.links.get('https://api.w.org/')

        if wp_api_rel:
            return wp_api_rel['url']
        else:
            # TODO: Rasie a better exception to the rel doesn't exist.
            raise Exception

    def _get(self, endpoint, params={}):
        """
        Private function for making GET requests.

        Arguments
        ---------

        endpoint : str
            WordPress endpoint.
        params : dict
            HTTP parameters when making the connection.

        Returns
        -------

        dict/list
            Returns the data from the endpoint.
        """
        url = urljoin(self.url, 'wp', self.version, endpoint)

        resp = requests.get(url, params=params, headers=self.headers)

        if not resp.status_code == 200:
            msg = ('WordPress REST API returned the status code '
                   '{0}.'.format(resp.status_code))
            raise Exception(msg)

        return resp.json()

    def _post(self, endpoint, data={}, params={}):
        """
        Private function for making POST requests.

        Arguments
        ---------

        endpoint : str
            WordPress endpoint.
        data : dict
            Data to send.
        params : dict
            HTTP parameters to use when making the connection.

        Returns
        -------

        dict/list
            Returns the data from the endpoint.
        """
        url = urljoin(self.url, 'wp', self.version, endpoint)

        resp = requests.get(url, data=data, params=params,
                            headers=self.headers)

        if not resp.status_code == 200:
            msg = ('WordPress REST API returned the status code '
                   '{0}.'.format(resp.status_code))
            raise Exception(msg)

        return resp.json()

    def _delete(self, endpoint, params={}):
        """
        Private function for making DELETE requests.

        Arguments
        ---------

        endpoint : str
            WordPress endpoint.
        params : dict
            HTTP parameters when making the connection.

        Returns
        -------

        dict/list
            Returns the data from the endpoint.
        """
        url = urljoin(self.url, 'wp', self.version, endpoint)

        resp = requests.delete(url, params=params, headers=self.headers)

        if not resp.status_code == 200:
            msg = ('WordPress REST API returned the status code '
                   '{0}.'.foramt(resp.status_code))
            raise Exception(msg)

        return resp.json()

    # Post Methods

    def list_posts(self, context='view', page=1, pre_page=10, search=None,
                   after=None, author=None, author_exclude=None, before=None,
                   exclude=None, include=None, offset=None, order='desc',
                   orderby='date', slug=None, status='publish',
                   categories=None, cateogries_exclude=None, tags=None,
                   tags_exclude=None, sticky=None):
        """
        Get a list of posts.

        Arguments
        ---------

        context : str
            Scope under which the request is made; determines fields present in
            response.

            Default: view

            One of: view, embed, edit
        page : int
            Current page of the collection.
        pre_page : int
            Maximum number of items to be returned in result set.
        search : str
            Limit results to those matching a string.
        after : datetime
            Limit response to posts published after a given date.
        author : int
            Limit result set to posts assigned to specific authors.
        author_exclude : int
            Ensure result set excludes posts assigned to specific authors.
        before : datetime
            Limit response to posts published before a given date.
        exclude : int
            Ensure result set excludes specific IDs.
        include : int
            Limit result set to specific IDs.
        offset : int
            Offset the result set by a specific number of items.
        order : str
            Order sort attribute ascending or descending.

            Default: desc

            One of: asc, desc
        orderby : str
            Sort collection by object attribute.
            Default: date
            One of: date, relevance, id, include, title, slug
        slug : str
            Limit result set to posts with one or more specific slugs.
        status : str
            Limit result set to posts assigned one or more statuses.

            Default: publish

            One of: publish, future, draft, pending, private
        categories : str
            Limit result set to all items that have the specified term assigned
            in the categories taxonomy.
        categories_exclude : str
            Limit result set to all items except those that have the specified
            term assigned in the categories taxonomy.
        tags : str
            Limit result set to all items that have the specified term assigned
            in the tags taxonomy.
        tags_exclude : str
            Limit result set to all items except those that have the specified
            term assigned in the tags taxonomy.
        sticky : bool
            Limit result set to items that are sticky.

        Returns
        -------

        list
            A list of wordpress.models.Post.
        """
        if context not in ['view', 'embed', 'edit']:
            raise ValueError('The context {0} is not allowed.'.format(context))

        if after:
            after = after.isoformat()

        if before:
            before = before.isoformat()

        if order not in ['asc', 'desc']:
            raise ValueError("You can't order {0}.".format(order))

        if orderby not in ['date', 'relevance', 'id', 'include', 'title',
                           'slug']:
            raise ValueError("You can't order by {0}.".format(orderby))

        posts = self._get('posts', params=locals())

        return Post.parse_list(self, posts)

    def get_post(self, pk, context='view', password=None):
        """
        Retrieve a Post.

        Arguments
        ---------

        pk : in
            The post id you want to retrieve.
        context : str
            Scope under which the request is made; determines fields present in
            response.

            Default: view

            One of: view, embed, edit
        password : str
            The password for the post if it is password protected.

        Returns
        -------

        wordpress.models.Post
        """
        post = self._get('posts/{0}'.format(pk), params=locals())

        return Post.parse(self, post)

    def create_post(self, date=None, date_gmt=None, slug=None, status=None,
                    password=None, title=None, content=None, author=None,
                    excerpt=None, featured_media=None, comment_status=None,
                    ping_status=None, format=None, meta=None, sticky=None,
                    template=None, categories=None, tags=None,
                    liveblog_links=None):
        """
        Create a Post.

        Arguments
        ---------

        date : datetime
            The date the object was published, in the site’s timezone.
        date_gmt : datetime
            The date the object was published, as GMT.
        slug : str
            An alphanumeric identifier for the object unique to its type.
        status : str
            A named status for the object.

            One of: publish, future, draft, pending, private
        password : str
            A password to protect access to the content and excerpt.
        title : str
            The title for the object.
        content : str
            The content for the object.
        author : id
            The ID for the author of the object.
        excerpt : str
            The excerpt for the object.
        featured_media : int
            The ID of the featured media for the object.
        comment_status : str
            Whether or not comments are open on the object.

            One of: open, closed
        ping_status : str
            Whether or not the object can be pinged.

            One of: open, closed
        format : str
            The format for the object.

            One of: standard
        meta : dict
            Meta fields.
        sticky : bool
            Whether or not the object should be treated as sticky.
        template : str
            The theme file to use to display the object.

            One of:
        categories : str
            The terms assigned to the object in the category taxonomy.
        tags : str
            The terms assigned to the object in the post_tag taxonomy.
        liveblog_likes : str
            The number of Liveblog Likes the post has.
        """
        post = self._post('posts', data=locals())

        return Post.parse(self, post)

    def update_post(self, pk, date=None, date_gmt=None, slug=None, status=None,
                    password=None, title=None, content=None, author=None,
                    excerpt=None, featured_media=None, comment_status=None,
                    ping_status=None, format=None, meta=None, sticky=None,
                    template=None, categories=None, tags=None,
                    liveblog_links=None):
        """
        Update a Post.

        Arguments
        ---------

        pk : int
            The ID of the post you want to update.
        date : datetime
            The date the object was published, in the site’s timezone.
        date_gmt : datetime
            The date the object was published, as GMT.
        slug : str
            An alphanumeric identifier for the object unique to its type.
        status : str
            A named status for the object.

            One of: publish, future, draft, pending, private
        password : str
            A password to protect access to the content and excerpt.
        title : str
            The title for the object.
        content : str
            The content for the object.
        author : id
            The ID for the author of the object.
        excerpt : str
            The excerpt for the object.
        featured_media : int
            The ID of the featured media for the object.
        comment_status : str
            Whether or not comments are open on the object.

            One of: open, closed
        ping_status : str
            Whether or not the object can be pinged.

            One of: open, closed
        format : str
            The format for the object.

            One of: standard
        meta : dict
            Meta fields.
        sticky : bool
            Whether or not the object should be treated as sticky.
        template : str
            The theme file to use to display the object.

            One of:
        categories : str
            The terms assigned to the object in the category taxonomy.
        tags : str
            The terms assigned to the object in the post_tag taxonomy.
        liveblog_likes : str
            The number of Liveblog Likes the post has.
        """
        post = self._post('posts/{0}'.format(pk), data=locals())

        return Post.parse(self, post)

    def delete_post(self, pk, force=False):
        """
        Delete a Post.

        Arguments
        ---------

        pk : int
            The post id you want to delete.
        force : bool
            Whether to bypass trash and force deletion.
        """
        resp = self._delete('posts/{0}'.format(pk), params=locals())

        if resp.status_code == 200:
            return True
        else:
            raise Exception(resp.json())

    # Post Reivion Methods

    def list_post_revisions(self, parent, context='view'):
        """
        List Post Revisions.

        Arguments
        ---------

        parent : int/wordpress.models.Post/wordpress.models.Page
            The id for the parent of the object.
        context : str
            Scope under which the request is made; determines fields present in
            response.

            Default: view

            One of: view

        Returns
        -------

        list
            A list of wordpress.models.PostRevision.
        """
        if type(parent) == int:
            parent_id = parent
        elif type(parent) in [Page, Post]:
            parent_id = parent.id

        resp = self._get('posts/{0}/revisions'.format(parent_id),
                         params=locals())

        return PostRevision.parse_list(self, resp.json())

    def get_post_revision(self, parent, pk, context='view'):
        """
        Get a Post Revision.

        Arguments
        ---------

        parent : int/wordpress.models.Post/wordpress.models.Page
            The id for the parent of the object.
        pk : int
            Unique identifier for the object.
        context : str
            Scope under which the request is made; determines fields present in
            response.

            Default: view

            One of: view

        Returns
        -------

        wordpress.models.PostRevision
        """
        if isinstance(parent, int):
            parent_id = parent
        elif isinstance(parent, Page) or isinstance(parent, Post):
            parent_id = parent.id

        resp = self._get('posts/{0}/revisions/{1}'.format(parent_id, pk),
                         params=locals())

        return PostRevision.parse(self, resp.json())

    def delete_post_revision(self, parent, pk):
        """
        Delete Post Revision.

        Arguments
        ---------

        parent : int/wordpress.models.Post/wordpress.models.Page
            The id for the parent of the object.
        pk : int
            Unique identifier for the object.

        Returns
        -------

        dict
        """
        if isinstance(parent, int):
            parent_id = parent
        elif isinstance(parent, Page) or isinstance(parent, Post):
            parent_id = parent.id

        resp = self._delete('posts/{0}/revisions/{1}'.format(parent_id, pk))

        return PostRevision.parse(self, resp.json())

    # Category Methods

    def list_categories(self, context='view', page=1, pre_page=10, search=None,
                        exclude=None, include=None, order='asc',
                        orderby='name', hide_empty=False, parent=None,
                        post=None, slug=None):
        """
        Get a list of categories.

        Arguments
        ---------

        context : str
            Scope under which the request is made; determines fields present in
            response.

            Default: view

            One of: view, embed, edit
        page : int
            Current page of the collection.

            Default: 1
        pre_page : int
            Maximum number of items to be returned in result set.

            Default: 10
        search : str
            Limit results to those matching a string.
        exclude : int
            Ensure result set excludes specific IDs.

            Default:
        include : int
            Limit result set to specific IDs.

            Default:
        order : str
            Order sort attribute ascending or descending.

            Default: asc

            One of: asc, desc
        orderby : str
            Sort collection by term attribute.

            Default: name

            One of: id, include, name, slug, term_group, description, count
        hide_empty : bool
            Whether to hide terms not assigned to any posts.
        parent : int/wordpress.models.Category
            Limit result set to terms assigned to a specific parent.
        post : int/wordpress.models.Post
            Limit result set to terms assigned to a specific post.
        slug : str
            Limit result set to terms with a specific slug.

        Returns
        -------

        list
            A list of wordpress.models.Category.
        """
        if context not in ['view', 'embed', 'edit']:
            raise ValueError('The context {0} is not allowed.'.format(context))

        if order not in ['asc', 'desc']:
            raise ValueError('The order {0} is not allowed.'.format(order))

        if orderby not in ['id', 'include', 'name', 'slug', 'term_group',
                           'description', 'count']:
            raise ValueError('The order by {0} is not '
                             'allowed.'.format(orderby))

        if isinstance(parent, Category):
            parent_id = Category.id
        elif isinstance(parent, int):
            parent_id = parent

        if isinstance(post, Post):
            post_id = Post.id
        elif isinstance(post, int):
            post_id = post

        category_list = self._get('categories', params=locals())

        return Category.parse_list(self, category_list)

    def get_category(self, pk, context='view'):
        """
        Retrieve a Category.

        Arguments
        ---------

        pk : int
            The category id you want to retrieve.
        context : str
            Scope under which the request is made; determines fields present in
            response.

            Default: view

            One of: view, embed, edit

        Returns
        -------

        wordpress.models.Category
        """
        if context not in ['view', 'embed', 'edit']:
            raise ValueError('The context {0} is not allowed.'.format(context))

        category = self._get('categories/{0}'.format(pk), params=locals())

        return Category.parse(self, category)

    # Tag Methods

    def list_tags(self, context='view', page=1, pre_page=10, search=None,
                  include=[], offset=0, order='asc', orderby='name',
                  hide_empty=False, post=None, slug=None):
        """
        Get a list of tags.

        Arguments
        ---------

        context : str
            Scope under which the request is made; determines fields present in
            response.

            Default: view

            One of: view, embed, edit
        page : int
            Current page of the collection.

            Default: 1
        per_page : int
            Maximum number of items to be returned in result set.

            Default: 10
        search : str
            Limit results to those matching a string.
        exclude : str
            Ensure result set excludes specific IDs.

            Default:
        include : list
            Limit result set to specific IDs.

            Default:
        offset : int
            Offset the result set by a specific number of items.
        order : str
            Order sort attribute ascending or descending.

            Default: asc

            One of: asc, desc
        orderby : str
            Sort collection by term attribute.

            Default: name

            One of: id, include, name, slug, term_group, description, count
        hide_empty : bool
            Whether to hide terms not assigned to any posts.
        post : int
            Limit result set to terms assigned to a specific post.
        slug : str
            Limit result set to terms with a specific slug.

        Returns
        -------

        list
            A list of wordpress.models.Category.
        """
        if context not in ['view', 'embed', 'edit']:
            raise ValueError('The context {0} is not allowed.'.format(context))

        if order not in ['asc', 'desc']:
            raise ValueError("You can't order {0}.".format(order))

        if orderby not in ['id', 'include', 'name', 'slug', 'term_group',
                           'description', 'count']:
            raise ValueError("You can't order by {0}.".format(orderby))

        tag_list = self._get('tags', params=locals())

        return Category.parse_list(self, tag_list)

    def get_tag(self, pk, context='view'):
        """
        Retrieve a Tag.

        Arguments
        ---------

        pk : int
            The tag id you want to retrieve.
        context : str
            Scope under which the request is made; determines fields present in
            response.

            Default: view

            One of: view, embed, edit

        Returns
        -------

        wordpress.models.Tag
        """
        if context not in ['view', 'embed', 'edit']:
            raise ValueError('The context {0} is not allowed.'.format(context))

        tag = self._get('tags/{0}'.format(pk), params=locals())

        return Tag.parse(self, tag)

    def create_tag(self, **kwargs):
        raise NotImplementedError

    def update_tag(self, **kwargs):
        raise NotImplementedError

    def delete_tag(self, **kwargs):
        raise NotImplementedError

    # Page Methods

    def list_pages(self, **kwargs):
        raise NotImplementedError

    def get_page(self, **kwargs):
        raise NotImplementedError

    def create_page(self, **kwargs):
        raise NotImplementedError

    def update_page(self, **kwargs):
        raise NotImplementedError

    def delete_page(self, **kwargs):
        raise NotImplementedError

    # Comment Methods

    def list_comments(self, **kwargs):
        raise NotImplementedError

    def get_comment(self, **kwargs):
        raise NotImplementedError

    def create_comment(self, **kwargs):
        raise NotImplementedError

    def update_comment(self, **kwargs):
        raise NotImplementedError

    def delete_comment(self, **kwargs):
        raise NotImplementedError

    # Taxonomy Methods

    def list_taxonomies(self, **kwargs):
        raise NotImplementedError

    def get_taxonomy(self, **kwargs):
        raise NotImplementedError

    # Media Methods

    def list_media(self, **kwargs):
        raise NotImplementedError

    def get_media(self, **kwargs):
        raise NotImplementedError

    def create_media(self, **kwargs):
        raise NotImplementedError

    def update_media(self, **kwargs):
        raise NotImplementedError

    def delete_media(self, **kwargs):
        raise NotImplementedError

    # User Methods

    def list_users(self, **kwargs):
        raise NotImplementedError

    def get_user(self, **kwargs):
        raise NotImplementedError

    def create_user(self, **kwargs):
        raise NotImplementedError

    def update_user(self, **kwargs):
        raise NotImplementedError

    def delete_user(self, **kwargs):
        raise NotImplementedError

    # Post Type Methods

    def list_post_types(self, **kwargs):
        raise NotImplementedError

    def get_post_type(self, **kwargs):
        raise NotImplementedError

    # Post Status Methods

    def list_post_statuses(self, context='view'):
        """
        Get a list of post statuses.

        Arguments
        ---------

        context : str
            Scope under which the request is made; determines fields present in
            response.

            Default: view

            One of: view, embed, edit

        Returns
        -------

        list
            A list of wordpress.models.PostStatus
        """
        if context not in ['view', 'embed', 'edit']:
            raise ValueError('The context {0} is not allowed.'.format(context))

        post_status_list = self._get('statuses', params=locals())

        return PostStatus.parse_list(self, post_status_list)

    def get_post_status(self, slug, context='view'):
        """
        Retrieve a Post statuses

        Arguments
        ---------

        slug : str
            The name of the status.
        context : str
            Scope under which the request is made; determines fields present in
            response.

            Default: view

            One of: view, embed, edit

        Returns
        -------

        wordpress.models.PostStatus
        """
        if context not in ['view', 'embed', 'edit']:
            raise ValueError('The context {0} is not allowed.'.format(context))

        post_status = self._get('statuses/{0}'.format(slug), params=locals())

    # Setting Methods

    def update_setting(self, title=None, description=None, url=None,
                       email=None, timezone=None, date_format=None,
                       time_format=None, start_of_week=None, language=None,
                       use_smilies=None, default_category=None,
                       default_post_format=None, post_pre_page=None):
        """
        Update WordPress settings.

        Arguments
        ---------

        title : str
            Site title.
        description : str
            Site description.
        url : str
            Site URL.
        email : str
            This address is used for admin purposes. If you change this we will
            send you an email at your new address to confirm it. The new
            address will not become active until confirmed.
        timezone : str
            A city in the same timezone as you.
        date_format : str
            A date format for all date strings.
        time_format : str
            A time format for all time strings.
        start_of_week : int
            A day number of the week that the week should start on.
        language : str
            WordPress locale code.
        use_smilies : bool
            Convert emoticons like :-) and :-P to graphics on display.
        default_category : int
            Default category.
        default_post_format : str
            Default post format.
        posts_per_page : int
            Blog pages show at most.
        """
        return self._post('settings', params=locals())