"""
ComicVine API Information & Documentation:
https://comicvine.gamespot.com/api/
https://comicvine.gamespot.com/api/documentation
"""
import logging
from django.conf import settings

import requests

logger = logging.getLogger(__name__)


class ComicVineClient(object):
    """
    Interacts with the ``search`` resource of the ComicVine API. Requires an
    account on https://comicvine.gamespot.com/ in order to obtain an API key.
    """

    # All API requests made by this client will be made to these URLs.
    API_URL = "https://comicvine.gamespot.com/api/search/"
    ISSUE_API_URL = "https://comicvine.gamespot.com/api/issue/4000-{issue_id}/"
    VOLUME_API_URL = "https://comicvine.gamespot.com/api/volume/4050-{volume_id}/"

    # A valid User-Agent header must be set in order for our API requests to
    # be accepted, otherwise our request will be rejected with a
    # **403 - Forbidden** error.
    HEADERS = {
        "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:7.0) "
        "Gecko/20130825 Firefox/36.0"
    }

    # A set of valid resource types to return in results.
    RESOURCE_TYPES = {
        "character",
        "issue",
        "location",
        "object",
        "person",
        "publisher",
        "story_arc",
        "team",
        "volume",
    }

    def __init__(self, api_key):
        """
        Store the API key in a class variable.

        :param api_key: Your personal ComicVine API key.
        :type  api_key: str
        """

        self.api_key = api_key

    def search(self, query, offset=0, limit=10, resources=None):
        """
        Perform a search against the API, using the provided query term. If
        required, a list of resource types to filter search results to can
        be included.

        Take the JSON contained in the response and provide it to the custom
        ``Response`` object's constructor. Return the ``Response`` object.

        :param query: The search query with which to make the request.
        :type  query: str
        :param offset: The index of the first record returned.
        :type  offset: int or None
        :param limit: How many records to return **(max 10)**
        :type  limit: int or None
        :param resources: A list of resources to include in the search results.
        :type  resources: list or None
        :type  use_cache: bool

        :return: The response object containing the results of the search
                 query.
        :rtype:  comicvine_search.response.Response
        """

        params = self._request_params(query, offset, limit, resources)
        json_data = self._query_api(params)

        return json_data

    def _request_params(self, query, offset, limit, resources):
        """
        Construct a dict containing the required key-value pairs of parameters
        required in order to make the API request.

        The documentation for the ``search`` resource can be found at
        https://comicvine.gamespot.com/api/documentation#toc-0-30.

        Regarding 'limit', as per the documentation:

            The number of results to display per page. This value defaults to
            10 and can not exceed this number.

        :param query: The search query with which to make the request.
        :type  query: str
        :param offset: The index of the first record returned.
        :type  offset: int
        :param limit: How many records to return **(max 10)**
        :type  limit: int
        :param resources: A list of resources to include in the search results.
        :type  resources: list or None

        :return: A dictionary of request parameters.
        :rtype:  dict
        """

        params = {
            "api_key": self.api_key,
            "format": "json",
            "limit": min(10, limit),  # hard limit of 10
            "offset": max(0, offset),  # cannot provide negative offset
            "query": query,
        }
        validated = self._validate_resources(resources)
        if validated:
            params["resources"] = validated
        return params

    def _validate_resources(self, resources):
        """
        Provided a list of resources, first convert it to a set and perform an
        intersection with the set of valid resource types, ``RESOURCE_TYPES``.
        Return a comma-separted string of the remaining valid resources, or
        None if the set is empty.

        :param resources: A list of resources to include in the search results.
        :type  resources: list or None

        :return: A comma-separated string of valid resources.
        :rtype:  str or None
        """

        if not resources:
            return None

        valid_resources = self.RESOURCE_TYPES & set(resources)
        return ",".join(valid_resources) if valid_resources else None

    def _query_api(self, params):
        """
        Query the ComicVine API's ``search`` resource, providing the required
        headers and parameters with the request.

        If an error occurs during the request, handle it accordingly. Upon
        success, return the JSON from the response.

        :param params: Parameters to include with the request.
        :type  params: dict

        :return: The JSON contained in the response.
        :rtype:  dict
        """

        response = requests.get(self.API_URL, headers=self.HEADERS, params=params)

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

        json_data = response.json()

        if json_data.get("status_code") != 1:
            error_msg = json_data.get("error", "Unknown ComicVine API error")
            logger.error(
                "ComicVine API returned status_code %s: %s",
                json_data.get("status_code"),
                error_msg,
            )
            return {}

        return json_data

    def _handle_http_error(self, response):
        """
        Provided a ``requests.Response`` object, if the status code is
        anything other than **200**, we will treat it as an error.

        Using the response's status code, determine which type of exception to
        raise. Construct an exception message from the response's status code
        and reason properties before raising the exception.

        :param response: The requests.Response object returned by the HTTP
                         request.
        :type  response: requests.Response

        :raises ComicVineUnauthorizedException: if no API key provided.
        :raises ComicVineForbiddenException: if no User-Agent header provided.
        :raises ComicVineApiException: if an unidentified error occurs.
        """

        exception = {
            401: Exception,
            403: Exception,
        }.get(response.status_code, Exception)
        message = f"{response.status_code} {response.reason}"

        raise exception(message)

    def get_issue(self, issue_id: str) -> dict:
        """
        Fetch a single issue by its ComicVine ID directly from the issue detail
        endpoint, which returns richer data than the search endpoint.

        :param issue_id: The ComicVine numeric ID for the issue (e.g. "538480")
        :type  issue_id: str

        :return: The full JSON response for the issue, or empty dict on failure.
        :rtype:  dict
        """
        params = {
            "api_key": self.api_key,
            "format": "json",
        }
        url = self.ISSUE_API_URL.format(issue_id=issue_id)
        response = requests.get(url, headers=self.HEADERS, params=params)

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

        json_data = response.json()

        if json_data.get("status_code") != 1:
            error_msg = json_data.get("error", "Unknown ComicVine API error")
            logger.error(
                "ComicVine API returned status_code %s: %s",
                json_data.get("status_code"),
                error_msg,
            )
            return {}

        return json_data.get("results", {})

    def get_volume(self, volume_id: str) -> dict:
        """
        Fetch a single volume by its ComicVine ID from the volume detail
        endpoint. Used to get publisher info and other volume-level metadata.

        :param volume_id: The ComicVine numeric ID for the volume (e.g. "91273")
        :type  volume_id: str

        :return: The full JSON response for the volume, or empty dict on failure.
        :rtype:  dict
        """
        params = {
            "api_key": self.api_key,
            "format": "json",
        }
        url = self.VOLUME_API_URL.format(volume_id=volume_id)
        response = requests.get(url, headers=self.HEADERS, params=params)

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

        json_data = response.json()

        if json_data.get("status_code") != 1:
            error_msg = json_data.get("error", "Unknown ComicVine API error")
            logger.error(
                "ComicVine API returned status_code %s: %s",
                json_data.get("status_code"),
                error_msg,
            )
            return {}

        return json_data.get("results", {})


