import dateutil.parser

from datetime import datetime

from .utils import snake_case
from .utils import camel_case
from .utils import base_path_for


"""
contentful_management.resource
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

This module implements the Resource, FieldResource, PublishResource, ArchiveResource and Link classes.

API reference: https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/common-resource-attributes

:copyright: (c) 2018 by Contentful GmbH.
:license: MIT, see LICENSE for more details.
"""


class Resource(object):
    """
    Base resource class.

    Implements common resource attributes.

    API reference: https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/common-resource-attributes
    """

    def __init__(self, item, default_locale='en-US', client=None):
        self.raw = item
        self.default_locale = default_locale
        self._client = client
        self.sys = self._hydrate_sys(item)

    @classmethod
    def base_url(klass, space_id='', resource_id=None, environment_id=None, **kwargs):
        """
        Returns the URI for the resource.
        """

        url = "spaces/{0}".format(
            space_id)

        if environment_id is not None:
            url = url = "{0}/environments/{1}".format(url, environment_id)

        url = "{0}/{1}".format(
            url,
            base_path_for(klass.__name__)
        )

        if resource_id:
            url = "{0}/{1}".format(url, resource_id)

        return url

    @classmethod
    def create_attributes(klass, attributes, previous_object=None):
        """
        Attributes for resource creation.
        """

        result = {}

        if previous_object is not None:
            result = {k: v for k, v in previous_object.to_json().items() if k != 'sys'}

        result.update(attributes)

        return result

    @classmethod
    def create_headers(klass, attributes):
        """
        Headers for resource creation.
        """

        return {}

    @classmethod
    def update_attributes_map(klass):
        """
        Defines keys and default values for non-generic attributes.
        """

        return {}

    def delete(self):
        """
        Deletes the resource.
        """

        return self._client._delete(
            self.__class__.base_url(
                self.sys['space'].id,
                self.sys['id'],
                environment_id=self._environment_id
            )
        )

    def update(self, attributes=None):
        """
        Updates the resource with attributes.
        """

        if attributes is None:
            attributes = {}

        headers = self.__class__.create_headers(attributes)
        headers.update(self._update_headers())

        result = self._client._put(
            self._update_url(),
            self.__class__.create_attributes(attributes, self),
            headers=headers
        )

        self._update_from_resource(result)

        return self

    def save(self):
        """
        Saves the current state of the resource.
        """

        return self.update()

    def reload(self, result=None):
        """
        Reloads the resource.
        """

        if result is None:
            result = self._client._get(
                self.__class__.base_url(
                    self.sys['space'].id,
                    self.sys['id'],
                    environment_id=self._environment_id
                )
            )

        self._update_from_resource(result)

        return self

    def to_link(self):
        """
        Returns a link for the resource.
        """

        link_type = self.link_type if self.type == 'Link' else self.type

        return Link({'sys': {'linkType': link_type, 'id': self.sys.get('id')}}, client=self._client)

    def to_json(self):
        """
        Returns the JSON representation of the resource.
        """

        result = {
            'sys': {}
        }
        for k, v in self.sys.items():
            if k in ['space', 'content_type', 'created_by',
                     'updated_by', 'published_by']:
                v = v.to_json()
            if k in ['created_at', 'updated_at', 'deleted_at',
                     'first_published_at', 'published_at', 'expires_at']:
                v = v.isoformat()
            result['sys'][camel_case(k)] = v

        return result

    def _hydrate_sys(self, item):
        sys = {}
        for k, v in item['sys'].items():
            if k in ['space', 'contentType', 'createdBy',
                     'updatedBy', 'publishedBy', 'environment']:
                v = self._build_link(v)
            if k in ['createdAt', 'updatedAt', 'deletedAt',
                     'firstPublishedAt', 'publishedAt', 'expiresAt']:
                v = dateutil.parser.parse(v)
            sys[snake_case(k)] = v
        return sys

    def _build_link(self, link):
        return Link(link, client=self._client)

    def _update_headers(self):
        return {'x-contentful-version': str(self.sys['version'])}

    def _update_url(self):
        return self.__class__.base_url(
                self.sys['space'].id,
                self.sys['id'],
                environment_id=self._environment_id
            )

    def _update_from_resource(self, other):
        if hasattr(other, 'sys'):
            self.sys = other.sys
        for attr, default in self.__class__.update_attributes_map().items():
            value = getattr(other, attr, default)
            self_value = getattr(self, attr)
            if value == default and value != self_value:
                value = self_value
            setattr(self, attr, value)

    @property
    def _environment_id(self):
        """
        Returns the Environment ID.
        """
        try:
            return super(Resource, self)._environment_id
        except AttributeError:
            # In Resources which do not inherit EnvironmentAwareResource an AttributeError will happen
            return None

    def __getattr__(self, name):
        if name in self.sys:
            return self.sys[name]
        raise AttributeError(
            "'{0}' object has no attribute '{1}'".format(
                self.__class__.__name__,
                name
            )
        )


