# -*- coding: utf-8 -*-

from datetime import datetime

import json
import logging
import time
import urlparse
from urllib import urlencode

import oauth2 as oauth

BASE_URL = 'https://www.instapaper.com'
API_VERSION = '1'
ACCESS_TOKEN = 'oauth/access_token'
LOGIN_URL = 'https://www.instapaper.com/user/login'
REQUEST_DELAY_SECS = 0.5

log = logging.getLogger(__name__)


class Instapaper(object):
    '''Instapaper client class.

    :param oauth_key str: Instapaper OAuth consumer key
    :param oauth_secret str: Instapaper OAuth consumer secret
    '''

    def __init__(self, oauth_key, oauth_secret):
        self.consumer = oauth.Consumer(oauth_key, oauth_secret)
        self.oauth_client = oauth.Client(self.consumer)
        self.token = None

    def login(self, username, password):
        '''Authenticate using XAuth variant of OAuth.

        :param str username: Username or email address for the relevant account
        :param str password: Password for the account
        '''
        response = self.request(
            ACCESS_TOKEN,
            {
                'x_auth_mode': 'client_auth',
                'x_auth_username': username,
                'x_auth_password': password
            },
            returns_json=False
        )
        token = dict(urlparse.parse_qsl(response['data']))
        self.token = oauth.Token(
            token['oauth_token'], token['oauth_token_secret'])
        self.oauth_client = oauth.Client(self.consumer, self.token)

    def request(self, path, params=None, returns_json=True,
                method='POST', api_version=API_VERSION):
        '''Process a request using the OAuth client's request method.

        :param str path: Path fragment to the API endpoint, e.g. "resource/ID"
        :param dict params: Parameters to pass to request
        :param str method: Optional HTTP method, normally POST for Instapaper
        :param str api_version: Optional alternative API version
        :returns: response headers and body
        :retval: dict
        '''
        time.sleep(REQUEST_DELAY_SECS)
        full_path = '/'.join([BASE_URL, 'api/%s' % api_version, path])
        params = urlencode(params) if params else None
        log.debug('URL: %s', full_path)
        request_kwargs = {'method': method}
        if params:
            request_kwargs['body'] = params
        response, content = self.oauth_client.request(
            full_path, **request_kwargs)
        log.debug('CONTENT: %s ...', content[:50])
        if returns_json:
            try:
                data = json.loads(content)
                if isinstance(data, list) and len(data) == 1:
                    # ugly -- API always returns a list even when you expect
                    # only one item
                    if data[0]['type'] == 'error':
                        raise Exception('Instapaper error %d: %s' % (
                            data[0]['error_code'],
                            data[0]['message'])
                        )
                    # TODO: PyInstapaperException custom class?
            except ValueError:
                # Instapaper API can be unpredictable/inconsistent, e.g.
                # bookmarks/get_text doesn't return JSON
                data = content
        else:
            data = content
        return {
            'response': response,
            'data': data
        }

    def get_bookmarks(self, folder='unread', limit=10, have=[]):
        """Return list of user's bookmarks.

        :param str folder: Optional. Possible values are unread (default),
            starred, archive, or a folder_id value.
        :param int limit: Optional. A number between 1 and 500, default 25.
        :returns: List of user's bookmarks
        :rtype: list
        """
        path = 'bookmarks/list'
        response = self.request(path, {'folder_id': folder, 'limit': limit, 'have': have})
        items = response['data']
        bookmarks = []
        for item in items:
            if item.get('type') == 'error':
                raise Exception(item.get('message'))
            elif item.get('type') == 'bookmark':
                bookmarks.append(Bookmark(self, **item))
        return bookmarks

    def get_folders(self):
        """Return list of user's folders.

        :rtype: list
        """
        path = 'folders/list'
        response = self.request(path)
        items = response['data']
        folders = []
        for item in items:
            if item.get('type') == 'error':
                raise Exception(item.get('message'))
            elif item.get('type') == 'folder':
                folders.append(Folder(self, **item))
        return folders


