# Copyright (C) 2014 Red Hat, Inc.
#
# Author: Rich Megginson <rmeggins@redhat.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 sys
import logging
import pprint
import json
import copy

import requests
from oslo_config import cfg

from designate.backend import impl_ipa
from designate.i18n import _LI
from designate.i18n import _LW
from designate.i18n import _LE
from designate import utils


logging.basicConfig()
LOG = logging.getLogger(__name__)

cfg.CONF.import_opt('api_base_uri', 'designate.api', 'service:api')
cfg.CONF.import_opt('backend_driver', 'designate.central', 'service:central')


class NoNameServers(Exception):
    pass


class AddServerError(Exception):
    pass


class DeleteServerError(Exception):
    pass


class AddDomainError(Exception):
    pass


class DeleteDomainError(Exception):
    pass


class AddRecordError(Exception):
    pass


cuiberrorstr = """ERROR: You cannot have Designate configured
to use the IPA backend when running this script.  It will wipe
out your IPA DNS data.  Please follow these steps:
* shutdown designate-central
* edit designate.conf
[service:central]
backend_driver = fake # or something other than ipa
* restart designate-central and other designate services
"""


class CannotUseIPABackend(Exception):
    pass


# create mapping of ipa record types to designate types
iparectype2designate = {}
for rectype, tup in list(impl_ipa.rectype2iparectype.items()):
    iparectype = tup[0]
    iparectype2designate[iparectype] = rectype


# using the all: True flag returns fields we can't use
# strip these keys from zones
zoneskips = ['dn', 'nsrecord', 'idnszoneactive', 'objectclass']


def rec2des(rec, zonename):
    """Convert an IPA record to Designate format.  A single IPA record
    returned from the search may translate into multiple Designate.
    IPA dnsrecord_find returns a "name".  Each DNS name may contain
    multiple record types.  Each record type may contain multiple
    values.  Each one of these values must be added separately to
    Designate.  This function returns all of those as a list of
    dict designate records.
    """
    # convert record name
    if rec['idnsname'][0] == '@':
        name = zonename
    else:
        name = rec['idnsname'][0] + "." + zonename
    # find all record types
    rectypes = []
    for k in rec:
        if k.endswith("record"):
            if k in iparectype2designate:
                rectypes.append(k)
            else:
                LOG.info(_LI("Skipping unknown record type "
                             "%(type)s in %(name)s"),
                         {'type': k, 'name': name})

    desrecs = []
    for rectype in rectypes:
        dtype = iparectype2designate[rectype]
        for ddata in rec[rectype]:
            desreq = {'name': name, 'type': dtype}
            if dtype == 'SRV' or dtype == 'MX':
                # split off the priority and send in a separate field
                idx = ddata.find(' ')
                desreq['priority'] = int(ddata[:idx])
                if dtype == 'SRV' and not ddata.endswith("."):
                    # if server is specified as relative, add zonename
                    desreq['data'] = ddata[(idx + 1):] + "." + zonename
                else:
                    desreq['data'] = ddata[(idx + 1):]
            else:
                desreq['data'] = ddata
            if rec.get('description', [None])[0]:
                desreq['description'] = rec.get('description')[0]
            if rec.get('ttl', [None])[0]:
                desreq['ttl'] = int(rec['dnsttl'][0])
            desrecs.append(desreq)
    return desrecs


def zone2des(ipazone):
    # next, try to add the fake domain to Designate
    zonename = ipazone['idnsname'][0].rstrip(".") + "."
    email = ipazone['idnssoarname'][0].rstrip(".").replace(".", "@", 1)
    desreq = {"name": zonename,
              "ttl": int(ipazone['idnssoarefresh'][0]),
              "email": email}
    return desreq


def getipadomains(ipabackend, version):
    # get the list of domains/zones from IPA
    ipareq = {'method': 'dnszone_find',
              'params': [[], {'version': version,
                              'all': True}]}
    iparesp = ipabackend._call_and_handle_error(ipareq)
    LOG.debug("Response: %s" % pprint.pformat(iparesp))
    return iparesp['result']['result']


