# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You 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.

__all__ = [
    'GoDaddyDNSDriver'
]

try:
    import simplejson as json
except Exception:
    import json

from libcloud.common.base import ConnectionKey, JsonResponse
from libcloud.common.types import LibcloudError
from libcloud.utils.py3 import httplib
from libcloud.dns.types import Provider, RecordType, RecordDoesNotExistError
from libcloud.dns.base import DNSDriver, Zone, Record

API_HOST = 'api.godaddy.com'
VALID_RECORD_EXTRA_PARAMS = ['prio', 'ttl']


class GoDaddyDNSException(LibcloudError):
    def __init__(self, code, message):
        self.code = code
        self.message = message
        self.args = (code, message)

    def __str__(self):
        return self.__repr__()

    def __repr__(self):
        return ('<GoDaddyDNSException in %s: %s>' % (self.code, self.message))


class GoDaddyDNSResponse(JsonResponse):
    valid_response_codes = [httplib.OK, httplib.ACCEPTED, httplib.CREATED,
                            httplib.NO_CONTENT]

    def parse_body(self):
        if not self.body:
            return None
        # json.loads doesn't like the regex expressions used in godaddy schema
        self.body = self.body.replace('\\.', '\\\\.')

        data = json.loads(self.body)
        return data

    def parse_error(self):
        data = self.parse_body()
        raise GoDaddyDNSException(code=data['code'], message=data['message'])

    def success(self):
        return self.status in self.valid_response_codes


class GoDaddyDNSConnection(ConnectionKey):
    responseCls = GoDaddyDNSResponse
    host = API_HOST

    allow_insecure = False

    def __init__(self, key, secret, secure=True, shopper_id=None, host=None,
                 port=None, url=None, timeout=None,
                 proxy_url=None, backoff=None, retry_delay=None):
        super(GoDaddyDNSConnection, self).__init__(
            key,
            secure=secure, host=host,
            port=port, url=url,
            timeout=timeout,
            proxy_url=proxy_url,
            backoff=backoff,
            retry_delay=retry_delay)
        self.key = key
        self.secret = secret
        self.shopper_id = shopper_id

    def add_default_headers(self, headers):
        if self.shopper_id is not None:
            headers['X-Shopper-Id'] = self.shopper_id
        headers['Content-type'] = 'application/json'
        headers['Authorization'] = "sso-key %s:%s" % \
            (self.key, self.secret)
        return headers


