# -*- coding: utf-8 -*-
import logging
import itertools
from typing import List

import requests
import backoff

from wood import util
from wood.comparison import Comparison
from wood.entities import Entity
from wood.invalidate import Invalidator as GenericInvalidator


logger = logging.getLogger(__name__)


class Invalidator(GenericInvalidator):
    """
    Invalidates URLs within a Cloudflare zone.
    """

    _API_BASE = 'https://api.cloudflare.com'
    _MAX_INVALIDATIONS_PER_REQUEST = 30

    def __init__(self, session: requests.Session, email: str, key: str,
                 zone: str, prefix: str):
        """
        Initialise a new Cloudflare cache invalidator.

        :param session: The requests session to use when interacting with
                        Cloudflare.
        :param zone: The zone ID to purge from.
        :param prefix: The full URL prefix to append asset paths to,
                       e.g. https://example.com/webroot/. Should always end
                       with a trailing slash.
        """
        self._session = session
        self._email = email
        self._key = key
        self._zone = zone
        self._prefix = prefix

    def invalidate(self, comparison: Comparison[Entity, Entity]) -> None:
        """
        Invalidate paths in a zone. See https://api.cloudflare.com
        /#zone-purge-individual-files-by-url-and-cache-tags

        :param comparison: The comparison whose changes to invalidate.
        :raises requests.exceptions.RequestException: On request failure.
        :raises RuntimeError: If the request succeeded but could not be carried
                              out.
        """

        @backoff.on_exception(backoff.expo,
                              requests.exceptions.RequestException,
                              max_tries=5,
                              giveup=lambda e:
                              400 <= e.response.status_code < 500)
        def _request(chunk: List[str]) -> requests.Response:
            """
            Send a purge cache request to Cloudflare. This method will
            automatically retry with a back-off in case of server-side error.

            :param chunk: The list of paths to purge. These should not have a
                          leading slash, and will be combined with the prefix
                          to form a URL.
            :return: Cloudflare's response to our successful request.
            :raises requests.exceptions.RequestException: If the request fails
                                                          on the 5th attempt.
            """
            response = self._session.delete(
                f'{self._API_BASE}/client/v4/zones/{self._zone}/purge_cache',
                headers={
                    'X-Auth-Email': self._email,
                    'X-Auth-Key': self._key
                },
                json={
                    'files': [self._prefix + path for path in chunk]
                })
            response.raise_for_status()
            return response

        paths = itertools.chain(comparison.deleted(), comparison.modified())
        for chunk_ in util.chunk(paths, self._MAX_INVALIDATIONS_PER_REQUEST):
            chunk_ = list(chunk_)
            if not chunk_:
                # nothing to do
                return
            logger.info('Invalidating %d paths (%s)', len(chunk_),
                        ', '.join(chunk_))
            response_ = _request(chunk_)
            logger.debug('Cloudflare invalidation response [%d]: %s',
                         response_.status_code,
                         response_.text)
            json_ = response_.json()
            if not json_['success']:
                # this would be strange - the API returned a success response
                # code, but success was not "true"
                # TODO more appropriate exception, with handling upstream
                raise RuntimeError('Cloudflare reported failure')
            logger.info('Created invalidation %s', json_['result']['id'])
