# 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.

try:
    from lxml import etree as ET
except ImportError:
    from xml.etree import ElementTree as ET

from libcloud.backup.base import BackupDriver, BackupTarget, BackupTargetJob
from libcloud.backup.types import BackupTargetType
from libcloud.backup.types import Provider
from libcloud.common.dimensiondata import dd_object_to_id
from libcloud.common.dimensiondata import DimensionDataConnection
from libcloud.common.dimensiondata import DimensionDataBackupClient
from libcloud.common.dimensiondata import DimensionDataBackupClientAlert
from libcloud.common.dimensiondata import DimensionDataBackupClientType
from libcloud.common.dimensiondata import DimensionDataBackupDetails
from libcloud.common.dimensiondata import DimensionDataBackupSchedulePolicy
from libcloud.common.dimensiondata import DimensionDataBackupStoragePolicy
from libcloud.common.dimensiondata import API_ENDPOINTS, DEFAULT_REGION
from libcloud.common.dimensiondata import TYPES_URN
from libcloud.common.dimensiondata import GENERAL_NS, BACKUP_NS
from libcloud.utils.xml import fixxpath, findtext, findall

# pylint: disable=no-member

DEFAULT_BACKUP_PLAN = 'Advanced'


class DimensionDataBackupDriver(BackupDriver):
    """
    DimensionData backup driver.
    """

    selected_region = None
    connectionCls = DimensionDataConnection
    name = 'Dimension Data Backup'
    website = 'https://cloud.dimensiondata.com/'
    type = Provider.DIMENSIONDATA
    api_version = 1.0

    network_domain_id = None

    def __init__(self, key, secret=None, secure=True, host=None, port=None,
                 api_version=None, region=DEFAULT_REGION, **kwargs):

        if region not in API_ENDPOINTS and host is None:
            raise ValueError(
                'Invalid region: %s, no host specified' % (region))
        if region is not None:
            self.selected_region = API_ENDPOINTS[region]

        super(DimensionDataBackupDriver, self).__init__(
            key=key, secret=secret,
            secure=secure, host=host,
            port=port,
            api_version=api_version,
            region=region,
            **kwargs)

    def _ex_connection_class_kwargs(self):
        """
            Add the region to the kwargs before the connection is instantiated
        """

        kwargs = super(DimensionDataBackupDriver,
                       self)._ex_connection_class_kwargs()
        kwargs['region'] = self.selected_region
        return kwargs

    def get_supported_target_types(self):
        """
        Get a list of backup target types this driver supports

        :return: ``list`` of :class:``BackupTargetType``
        """
        return [BackupTargetType.VIRTUAL]

    def list_targets(self):
        """
        List all backuptargets

        :rtype: ``list`` of :class:`BackupTarget`
        """
        targets = self._to_targets(
            self.connection.request_with_orgId_api_2('server/server').object)
        return targets

    def create_target(self, name, address,
                      type=BackupTargetType.VIRTUAL, extra=None):
        """
        Creates a new backup target

        :param name: Name of the target (not used)
        :type name: ``str``

        :param address: The ID of the node in Dimension Data Cloud
        :type address: ``str``

        :param type: Backup target type, only Virtual supported
        :type type: :class:`BackupTargetType`

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

        :rtype: Instance of :class:`BackupTarget`
        """
        if extra is not None:
            service_plan = extra.get('servicePlan', DEFAULT_BACKUP_PLAN)
        else:
            service_plan = DEFAULT_BACKUP_PLAN
            extra = {'servicePlan': service_plan}

        create_node = ET.Element('NewBackup',
                                 {'xmlns': BACKUP_NS})
        create_node.set('servicePlan', service_plan)

        response = self.connection.request_with_orgId_api_1(
            'server/%s/backup' % (address),
            method='POST',
            data=ET.tostring(create_node)).object

        asset_id = None
        for info in findall(response,
                            'additionalInformation',
                            GENERAL_NS):
            if info.get('name') == 'assetId':
                asset_id = findtext(info, 'value', GENERAL_NS)

        return BackupTarget(
            id=asset_id,
            name=name,
            address=address,
            type=type,
            extra=extra,
            driver=self
        )

    def create_target_from_node(self, node, type=BackupTargetType.VIRTUAL,
                                extra=None):
        """
        Creates a new backup target from an existing node

        :param node: The Node to backup
        :type  node: ``Node``

        :param type: Backup target type (Physical, Virtual, ...).
        :type type: :class:`BackupTargetType`

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

        :rtype: Instance of :class:`BackupTarget`
        """
        return self.create_target(name=node.name,
                                  address=node.id,
                                  type=BackupTargetType.VIRTUAL,
                                  extra=extra)

    def create_target_from_container(self, container,
                                     type=BackupTargetType.OBJECT,
                                     extra=None):
        """
        Creates a new backup target from an existing storage container

        :param node: The Container to backup
        :type  node: ``Container``

        :param type: Backup target type (Physical, Virtual, ...).
        :type type: :class:`BackupTargetType`

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

        :rtype: Instance of :class:`BackupTarget`
        """
        return NotImplementedError(
            'create_target_from_container not supported for this driver')

    def update_target(self, target, name=None, address=None, extra=None):
        """
        Update the properties of a backup target, only changing the serviceplan
        is supported.

        :param target: Backup target to update
        :type  target: Instance of :class:`BackupTarget` or ``str``

        :param name: Name of the target
        :type name: ``str``

        :param address: Hostname, FQDN, IP, file path etc.
        :type address: ``str``

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

        :rtype: Instance of :class:`BackupTarget`
        """
        if extra is not None:
            service_plan = extra.get('servicePlan', DEFAULT_BACKUP_PLAN)
        else:
            service_plan = DEFAULT_BACKUP_PLAN
        request = ET.Element('ModifyBackup',
                             {'xmlns': BACKUP_NS})
        request.set('servicePlan', service_plan)
        server_id = self._target_to_target_address(target)
        self.connection.request_with_orgId_api_1(
            'server/%s/backup/modify' % (server_id),
            method='POST',
            data=ET.tostring(request)).object
        if isinstance(target, BackupTarget):
            target.extra = extra
        else:
            target = self.ex_get_target_by_id(server_id)
        return target

    def delete_target(self, target):
        """
        Delete a backup target

        :param target: Backup target to delete
        :type  target: Instance of :class:`BackupTarget` or ``str``

        :rtype: ``bool``
        """
        server_id = self._target_to_target_address(target)
        response = self.connection.request_with_orgId_api_1(
            'server/%s/backup?disable' % (server_id),
            method='GET').object
        response_code = findtext(response, 'result', GENERAL_NS)
        return response_code in ['IN_PROGRESS', 'SUCCESS']

    def list_recovery_points(self, target, start_date=None, end_date=None):
        """
        List the recovery points available for a target

        :param target: Backup target to delete
        :type  target: Instance of :class:`BackupTarget`

        :param start_date: The start date to show jobs between (optional)
        :type  start_date: :class:`datetime.datetime`

        :param end_date: The end date to show jobs between (optional)
        :type  end_date: :class:`datetime.datetime``

        :rtype: ``list`` of :class:`BackupTargetRecoveryPoint`
        """
        raise NotImplementedError(
            'list_recovery_points not implemented for this driver')

    def recover_target(self, target, recovery_point, path=None):
        """
        Recover a backup target to a recovery point

        :param target: Backup target to delete
        :type  target: Instance of :class:`BackupTarget`

        :param recovery_point: Backup target with the backup data
        :type  recovery_point: Instance of :class:`BackupTarget`

        :param path: The part of the recovery point to recover (optional)
        :type  path: ``str``

        :rtype: Instance of :class:`BackupTargetJob`
        """
        raise NotImplementedError(
            'recover_target not implemented for this driver')

    def recover_target_out_of_place(self, target, recovery_point,