def getiparecords(ipabackend, zonename, version):
    ipareq = {'method': 'dnsrecord_find',
              'params': [[zonename], {"version": version,
                                      "all": True}]}
    iparesp = ipabackend._call_and_handle_error(ipareq)
    return iparesp['result']['result']


def syncipaservers2des(servers, designatereq, designateurl):
    # get existing servers from designate
    dservers = {}
    srvurl = designateurl + "/servers"
    resp = designatereq.get(srvurl)
    LOG.debug("Response: %s" % pprint.pformat(resp.json()))
    if resp and resp.status_code == 200 and resp.json() and \
            'servers' in resp.json():
        for srec in resp.json()['servers']:
            dservers[srec['name']] = srec['id']
    else:
        LOG.warning(_LW("No servers in designate"))

    # first - add servers from ipa not already in designate
    for server in servers:
        if server in dservers:
            LOG.info(_LI("Skipping ipa server %s already in designate"),
                     server)
        else:
            desreq = {"name": server}
            resp = designatereq.post(srvurl, data=json.dumps(desreq))
            LOG.debug("Response: %s" % pprint.pformat(resp.json()))
            if resp.status_code == 200:
                LOG.info(_LI("Added server %s to designate"), server)
            else:
                raise AddServerError("Unable to add %s: %s" %
                                     (server, pprint.pformat(resp.json())))

    # next - delete servers in designate not in ipa
    for server, sid in list(dservers.items()):
        if server not in servers:
            delresp = designatereq.delete(srvurl + "/" + sid)
            if delresp.status_code == 200:
                LOG.info(_LI("Deleted server %s"), server)
            else:
                raise DeleteServerError("Unable to delete %s: %s" %
                                        (server,
                                         pprint.pformat(delresp.json())))


