#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Stashr - Comicvine Data Retrieval
"""

"""
MIT License

Copyright (c) 2020 Andrew Vanderbye

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""

"""-------------------------------------------------------------------------------------------
-- IMPORTS
-------------------------------------------------------------------------------------------"""

""" --- PYTHON IMPORTS --- """
import requests
import requests_cache
import datetime

""" --- STASHR CORE IMPORTS --- """
from stashr import log
from stashr.config import stashrconfig

""" --- CREATE LOGGER --- """
logger = log.stashr_logger(__name__)

"""-------------------------------------------------------------------------------------------
-- COMICVINE CLIENT
-------------------------------------------------------------------------------------------"""


class ComicVineApiError(Exception):
    pass


class ComicVineUnauthorizedError(Exception):
    pass


class ComicVineForbiddenError(Exception):
    pass


class ComicVineClient(object):

    API_BASE_URL = 'https://www.comicvine.com/api/'

    HEADERS = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:7.0) '
                             'Gecko/20130825 Firefox/36.0'}

    SEARCH_RESOURCE_TYPES = {
        'character', 'concept', 'origin', 'object', 'location', 'issue', 'story_arc',
        'volume', 'publisher', 'person', 'team', 'video'
    }

    TYPES = {
        'ISSUE': 4000,
        'CHARACTER': 4005,
        'PUBLISHER': 4010,
        'CONCEPT': 4015,
        'LOCATION': 4020,
        'PERSON': 4040,
        'STORY_ARC': 4045,
        'VOLUME': 4050,
        'TEAM': 4060,
    }

    ISSUE_FIELDS = {
        'aliases', 'api_detail_url', 'character_credits', 'characters_died_in', 'concept_credits', 'cover_date',
        'date_added', 'date_last_updated', 'deck', 'description', 'disbanded_teams', 'first_appearance_characters',
        'first_appearance_concepts', 'first_appearance_locations', 'first_appearance_objects', 'first_appearance_teams',
        'has_staff_review', 'id', 'image', 'issue_number', 'location_credits', 'name', 'object_credits',
        'person_credits', 'site_detail_url', 'store_date', 'story_arc_credits', 'team_credits', 'teams_disbanded_in',
        'volume'
    }

    ISSUES_FIELDS = {
        'aliases', 'api_detail_url', 'cover_date', 'date_added', 'date_last_updated', 'deck', 'description',
        'has_staff_review', 'id', 'image', 'issue_number', 'name', 'site_detail_url', 'store_date', 'volume'
    }

    CHARACTER_FIELDS = {
        'aliases', 'api_detail_url', 'birth', 'character_enemies', 'character_friends', 'count_of_issue_appearances',
        'creators', 'date_added', 'date_last_updated', 'deck', 'description', 'first_appeared_in_issue', 'gender', 'id',
        'image', 'issue_credits', 'issues_dies_in', 'movies', 'name', 'origin', 'powers', 'publisher', 'real_name',
        'site_detail_url', 'story_arc_credits', 'team_enemies', 'team_friends', 'teams', 'volume_credits'
    }

    PUBLISHER_FIELDS = {
        'aliases', 'api_detail_url', 'characters', 'date_added', 'date_last_updated', 'deck', 'description', 'id',
        'image', 'location_address', 'location_city', 'location_state', 'name', 'site_detail_url', 'story_arcs',
        'teams', 'volumes'
    }

    CONCEPT_FIELDS = {
        'aliases', 'api_detail_url', 'count_of_issue_appearances', 'date_added', 'date_last_updated', 'deck',
        'description', 'first_appeared_in_issue', 'id', 'image', 'issue_credits', 'movies', 'name', 'site_detail_url',
        'start_year', 'volume_credits'
    }

    LOCATION_FIELDS = {
        'aliases', 'api_detail_url', 'count_of_issue_appearances', 'date_added', 'date_last_updated', 'deck',
        'description', 'first_appeared_in_issue', 'id', 'image', 'issue_credits', 'movies', 'name', 'site_detail_url',
        'start_year', 'story_arc_credits', 'volume_credits'
    }

    PERSON_FIELDS = {
        'aliases', 'api_detail_url', 'birth', 'count_of_issue_appearances', 'country', 'created_characters',
        'date_added', 'date_last_updated', 'death', 'deck', 'description', 'email', 'gender', 'hometown', 'id', 'image',
        'issue_credits', 'name', 'site_detail_url', 'story_arc_credits', 'volume_credits', 'website'
    }

    STORY_ARC_FIELDS = {
        'aliases', 'api_detail_url', 'count_of_issue_appearances', 'date_added', 'date_last_updated', 'deck',
        'description', 'first_appeared_in_issue', 'id', 'image', 'issues', 'movies', 'name', 'publisher',
        'site_detail_url'
    }

    VOLUME_FIELDS = {
        'aliases', 'api_detail_url', 'character_credits', 'concept_credits', 'count_of_issues', 'date_added',
        'date_last_updated', 'deck',  'description', 'first_issue', 'id', 'image', 'last_issue', 'location_credits',
        'name', 'object_credits', 'person_credits', 'publisher', 'site_detail_url', 'start_year', 'team_credits'
    }

    TEAM_FIELDS = {
        'aliases', 'api_detail_url', 'character_enemies', 'character_friends', 'characters',
        'count_of_issue_appearances', 'count_of_Team_members', 'date_added', 'date_lat_updated', 'deck', 'description',
        'disbanded_in_issues', 'first_appeared_in_issues', 'id', 'image', 'issue_credits', 'issues_disbanded_in',
        'movies', 'name', 'publisher', 'site_detail_url', 'story_arc_credits', 'volume_credits'
    }

    def __init__(self, api_key, expire_after=300):
        self.api_key = api_key
        self._install_requests_cache(expire_after)

    def _install_requests_cache(self, expire_after):

        requests_cache.install_cache(
            __name__,
            backend='memory',
            expire_after=expire_after
        )

    def update_api_key(self, api_key):
        self.api_key = api_key

    def search(self, query, offset=0, limit=10, resources=None, use_cache=True):
        url = f'{self.API_BASE_URL}search/'
        params = self._get_search_params(query, offset, limit, resources)
        json = self._get_from_api(url, params, use_cache)
        return Response(json)

    def get_issue(self, id, offset=0, limit=1, fields=None, use_cache=True):
        url = f'{self.API_BASE_URL}issue/{self.TYPES["ISSUE"]}-{id}/'
        params = self._get_issue_params(offset, limit, fields)
        json = self._get_from_api(url, params, use_cache)
        return Response(json)

    def get_character(self, id, offset=0, limit=1, fields=None, use_cache=True):
        url = f'{self.API_BASE_URL}character/{self.TYPES["CHARACTER"]}-{id}/'
        params = self._get_character_params(offset, limit, fields)
        json = self._get_from_api(url, params, use_cache)
        return Response(json)

    def get_publisher(self, id, offset=0, limit=1, fields=None, use_cache=True):
        url = f'{self.API_BASE_URL}publisher/{self.TYPES["PUBLISHER"]}-{id}/'
        params = self._get_publisher_params(offset, limit, fields)
        json = self._get_from_api(url, params, use_cache)
        return Response(json)

    def get_concept(self, id, offset=0, limit=1, fields=None, use_cache=True):
        url = f'{self.API_BASE_URL}concept/{self.TYPES["CONCEPT"]}-{id}/'
        params = self._get_concept_params(offset, limit, fields)
        json = self._get_from_api(url, params, use_cache)
        return Response(json)

    def get_location(self, id, offset=0, limit=1, fields=None, use_cache=True):
        url = f'{self.API_BASE_URL}location/{self.TYPES["LOCATION"]}-{id}/'
        params = self._get_location_params(offset, limit, fields)
        json = self._get_from_api(url, params, use_cache)
        return Response(json)

    def get_person(self, id, offset=0, limit=1, fields=None, use_cache=True):
        url = f'{self.API_BASE_URL}person/{self.TYPES["PERSON"]}-{id}/'
        params = self._get_person_params(offset, limit, fields)
        json = self._get_from_api(url, params, use_cache)
        return Response(json)

    def get_story_arc(self, id, offset=0, limit=1, fields=None, use_cache=True):
        url = f'{self.API_BASE_URL}story_arc/{self.TYPES["STORY_ARC"]}-{id}/'
        params = self._get_story_arc_params(offset, limit, fields)
        json = self._get_from_api(url, params, use_cache)
        return Response(json)

    def get_volume(self, id, offset=0, limit=1, fields=None, use_cache=True):
        url = f'{self.API_BASE_URL}volume/{self.TYPES["VOLUME"]}-{id}/'
        params = self._get_volume_params(offset, limit, fields)
        json = self._get_from_api(url, params, use_cache)
        return Response(json)

    def get_team(self, id, offset=0, limit=1, fields=None, use_cache=True):
        url = f'{self.API_BASE_URL}team/{self.TYPES["TEAM"]}-{id}/'
        params = self._get_team_params(offset, limit, fields)
        json = self._get_from_api(url, params, use_cache)
        return Response(json)

    def get_new_releases(self, weekday=2, offset=0, limit=100, resources=None, use_cache=True):
        url = f'{self.API_BASE_URL}issues/'
        params = self._get_new_releases_params(weekday, offset, limit, resources)
        json = self._get_from_api(url, params, use_cache)
        return Response(json)

    """ --- GET MULTIPLES OF EACH --- """

    # FILTERS: format, field_list, limit, offset, sort, filter

    def get_issues(self, sort=None, filters=None, resources=None, offset=0, limit=100, use_cache=True):
        url = f'{self.API_BASE_URL}issues/'
        params = self._get_issues_params(sort, filters, resources, offset, limit, use_cache)
        json = self._get_from_api(url, params, use_cache)
        return Response(json)

    def _get_issues_params(self, sort, filters, resources, offset, limit, use_cache):

        return {
            'api_key': self.api_key,
            'format': 'json',
            'limit': limit,
            'offset': offset,
            'sort': sort,
            'filter': self._validate_issues_filters(filters),
            'field_list': self._validate_issues_resources(resources)
        }

    def _validate_issues_resources(self, resources):
        if not resources:
            return None
        valid_resources = self.ISSUES_FIELDS & set(resources)
        return ','.join(valid_resources) if valid_resources else None

    def _validate_issues_filters(self, filters):
        if not filters:
            return None
        return_filters = {}
        valid_filters = self.ISSUES_FIELDS & set(filters)
        for valid_filter in valid_filters:
            return_filters[valid_filter] = filters[valid_filter]
        return ','.join(f'{k}:{v}' for k, v in return_filters.items()) if len(return_filters) > 0 else None

    """ --- DONE --- """

    def _get_search_params(self, query, offset, limit, resources):

        return {
            'api_key': self.api_key,
            'format': 'json',
            'limit': limit,
            'offset': offset,
            'query': query,
            'resources': self._validate_search_resources(resources)
        }

    def _get_issue_params(self, offset, limit, fields):

        return {
            'api_key': self.api_key,
            'format': 'json',
            'limit': limit,
            'offset': offset,
            'field_list': self._validate_issue_fields(fields)
        }

    def _get_character_params(self, offset, limit, fields):

        return {
            'api_key': self.api_key,
            'format': 'json',
            'limit': limit,
            'offset': offset,
            'field_list': self._validate_character_fields(fields)
        }

    def _get_publisher_params(self, offset, limit, fields):

        return {
            'api_key': self.api_key,
            'format': 'json',
            'limit': limit,
            'offset': offset,
            'field_list': self._validate_publisher_fields(fields)
        }

    def _get_concept_params(self, offset, limit, fields):

        return {
            'api_key': self.api_key,
            'format': 'json',
            'limit': limit,
            'offset': offset,
            'field_list': self._validate_concept_fields(fields)
        }

    def _get_location_params(self, offset, limit, fields):

        return {
            'api_key': self.api_key,
            'format': 'json',
            'limit': limit,
            'offset': offset,
            'field_list': self._validate_location_fields(fields)
        }

    def _get_person_params(self, offset, limit, fields):

        return {
            'api_key': self.api_key,
            'format': 'json',
            'limit': limit,
            'offset': offset,
            'field_list': self._validate_person_fields(fields)
        }

    def _get_story_arc_params(self, offset, limit, fields):

        return {
            'api_key': self.api_key,
            'format': 'json',
            'limit': limit,
            'offset': offset,
            'field_list': self._validate_story_arc_fields(fields)
        }

    def _get_volume_params(self, offset, limit, fields):

        return {
            'api_key': self.api_key,
            'format': 'json',
            'limit': limit,
            'offset': offset,
            'field_list': self._validate_volume_fields(fields)
        }

    def _get_team_params(self, offset, limit, fields):

        return {
            'api_key': self.api_key,
            'format': 'json',
            'limit': limit,
            'offset': offset,
            'field_list': self._validate_team_fields(fields)
        }

    def _get_new_releases_params(self, weekday, offset, limit, resources):

        today_date = datetime.date.today()

        delta_days = today_date.weekday() - weekday

        if delta_days == 0:
            start_date = today_date
        else:
            start_date = today_date - datetime.timedelta(delta_days)

        end_date = start_date + datetime.timedelta(7)

        return {
            'api_key': self.api_key,
            'format': 'json',
            'limit': limit,
            'offset': offset,
            'filter': f'store_date:{start_date}|{end_date}'
        }

    def _validate_search_resources(self, resources):
        if not resources:
            return None
        valid_resources = self.SEARCH_RESOURCE_TYPES & set(resources)
        return ','.join(valid_resources) if valid_resources else None

    def _validate_issue_fields(self, fields):
        if not fields:
            return None
        valid_fields = self.ISSUE_FIELDS & set(fields)
        return ','.join(valid_fields) if valid_fields else None

    def _validate_character_fields(self, fields):
        if not fields:
            return None
        valid_fields = self.CHARACTER_FIELDS & set(fields)
        return ','.join(valid_fields) if valid_fields else None

    def _validate_publisher_fields(self, fields):
        if not fields:
            return None
        valid_fields = self.PUBLISHER_FIELDS & set(fields)
        return ','.join(valid_fields) if valid_fields else None

    def _validate_concept_fields(self, fields):
        if not fields:
            return None
        valid_fields = self.CONCEPT_FIELDS & set(fields)
        return ','.join(valid_fields) if valid_fields else None

    def _validate_location_fields(self, fields):
        if not fields:
            return None
        valid_fields = self.LOCATION_FIELDS & set(fields)
        return ','.join(valid_fields) if valid_fields else None

    def _validate_person_fields(self, fields):
        if not fields:
            return None
        valid_fields = self.PERSON_FIELDS & set(fields)
        return ','.join(valid_fields) if valid_fields else None

    def _validate_story_arc_fields(self, fields):
        if not fields:
            return None
        valid_fields = self.STORY_ARC_FIELDS & set(fields)
        return ','.join(valid_fields) if valid_fields else None

    def _validate_volume_fields(self, fields):
        if not fields:
            return None
        valid_fields = self.VOLUME_FIELDS & set(fields)
        return ','.join(valid_fields) if valid_fields else None

    def _validate_team_fields(self, fields):
        if not fields:
            return None
        valid_fields = self.TEAM_FIELDS & set(fields)
        return ','.join(valid_fields) if valid_fields else None

    def _get_from_api(self, url, params, use_cache):

        def _httpget():
            response = requests.get(
                url, headers=self.HEADERS, params=params
            )

            if not response.ok:
                self._handle_http_error(response)

            return response.json()

        if not use_cache:
            with requests_cache.disabled():
                return _httpget()

        return _httpget()

    def _handle_http_error(self, response):

        exception = {
            401: ComicVineUnauthorizedError,
            403: ComicVineForbiddenError
        }.get(response.status_code, ComicVineApiError)
        message = f'{response.status_code} {response.reason}'

        raise exception(message)


class Response(object):

    def __init__(self, json):

        self.status_code = json.get('status_code', 0)
        self.error = json.get('error', '')
        self.number_of_total_results = json.get('number_of_total_results', 0)
        self.number_of_page_results = json.get('number_of_page_results', 0)
        self.limit = json.get('limit', 0)
        self.offset = json.get('offset', 0)
        self.results = json.get('results', 0)
        self.timestamp = datetime.datetime.utcnow()

    @property
    def has_error(self):
        return self.status_code != 1

    def __repr__(self):
        return f'<comicvine_client.response.Response(' \
               f'status_code={self.status_code!r}) ' \
               f'object at {hex(id(self))}>'

"""-------------------------------------------------------------------------------------------
-- COMICVINE CLIENT DEFINITION
-------------------------------------------------------------------------------------------"""

cv = ComicVineClient(stashrconfig['APP']['comicvine_api_key'])