def lookup_comic_from_comicvine(title: str) -> dict:
    original_title = title

    issue_number = None
    resource_type = "issue"
    if "Issue " in title:
        issue_number = title.split("Issue ")[1]
    volume_number = None
    if "Volume " in title:
        resource_type = "volume"
        volume_number = title.split("Volume ")[1]

    api_key = getattr(settings, "COMICVINE_API_KEY", "")
    if not api_key:
        logger.warning("No ComicVine API key configured, not looking anything up")
        return {}

    client = ComicVineClient(api_key=api_key)

    raw_results = client.search(title)
    if not raw_results:
        return {}
    results = raw_results.get("results", [])
    results = [r for r in results if r.get("resource_type") == resource_type]
    if not results:
        logger.warning("No comic found on ComicVine")
        return {}

    found_result = None
    for result in results:
        if issue_number is not None and result.get("issue_number") == str(issue_number):
            found_result = result
            break
        if volume_number is not None and result.get("volume_number") == str(volume_number):
            found_result = result
            break

    if not found_result:
        found_result = results[0]

    data_dict = _build_data_dict_from_issue(found_result, original_title)
    _enrich_with_volume_data(client, data_dict)
    return data_dict


def lookup_issue_by_comicvine_id(comicvine_id: str) -> dict:
    """
    Look up an issue directly by its ComicVine ID using the issue detail
    endpoint. Returns richer data than the search-based lookup.

    :param comicvine_id: The ComicVine numeric ID for the issue (e.g. "538480")
    :type  comicvine_id: str

    :return: A dict of extracted book metadata, or empty dict on failure.
    :rtype:  dict
    """
    if not comicvine_id:
        return {}

    api_key = getattr(settings, "COMICVINE_API_KEY", "")
    if not api_key:
        logger.warning("No ComicVine API key configured, not looking anything up")
        return {}

    client = ComicVineClient(api_key=api_key)
    issue_data = client.get_issue(comicvine_id)
    if not issue_data:
        logger.warning("No issue found on ComicVine for ID %s", comicvine_id)
        return {}

    data_dict = _build_data_dict_from_issue(issue_data, issue_data.get("name", ""))
    _enrich_with_volume_data(client, data_dict)
    return data_dict


