# -*- coding: utf-8 -*-

# --------------------------------------------------------------------------
# Copyright Commvault Systems, Inc.
#
# 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.
# --------------------------------------------------------------------------

"""Main file for performing backup set operations.

Backupsets and Backupset are 2 classes defined in this file.

Backupsets: Class for representing all the backup sets associated with a specific agent

Backupset:  Class for a single backup set selected for an agent,
and to perform operations on that backup set


Backupsets:
===========
    __init__(class_object)          -- initialise object of Backupsets class associated with
    the specified agent/instance

    __str__()                       -- returns all the backupsets associated with the agent

    __repr__()                      -- returns the string for the instance of the Backupsets class

    __len__()                       -- returns the number of backupsets associated with the Agent

    __getitem__()                   -- returns the name of the backupset for the given backupset Id
    or the details for the given backupset name

    _get_backupsets()               -- gets all the backupsets associated with the agent specified

    default_backup_set()            -- returns the name of the default backup set

    all_backupsets()                -- returns the dict of all the backupsets for the Agent /
    Instance of the selected Client

    has_backupset(backupset_name)   -- checks if a backupset exists with the given name or not

    _process_add_response()         -- to process the add backupset request using API call

    add(backupset_name)             -- adds a new backupset to the agent of the specified client
    
    add_archiveset(archiveset_name)   -- adds a new archiveset to the agent of the specified client

    add_v1_sharepoint_client()      -- Adds a new Office 365 V1 Share Point Pseudo Client to the Commcell.

    add_salesforce_backupset()      -- adds a new salesforce backupset

    get(backupset_name)             -- returns the Backupset class object
    of the input backup set name

    delete(backupset_name)          -- removes the backupset from the agent of the specified client

    refresh()                       -- refresh the backupsets associated with the agent


Backupset:
==========
    __init__()                      -- initialise object of Backupset with the specified backupset
    name and id, and associated to the specified instance

    __getattr__()                   -- provides access to restore helper methods

    __repr__()                      -- return the backupset name, the instance is associated with

    _get_backupset_id()             -- method to get the backupset id, if not specified in __init__

    _get_backupset_properties()     -- get the properties of this backupset

    _run_backup()                   -- runs full backup for the specified subclient,
    and appends the job object to the return list

    _update()                       -- updates the properties of the backupset

    _get_epoch_time()               -- gets the Epoch time given the input time is in format

                                            %Y-%m-%d %H:%M:%S

    _set_defaults()                 -- recursively sets default values on a dictionary

    _prepare_browse_options()       -- prepares the options for the Browse/find operation

    _prepare_browse_json()          -- prepares the JSON object for the browse request

    _process_browse_response()      -- retrieves the items from browse response

    _process_update_request()       --  to process the request using API call

    _do_browse()                    -- performs a browse operation with the given options

    update_properties()             -- updates the backupset properties

    set_default_backupset()         -- sets the backupset as the default backup set for the agent,
    if not already default

    backup()                        -- runs full backup for all subclients
    associated with this backupset

    browse()                        -- browse the content of the backupset

    find()                          -- find content in the backupset

    list_media()                    -- List media required to browse and restore backed up data from the backupset

    refresh()                       -- refresh the properties of the backupset

    delete_data()                   -- deletes items from the backupset and makes then unavailable
    to browse and restore

    backed_up_files_count()         -- Returns the count of the total number of files present in the backed up data
                                       of all the subclients of the given backupset.

Backupset instance Attributes
-----------------------------

    **properties**                  -- returns the properties of backupset

    **name**                        -- returns the name of the backupset

    **guid**                        -- treats the backupset GUID as a property
    of the Backupset class

"""

from __future__ import absolute_import
from __future__ import unicode_literals

import threading
import time
import copy

from base64 import b64encode

from .subclient import Subclients
from .schedules import Schedules
from .exception import SDKException