class GoDaddyDNSDriver(DNSDriver):
    """
    A driver for GoDaddy DNS.

    This is for customers of GoDaddy
    who wish to purchase, update existing domains
    and manage records for DNS zones owned by GoDaddy NS servers.
    """
    type = Provider.GODADDY
    name = 'GoDaddy DNS'
    website = 'https://www.godaddy.com/'
    connectionCls = GoDaddyDNSConnection

    RECORD_TYPE_MAP = {
        RecordType.A: 'A',
        RecordType.AAAA: 'AAAA',
        RecordType.CNAME: 'CNAME',
        RecordType.MX: 'MX',
        RecordType.NS: 'SPF',
        RecordType.SRV: 'SRV',
        RecordType.TXT: 'TXT',
    }

    def __init__(self, shopper_id, key, secret,
                 secure=True, host=None, port=None):
        """
        Instantiate a new `GoDaddyDNSDriver`

        :param  shopper_id: Your customer ID or shopper ID with GoDaddy
        :type   shopper_id: ``str``

        :param  key: Your access key from developer.godaddy.com
        :type   key: ``str``

        :param  secret: Your access key secret
        :type   secret: ``str``
        """
        self.shopper_id = shopper_id
        super(GoDaddyDNSDriver, self).__init__(key=key, secret=secret,
                                               secure=secure,
                                               host=host, port=port,
                                               shopper_id=str(shopper_id))

    def list_zones(self):
        """
        Return a list of zones (purchased domains)

        :return: ``list`` of :class:`Zone`
        """
        result = self.connection.request(
            '/v1/domains/').object
        zones = self._to_zones(result)
        return zones

    def list_records(self, zone):
        """
        Return a list of records for the provided zone.

        :param zone: Zone to list records for.
        :type zone: :class:`Zone`

        :return: ``list`` of :class:`Record`
        """
        result = self.connection.request(
            '/v1/domains/%s/records' % (zone.domain)).object
        records = self._to_records(items=result, zone=zone)
        return records

    def create_record(self, name, zone, type, data, extra=None):
        """
        Create a new record.

        :param name: Record name without the domain name (e.g. www).
                     Note: If you want to create a record for a base domain
                     name, you should specify empty string ('') for this
                     argument.
        :type  name: ``str``

        :param zone: Zone where the requested record is created.
        :type  zone: :class:`Zone`

        :param type: DNS record type (A, AAAA, ...).
        :type  type: :class:`RecordType`

        :param data: Data for the record (depends on the record type).
        :type  data: ``str``

        :param extra: Extra attributes (driver specific). (optional)
        :type extra: ``dict``

        :rtype: :class:`Record`
        """
        new_record = self._format_record(name, type, data, extra)
        self.connection.request(
            '/v1/domains/%s/records' % (zone.domain), method='PATCH',
            data=json.dumps([new_record]))
        id = self._get_id_of_record(name, type)
        return Record(
            id=id, name=name,
            type=type, data=data,
            zone=zone, driver=self,
            ttl=new_record['ttl'],
            extra=extra)

    def update_record(self, record, name, type, data, extra=None):
        """
        Update an existing record.

        :param record: Record to update.
        :type  record: :class:`Record`

        :param name: Record name without the domain name (e.g. www).
                     Note: If you want to create a record for a base domain
                     name, you should specify empty string ('') for this
                     argument.
        :type  name: ``str``

        :param type: DNS record type (A, AAAA, ...).
        :type  type: :class:`RecordType`

        :param data: Data for the record (depends on the record type).
        :type  data: ``str``

        :param extra: (optional) Extra attributes (driver specific).
        :type  extra: ``dict``

        :rtype: :class:`Record`
        """
        new_record = self._format_record(name, type, data, extra)
        self.connection.request(
            '/v1/domains/%s/records/%s/%s' % (record.zone.domain,
                                              record.type,
                                              record.name),
            method='PUT',
            data=json.dumps([new_record]))
        id = self._get_id_of_record(name, type)
        return Record(
            id=id, name=name,
            type=type, data=data,
            zone=record.zone, driver=self,
            ttl=new_record['ttl'],
            extra=extra)

    def get_record(self, zone_id, record_id):
        """
        Return a Record instance.

        :param zone_id: ID of the required zone
        :type  zone_id: ``str``

        :param record_id: ID of the required record
        :type  record_id: ``str``

        :rtype: :class:`Record`
        """
        parts = record_id.split(':')
        result = self.connection.request(
            '/v1/domains/%s/records/%s/%s' % (
                zone_id,
                parts[1],
                parts[0])).object
        if len(result) == 0:
            raise RecordDoesNotExistError(record_id,
                                          driver=self,
                                          record_id=record_id)
        return self._to_record(result[0],
                               self.get_zone(zone_id))

    def get_zone(self, zone_id):
        """
        Get a zone (by domain)

        :param  zone_id: The domain, not the ID
        :type   zone_id: ``str``

        :rtype:  :class:`Zone`
        """
        result = self.connection.request(
            '/v1/domains/%s/' % zone_id).object
        zone = self._to_zone(result)
        return zone

    def delete_zone(self, zone):
        """
        Delete a zone.

        Note: This will CANCEL a purchased domain

        :param zone: Zone to delete.
        :type  zone: :class:`Zone`

        :rtype: ``bool``
        """
        self.connection.request(
            '/v1/domains/%s' % (zone.domain),
            method='DELETE')
        # no error means ok
        return True

    def ex_check_availability(self, domain, for_transfer=False):
        """
        Check the availability of the domain

        :param   domain: the domain name e.g. wazzlewobbleflooble.com
        :type    domain: ``str``

        :param   for_transfer: Check if domain is available for transfer
        :type    for_transfer: ``bool``

        :rtype: `list` of :class:`GoDaddyAvailability`
        """
        result = self.connection.request(
            '/v1/domains/available',
            method='GET',
            params={
                'domain': domain,
                'forTransfer': str(for_transfer)
            }
        ).object
        return GoDaddyAvailability(
            domain=result['domain'],
            available=result['available'],
            price=result['price'],
            currency=result['currency'],
            period=result['period']
        )

    def ex_list_tlds(self):
        """
        List available TLDs for sale

        :rtype: ``list`` of :class:`GoDaddyTLD`
        """
        result = self.connection.request(
            '/v1/domains/tlds',
            method='GET'
        ).object
        return self._to_tlds(result)

    def ex_get_purchase_schema(self, tld):
        """
        Get the schema that needs completing to purchase a new domain
        Use this in conjunction with ex_purchase_domain

        :param   tld: The top level domain e.g com, eu, uk
        :type    tld: ``str``

        :rtype: `dict` the JSON Schema
        """
        result = self.connection.request(
            '/v1/domains/purchase/schema/%s' % tld,
            method='GET'
        ).object
        return result

    def ex_get_agreements(self, tld, privacy=True):
        """
        Get the legal agreements for a tld
        Use this in conjunction with ex_purchase_domain

        :param   tld: The top level domain e.g com, eu, uk
        :type    tld: ``str``

        :rtype: `dict` the JSON Schema
        """
        result = self.connection.request(
            '/v1/domains/agreements',
            params={
                'tlds': tld,
                'privacy': str(privacy)
            },
            method='GET'
        ).object
        agreements = []
        for item in result:
            agreements.append(
                GoDaddyLegalAgreement(
                    agreement_key=item['agreementKey'],
                    title=item['title'],
                    url=item['url'],
                    content=item['content']))
        return agreements

    def ex_purchase_domain(self, purchase_request):
        """
        Purchase a domain with GoDaddy

        :param  purchase_request: The completed document
            from ex_get_purchase_schema
        :type   purchase_request: ``dict``

        :rtype: :class:`GoDaddyDomainPurchaseResponse` Your order
        """
        result = self.connection.request(
            '/v1/domains/purchase',
            data=purchase_request,
            method='POST'
        ).object
        return GoDaddyDomainPurchaseResponse(
            order_id=result['orderId'],
            item_count=result['itemCount'],
            total=result['total'],
            currency=result['currency']
        )

    def _format_record(self, name, type, data, extra):
        if extra is None:
            extra = {}
        new_record = {}
        if type == RecordType.SRV:
            new_record = {
                'type': type,
                'name': name,
                'data': data,
                'priority': 1,
                'ttl': extra.get('ttl', 5),
                'service': extra.get('service', ''),
                'protocol': extra.get('protocol', ''),
                'port': extra.get('port', ''),
                'weight': extra.get('weight', '1')
            }
        else:
            new_record = {
                'type': type,
                'name': name,
                'data': data,
                'ttl': extra.get('ttl', 5)
            }
        if type == RecordType.MX:
            new_record['priority'] = 1
        return new_record

    def _to_zones(self, items):
        zones = []
        for item in items:
            zones.append(self._to_zone(item))
        return zones

    def _to_zone(self, item):
        extra = {"expires": item['expires']}
        zone = Zone(id=item['domainId'], domain=item['domain'],
                    type='master', ttl=None,
                    driver=self, extra=extra)
        return zone

    def _to_records(self, items, zone=None):
        records = []

        for item in items:
            records.append(self._to_record(item=item, zone=zone))
        return records

    def _to_record(self, item, zone=None):
        ttl = item['ttl']
        type = self._string_to_record_type(item['type'])
        name = item['name']
        id = self._get_id_of_record(name, type)
        record = Record(id=id, name=name,
                        type=type, data=item['data'],
                        zone=zone, driver=self,
                        ttl=ttl)
        return record

    def _to_tlds(self, items):
        tlds = []
        for item in items:
            tlds.append(self._to_tld(item))
        return tlds

    def _to_tld(self, item):
        return GoDaddyTLD(
            name=item['name'],
            tld_type=item['type']
        )

    def _get_id_of_record(self, name, type):
        return '%s:%s' % (name, type)

    def _ex_connection_class_kwargs(self):
        return {'shopper_id': self.shopper_id}


class GoDaddyAvailability(object):
    def __init__(self, domain, available, price, currency, period):
        self.domain = domain
        self.available = bool(available)
        # currency comes in micro-units, convert to dollars.
        self.price = float(price) / 1000000
        self.currency = currency
        self.period = int(period)


class GoDaddyTLD(object):
    def __init__(self, name, tld_type):
        self.name = name
        self.type = tld_type


class GoDaddyDomainPurchaseResponse(object):
    def __init__(self, order_id, item_count, total, currency):
        self.order_id = order_id
        self.item_count = item_count
        self.total = total
        self.current = currency


class GoDaddyLegalAgreement(object):
    def __init__(self, agreement_key, title, url, content):
        self.agreement_key = agreement_key
        self.title = title
        self.url = url
        self.content = content