def _build_data_dict_from_issue(issue_data: dict, original_title: str = "") -> dict:
    """
    Build a book metadata dict from a ComicVine issue resource (either from
    search results or issue detail endpoint). Both return the same shape of
    issue data.

    :param issue_data: The issue resource dict from ComicVine.
    :param original_title: The original search term, if any.

    :return: A dict of extracted book metadata.
    :rtype:  dict
    """
    title = issue_data.get("name")
    if issue_data.get("volume"):
        title = issue_data.get("volume").get("name")

    cover_url = None
    if issue_data.get("image"):
        cover_url = issue_data["image"].get("original_url")

    volume_name = None
    volume_cv_id = None
    publisher_name = None
    volume_data = issue_data.get("volume")
    if volume_data:
        volume_name = volume_data.get("name")
        volume_cv_id = volume_data.get("id")
        publisher_data = volume_data.get("publisher")
        if publisher_data:
            publisher_name = publisher_data.get("name")

    data_dict = {
        "title": title,
        "original_title": original_title,
        "issue_number": issue_data.get("issue_number"),
        "volume_number": issue_data.get("volume_number"),
        "volume": volume_name,
        "volume_comicvine_id": volume_cv_id,
        "publisher": publisher_name,
        "cover_url": cover_url,
        "comicvine_id": issue_data.get("id"),
        "summary": issue_data.get("description"),
        "publish_date": issue_data.get("cover_date"),
        "first_publish_year": (issue_data.get("cover_date") or "")[:4],
        "tags": ["comicbook"],
    }

    return data_dict


def _enrich_with_volume_data(client: ComicVineClient, data_dict: dict) -> None:
    """
    Follow-up a successful issue lookup by fetching the volume detail and
    filling in publisher and other volume-level metadata that the issue
    endpoint doesn't provide.

    :param client: An initialised ComicVineClient instance.
    :param data_dict: The data dict from an issue lookup (mutated in place).
    """
    volume_cv_id = data_dict.get("volume_comicvine_id")
    if not volume_cv_id:
        return

    volume_data = client.get_volume(str(volume_cv_id))
    if not volume_data:
        return

    publisher_data = volume_data.get("publisher")
    if publisher_data:
        publisher_name = publisher_data.get("name")
        if publisher_name and not data_dict.get("publisher"):
            data_dict["publisher"] = publisher_name

    if not data_dict.get("volume"):
        data_dict["volume"] = volume_data.get("name")