class InstapaperObject(object):

    '''Base class for Instapaper objects like Bookmark.

    :param client: instance of the OAuth client for making requests
    :type client: ``oauth2.Client``
    :param dict data: key/value pairs of object attributes, e.g. title, etc.
    '''

    def __init__(self, client, **data):
        self.client = client
        for attrib in self.ATTRIBUTES:
            val = data.get(attrib)
            if hasattr(self, 'TIMESTAMP_ATTRS'):
                if attrib in self.TIMESTAMP_ATTRS:
                    try:
                        val = datetime.fromtimestamp(int(val))
                    except ValueError:
                        log.warn(
                            'Could not cast %s for %s as datetime',
                            val, attrib
                        )
            setattr(self, attrib, val)
        self.object_id = getattr(self, self.RESOURCE_ID_ATTRIBUTE)
        for action in self.SIMPLE_ACTIONS:
            setattr(self, action, lambda x: self._simple_action(x))
            instance_method = getattr(self, action)
            instance_method.func_defaults = (action,)

    def add(self):
        '''Save an object to Instapaper after instantiating it.

        Example::

            folder = Folder(instapaper, title='stuff')
            result = folder.add()
        '''
        # TODO validation per object type
        submit_attribs = {}
        for attrib in self.ATTRIBUTES:
            val = getattr(self, attrib, None)
            if val:
                submit_attribs[attrib] = val
        path = '/'.join([self.RESOURCE, 'add'])
        result = self.client.request(path, submit_attribs)
        return result

    def _simple_action(self, action=None):
        '''Issue a request for an API method whose only param is the obj ID.

        :param str action: The name of the action for the resource
        :returns: Response from the API
        :rtype: dict
        '''
        if not action:
            raise Exception('No simple action defined')
        path = "/".join([self.RESOURCE, action])
        response = self.client.request(
            path, {self.RESOURCE_ID_ATTRIBUTE: self.object_id}
        )
        return response


class Bookmark(InstapaperObject):

    '''Object representing an Instapaper bookmark/article.'''

    RESOURCE = 'bookmarks'
    RESOURCE_ID_ATTRIBUTE = 'bookmark_id'
    # TODO: identify which fields to convert from timestamp to Python datetime
    ATTRIBUTES = [
        'bookmark_id',
        'title',
        'description',
        'hash',
        'url',
        'progress_timestamp',
        'time',
        'progress',
        'starred',
        'type',
        'private_source'
    ]
    TIMESTAMP_ATTRS = [
        'progress_timestamp',
        'time'
    ]
    SIMPLE_ACTIONS = [
        'delete',
        'star',
        'archive',
        'unarchive',
        'get_text'
    ]

    def __str__(self):
        return 'Bookmark %s: %s' % (self.object_id, self.title.encode('utf-8'))

    def get_highlights(self):
        '''Get highlights for Bookmark instance.

        :return: list of ``Highlight`` objects
        :rtype: list
        '''
        # NOTE: all Instapaper API methods use POST except this one!
        path = '/'.join([self.RESOURCE, str(self.object_id), 'highlights'])
        response = self.client.request(path, method='GET', api_version='1.1')
        items = response['data']
        highlights = []
        for item in items:
            if item.get('type') == 'error':
                raise Exception(item.get('message'))
            elif item.get('type') == 'highlight':
                highlights.append(Highlight(self, **item))
        return highlights


class Folder(InstapaperObject):

    '''Object representing an Instapaper folder.'''

    RESOURCE = 'folders'
    RESOURCE_ID_ATTRIBUTE = 'folder_id'
    ATTRIBUTES = [
        'folder_id',
        'title',
        'display_title',
        'sync_to_mobile',
        'folder_id',
        'position',
        'type',
        'slug',
    ]
    SIMPLE_ACTIONS = [
        'delete',
    ]

    def __str__(self):
        return 'Folder %s: %s' % (self.object_id, self.title)

    def set_order(self, folder_ids):
        """Order the user's folders

        :param list folders: List of folder IDs in the desired order.
        :returns: List Folder objects in the new order.
        :rtype: list
        """
        # TODO
        raise NotImplementedError


class Highlight(InstapaperObject):

    '''Object representing an Instapaper highlight.'''

    RESOURCE = 'highlights'
    RESOURCE_ID_ATTRIBUTE = 'highlight_id'

    ATTRIBUTES = [
        'highlight_id',
        'text',
        'note',
        'time',
        'position',
        'bookmark_id',
        'type',
        'slug',
    ]
    TIMESTAMP_ATTRS = [
        'time',
    ]
    SIMPLE_ACTIONS = [
        'delete',
    ]

    def __str__(self):
        return 'Highlight %s for Article %s' % (
            self.object_id, self.bookmark_id)

    def create(self):
        # TODO
        raise NotImplementedError