class Backupsets(object):
    """Class for getting all the backupsets associated with a client."""

    def __init__(self, class_object):
        """Initialize object of the Backupsets class.

            Args:
                class_object    (object)    --  instance of the Agent / Instance class

            Returns:
                object  -   instance of the Backupsets class

            Raises:
                SDKException:
                    if class object is not an instance of the Agent / Instance class

        """
        from .agent import Agent
        from .instance import Instance

        self._instance_object = None

        if isinstance(class_object, Agent):
            self._agent_object = class_object
        elif isinstance(class_object, Instance):
            self._instance_object = class_object
            self._agent_object = class_object._agent_object
        else:
            raise SDKException('Backupset', '103')

        self._client_object = self._agent_object._client_object

        self._commcell_object = self._agent_object._commcell_object

        self._cvpysdk_object = self._commcell_object._cvpysdk_object
        self._services = self._commcell_object._services
        self._update_response_ = self._commcell_object._update_response_

        self._BACKUPSETS = self._services['GET_ALL_BACKUPSETS'] % (self._client_object.client_id)
        if self._agent_object:
            self._BACKUPSETS += '&applicationId=' + self._agent_object.agent_id

        if self._agent_object.agent_name in ['cloud apps', 'sql server', 'sap hana']:
            self._BACKUPSETS += '&excludeHidden=0'

        self._backupsets = None
        self._default_backup_set = None
        self.refresh()

    def __str__(self):
        """Representation string consisting of all backupsets of the agent of a client.

            Returns:
                str - string of all the backupsets of an agent of a client
        """
        representation_string = '{:^5}\t{:^20}\t{:^20}\t{:^20}\t{:^20}\n\n'.format(
            'S. No.', 'Backupset', 'Instance', 'Agent', 'Client'
        )

        for index, backupset in enumerate(self._backupsets):
            sub_str = '{:^5}\t{:20}\t{:20}\t{:20}\t{:20}\n'.format(
                index + 1,
                backupset.split('\\')[-1],
                self._backupsets[backupset]['instance'],
                self._agent_object.agent_name,
                self._client_object.client_name
            )
            representation_string += sub_str

        return representation_string.strip()

    def __repr__(self):
        """Representation string for the instance of the Backupsets class."""
        return "Backupsets class instance for Agent: '{0}'".format(self._agent_object.agent_name)

    def __len__(self):
        """Returns the number of the backupsets for the selected Agent."""
        return len(self.all_backupsets)

    def __getitem__(self, value):
        """Returns the name of the backupset for the given backupset ID or
            the details of the backupset for given backupset Name.

            Args:
                value   (str / int)     --  Name or ID of the backupset

            Returns:
                str     -   name of the backupset, if the backupset id was given

                dict    -   dict of details of the backupset, if backupset name was given

            Raises:
                IndexError:
                    no backupset exists with the given Name / Id

        """
        value = str(value)

        if value in self.all_backupsets:
            return self.all_backupsets[value]
        else:
            try:
                return list(
                    filter(lambda x: x[1]['id'] == value, self.all_backupsets.items())
                )[0][0]
            except IndexError:
                raise IndexError('No backupset exists with the given Name / Id')

    def _get_backupsets(self):
        """Gets all the backupsets associated to the agent specified by agent_object.

            Returns:
                dict - consists of all backupsets of the agent
                    {
                         "backupset1_name": {
                             "id": backupset1_id,
                             "instance": instance
                         },
                         "backupset2_name": {
                             "id": backupset2_id,
                             "instance": instance
                         }
                    }

            Raises:
                SDKException:
                    if response is empty

                    if response is not success
        """
        flag, response = self._cvpysdk_object.make_request('GET', self._BACKUPSETS)

        if flag:
            if response.json() and 'backupsetProperties' in response.json():
                return_dict = {}

                for dictionary in response.json()['backupsetProperties']:
                    agent = dictionary['backupSetEntity']['appName'].lower()
                    instance = dictionary['backupSetEntity']['instanceName'].lower()

                    if self._instance_object is not None:
                        if (self._instance_object.instance_name in instance and
                                self._agent_object.agent_name in agent):
                            temp_name = dictionary['backupSetEntity']['backupsetName'].lower()
                            temp_id = str(dictionary['backupSetEntity']['backupsetId']).lower()
                            return_dict[temp_name] = {
                                "id": temp_id,
                                "instance": instance
                            }

                            if dictionary['commonBackupSet'].get('isDefaultBackupSet'):
                                self._default_backup_set = temp_name

                    elif self._agent_object.agent_name in agent:
                        temp_name = dictionary['backupSetEntity']['backupsetName'].lower()
                        temp_id = str(dictionary['backupSetEntity']['backupsetId']).lower()

                        if len(self._agent_object.instances.all_instances) > 1:
                            return_dict["{0}\\{1}".format(instance, temp_name)] = {
                                "id": temp_id,
                                "instance": instance
                            }

                            if dictionary['commonBackupSet'].get('isDefaultBackupSet'):
                                self._default_backup_set = "{0}\\{1}".format(instance, temp_name)
                        else:
                            return_dict[temp_name] = {
                                "id": temp_id,
                                "instance": instance
                            }

                            if dictionary['commonBackupSet'].get('isDefaultBackupSet'):
                                self._default_backup_set = temp_name

                return return_dict
            else:
                return {}
        else:
            raise SDKException('Response', '101', self._update_response_(response.text))

    @property
    def all_backupsets(self):
        """Returns the dict of backupsets for the Agent / Instance of the selected Client

            dict - consists of all backupsets
                    {
                         "backupset1_name": {
                             "id": backupset1_id,
                             "instance": instance
                         },
                         "backupset2_name": {
                             "id": backupset2_id,
                             "instance": instance
                         }
                    }
        """
        return self._backupsets

    def has_backupset(self, backupset_name):
        """Checks if a backupset exists for the agent with the input backupset name.

            Args:
                backupset_name (str)  --  name of the backupset

            Returns:
                bool - boolean output whether the backupset exists for the agent or not

            Raises:
                SDKException:
                    if type of the backupset name argument is not string
        """
        if not isinstance(backupset_name, str):
            raise SDKException('Backupset', '101')

        return self._backupsets and backupset_name.lower() in self._backupsets

    def _process_add_response(self, backupset_name, request_json):
        """Runs the Backupset Add API with the request JSON provided,
            and returns the contents after parsing the response.

            Args:
                backupset_name   (str)  --   backupset name
                request_json    (dict)  --  JSON request to run for the API

            Returns:
                (bool, str, str):
                    bool -  flag specifies whether success / failure

                    str  -  error code received in the response

                    str  -  error message received

            Raises:
                SDKException:
                    if response is empty

                    if response is not success
        """
        flag, response = self._cvpysdk_object.make_request('POST', self._services['ADD_BACKUPSET'], request_json)

        if flag:
            if response.json():
                if 'response' in response.json():
                    error_code = response.json()['response'][0]['errorCode']

                    if error_code != 0:
                        error_string = response.json()['response'][0]['errorString']
                        o_str = 'Failed to create backupset\nError: "{0}"'.format(error_string)
                        raise SDKException('Backupset', '102', o_str)
                    else:
                        # initialize the backupsets again
                        # so the backupset object has all the backupsets
                        self.refresh()
                        return self.get(backupset_name)
                elif 'errorMessage' in response.json():
                    error_string = response.json()['errorMessage']
                    o_str = 'Failed to create backuspet\nError: "{0}"'.format(error_string)
                    raise SDKException('Backupset', '102', o_str)
                else:
                    raise SDKException('Response', '102')
            else:
                raise SDKException('Response', '102')
        else:
            raise SDKException('Response', '101', self._update_response_(response.text))

    def add(self, backupset_name, on_demand_backupset=False, **kwargs):
        """Adds a new backup set to the agent.

            Args:
                backupset_name      (str)   --  name of the new backupset to add

                on_demand_backupset (bool)  --  flag to specify whether the backupset to be added
                is a simple backupset or an on-demand backupset

                    default: False

                **kwargs    --  dict of keyword arguments as follows:

                    storage_policy  (str)   --  name of the storage policy to associate to the
                    backupset

                    plan_name       (str)   -- name of the plan to associate to the backupset

                    is_nas_turbo_backupset  (bool)    --  True for NAS based client.


            Returns:
                object - instance of the Backupset class, if created successfully

            Raises:
                SDKException:
                    if type of the backupset name argument is not string

                    if failed to create a backupset

                    if response is empty

                    if response is not success

                    if backupset with same name already exists
        """
        if not (isinstance(backupset_name, str) and isinstance(on_demand_backupset, bool)):
            raise SDKException('Backupset', '101')
        else:
            backupset_name = backupset_name.lower()

        if self.has_backupset(backupset_name):
            raise SDKException(
                'Backupset', '102', 'Backupset "{0}" already exists.'.format(backupset_name)
            )

        if self._instance_object is None:
            if self._agent_object.instances.has_instance('DefaultInstanceName'):
                self._instance_object = self._agent_object.instances.get('DefaultInstanceName')
            else:
                self._instance_object = self._agent_object.instances.get(
                    sorted(self._agent_object.instances.all_instances)[0]
                )

        request_json = {
            "association": {
                "entity": [{
                    "clientName": self._client_object.client_name,
                    "appName": self._agent_object.agent_name,
                    "instanceName": self._instance_object.instance_name,
                    "backupsetName": backupset_name
                }]
            },
            "backupSetInfo": {
                "commonBackupSet": {
                    "onDemandBackupset": on_demand_backupset
                }
            }
        }

        if kwargs.get('is_nas_turbo_type'):
            request_json["backupSetInfo"]["commonBackupSet"]["isNasTurboBackupSet"] = kwargs.get('is_nas_turbo_type',
                                                                                                 False)

        agent_settings = {
            'db2': """
request_json['backupSetInfo'].update({
    'db2BackupSet': {
        'dB2DefaultIndexSP': {
            'storagePolicyName': kwargs.get('storage_policy', '')
        }
    }
})
            """
        }

        exec(agent_settings.get(self._agent_object.agent_name, ''))

        if kwargs.get('plan_name'):
            plan_entity_dict = {
                "planName": kwargs.get('plan_name')
            }
            request_json['backupSetInfo']['planEntity'] = plan_entity_dict

        flag, response = self._cvpysdk_object.make_request(
            'POST', self._services['ADD_BACKUPSET'], request_json
        )

        if flag:
            if response.json():
                if 'response' in response.json():
                    response_value = response.json()['response'][0]
                    error_code = str(response_value['errorCode'])
                    error_message = None

                    if 'errorString' in response_value:
                        error_message = response_value['errorString']

                    if error_message:
                        o_str = 'Failed to create new backupset\nError: "{0}"'.format(
                            error_message
                        )
                        raise SDKException('Backupset', '102', o_str)
                    else:
                        if error_code == '0':
                            # initialize the backupsets again
                            # so the backupsets object has all the backupsets
                            self.refresh()

                            return self.get(backupset_name)

                        else:
                            o_str = ('Failed to create new backupset with error code: "{0}"\n'
                                     'Please check the documentation for '
                                     'more details on the error').format(error_code)

                            raise SDKException('Backupset', '102', o_str)
                else:
                    error_code = response.json()['errorCode']
                    error_message = response.json()['errorMessage']
                    o_str = 'Failed to create new backupset\nError: "{0}"'.format(
                        error_message
                    )
                    raise SDKException('Backupset', '102', o_str)
            else:
                raise SDKException('Response', '102')
        else:
            raise SDKException('Response', '101', self._update_response_(response.text))

    def add_archiveset(self, archiveset_name, is_nas_turbo_backupset=False):
        """ 
        Adds a new archiveset to the agent. It is just a backupset but is mainly used for archive only items

        Args:
            archiveset_name     (str) -- name of new archiveset to add

            is_nas_turbo_backupset  (bool) -- True for NAS based client.
                default -   False
            
        Returns:
        object - instance of the Backupset class, if created successfully

        Raises:
            SDKException:
                if type of the archiveset name argument is not string

                if failed to create a archiveset

                if response is empty

                if response is not success

                if archiveset with same name already exists
                

        """        
        if not (isinstance(archiveset_name, str)):
            raise SDKException('Backupset', '101')
        else:
            archiveset_name = archiveset_name.lower()

        if self.has_backupset(archiveset_name):
            raise SDKException('archiveset_name', '102', 'Archiveset "{0}" already exists.'.format(archiveset_name))

        if self._agent_object.agent_id not in ['29', '33']:
            raise SDKException('Backupset', '101', "Archiveset is not applicable to this application type.")



        request_json = {
            "backupSetInfo": {
                "useContentFromPlan": False,
                "planEntity": {},
                "commonBackupSet": {
                    "isNasTurboBackupSet": is_nas_turbo_backupset,
                    "isArchivingEnabled": True,
                    "isDefaultBackupSet": False
                },
                "backupSetEntity": {
                    "_type_": 6,
                    "clientId": int(self._client_object.client_id),
                    "backupsetName": archiveset_name,
                    "applicationId": int(self._agent_object.agent_id)
                },
                "subClientList": [
                    {
                        "contentOperationType": 1,
                        "fsSubClientProp": {
                            "useGlobalFilters": 2,
                            "forcedArchiving": True,
                            "diskCleanupRules": {
                                "enableArchivingWithRules": True,
                                "diskCleanupFileTypes": {}
                            }
                        },
                        "content": [
                            {
                                "path": ""
                            }
                        ]
                    }
                ]
            }
        }
        
        flag, response = self._cvpysdk_object.make_request(
            'POST', self._services['ADD_BACKUPSET'], request_json
        )

        if flag:
            if response.json():
                if 'response' in response.json():
                    response_value = response.json()['response'][0]
                    error_code = str(response_value['errorCode'])
                    error_message = None

                    if 'errorString' in response_value:
                        error_message = response_value['errorString']

                    if error_message:
                        o_str = 'Failed to create new Archiveset\nError: "{0}"'.format(
                            error_message
                        )
                        raise SDKException('Archiveset', '102', o_str)
                    else:
                        if error_code == '0':
                            # initialize the backupsets again
                            # so the backupsets object has all the backupsets
                            self.refresh()
                            return self.get(archiveset_name)
                        
                        else:
                            o_str = ('Failed to create new Archiveset with error code: "{0}"\n'
                                     'Please check the documentation for '
                                     'more details on the error').format(error_code)

                            raise SDKException('Backupset', '102', o_str)
                else:
                    error_code = response.json()['errorCode']
                    error_message = response.json()['errorMessage']
                    o_str = 'Failed to create new Archiveset\nError: "{0}"'.format(
                        error_message
                    )
                    raise SDKException('Backupset', '102', o_str)
            else:
                raise SDKException('Response', '102')
        else:
            raise SDKException('Response', '101', self._update_response_(response.text))

    def add_v1_sharepoint_client(
            self,
            backupset_name,
            server_plan,
            client_name,
            **kwargs):
        """
        for sharepoint v1 client creation is a backupset
        Adds a new Office 365 V1 Share Point Pseudo Client to the Commcell.

                Args:
                    backupset_name                 (str)   --  name of the new Sharepoint Pseudo Client

                    server_plan                 (str)   --  server_plan to associate with the client

                    client_name                 (str) -- the access node for which Pseudo Client will be created


                Kwargs :

                    tenant_url                  (str)   --  url of sharepoint tenant

                    azure_username              (str)   --  username of azure app

                    azure_secret                (str)   --  secret key of azure app

                    user_username        (str)   --  username of Sharepoint admin

                    user_password           (str)  -- password of Sharepoint admin

                    azure_app_id            (str)       --  azure app id for sharepoint online

                    azure_app_key_id        (str)       --  app key for sharepoint online

                    azure_directory_id    (str)   --  azure directory id for sharepoint online


                Returns:
                    object  -   instance of the Client class for this new client

                Raises:
                    SDKException:
                        if client with given name already exists

                        if index_server is not found

                        if server_plan is not found

                        if failed to add the client

                        if response is empty

                        if response is not success

        """
        if self.has_backupset(backupset_name):
            raise SDKException(
                'Backupset', '102', 'Backupset "{0}" already exists.'.format(backupset_name))
        if self._commcell_object.plans.has_plan(server_plan):
            server_plan_object = self._commcell_object.plans.get(server_plan)
            server_plan_dict = {
                "planId": int(server_plan_object.plan_id)
            }
        else:
            raise SDKException('Backupset', '102')
        backup_set = {
            "_type_": 6,
            "applicationId": 78,
            "backupsetName": backupset_name,
            "clientId": int(self._client_object.client_id)
        }
        request_json = {
            "backupSetInfo": {
                "planEntity": server_plan_dict,
                "backupSetEntity": backup_set,
                "sharepointBackupSet": {
                    "sharepointBackupSetType": 4
                }
            }
        }
        tenant_url = kwargs.get('tenant_url')
        user_username = kwargs.get('user_username')
        is_modern_auth_enabled = kwargs.get('is_modern_auth_enabled',False)
        azure_secret = b64encode(kwargs.get('azure_secret').encode()).decode()
        azure_app_key_id = b64encode(kwargs.get('azure_app_key_id').encode()).decode()
        user_password = b64encode(kwargs.get('user_password').encode()).decode()
        request_json["backupSetInfo"]["sharepointBackupSet"][
            "spOffice365BackupSetProp"] = {
            "azureUserAccount": kwargs.get('azure_username'),
            "azureAccountKey": azure_secret,
            "tenantUrlItem": tenant_url,
            "isModernAuthEnabled": is_modern_auth_enabled,
            "office365Credentials": {
                "userName": user_username,
                "password": user_password
            },
        }
        if is_modern_auth_enabled:
            request_json["backupSetInfo"]["sharepointBackupSet"][
                "spOffice365BackupSetProp"]["azureAppList"] = {
                "azureApps": [
                    {
                        "azureAppId": kwargs.get('azure_app_id'),
                        "azureAppKeyValue": azure_app_key_id,
                        "azureDirectoryId": kwargs.get('azure_directory_id')
                    }
                ]
            }

        self._process_add_response(backupset_name, request_json)

    def add_salesforce_backupset(
            self,
            salesforce_options,
            db_options=None, **kwargs):
        """Adds a new Salesforce Backupset to the Commcell.

            Args:

                salesforce_options         (dict)       --  salesforce options
                                                            {
                                                                "salesforce_user_name": 'salesforce login user',
                                                                "salesforce_user_password": 'salesforce user password',
                                                                "salesforce_user_token": 'salesforce user token'
                                                            }

                db_options                 (dict)       --  database options to configure sync db
                                                            {
                                                                "db_enabled": 'True or False',
                                                                "db_type": 'SQLSERVER or POSTGRESQL',
                                                                "db_host_name": 'database hostname',
                                                                "db_instance": 'database instance name',
                                                                "db_name": 'database name',
                                                                "db_port": 'port of the database',
                                                                "db_user_name": 'database user name',
                                                                "db_user_password": 'database user password'
                                                            }

                **kwargs                   (dict)       --     dict of keyword arguments as follows

                                                            download_cache_path     (str)   -- download cache path
                                                            mutual_auth_path        (str)   -- mutual auth cert path
                                                            storage_policy          (str)   -- storage policy
                                                            streams                 (int)   -- number of streams

            Returns:
                object  -   instance of the Backupset class for this new backupset

            Raises:
                SDKException:
                    if backupset with given name already exists

                    if failed to add the backupset

                    if response is empty

                    if response is not success
        """

        if db_options is None:
            db_options = {'db_enabled': False}
        if self.has_backupset(salesforce_options.get('salesforce_user_name')):
            raise SDKException('Backupset', '102',
                               'Backupset "{0}" already exists.'.format(salesforce_options.get('salesforce_user_name')))

        salesforce_password = b64encode(salesforce_options.get('salesforce_user_password').encode()).decode()
        salesforce_token = b64encode(salesforce_options.get('salesforce_user_token', '').encode()).decode()
        db_user_password = ""
        if db_options.get('db_enabled', False):
            db_user_password = b64encode(db_options.get('db_user_password').encode()).decode()

        request_json = {
            "backupSetInfo": {
                "backupSetEntity": {
                    "clientName": self._client_object.client_name,
                    "instanceName": self._instance_object.instance_name,
                    "backupsetName": salesforce_options.get('salesforce_user_name'),
                    "appName": self._agent_object.agent_name
                },
                "cloudAppsBackupset": {
                    "instanceType": 3,
                    "salesforceBackupSet": {
                        "enableREST": True,
                        "downloadCachePath": kwargs.get('download_cache_path', '/tmp'),
                        "mutualAuthPath": kwargs.get('mutual_auth_path', ''),
                        "token": salesforce_token,
                        "userPassword": {
                            "userName": salesforce_options.get('salesforce_user_name'),
                            "password": salesforce_password,
                        },
                        "syncDatabase": {
                            "dbEnabled": db_options.get('db_enabled', False),
                            "dbPort": db_options.get('db_port', '1433'),
                            "dbInstance": db_options.get('db_instance', ''),
                            "dbName": db_options.get('db_name', self._instance_object.instance_name),
                            "dbType": db_options.get('db_type', "SQLSERVER"),
                            "dbHost": db_options.get('db_host_name', ''),
                            "dbUserPassword": {
                                "userName": db_options.get('db_user_name', ''),
                                "password": db_user_password,

                            },
                        },
                    },
                    "generalCloudProperties": {
                        "numberOfBackupStreams": kwargs.get('streams', 2),
                        "storageDevice": {
                            "dataBackupStoragePolicy": {
                                "storagePolicyName": kwargs.get('storage_policy', '')
                            },
                        },
                    },
                },
            },
        }

        self._process_add_response(salesforce_options.get('salesforce_user_name'), request_json)

    def get(self, backupset_name):
        """Returns a backupset object of the specified backupset name.

            Args:
                backupset_name (str)  --  name of the backupset

            Returns:
                object - instance of the Backupset class for the given backupset name

            Raises:
                SDKException:
                    if type of the backupset name argument is not string

                    if no backupset exists with the given name
        """
        if not isinstance(backupset_name, str):
            raise SDKException('Backupset', '101')
        else:
            backupset_name = backupset_name.lower()

            if self.has_backupset(backupset_name):
                if self._instance_object is None:
                    self._instance_object = self._agent_object.instances.get(
                        self._backupsets[backupset_name]['instance']
                    )
                return Backupset(
                    self._instance_object,
                    backupset_name,
                    self._backupsets[backupset_name]["id"]
                )

            raise SDKException(
                'Backupset', '102', 'No backupset exists with name: "{0}"'.format(backupset_name)
            )

    def delete(self, backupset_name):
        """Deletes the backup set from the agent.

            Args:
                backupset_name (str)  --  name of the backupset

            Raises:
                SDKException:
                    if type of the backupset name argument is not string

                    if failed to delete the backupset

                    if response is empty

                    if response is not success

                    if no backupset exists with the given name
        """
        if not isinstance(backupset_name, str):
            raise SDKException('Backupset', '101')
        else:
            backupset_name = backupset_name.lower()

        if self.has_backupset(backupset_name):
            delete_backupset_service = self._services['BACKUPSET'] % (
                self._backupsets[backupset_name]['id']
            )

            flag, response = self._cvpysdk_object.make_request('DELETE', delete_backupset_service)

            if flag:
                if response.json():
                    if 'response' in response.json():
                        response_value = response.json()['response'][0]
                        error_code = str(response_value['errorCode'])
                        error_message = None

                        if 'errorString' in response_value:
                            error_message = response_value['errorString']

                        if error_message:
                            o_str = 'Failed to delete backupset\nError: "{0}"'
                            raise SDKException('Backupset', '102', o_str.format(error_message))
                        else:
                            if error_code == '0':
                                # initialize the backupsets again
                                # so the backupsets object has all the backupsets
                                self.refresh()
                            else:
                                o_str = ('Failed to delete backupset with error code: "{0}"\n'
                                         'Please check the documentation for '
                                         'more details on the error').format(error_code)
                                raise SDKException('Backupset', '102', o_str)
                    else:
                        error_code = response.json()['errorCode']
                        error_message = response.json()['errorMessage']
                        o_str = 'Failed to delete backupset\nError: "{0}"'.format(error_message)
                        raise SDKException('Backupset', '102', o_str)
                else:
                    raise SDKException('Response', '102')
            else:
                raise SDKException('Response', '101', self._update_response_(response.text))
        else:
            raise SDKException(
                'Backupset', '102', 'No backupset exists with name: "{0}"'.format(backupset_name)
            )

    def refresh(self):
        """Refresh the backupsets associated with the Agent / Instance."""
        self._backupsets = self._get_backupsets()

    @property
    def default_backup_set(self):
        """Returns the name of the default backup set for the selected Client and Agent."""
        return self._default_backup_set