def main():
    # HACK HACK HACK - allow required config params to be passed
    # via the command line
    cfg.CONF['service:api']._group._opts['api_base_uri']['cli'] = True
    for optdict in cfg.CONF['backend:ipa']._group._opts.values():
        if 'cli' in optdict:
            optdict['cli'] = True
    # HACK HACK HACK - allow api url to be passed in the usual way
    utils.read_config('designate', sys.argv)
    if cfg.CONF['service:central'].backend_driver == 'ipa':
        raise CannotUseIPABackend(cuiberrorstr)
    if cfg.CONF.debug:
        LOG.setLevel(logging.DEBUG)
    else:
        LOG.setLevel(logging.INFO)
    ipabackend = impl_ipa.IPABackend(None)
    ipabackend.start()
    version = cfg.CONF['backend:ipa'].ipa_version
    designateurl = cfg.CONF['service:api'].api_base_uri + "v1"

    # get the list of domains/zones from IPA
    ipazones = getipadomains(ipabackend, version)
    # get unique list of name servers
    servers = {}
    for zonerec in ipazones:
        for nsrec in zonerec['nsrecord']:
            servers[nsrec] = nsrec
    if not servers:
        raise NoNameServers("Error: no name servers found in IPA")

    # let's see if designate is using the IPA backend
    # create a fake domain in IPA
    # create a fake server in Designate
    # try to create the same fake domain in Designate
    # if we get a DuplicateZone error from Designate, then
    # raise the CannotUseIPABackend error, after deleting
    # the fake server and fake domain
    # find the first non-reverse zone
    zone = {}
    for zrec in ipazones:
        if not zrec['idnsname'][0].endswith("in-addr.arpa.") and \
                zrec['idnszoneactive'][0] == 'TRUE':
            # ipa returns every data field as a list
            # convert the list to a scalar
            for n, v in list(zrec.items()):
                if n in zoneskips:
                    continue
                if isinstance(v, list):
                    zone[n] = v[0]
                else:
                    zone[n] = v
            break

    assert(zone)

    # create a fake subdomain of this zone
    domname = "%s.%s" % (utils.generate_uuid(), zone['idnsname'])
    args = copy.copy(zone)
    del args['idnsname']
    args['version'] = version
    ipareq = {'method': 'dnszone_add',
              'params': [[domname], args]}
    iparesp = ipabackend._call_and_handle_error(ipareq)
    LOG.debug("Response: %s" % pprint.pformat(iparesp))
    if iparesp['error']:
        raise AddDomainError(pprint.pformat(iparesp))

    # set up designate connection
    designatereq = requests.Session()
    xtra_hdrs = {'Content-Type': 'application/json'}
    designatereq.headers.update(xtra_hdrs)

    # sync ipa name servers to designate
    syncipaservers2des(servers, designatereq, designateurl)

    domainurl = designateurl + "/domains"
    # next, try to add the fake domain to Designate
    email = zone['idnssoarname'].rstrip(".").replace(".", "@", 1)
    desreq = {"name": domname,
              "ttl": int(zone['idnssoarefresh'][0]),
              "email": email}
    resp = designatereq.post(domainurl, data=json.dumps(desreq))
    exc = None
    fakezoneid = None
    if resp.status_code == 200:
        LOG.info(_LI("Added domain %s"), domname)
        fakezoneid = resp.json()['id']
        delresp = designatereq.delete(domainurl + "/" + fakezoneid)
        if delresp.status_code != 200:
            LOG.error(_LE("Unable to delete %(name)s: %(response)s") %
                      {'name': domname, 'response': pprint.pformat(
                          delresp.json())})
    else:
        exc = CannotUseIPABackend(cuiberrorstr)

    # cleanup fake stuff
    ipareq = {'method': 'dnszone_del',
              'params': [[domname], {'version': version}]}
    iparesp = ipabackend._call_and_handle_error(ipareq)
    LOG.debug("Response: %s" % pprint.pformat(iparesp))
    if iparesp['error']:
        LOG.error(_LE("%s") % pprint.pformat(iparesp))

    if exc:
        raise exc

    # get and delete existing domains
    resp = designatereq.get(domainurl)
    LOG.debug("Response: %s" % pprint.pformat(resp.json()))
    if resp and resp.status_code == 200 and resp.json() and \
            'domains' in resp.json():
        # domains must be deleted in child/parent order i.e. delete
        # sub-domains before parent domains - simple way to get this
        # order is to sort the domains in reverse order of name len
        dreclist = sorted(resp.json()['domains'],
                          key=lambda drec: len(drec['name']),
                          reverse=True)
        for drec in dreclist:
            delresp = designatereq.delete(domainurl + "/" + drec['id'])
            if delresp.status_code != 200:
                raise DeleteDomainError("Unable to delete %s: %s" %
                                        (drec['name'],
                                         pprint.pformat(delresp.json())))

    # key is zonename, val is designate rec id
    zonerecs = {}
    for zonerec in ipazones:
        desreq = zone2des(zonerec)
        resp = designatereq.post(domainurl, data=json.dumps(desreq))
        if resp.status_code == 200:
            LOG.info(_LI("Added domain %s"), desreq['name'])
        else:
            raise AddDomainError("Unable to add domain %s: %s" %
                                 (desreq['name'], pprint.pformat(resp.json())))
        zonerecs[desreq['name']] = resp.json()['id']

    # get the records for each zone
    for zonename, domainid in list(zonerecs.items()):
        recurl = designateurl + "/domains/" + domainid + "/records"
        iparecs = getiparecords(ipabackend, zonename, version)
        for rec in iparecs:
            desreqs = rec2des(rec, zonename)
            for desreq in desreqs:
                resp = designatereq.post(recurl, data=json.dumps(desreq))
                if resp.status_code == 200:
                    LOG.info(_LI("Added record %(record)s "
                                 "for domain %(domain)s"),
                             {'record': desreq['name'], 'domain': zonename})
                else:
                    raise AddRecordError("Could not add record %s: %s" %
                                         (desreq['name'],
                                          pprint.pformat(resp.json())))

if __name__ == '__main__':
    sys.exit(main())