# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# Author: Endre Karlson <endre.karlson@hpe.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import time

from oslo_log import log as logging
from oslo_serialization import jsonutils
import requests
from requests.adapters import HTTPAdapter

from designate.backend import base
import designate.conf
from designate import exceptions
from designate import utils

CONF = designate.conf.CONF
LOG = logging.getLogger(__name__)
CFG_GROUP_NAME = 'backend:dynect'


class DynClientError(exceptions.Backend):
    """The base exception class for all HTTP exceptions.
    """

    def __init__(self, data=None, job_id=None, msgs=None,
                 http_status=None, url=None, method=None, details=None):
        self.data = data
        self.job_id = job_id
        self.msgs = msgs

        self.http_status = http_status
        self.url = url
        self.method = method
        self.details = details
        formatted_string = (
            f'{self.msgs} (HTTP {self.method} to {self.url} - '
            f'{self.http_status}) - {self.details}'
        )
        if job_id:
            formatted_string += f' (Job-ID: {job_id})'
        super().__init__(formatted_string)

    @staticmethod
    def from_response(response, details=None):
        data = response.json()

        exc_kwargs = dict(
            data=data['data'],
            job_id=data['job_id'],
            msgs=data['msgs'],
            http_status=response.status_code,
            url=response.url,
            method=response.request.method,
            details=details)

        for msg in data.get('msgs', []):
            if msg['INFO'].startswith('login:'):
                raise DynClientAuthError(**exc_kwargs)
            elif 'Operation blocked' in msg['INFO']:
                raise DynClientOperationBlocked(**exc_kwargs)
        return DynClientError(**exc_kwargs)


class DynClientAuthError(DynClientError):
    pass


class DynTimeoutError(exceptions.Backend):
    """
    A job timedout.
    """
    error_code = 408
    error_type = 'dyn_timeout'


class DynClientOperationBlocked(exceptions.BadRequest, DynClientError):
    error_type = 'operation_blocked'


class DynClient:
    """
    DynECT service client.

    https://help.dynect.net/rest/
    """

    def __init__(self, customer_name, user_name, password, timeout, timings,
                 verify=True, retries=1, pool_maxsize=10, pool_connections=10):
        self.endpoint = 'https://api.dynect.net:443'
        self.api_version = '3.5.6'

        self.customer_name = customer_name
        self.user_name = user_name
        self.password = password

        self.times = []  # [("item", starttime, endtime), ...]
        self.timings = timings
        self.timeout = timeout

        self.authing = False
        self.token = None

        session = requests.Session()
        session.verify = verify

        session.headers = {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
            'API-Version': self.api_version,
            'User-Agent': 'DynECTClient'
        }

        adapter = HTTPAdapter(
            max_retries=int(retries),
            pool_maxsize=int(pool_maxsize),
            pool_connections=int(pool_connections),
            pool_block=True
        )
        session.mount(self.endpoint, adapter)
        self.http = session

    def _http_log_req(self, method, url, kwargs):
        string_parts = [
            "curl -i",
            "-X '%s'" % method,
            "'%s'" % url,
        ]

        for element in kwargs['headers']:
            header = "-H '{}: {}'".format(element, kwargs['headers'][element])
            string_parts.append(header)

        LOG.debug('REQ: %s', ' '.join(string_parts))
        if 'data' in kwargs:
            LOG.debug('REQ BODY: %s\n', kwargs['data'])

    def _http_log_resp(self, resp):
        LOG.debug('RESP: [%s] %s\n', resp.status_code, resp.headers)

    def _request(self, method, url, **kwargs):
        """
        Low level request helper that actually executes the request towards a
        wanted URL.

        This does NOT do any authentication.
        """
        # NOTE: Allow passing the url as just the path or a full url
        if not url.startswith('http'):
            if not url.startswith('/REST'):
                url = '/REST' + url
            url = self.endpoint + url

        kwargs.setdefault("headers", kwargs.get("headers", {}))
        kwargs['proxies'] = utils.get_proxies()

        if self.token is not None:
            kwargs['headers']['Auth-Token'] = self.token
        if self.timeout is not None:
            kwargs.setdefault("timeout", self.timeout)

        data = kwargs.get('data')
        if data is not None:
            kwargs['data'] = data.copy()

            # NOTE: We don't want to log the credentials (password) that are
            # used in a auth request.
            if 'password' in kwargs['data']:
                kwargs['data']['password'] = '**SECRET**'

            self._http_log_req(method, url, kwargs)

            # NOTE: Set it back to the original data and serialize it.
            kwargs['data'] = jsonutils.dump_as_bytes(data)
        else:
            self._http_log_req(method, url, kwargs)

        start_time = time.monotonic()
        resp = self.http.request(method, url, **kwargs)
        if self.timings:
            self.times.append((f"{method} {url}",
                               start_time, time.monotonic()))
        self._http_log_resp(resp)

        if resp.status_code >= 400:
            LOG.debug('Request returned failure status: %s', resp.status_code)
            raise DynClientError.from_response(resp)
        return resp

    def request(self, method, url, retries=2, **kwargs):
        if self.token is None and not self.authing:
            self.login()

        try:
            response = self._request(method, url, **kwargs)
        except DynClientAuthError:
            if retries > 0:
                self.token = None
                retries = retries - 1
                return self.request(method, url, retries, **kwargs)
            else:
                raise

        return response.json()

    def login(self):
        self.authing = True
        data = {
            'customer_name': self.customer_name,
            'user_name': self.user_name,
            'password': self.password
        }
        response = self.post('/Session', data=data)
        self.token = response['data']['token']
        self.authing = False

    def logout(self):
        self.delete('/Session')
        self.token = None

    def post(self, *args, **kwargs):
        response = self.request('POST', *args, **kwargs)
        return response

    def put(self, *args, **kwargs):
        response = self.request('PUT', *args, **kwargs)
        return response

    def delete(self, *args, **kwargs):
        response = self.request('DELETE', *args, **kwargs)
        return response