class FieldsResource(Resource):
    """
    Fields resource class.

    Implements locale handling for resource fields.
    """

    @classmethod
    def create_attributes(klass, attributes, previous_object=None):
        """
        Attributes for resource creation.
        """

        if 'fields' not in attributes:
            if previous_object is None:
                attributes['fields'] = {}
            else:
                attributes['fields'] = previous_object.to_json()['fields']
        return {'fields': attributes['fields']}

    def __init__(self, item, **kwargs):
        super(FieldsResource, self).__init__(item, **kwargs)
        self._fields = self._hydrate_fields(item)

    def fields(self, locale=None):
        """
        Get fields for a specific locale.

        :param locale: (optional) Locale to fetch, defaults to default_locale.
        """

        if locale is None:
            locale = self._locale()
        return self._fields.get(locale, {})

    def fields_with_locales(self):
        """
        Get fields with locales per field.
        """

        result = {}
        for locale, fields in self._fields.items():
            for k, v in fields.items():
                real_field_id = self._real_field_id_for(k)
                if real_field_id not in result:
                    result[real_field_id] = {}
                result[real_field_id][locale] = self._serialize_value(v)
        return result

    def to_json(self):
        """
        Returns the JSON Representation of the resource.
        """

        result = super(FieldsResource, self).to_json()
        result['fields'] = self.fields_with_locales()
        return result

    @property
    def locale(self):
        """
        Returns the resource locale.
        """

        return self.sys.get('locale', None)

    def _real_field_id_for(self, field_id):
        for raw_field_id in self.raw['fields'].keys():
            if snake_case(raw_field_id) == field_id:
                return raw_field_id

    def _serialize_value(self, value):
        if isinstance(value, Resource):
            return value.to_link().to_json()
        elif isinstance(value, list) and value:
            if isinstance(value[0], Resource):
                return [resource.to_link().to_json() for resource in value]
        elif isinstance(value, datetime):
            return value.isoformat()
        return value

    def _hydrate_fields(self, item):
        fields = {}

        if 'fields' not in item:
            return fields

        for k, locales in item['fields'].items():
            for locale, v in locales.items():
                if locale not in fields:
                    fields[locale] = {}
                fields[locale][snake_case(k)] = self._coerce(v)

        return fields

    def _coerce(self, value):
        return value

    def _locale(self):
        return self.locale or self.__dict__['default_locale']

    def _update_from_resource(self, other):
        super(FieldsResource, self)._update_from_resource(other)
        if hasattr(other, '_fields'):
            self._fields = other._fields

    def __getattr__(self, name):
        locale = self._locale()
        if name in self._fields.get(locale, {}):
            return self._fields[locale][name]
        return super(FieldsResource, self).__getattr__(name)

    def __setattr__(self, name, value):
        if name not in ['raw', 'sys', 'default_locale',
                        '_client', '_fields', '__CONTENT_TYPE__']:
            locale = self._locale()
            if (name in self._fields.get(locale, {}) or
                    self._is_missing_field(name)):
                if locale not in self._fields:
                    self._fields[locale] = {}
                self._fields[locale][name] = value
                return self._fields[locale][name]
        return super(FieldsResource, self).__setattr__(name, value)

    def _is_missing_field(self, name):
        """
        By default, fields not appearing on responses are considered
        as object meta-data, and they will not be added to `_fields`,
        making them not part of the serialization when sent back to
        the API for saving.
        """

        return False


class PublishResource(object):
    """
    Allows for resource publish/unpublish.
    """

    @property
    def is_published(self):
        """
        Checks if resource is published.
        """

        return bool(self.sys.get('published_at', False))

    def publish(self):
        """
        Publishes the resource.
        """

        result = self._client._put(
            "{0}/published".format(
                self.__class__.base_url(
                    self.sys['space'].id,
                    self.sys['id'],
                    environment_id=self._environment_id
                ),
            ),
            {},
            headers=self._update_headers()
        )

        return self.reload(result)

    def unpublish(self):
        """
        Unpublishes the resource.
        """

        self._client._delete(
            "{0}/published".format(
                self.__class__.base_url(
                    self.sys['space'].id,
                    self.sys['id'],
                    environment_id=self._environment_id
                ),
            ),
            headers=self._update_headers()
        )

        return self.reload()


class ArchiveResource(object):
    """
    Allows for resource archive/unarchive.
    """

    @property
    def is_archived(self):
        """
        Checks if Resource is archived.
        """

        return bool(self.sys.get('archived_version', False))

    def archive(self):
        """
        Archives the resource.
        """

        self._client._put(
            "{0}/archived".format(
                self.__class__.base_url(
                    self.sys['space'].id,
                    self.sys['id'],
                    environment_id=self._environment_id
                ),
            ),
            {},
            headers=self._update_headers()
        )

        return self.reload()

    def unarchive(self):
        """
        Unarchives the resource.
        """

        self._client._delete(
            "{0}/archived".format(
                self.__class__.base_url(
                    self.sys['space'].id,
                    self.sys['id'],
                    environment_id=self._environment_id
                ),
            ),
            headers=self._update_headers()
        )

        return self.reload()


class EnvironmentAwareResource(object):
    """
    Allows environment aware resources to resolve the environment ID.
    """

    @property
    def _environment_id(self):
        """
        Returns the Environment ID.
        """

        environment = self.sys.get('environment', None)

        if environment is not None:
            return environment.id
        return 'master'


class Link(Resource):
    """
    Link Class

    API reference: https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/links
    """

    def resolve(self, space_id=None, environment_id=None):
        """
        Resolves link to a specific resource.
        """

        proxy_method = getattr(
            self._client,
            base_path_for(self.link_type)
        )
        if self.link_type == 'Space':
            return proxy_method().find(self.id)
        elif environment_id is not None:
            return proxy_method(space_id, environment_id).find(self.id)
        else:
            return proxy_method(space_id).find(self.id)

    def to_json(self):
        """
        Returns the JSON representation of the link.
        """

        return {
            'sys': {
                'type': 'Link',
                'linkType': self.sys.get('link_type'),
                'id': self.sys.get('id')
            }
        }

    def __repr__(self):
        return "<Link[{0}] id='{1}'>".format(
            self.link_type,
            self.id
        )