class Backupset(object):
    """Class for performing backupset operations for a specific backupset."""

    def __new__(cls, instance_object, backupset_name, backupset_id=None):
        """Class composition for CV backupsets"""
        from .backupsets.fsbackupset import FSBackupset
        from .backupsets.nasbackupset import NASBackupset
        from .backupsets.hanabackupset import HANABackupset
        from .backupsets.cabackupset import CloudAppsBackupset
        from .backupsets.postgresbackupset import PostgresBackupset
        from .backupsets.adbackupset import ADBackupset
        from .backupsets.db2backupset import DB2Backupset
        from .backupsets.vsbackupset import VSBackupset
        from .backupsets.aadbackupset import AzureAdBackupset
        from .backupsets.sharepointbackupset import SharepointBackupset

        _backupsets_dict = {
            'file system': FSBackupset,
            'nas': NASBackupset,        # SP11 or lower CS honors NAS as the Agent Name
            'ndmp': NASBackupset,       # SP12 and above honors NDMP as the Agent Name
            'sap hana': HANABackupset,
            'cloud apps': CloudAppsBackupset,
            'postgresql': PostgresBackupset,
            "active directory": ADBackupset,
            'db2': DB2Backupset,
            'virtual server': VSBackupset,
            "azure ad": AzureAdBackupset,
            'sharepoint server': SharepointBackupset
        }

        if instance_object._agent_object.agent_name in _backupsets_dict.keys():
            _class = _backupsets_dict.get(instance_object._agent_object.agent_name, cls)
            if _class.__new__ == cls.__new__:
                return object.__new__(_class)
            return _class.__new__(_class, instance_object, backupset_name, backupset_id)
        else:
            return object.__new__(cls)

    def __init__(self, instance_object, backupset_name, backupset_id=None):
        """Initialise the backupset object.

            Args:
                instance_object     (object)  --  instance of the Instance class

                backupset_name      (str)     --  name of the backupset

                backupset_id        (str)     --  id of the backupset
                    default: None

            Returns:
                object - instance of the Backupset class
        """
        self._instance_object = instance_object
        self._agent_object = self._instance_object._agent_object
        self._client_object = self._agent_object._client_object

        self._commcell_object = self._agent_object._commcell_object

        self._cvpysdk_object = self._commcell_object._cvpysdk_object
        self._services = self._commcell_object._services
        self._update_response_ = self._commcell_object._update_response_

        # self._restore_methods = ['_process_restore_response', '_filter_paths', '_restore_json']
        self._restore_methods = [
            '_process_restore_response',
            '_filter_paths',
            '_restore_json',
            '_impersonation_json',
            '_restore_browse_option_json',
            '_restore_common_options_json',
            '_restore_destination_json',
            '_restore_fileoption_json',
            '_json_restore_subtask'
        ]

        self._restore_options_json = [
            '_impersonation_json_',
            '_browse_restore_json',
            '_destination_restore_json',
            '_commonoption_restore_json',
            '_fileoption_restore_json',
        ]

        self._backupset_name = backupset_name.split('\\')[-1].lower()
        self._description = None

        if backupset_id:
            # Use the backupset id provided in the arguments
            self._backupset_id = str(backupset_id)
        else:
            # Get the id associated with this backupset
            self._backupset_id = self._get_backupset_id()

        self._BACKUPSET = self._services['BACKUPSET'] % (self.backupset_id)
        self._BROWSE = self._services['BROWSE']
        self._RESTORE = self._services['RESTORE']

        self._is_default = False
        self._is_on_demand_backupset = False
        self._properties = None
        self._backupset_association = {}
        self._plan_name = None
        self._plan_obj = None

        self.subclients = None
        self.schedules = None
        self.refresh()

        self._default_browse_options = {
            'operation': 'browse',
            'show_deleted': False,
            'from_time': 0,  # value should either be the Epoch time or the Timestamp
            'to_time': 0,  # value should either be the Epoch time or the Timestamp
            'path': '\\',
            'copy_precedence': 0,
            'media_agent': '',
            'page_size': 100000,
            'skip_node': 0,
            'restore_index': True,
            'vm_disk_browse': False,
            'filters': [],
            'job_id': 0,
            'commcell_id': self._commcell_object.commcell_id,
            'include_aged_data': False,
            'include_meta_data':False,
            'include_hidden': False,
            'include_running_jobs': False,
            'compute_folder_size': False,
            'vs_volume_browse': False,
            'browse_view_name': 'VOLUMEVIEW',

            '_subclient_id': 0,
            '_raw_response': False,
            '_custom_queries': False
        }

    def __getattr__(self, attribute):
        """Returns the persistent attributes"""
        if attribute in self._restore_methods:
            return getattr(self._instance_object, attribute)
        elif attribute in self._restore_options_json:
            return getattr(self._instance_object, attribute)

        return super(Backupset, self).__getattribute__(attribute)

    def __repr__(self):
        """String representation of the instance of this class."""
        representation_string = ('Backupset class instance for Backupset: "{0}" '
                                 'for Instance: "{1}" of Agent: "{2}"')
        return representation_string.format(
            self.backupset_name,
            self._instance_object.instance_name,
            self._agent_object.agent_name
        )

    def _get_backupset_id(self):
        """Gets the backupset id associated with this backupset.

            Returns:
                str - id associated with this backupset
        """
        backupsets = Backupsets(self._instance_object)
        return backupsets.get(self.backupset_name).backupset_id

    def _get_backupset_properties(self):
        """Gets the properties of this backupset.

            Raises:
                SDKException:
                    if response is empty

                    if response is not success
        """
        flag, response = self._cvpysdk_object.make_request('GET', self._BACKUPSET)

        if flag:
            if response.json() and "backupsetProperties" in response.json():
                self._properties = response.json()["backupsetProperties"][0]

                backupset_name = self._properties["backupSetEntity"]["backupsetName"]
                self._backupset_name = backupset_name.lower()

                self._backupset_association = self._properties['backupSetEntity']

                self._is_default = bool(self._properties["commonBackupSet"]["isDefaultBackupSet"])

                if 'commonBackupSet' in self._properties:
                    if 'onDemandBackupset' in self._properties['commonBackupSet']:
                        self._is_on_demand_backupset = bool(
                            self._properties['commonBackupSet']['onDemandBackupset']
                        )

                if "userDescription" in self._properties["commonBackupSet"]:
                    self._description = self._properties["commonBackupSet"]["userDescription"]

                if "planName" in self._properties["planEntity"]:
                    self._plan_name = self._properties["planEntity"]["planName"]
                else:
                    self._plan_name = None
            else:
                raise SDKException('Response', '102')
        else:
            raise SDKException('Response', '101', self._update_response_(response.text))

    def _run_backup(self, subclient_name, return_list, **kwargs):
        """Triggers backup job for the given subclient, and appends its Job object to the list.
            Backup job is started only when backup activity is enabled and storage policy is set for it.

            The SDKException class instance is appended to the list,
            if any exception is raised while running the backup job for the Subclient.

            Args:
                subclient_name (str)   --  name of the subclient to trigger the backup for

                return_list    (list)  --  list to append the job object to

            Kwargs:
                All arguments used by subclient.backup() can be used here. Commonly used arguments are

                backup_level   (str)   --  The type of backup to run

                advanced_options   (dict)  --  advanced backup options to be included while
                                    making the request

            Returns:
                None

        """
        try:
            subclient = self.subclients.get(subclient_name)
            if subclient.is_backup_enabled and subclient.storage_policy is not None:
                job = subclient.backup(**kwargs)
                return_list.append(job)
                time.sleep(2)  # Staggering the next backup job to be started
        except SDKException as excp:
            return_list.append(excp)

    def _process_update_reponse(self, request_json):
        """Runs the Backupset update API with the request JSON provided,
            and returns the contents after parsing the response.

            Args:
                request_json    (dict)  --  JSON request to run for the API

            Returns:
                (bool, str, str):
                    bool -  flag specifies whether success / failure

                    str  -  error code received in the response

                    str  -  error message received

            Raises:
                SDKException:
                    if response is empty

                    if response is not success
        """
        flag, response = self._cvpysdk_object.make_request('POST', self._BACKUPSET, request_json)

        self._get_backupset_properties()

        if flag:
            if response.json() and "response" in response.json():
                error_code = str(response.json()["response"][0]["errorCode"])

                if error_code == "0":
                    return True, "0", ""
                else:
                    error_string = ""

                    if "errorString" in response.json()["response"][0]:
                        error_string = response.json()["response"][0]["errorString"]

                    if error_string:
                        return False, error_code, error_string
                    else:
                        return False, error_code, ""
            else:
                raise SDKException('Response', '102')
        else:
            raise SDKException('Response', '101', self._update_response_(response.text))

    def _update(self, backupset_name, backupset_description, default_backupset):
        """Updates the properties of the backupset.

            Args:
                backupset_name        (str)   --  new name of the backupset

                backupset_description (str)   --  description of the backupset

                default_backupset     (bool)  --  default backupset property

            Returns:
                (bool, str, str):
                    bool -  flag specifies whether success / failure

                    str  -  error code received in the response

                    str  -  error message received

            Raises:
                SDKException:
                    if response is empty

                    if response is not success
        """

        request_json = {
            "association": {
                "entity": [{
                    "clientName": self._client_object.client_name,
                    "appName": self._agent_object.agent_name,
                    "instanceName": self._instance_object.instance_name,
                    "backupsetName": self.backupset_name
                }]
            },
            "backupsetProperties": {
                "commonBackupSet": {
                    "newBackupSetName": backupset_name,
                    "isDefaultBackupSet": default_backupset
                }
            }
        }

        if backupset_description is not None:
            request_json["backupsetProperties"]["commonBackupSet"][
                "userDescription"] = backupset_description

        return self._process_update_reponse(request_json)

    @staticmethod
    def _get_epoch_time(timestamp):
        """Returns the Epoch time given the input time is in format %Y-%m-%d %H:%M:%S.

            Args:
                timestamp   (int / str)     --  value should either be the Epoch time or, the
                                                    Timestamp of the format %Y-%m-%d %H:%M:%S

            Returns:
                int - epoch time converted from the input timestamp

            Raises:
                SDKException:
                    if the input timestamp is not of correct format
        """
        if str(timestamp) == '0':
            return 0

        try:
            # return the timestamp value in int type
            return int(timestamp)
        except ValueError:
            # if not convertible to int, then convert the timestamp input to Epoch time
            try:
                return int(time.mktime(time.strptime(timestamp, "%Y-%m-%d %H:%M:%S")))
            except Exception:
                raise SDKException('Subclient', '106')

    def _set_defaults(self, final_dict, defaults_dict):
        """Iterates over the defaults_dict, and adds the default value to the final_dict,
            for the key which is not present in the final dict.

            Recursively sets default values on the final_dict dictionary.

            Args:
                final_dict      (dict)  --  the dictionary to be set with defaults, and to be used
                                                to generate the Browse / Find JSON

                defaults_dict   (dict)  --  the dictionary with default values

            Returns:
                None
        """
        for key in defaults_dict:
            if key not in final_dict:
                final_dict[key] = defaults_dict[key]

            if isinstance(defaults_dict[key], dict):
                self._set_defaults(final_dict[key], defaults_dict[key])

    def _prepare_browse_options(self, options):
        """Prepares the options for the Browse/find operation.

            Args:
                options     (dict)  --  a dictionary of browse options

            Returns:
                dict - The browse options with all the default options set
        """
        self._set_defaults(options, self._default_browse_options)
        return options

    def _prepare_browse_json(self, options):
        """Prepares the JSON object for the browse request.

            Args:
                options     (dict)  --  the browse options dictionary

            Returns:
                dict - A JSON object for the browse response
        """
        operation_types = {
            'browse': 0,
            'find': 1,
            'all_versions': 2,
            'list_media': 3,
            'delete_data': 7
        }

        options['operation'] = options['operation'].lower()

        if options['operation'] not in operation_types:
            options['operation'] = 'find'

        # add the browse mode value here, if it is different for an agent
        # if agent is not added in the dict, default value 2 will be used
        browse_mode = {
            'virtual server': 4,
            'cloud apps': 3,
            'azure ad': 3
        }

        mode = 2
        paths = []

        if isinstance(options['path'], str):
            paths.append(options['path'])
        elif isinstance(options['path'], list):
            paths = options['path']
        else:
            paths = ['\\']

        if self._agent_object.agent_name in browse_mode:
            mode = browse_mode[self._agent_object.agent_name]

        request_json = {
            "opType": operation_types[options['operation']],
            "mode": {
                "mode": mode
            },
            "paths": [{"path": path} for path in paths],
            "options": {
                "showDeletedFiles": options['show_deleted'],
                "restoreIndex": options['restore_index'],
                "vsDiskBrowse": options['vm_disk_browse'],
                "vsFileBrowse": options.get('vs_file_browse', False),
                "includeMetadata": options.get('include_meta_data', False),
                "hideUserHidden": options.get('hide_user_hidden', False)
            },
            "entity": {
                "clientName": self._client_object.client_name,
                "clientId": int(self._client_object.client_id),
                "applicationId": int(self._agent_object.agent_id),
                "instanceId": int(self._instance_object.instance_id),
                "backupsetId": int(self.backupset_id),
                "subclientId": int(options['_subclient_id'])
            },
            "timeRange": {
                "fromTime": self._get_epoch_time(options['from_time']),
                "toTime": self._get_epoch_time(options['to_time'])
            },
            "advOptions": {
                "copyPrecedence": int(options['copy_precedence'])
            },
            "ma": {
                "clientName": options['media_agent']
            },
            "queries": [{
                "type": 0,
                "queryId": "dataQuery",
                "dataParam": {
                    "sortParam": {
                        "ascending": False,
                        "sortBy": [0]
                    },
                    "paging": {
                        "pageSize": int(options['page_size']),
                        "skipNode": int(options['skip_node']),
                        "firstNode": 0
                    }
                }
            }]
        }

        if options['filters']:
            # [('FileName', '*.txt'), ('FileSize','GT','100')]
            request_json['queries'][0]['whereClause'] = []

            for browse_filter in options['filters']:
                if browse_filter[0] in ('FileName', 'FileSize'):
                    temp_dict = {
                        'connector': 0,
                        'criteria': {
                            'field': browse_filter[0],
                            'values': [browse_filter[1]]
                        }
                    }

                    if browse_filter[0] == 'FileSize':
                        temp_dict['criteria']['dataOperator'] = browse_filter[2]

                    request_json['queries'][0]['whereClause'].append(temp_dict)

        if options['job_id'] != 0:
            request_json['advOptions']['advConfig'] = {
                'browseAdvancedConfigBrowseByJob': {
                    'commcellId': options['commcell_id'],
                    'jobId': options['job_id']
                }
            }

        if options['include_aged_data']:
            request_json['options']['includeAgedData'] = True

        if options['include_meta_data']:
            request_json['options']['includeMetadata'] = True

        if options['include_hidden']:
            request_json['options']['includeHidden'] = True

        if options['include_running_jobs']:
            request_json['options']['includeRunningJobs'] = True

        if options['compute_folder_size']:
            request_json['options']['computeFolderSizeForFilteredBrowse'] = True

        if options['vs_volume_browse']:
            request_json['mode']['mode'] = 3
            request_json['options']['vsVolumeBrowse'] = True
            request_json['advOptions']['browseViewName'] = options['browse_view_name']

        if options['operation'] == 'list_media':
            request_json['options']['doPrediction'] = True

        if options['_custom_queries']:
            request_json['queries'] = options['_custom_queries']

        return request_json

    def _process_browse_all_versions_response(self, result_set):
        """Retrieves the items from browse response.

        Args:
            result_set  (list of dict)  --  browse response dict obtained from server

        Returns:
            dict - Dictionary of the specified file with list of all the file versions and
                    additional metadata retrieved from browse

        Raises:
            SDKException:
                if failed to browse/search for content

                if response is empty

                if response is not success
        """
        path = None
        versions_list = []

        for result in result_set:
            name = result['displayName']
            path = result['path']

            if 'modificationTime' in result:
                mod_time = time.localtime(int(result['modificationTime']))
                mod_time = time.strftime('%d/%m/%Y %H:%M:%S', mod_time)
            else:
                mod_time = None
            
            if 'backupTime' in result['advancedData'] and int(result['advancedData']['backupTime']) > 0:
                bkp_time = time.localtime(int(result['advancedData']['backupTime']))
                bkp_time = time.strftime('%d/%m/%Y %H:%M:%S', bkp_time)
            else:
                bkp_time = None

            if 'file' in result['flags']:
                if result['flags']['file'] in (True, '1'):
                    file_or_folder = 'File'
                else:
                    file_or_folder = 'Folder'
            else:
                file_or_folder = 'Folder'

            if 'size' in result:
                size = result['size']
            else:
                size = None

            if 'version' in result:
                version = result['version']
            else:
                version = None

            paths_dict = {
                'name': name,
                'version': version,
                'size': size,
                'modified_time': mod_time,
                'type': file_or_folder,
                'backup_time': bkp_time,
                'advanced_data': result['advancedData']
            }

            versions_list.append(paths_dict)

        all_versions_dict = dict()
        all_versions_dict[path] = versions_list

        return all_versions_dict

    def _process_browse_response(self, flag, response, options):
        """Retrieves the items from browse response.

        Args:
            flag        (bool)  --  boolean, whether the response was success or not

            response    (dict)  --  JSON response received for the request from the Server

            options     (dict)  --  The browse options dictionary

        Returns:
            list - List of only the file / folder paths from the browse response

            dict - Dictionary of all the paths with additional metadata retrieved from browse

        Raises:
            SDKException:
                if failed to browse/search for content

                if response is empty

                if response is not success
        """

        operation_types = {
            "browse": ('110', 'Failed to browse for subclient backup content\nError: "{0}"'),
            "find": ('111', 'Failed to Search\nError: "{0}"'),
            "all_versions": (
                '112', 'Failed to browse all version for specified content\nError: "{0}"'
            ),
            "delete_data": (
                '113', 'Failed to perform delete data operation for given content\nError: "{0}"'
            ),
            "list_media": (
                '113', 'Failed to perform list media operation for given content\nError: "{0}"'
            )
        }

        exception_code = operation_types[options['operation']][0]
        exception_message = operation_types[options['operation']][1]

        if flag:

            response_json = response.json()
            paths_dict = {}
            paths = []
            result_set = None
            browse_result = None

            # Send raw result as browse response for advanced use cases
            if options['_raw_response']:
                return [], response_json

            if response_json and 'browseResponses' in response_json:
                _browse_responses = response_json['browseResponses']
                for browse_response in _browse_responses:
                    if "browseResult" in browse_response:
                        browse_result = browse_response['browseResult']
                        if 'dataResultSet' in browse_result:
                            result_set = browse_result['dataResultSet']
                            break

                if not browse_result:
                    try:
                        message = response_json['browseResponses'][0]['messages'][0]
                        error_message = message['errorMessage']

                        o_str = exception_message
                        raise SDKException('Subclient', '102', o_str.format(error_message))
                    except KeyError:
                        return [], {}

                if not result_set:
                    raise SDKException('Subclient', exception_code)

                if not isinstance(result_set, list):
                    result_set = [result_set]

                if 'all_versions' in options['operation']:
                    return self._process_browse_all_versions_response(result_set)

                for result in result_set:
                    name = result.get('displayName')
                    snap_display_name = result.get('name')

                    if 'path' in result:
                        path = result['path']
                    else:
                        path = '\\'.join([options['path'], name])

                    if 'modificationTime' in result and int(result['modificationTime']) > 0:
                        mod_time = time.localtime(int(result['modificationTime']))
                        mod_time = time.strftime('%d/%m/%Y %H:%M:%S', mod_time)
                    else:
                        mod_time = None
                    
                    if 'backupTime' in result['advancedData'] and int(result['advancedData']['backupTime']) > 0:
                        bkp_time = time.localtime(int(result['advancedData']['backupTime']))
                        bkp_time = time.strftime('%d/%m/%Y %H:%M:%S', bkp_time)
                    else:
                        bkp_time = None

                    if 'file' in result['flags']:
                        if result['flags']['file'] in (True, '1'):
                            file_or_folder = 'File'
                        else:
                            file_or_folder = 'Folder'
                    else:
                        file_or_folder = 'Folder'

                    if 'size' in result:
                        size = result['size']
                    else:
                        size = None

                    paths_dict[path] = {
                        'name': name,
                        'snap_display_name': snap_display_name,
                        'size': size,
                        'modified_time': mod_time,
                        'type': file_or_folder,
                        'backup_time': bkp_time,
                        'advanced_data': result['advancedData']
                    }

                    paths.append(path)

                return paths, paths_dict
            else:
                raise SDKException('Response', '102')
        else:
            raise SDKException('Response', '101', self._update_response_(response.text))

    def _do_browse(self, options=None, retry=10):
        """Performs a browse operation with the given options.

        Args:
            options     (dict)  --  dictionary of browse options

            retry       (int)   --  Number of times to retry for browse

        Returns:
            list - List of only the file, folder paths from the browse response

            dict - Dictionary of all the paths with additional metadata retrieved from browse
        """
        if options is None:
            options = {}

        options = self._prepare_browse_options(options)
        request_json = self._prepare_browse_json(options)

        flag, response = self._cvpysdk_object.make_request('POST', self._BROWSE, request_json)

        attempt = 1
        while attempt <= retry:
            if response.json() == {}:
                time.sleep(120)
                flag, response = self._cvpysdk_object.make_request('POST', self._BROWSE, request_json)
            else:
                break
            attempt += 1
        return self._process_browse_response(flag, response, options)

    def update_properties(self, properties_dict):
        """Updates the backupset properties

            Args:
                properties_dict (dict)  --  Backupset property dict which is to be updated

            Returns:
                None

            Raises:
                SDKException:
                    if failed to add

                    if response is empty

                    if response code is not as expected

        **Note** self.properties can be used to get a deep copy of all the properties, modify the properties which you
        need to change and use the update_properties method to set the properties

        """
        request_json = {
            "backupsetProperties": {},
            "association": {
                "entity": [
                    {
                        "clientName": self._client_object.client_name,
                        "backupsetName": self.backupset_name,
                        "instanceName": self._instance_object.instance_name,
                        "appName": self._agent_object.agent_name
                    }
                ]
            }
        }

        request_json['backupsetProperties'].update(properties_dict)
        status, _, error_string = self._process_update_reponse(request_json)

        if not status:
            raise SDKException(
                'Backupset',
                '102',
                'Failed to update backupset property\nError: "{0}"'.format(error_string))

    @property
    def properties(self):
        """Returns the backupset properties"""
        return copy.deepcopy(self._properties)

    @property
    def name(self):
        """Returns the Backupset display name"""
        return self._properties["backupSetEntity"]["backupsetName"]

    @property
    def backupset_id(self):
        """Treats the backupset id as a read-only attribute."""
        return self._backupset_id

    @property
    def backupset_name(self):
        """Treats the backupset name as a property of the Backupset class."""
        return self._backupset_name

    @property
    def description(self):
        """Treats the backupset description as a property of the Backupset class."""
        return self._description

    @property
    def is_default_backupset(self):
        """Treats the is default backupset as a read-only attribute."""
        return self._is_default

    @property
    def is_on_demand_backupset(self):
        """Treats the is on demand backupset as a read-only attribute."""
        return self._is_on_demand_backupset

    @property
    def plan(self):
        """Treats the backupset plan as a property of the Backupset class."""
        if self._plan_obj is not None:
            return self._plan_obj
        elif self._plan_name is not None:
            self._plan_obj = self._commcell_object.plans.get(self._plan_name)
            return self._plan_obj
        else:
            return None

    @property
    def guid(self):
        """Treats the backupset GUID as a property of the Backupset class."""
        if self._properties.get('backupSetEntity'):
            if self._properties['backupSetEntity'].get('backupsetGUID'):
                return self._properties['backupSetEntity']['backupsetGUID']
            raise SDKException('Backupset', '102', 'Backupset GUID not found')
        raise SDKException('Backupset', '102', 'Backupset entity not found')

    @backupset_name.setter
    def backupset_name(self, value):
        """Sets the name of the backupset as the value provided as input.

            Raises:
                SDKException:
                    if failed to update the backupset name

                    if type of value input is not string
        """
        if isinstance(value, str):
            output = self._update(
                backupset_name=value,
                backupset_description=self.description,
                default_backupset=self.is_default_backupset
            )

            if output[0]:
                return
            o_str = 'Failed to update the name of the backupset\nError: "{0}"'
            raise SDKException('Backupset', '102', o_str.format(output[2]))

        raise SDKException('Backupset', '102', 'Backupset name should be a string value')

    @description.setter
    def description(self, value):
        """Sets the description of the backupset as the value provided as input.

            Raises:
                SDKException:
                    if failed to update the backupset description

                    if type of value input is not string

                    if description cannot be modified for this backupset
        """
        if self.description is not None:
            if isinstance(value, str):
                output = self._update(
                    backupset_name=self.backupset_name,
                    backupset_description=value,
                    default_backupset=self.is_default_backupset
                )

                if output[0]:
                    return

                o_str = 'Failed to update the description of the backupset\nError: "{0}"'
                raise SDKException('Backupset', '102', o_str.format(output[2]))

            raise SDKException(
                'Backupset', '102', 'Backupset description should be a string value'
            )

        raise SDKException('Backupset', '102', 'Description cannot be modified')

    @plan.setter
    def plan(self, value):
        """Associates the plan to the backupset

            Args:
                value   (object)    --  the Plan object which is to be associated
                                        with the backupset

                value   (str)       --  name of the plan which is to be associated
                                        with the backupset

                value   (None)      --  set value to None to remove plan associations

            Raises:
                SDKException:

                    if plan does not exist

                    if plan association fails

                    if plan is not eligible to be associated
        """
        from .plan import Plan
        if isinstance(value, Plan):
            self._plan_obj = value
        elif isinstance(value, str):
            self._plan_obj = self._commcell_object.plans.get(value)
        elif value is None:
            self._plan_obj = None
        else:
            raise SDKException('Backupset', '102', 'Input value is not of supported type')

        plans_obj = self._commcell_object.plans
        entity_dict = {
            'clientId': int(self._client_object.client_id),
            'appId': int(self._agent_object.agent_id),
            'backupsetId': int(self.backupset_id)
        }
        if value is not None and self._plan_obj.plan_name in plans_obj.get_eligible_plans(entity_dict):
            request_json = {
                'backupsetProperties': {
                    'planEntity': {
                        'planSubtype': int(self._plan_obj.subtype),
                        '_type_': 158,
                        'planType': int(self._plan_obj.plan_type),
                        'planName': self._plan_obj.plan_name,
                        'planId': int(self._plan_obj.plan_id)
                    }
                }
            }

            response = self._process_update_reponse(
                request_json
            )

            if response[0]:
                return
            else:
                o_str = 'Failed to asspciate plan to the backupset\nError: "{0}"'
                raise SDKException('Backupset', '102', o_str.format(response[2]))
        elif value is None:
            request_json = {
                'backupsetProperties': {
                    'removePlanAssociation': True
                }
            }

            response = self._process_update_reponse(
                request_json
            )

            if response[0]:
                return
            else:
                o_str = 'Failed to dissociate plan from backupset\nError: "{0}"'
                raise SDKException('Backupset', '102', o_str.format(response[2]))
        else:
            raise SDKException(
                'Backupset',
                '102',
                'Plan not eligible to be associated with the backupset'
            )

    def set_default_backupset(self):
        """Sets the backupset represented by this Backupset class instance as the default backupset
            if it is not the default backupset.

            Raises:
                SDKException:
                    if failed to set this as the default backupset
        """
        if self.is_default_backupset is False:
            output = self._update(
                backupset_name=self.backupset_name,
                backupset_description=self.description,
                default_backupset=True
            )

            if output[0]:
                return

            o_str = 'Failed to set this as the Default Backup Set\nError: "{0}"'
            raise SDKException('Backupset', '102', o_str.format(output[2]))

    def backup(self, **kwargs):
        """Runs backup job for all subclients in this backupset.

            kwargs:
                Please refer subclient.backup() for all the supported arguments. Commonly used arguments are,

                backup_level        (str)   --  level of backup the user wish to run
                        Full / Incremental / Differential / Synthetic_full
                    default: Incremental

                advanced_options   (dict)  --  advanced backup options to be included while
                                                    making the request

                common_backup_options      (dict)  --  advanced job options to be included while
                                                        making request

            Returns:
                list    -   list consisting of the job objects for the backup jobs started for
                the subclients in the backupset

        """
        return_list = []
        thread_list = []

        if self.subclients.all_subclients:
            for subclient in self.subclients.all_subclients:
                thread = threading.Thread(
                    target=self._run_backup, args=(subclient, return_list), kwargs=kwargs
                )
                thread_list.append(thread)
                thread.start()

        for thread in thread_list:
            thread.join()

        return return_list

    def browse(self, *args, **kwargs):
        """Browses the content of the Backupset.

            Args:
                Dictionary of browse options:
                    Example:

                        browse({
                            'path': 'c:\\hello',

                            'show_deleted': True,

                            'from_time': '2014-04-20 12:00:00',

                            'to_time': '2016-04-21 12:00:00'
                        })

            Kwargs:
                Keyword argument of browse options:
                    Example:

                        browse(
                            path='c:\\hello',

                            show_deleted=True,

                            from_time='2014-04-20 12:00:00',

                            to_time='2016-04-21 12:00:00'
                        )

            Returns:
                (list, dict)
                    list    -   List of only the file, folder paths from the browse response

                    dict    -   Dictionary of all the paths with additional metadata retrieved
                    from browse operation


            Refer `default_browse_options`_ for all the supported options.

            .. _default_browse_options: https://github.com/CommvaultEngg/cvpysdk/blob/master/cvpysdk/backupset.py#L565

        """
        if args and isinstance(args[0], dict):
            options = args[0]
        else:
            options = kwargs

        options['operation'] = 'browse'

        return self._do_browse(options)

    def find(self, *args, **kwargs):
        """Searches a file/folder in the backed up content of the backupset,
            and returns all the files matching the filters given.

            Args:
                Dictionary of browse options:
                    Example:

                        find({
                            'file_name': '*.txt',

                            'show_deleted': True,

                            'from_time': '2014-04-20 12:00:00',

                            'to_time': '2016-04-31 12:00:00'
                        })

            Kwargs:
                Keyword argument of browse options:
                    Example:

                        find(
                            file_name='*.txt',

                            show_deleted=True,

                            'from_time': '2014-04-20 12:00:00',

                            to_time='2016-04-31 12:00:00'
                        )

            Returns:
                (list, dict)
                    list    -   List of only the file, folder paths from the browse response

                    dict    -   Dictionary of all the paths with additional metadata retrieved
                    from browse operation


            Refer `default_browse_options`_ for all the supported options.

            Additional options supported:
                file_name       (str)   --  Find files with name

                file_size_gt    (int)   --  Find files with size greater than size

                file_size_lt    (int)   --  Find files with size lesser than size

                file_size_et    (int)   --  Find files with size equal to size

            .. _default_browse_options: https://github.com/CommvaultEngg/cvpysdk/blob/master/cvpysdk/backupset.py#L565

        """
        if args and isinstance(args[0], dict):
            options = args[0]
        else:
            options = kwargs

        if 'operation' not in options:
            options['operation'] = 'find'

        if 'path' not in options:
            options['path'] = '\\**\\*'

        if 'filters' not in options:
            options['filters'] = []

        if 'file_name' in options:
            options['filters'].append(('FileName', options['file_name']))

        if 'file_size_gt' in options:
            options['filters'].append(('FileSize', options['file_size_gt'], 'GTE'))

        if 'file_size_lt' in options:
            options['filters'].append(('FileSize', options['file_size_lt'], 'LTE'))

        if 'file_size_et' in options:
            options['filters'].append(('FileSize', options['file_size_et'], 'EQUALSBLAH'))

        return self._do_browse(options)

    def delete_data(self, paths):
        """Deletes items for the backupset in the Index and makes them unavailable for
        browsing and recovery

            Args:
                paths       (str/list)      --      The list of paths or single path to delete
                from the backupset

            Returns:
                None        --      If delete request is sent successfully

            Raises:
                Exception, if unable to prepare, response is invalid or send the
                delete data request

        """

        options = {
            'operation': 'delete_data',
            'path': paths
        }

        files, _ = self._do_browse(options)

        # Delete operation does not return any result, hence consider the operation successful
        if files:
            raise SDKException('Backupset', '102', 'Delete data operation gave unexpected results')

    def list_media(self, *args, **kwargs):
        """List media required to browse and restore backed up data from the backupset

            Args:
                Dictionary of options:
                    Example:

                        list_media({
                            'path': 'c:\\hello',
                            'show_deleted': True,
                            'from_time': '2020-04-20 12:00:00',
                            'to_time': '2021-04-19 12:00:00'
                        })

            Kwargs:
                Keyword argument of options:
                    Example:

                        list_media(
                            path='c:\\hello',
                            show_deleted=True,
                            from_time='2020-04-20 12:00:00',
                            to_time='2021-04-19 12:00:00'
                        )

            Note:
                Refer `_default_browse_options` for all the supported options.

            Returns:
                (List, Dict) -
                    List - List of all the media required for the given options

                    Dict - Total size of the media

            Raises:
                SDKException:
                    if failed to list media for content

                    if response is not success

        """

        if args and isinstance(args[0], dict):
            options = args[0]
        else:
            options = kwargs

        options['operation'] = 'list_media'
        options['_raw_response'] = True

        _, response = self._do_browse(options)

        if response and 'browseResponses' in response:
            responses = response['browseResponses']
            list_media_response = responses[0]

            prediction_data = list_media_response.get('predictionData', [])
            browse_result = list_media_response.get('browseResult', {})

            return prediction_data, browse_result
        else:
            raise SDKException('Backupset', '102', 'List media operation gave unexpected results')

    def refresh(self):
        """Refresh the properties of the Backupset."""
        self._get_backupset_properties()

        self.subclients = Subclients(self)
        self.schedules = Schedules(self)

    def backed_up_files_count(self, path="\\**\\*"):
        """Returns the count of the total number of files present in the backed up data
         of all the subclients of the given backupset and given path.

                Args:

                    path        (str)       --  Folder path to find no of backed up files
                                                    (Default: \\**\\*)

                Returns:

                    int --  No of backed up files count in given path

                Raises:

                    Exception:

                        if browse response is not proper
         """
        options_dic = {"operation": "find", "opType": 1, "path": path,
                       "_custom_queries": [{"type": "AGGREGATE", "queryId": "2",
                                            "aggrParam": {"aggrType": "COUNT"}, "whereClause": [{
                                                "criteria": {
                                                    "field": "Flags",
                                                    "dataOperator": "IN",
                                                    "values": ["file"]
                                                }
                                            }]}], "_raw_response": True}

        browse_response = self._do_browse(options_dic)
        if not len(browse_response) > 1:
            raise SDKException('Backupset', '102', 'Browse response is not proper')
        browse_response = browse_response[1]
        if 'browseResponses' not in browse_response or len(browse_response['browseResponses']) == 0:
            raise SDKException('Backupset', '102', 'Browse response is missing browseResponses')
        browse_response = browse_response['browseResponses'][0]
        if 'browseResult' not in browse_response:
            raise SDKException('Backupset', '102', 'Browse response is missing browseResult')
        browse_result = browse_response['browseResult']
        if 'aggrResultSet' not in browse_result or len(browse_result['aggrResultSet']) == 0:
            raise SDKException('Backupset', '102', 'Browse response is missing aggrResultSet')
        return browse_result['aggrResultSet'][0].get('count', 0)