class DynECTBackend(base.Backend):
    """
    Support for DynECT as a secondary DNS.
    """
    __plugin_name__ = 'dynect'

    __backend_status__ = 'untested'

    def __init__(self, target):
        super().__init__(target)

        self.customer_name = self.options.get('customer_name')
        self.username = self.options.get('username')
        self.password = self.options.get('password')
        self.contact_nickname = self.options.get('contact_nickname', None)
        self.tsig_key_name = self.options.get('tsig_key_name', None)

        for m in self.masters:
            if m.port != 53:
                raise exceptions.ConfigurationError(
                    "DynECT only supports mDNS instances on port 53")

    def get_client(self):
        return DynClient(
            customer_name=self.customer_name,
            user_name=self.username,
            password=self.password,
            timeout=CONF[CFG_GROUP_NAME].timeout,
            timings=CONF[CFG_GROUP_NAME].timings)

    def create_zone(self, context, zone):
        LOG.info(
            'Creating zone %(d_id)s / %(d_name)s',
            {
                'd_id': zone['id'],
                'd_name': zone['name']
            }
        )

        url = '/Secondary/%s' % zone['name'].rstrip('.')
        data = {
            'masters': [m.host for m in self.masters]
        }

        if self.contact_nickname is not None:
            data['contact_nickname'] = self.contact_nickname

        if self.tsig_key_name is not None:
            data['tsig_key_name'] = self.tsig_key_name

        client = self.get_client()

        try:
            client.post(url, data=data)
        except DynClientError as e:
            for emsg in e.msgs:
                if emsg['ERR_CD'] == 'TARGET_EXISTS':
                    LOG.info(
                        'Zone already exists, updating existing zone '
                        'instead %s', zone['name']
                    )
                    client.put(url, data=data)
                    break
            else:
                raise

        client.put(url, data={'activate': True})
        client.logout()

    def delete_zone(self, context, zone, zone_params=None):
        LOG.info(
            'Deleting zone %(d_id)s / %(d_name)s', {
                'd_id': zone['id'],
                'd_name': zone['name']
            }
        )
        url = '/Zone/%s' % zone['name'].rstrip('.')
        client = self.get_client()
        try:
            client.delete(url)
        except DynClientError as e:
            if e.http_status == 404:
                LOG.warning(
                    'Attempt to delete %(d_id)s / %(d_name)s caused 404, '
                    'ignoring.', {
                        'd_id': zone['id'],
                        'd_name': zone['name']
                    }
                )
                pass
            else:
                raise
        client.logout()
