# -*- mode:python -*-
# vim: ts=4 sw=4 smarttab expandtab
#
# Processed in Makefile to add python #! line and version variable
#
#

import random
import string
import json
import socket
import base64
import logging
import errno

from .types import RGWAMException, RGWAMCmdRunException, RGWPeriod, RGWUser, RealmToken
from .diff import RealmsEPs

DEFAULT_PORT = 8000

log = logging.getLogger(__name__)


def bool_str(x):
    return 'true' if x else 'false'


def rand_alphanum_lower(k):
    return ''.join(random.choices(string.ascii_lowercase + string.digits, k=k))


def gen_name(prefix, suffix_len):
    return prefix + rand_alphanum_lower(suffix_len)


def set_or_gen(val, gen, prefix):
    if val:
        return val
    if gen:
        return gen_name(prefix, 8)

    return None


def get_endpoints(endpoints, period=None):
    if endpoints:
        return endpoints

    hostname = socket.getfqdn()

    port = DEFAULT_PORT

    while True:
        ep = 'http://%s:%d' % (hostname, port)
        if not period or not period.endpoint_exists(ep):
            return ep
        port += 1


class EnvArgs:
    def __init__(self, mgr):
        self.mgr = mgr


class EntityKey:
    def __init__(self, name=None, id=None):
        self.name = name
        self.id = id

    def safe_vals(ek):
        if not ek:
            return None, None
        return ek.name, ek.id


class EntityName(EntityKey):
    def __init__(self, name=None):
        super().__init__(name=name)


class EntityID(EntityKey):
    def __init__(self, id=None):
        super().__init__(id=id)


class ZoneEnv:
    def __init__(self, env: EnvArgs, realm: EntityKey = None, zg: EntityKey = None,
                 zone: EntityKey = None):
        self.env = env
        self.realm = realm
        self.zg = zg
        self.zone = zone

    def set(self, env: EnvArgs = None, realm: EntityKey = None, zg: EntityKey = None,
            zone: EntityKey = None):
        if env:
            self.env = env
        if realm:
            self.realm = realm
        if zg:
            self.zg = zg
        if zone:
            self.zone = zone

        return self

    def _init_entity(self, ek: EntityKey, gen, prefix):
        name, id = EntityKey.safe_vals(ek)
        name = set_or_gen(name, gen, prefix)

        return EntityKey(name, id)

    def init_realm(self, realm: EntityKey = None, gen=False):
        self.realm = self._init_entity(realm, gen, 'realm-')
        return self

    def init_zg(self, zg: EntityKey = None, gen=False):
        self.zg = self._init_entity(zg, gen, 'zg-')
        return self

    def init_zone(self, zone: EntityKey = None, gen=False):
        self.zone = self._init_entity(zone, gen, 'zone-')
        return self


def opt_arg(params, cmd, arg):
    if arg:
        params += [cmd, arg]


def opt_arg_bool(params, flag, arg):
    if arg:
        params += [flag]


class RGWCmdBase:
    def __init__(self, prog, zone_env: ZoneEnv):
        self.env = zone_env.env
        self.mgr = self.env.mgr
        self.prog = prog
        self.cmd_suffix = []
        if zone_env.realm:
            opt_arg(self.cmd_suffix, '--rgw-realm', zone_env.realm.name)
            opt_arg(self.cmd_suffix, '--realm-id', zone_env.realm.id)
        if zone_env.zg:
            opt_arg(self.cmd_suffix, '--rgw-zonegroup', zone_env.zg.name)
            opt_arg(self.cmd_suffix, '--zonegroup-id', zone_env.zg.id)
        if zone_env.zone:
            opt_arg(self.cmd_suffix, '--rgw-zone', zone_env.zone.name)
            opt_arg(self.cmd_suffix, '--zone-id', zone_env.zone.id)

    def run(self, cmd, stdin=None):
        args = cmd + self.cmd_suffix
        cmd, returncode, stdout, stderr = self.mgr.tool_exec(self.prog, args, stdin)

        log.debug('cmd=%s' % str(cmd))
        log.debug(f'stdin={stdin}')
        log.debug('stdout=%s' % stdout)

        if returncode != 0:
            cmd_str = ' '.join(cmd)
            log.error('ERROR: command exited with error status (%d): %s\nstdout=%s\nstderr=%s' %
                      (returncode, cmd_str, stdout, stderr))
            raise RGWAMCmdRunException(cmd_str, -returncode, stdout, stderr)

        return (stdout, stderr)


class RGWAdminCmd(RGWCmdBase):
    def __init__(self, zone_env: ZoneEnv):
        super().__init__('radosgw-admin', zone_env)


class RGWAdminJSONCmd(RGWAdminCmd):
    def __init__(self, zone_env: ZoneEnv):
        super().__init__(zone_env)

    def run(self, cmd, stdin=None):
        stdout, _ = RGWAdminCmd.run(self, cmd, stdin)

        return json.loads(stdout)


class RGWCmd(RGWCmdBase):
    def __init__(self, zone_env: ZoneEnv):
        super().__init__('radosgw', zone_env)


class RealmOp:
    def __init__(self, env: EnvArgs):
        self.env = env

    def list(self):
        try:
            ze = ZoneEnv(self.env)
            params = ['realm', 'list']
            output = RGWAdminJSONCmd(ze).run(params)
            return output.get('realms') or []
        except RGWAMException as e:
            logging.info(f'Exception while listing realms {e.message}')
            # in case the realm list is empty an exception is raised
            return []

    def get(self, realm: EntityKey = None):
        ze = ZoneEnv(self.env, realm=realm)
        params = ['realm', 'get']
        return RGWAdminJSONCmd(ze).run(params)

    def create(self, realm: EntityKey = None):
        ze = ZoneEnv(self.env).init_realm(realm=realm, gen=True)
        params = ['realm', 'create']
        return RGWAdminJSONCmd(ze).run(params)

    def pull(self, realm, url, access_key, secret):
        params = ['realm',
                  'pull',
                  '--url', url,
                  '--access-key', access_key,
                  '--secret', secret]
        ze = ZoneEnv(self.env, realm=realm)
        return RGWAdminJSONCmd(ze).run(params)


class ZonegroupOp:
    def __init__(self, env: EnvArgs):
        self.env = env

    def list(self):
        try:
            ze = ZoneEnv(self.env)
            params = ['zonegroup', 'list']
            output = RGWAdminJSONCmd(ze).run(params)
            return output.get('zonegroups') or []
        except RGWAMException as e:
            logging.info(f'Exception while listing zonegroups {e.message}')
            return []

    def get(self, zonegroup: EntityKey = None):
        ze = ZoneEnv(self.env, zg=zonegroup)
        params = ['zonegroup', 'get']
        return RGWAdminJSONCmd(ze).run(params)

    def set(self, zonegroup: EntityKey, zg_json: str):
        ze = ZoneEnv(self.env)
        params = ['zonegroup', 'set']
        return RGWAdminJSONCmd(ze).run(params, stdin=zg_json.encode('utf-8'))

    def create(self, realm: EntityKey, zg: EntityKey = None, endpoints=None, is_master=True):
        ze = ZoneEnv(self.env, realm=realm).init_zg(zg, gen=True)

        params = ['zonegroup',
                  'create']

        opt_arg_bool(params, '--master', is_master)
        opt_arg(params, '--endpoints', endpoints)

        stdout, _ = RGWAdminCmd(ze).run(params)

        return json.loads(stdout)

    def modify(self, realm: EntityKey, zg: EntityKey, endpoints=None):
        ze = ZoneEnv(self.env, realm=realm, zg=zg)
        params = ['zonegroup', 'modify']
        opt_arg(params, '--endpoints', endpoints)
        return RGWAdminJSONCmd(ze).run(params)


class ZoneOp:
    def __init__(self, env: EnvArgs):
        self.env = env

    def list(self):
        try:
            ze = ZoneEnv(self.env)
            params = ['zone', 'list']
            output = RGWAdminJSONCmd(ze).run(params)
            return output.get('zones') or []
        except RGWAMException as e:
            logging.info(f'Exception while listing zones {e.message}')
            return []

    def get(self, zone: EntityKey):
        ze = ZoneEnv(self.env, zone=zone)

        params = ['zone',
                  'get']

        return RGWAdminJSONCmd(ze).run(params)

    def create(self, realm: EntityKey, zonegroup: EntityKey, zone: EntityKey = None,
               endpoints=None, is_master=True,
               access_key=None, secret=None):

        ze = ZoneEnv(self.env, realm=realm, zg=zonegroup).init_zone(zone, gen=True)

        params = ['zone',
                  'create']

        opt_arg_bool(params, '--master', is_master)
        opt_arg(params, '--access-key', access_key)
        opt_arg(params, '--secret', secret)
        opt_arg(params, '--endpoints', endpoints)

        return RGWAdminJSONCmd(ze).run(params)

    def modify(self, zone: EntityKey, zg: EntityKey, is_master=None,
               access_key=None, secret=None, endpoints=None):
        ze = ZoneEnv(self.env, zone=zone, zg=zg)

        params = ['zone',
                  'modify']

        opt_arg_bool(params, '--master', is_master)
        opt_arg(params, '--access-key', access_key)
        opt_arg(params, '--secret', secret)
        opt_arg(params, '--endpoints', endpoints)

        return RGWAdminJSONCmd(ze).run(params)


class PeriodOp:
    def __init__(self, env):
        self.env = env

    def update(self, realm: EntityKey, zonegroup: EntityKey, zone: EntityKey, commit=True):
        ze = ZoneEnv(self.env, realm=realm,  zg=zonegroup, zone=zone)
        params = ['period', 'update']
        opt_arg_bool(params, '--commit', commit)
        return RGWAdminJSONCmd(ze).run(params)

    def get_master_zone(self, realm, zonegroup):
        try:
            # Fetch the realm period
            realm_period = self.get(realm)
            zonegroups = realm_period['period_map']['zonegroups']

            # Find the master zone in the realm period data
            for zonegroup_inf in zonegroups:
                if zonegroup_inf['name'] == zonegroup.name:
                    for zone in zonegroup_inf.get('zones', []):
                        if zone['id'] == zonegroup_inf['master_zone']:
                            return zone
            return None

        except RGWAMCmdRunException as e:
            log.error(f"Failed to fetch master zone: {e}")
            return None

    def get_master_zone_ep(self, realm):
        try:
            realm_period = self.get(realm)
            for zg in realm_period['period_map']['zonegroups']:
                if not bool(zg['is_master']):
                    continue
                for zone in zg['zones']:
                    if zone['id'] == zg['master_zone']:
                        return zone['endpoints']
            return None
        except RGWAMCmdRunException:
            return None

    def get_master_zonegroup(self, realm):
        try:
            # Fetch the realm period
            realm_period = self.get(realm)
            master_zonegroup_id = realm_period['master_zonegroup']
            zonegroups = realm_period['period_map']['zonegroups']

            # Find the master zonegroup in the realm period data
            for zonegroup in zonegroups:
                if zonegroup['id'] == master_zonegroup_id:
                    return zonegroup
            return None

        except RGWAMCmdRunException as e:
            log.error(f"Failed to fetch master zonegroup: {e}")
            return None

    def get(self, realm=None):
        ze = ZoneEnv(self.env, realm=realm)
        params = ['period', 'get']
        return RGWAdminJSONCmd(ze).run(params)


class UserOp:
    def __init__(self, env):
        self.env = env

    def create(self, zone: EntityKey, zg: EntityKey, uid=None, uid_prefix=None, display_name=None,
               email=None, is_system=False):
        ze = ZoneEnv(self.env, zone=zone, zg=zg)

        u = uid or gen_name(uid_prefix or 'user-', 6)

        dn = display_name or u

        params = ['user',
                  'create',
                  '--uid', u,
                  '--display-name', dn]

        opt_arg(params, '--email', email)
        opt_arg_bool(params, '--system', is_system)

        return RGWAdminJSONCmd(ze).run(params)

    def info(self, zone: EntityKey, zg: EntityKey, uid=None, access_key=None):
        ze = ZoneEnv(self.env, zone=zone, zg=zg)

        params = ['user',
                  'info']

        opt_arg(params, '--uid', uid)
        opt_arg(params, '--access-key', access_key)

        return RGWAdminJSONCmd(ze).run(params)

    def rm(self, zone: EntityKey, zg: EntityKey, uid=None, access_key=None):
        ze = ZoneEnv(self.env, zone=zone, zg=zg)

        params = ['user',
                  'rm']

        opt_arg(params, '--uid', uid)
        opt_arg(params, '--access-key', access_key)

        return RGWAdminCmd(ze).run(params)

    def rm_key(self, zone: EntityKey, zg: EntityKey, access_key=None):
        ze = ZoneEnv(self.env, zone=zone, zg=zg)

        params = ['key',
                  'remove']

        opt_arg(params, '--access-key', access_key)

        return RGWAdminCmd(ze).run(params)


class RGWAM:
    def __init__(self, env):
        self.env = env

    def realm_op(self):
        return RealmOp(self.env)

    def period_op(self):
        return PeriodOp(self.env)

    def zonegroup_op(self):
        return ZonegroupOp(self.env)

    def zone_op(self):
        return ZoneOp(self.env)

    def user_op(self):
        return UserOp(self.env)

    def get_realm(self, realm_name):
        try:
            realm_info = self.realm_op().get(EntityName(realm_name))
            realm = EntityKey(realm_info['name'], realm_info['id'])
            return realm
        except RGWAMException:
            raise None

    def create_realm(self, realm_name):
        try:
            realm_info = self.realm_op().create(EntityName(realm_name))
            realm = EntityKey(realm_info['name'], realm_info['id'])
            logging.info(f'Created realm name={realm.name} id={realm.id}')
            return realm
        except RGWAMException as e:
            raise RGWAMException('failed to create realm', e)

    def create_zonegroup(self, realm, zonegroup_name, zonegroup_is_master, endpoints=None):
        try:
            zg_info = self.zonegroup_op().create(realm,
                                                 EntityName(zonegroup_name),
                                                 endpoints,
                                                 is_master=zonegroup_is_master)
            zonegroup = EntityKey(zg_info['name'], zg_info['id'])
            logging.info(f'Created zonegroup name={zonegroup.name} id={zonegroup.id}')
            return zonegroup
        except RGWAMException as e:
            raise RGWAMException('failed to create zonegroup', e)

    def create_zone(self, realm, zg, zone_name, zone_is_master, access_key=None,
                    secret=None, endpoints=None):
        try:
            zone_info = self.zone_op().create(realm, zg,
                                              EntityName(zone_name),
                                              endpoints,
                                              is_master=zone_is_master,
                                              access_key=access_key,
                                              secret=secret)

            zone = EntityKey(zone_info['name'], zone_info['id'])
            logging.info(f'Created zone name={zone.name} id={zone.id}')
            return zone
        except RGWAMException as e:
            raise RGWAMException('failed to create zone', e)

    def create_system_user(self, realm, zonegroup, zone):
        try:
            sys_user_info = self.user_op().create(
                zone,
                zonegroup,
                uid=f'sysuser-{realm.name}',
                uid_prefix='user-sys',
                is_system=True
            )
            sys_user = RGWUser(sys_user_info)
            logging.info(f'Created system user: {sys_user.uid} on'
                         f'{realm.name}/{zonegroup.name}/{zone.name}')
            return sys_user
        except RGWAMException as e:
            if e.retcode == -errno.EEXIST:
                # You get this error (EEXIST) when the user already exists. This
                # can happen if you delete the zone/zg/realm for the user but not
                # the user itself and then try to call "rgw realm bootstrap"
                # with the same zone/zg/realm names again. In this case, let's try
                # to get the existing user's info
                try:
                    sys_user_info = self.user_op().info(
                        zone,
                        zonegroup,
                        uid=f'sysuser-{realm.name}',
                    )
                    sys_user = RGWUser(sys_user_info)
                    logging.info(f'Found existing system user: sysuser-{realm.name}')
                    return sys_user
                except RGWAMException as e2:
                    RGWAMException(
                        f'System user sysuser-{realm.name} already existed. '
                        'Failed getting info for user', e2
                    )
            raise RGWAMException('failed to create system user', e)

    def create_normal_user(self, zg, zone, uid=None):
        try:
            user_info = self.user_op().create(zone, zg, uid=uid, is_system=False)
            user = RGWUser(user_info)
            logging.info('Created regular user {user.uid} on'
                         '{realm.name}/{zonegroup.name}/{zone.name}')
            return user
        except RGWAMException as e:
            raise RGWAMException('failed to create user', e)

    def update_period(self, realm, zg, zone=None):
        try:
            period_info = self.period_op().update(realm, zg, zone, commit=True)
            period = RGWPeriod(period_info)
            logging.info('Period: ' + period.id)
        except RGWAMCmdRunException as e:
            raise RGWAMException('failed to update period', e)

    def realm_bootstrap(self, rgw_spec, start_radosgw=True, skip_realm_components=False):

        realm_name = rgw_spec.rgw_realm
        zonegroup_name = rgw_spec.rgw_zonegroup
        zone_name = rgw_spec.rgw_zone

        # Some sanity checks
        if not skip_realm_components:
            existing = []
            if realm_name in self.realm_op().list():
                existing.append(f"realm: {realm_name}")
                # raise RGWAMException(f'Realm {realm_name} already exists')
            if zonegroup_name in self.zonegroup_op().list():
                existing.append(f"zonegroup: {zonegroup_name}")
                # raise RGWAMException(f'Zonegroup {zonegroup_name} already exists')
            if zone_name in self.zone_op().list():
                existing.append(f"zone: {zone_name}")
                # raise RGWAMException(f'Zone {zone_name} already exists')
            if existing:
                raise RGWAMException(
                    f"The following components already exist: {', '.join(existing)}"
                )

        # Create RGW entities and update the period
        realm = self.create_realm(realm_name)
        zonegroup = self.create_zonegroup(realm, zonegroup_name, zonegroup_is_master=True)
        zone = self.create_zone(realm, zonegroup, zone_name, zone_is_master=True)
        self.update_period(realm, zonegroup, zone)

        # Create system user, normal user and update the master zone
        sys_user = self.create_system_user(realm, zonegroup, zone)
        rgw_acces_key = sys_user.get_key(0)
        access_key = rgw_acces_key.access_key if rgw_acces_key else ''
        secret = rgw_acces_key.secret_key if rgw_acces_key else ''
        self.zone_op().modify(zone, zonegroup, None,
                              access_key, secret, endpoints=rgw_spec.zone_endpoints)
        self.update_period(realm, zonegroup, zone)

        if start_radosgw and rgw_spec.zone_endpoints is None:
            # Instruct the orchestrator to start RGW daemons, asynchronically, this will
            # call back the rgw module to update the master zone with the corresponding endpoints
            realm_token = RealmToken(realm_name,
                                     realm.id,
                                     None,   # no endpoint
                                     access_key, secret)
            realm_token_b = realm_token.to_json().encode('utf-8')
            realm_token_s = base64.b64encode(realm_token_b).decode('utf-8')
            rgw_spec.rgw_realm_token = realm_token_s
            rgw_spec.update_endpoints = True
            self.env.mgr.apply_rgw(rgw_spec)

    def realm_new_zone_creds(self, realm_name, endpoints, sys_uid):
        try:
            period_info = self.period_op().get(EntityName(realm_name))
        except RGWAMException as e:
            raise RGWAMException('failed to fetch period info', e)

        period = RGWPeriod(period_info)

        master_zg = EntityID(period.master_zonegroup)
        master_zone = EntityID(period.master_zone)

        try:
            zone_info = self.zone_op().get(zone=master_zone)
        except RGWAMException as e:
            raise RGWAMException('failed to access master zone', e)

        zone_id = zone_info['id']

        logging.info('Period: ' + period.id)
        logging.info('Master zone: ' + period.master_zone)

        if period.master_zone != zone_id:
            return (-errno.EINVAL, '', 'Command needs to run on master zone')

        ep = ''
        if not endpoints:
            eps = period.get_zone_endpoints(period.master_zonegroup, period.master_zone)
        else:
            eps = endpoints.split(',')

        if len(eps) > 0:
            ep = eps[0]

        try:
            sys_user_info = self.user_op().create(master_zone, master_zg, uid=sys_uid,
                                                  uid_prefix='user-sys', is_system=True)
        except RGWAMException as e:
            raise RGWAMException('failed to create system user', e)

        sys_user = RGWUser(sys_user_info)

        logging.info('Created system user: %s' % sys_user.uid)

        sys_access_key = ''
        sys_secret = ''

        if len(sys_user.keys) > 0:
            sys_access_key = sys_user.keys[0].access_key
            sys_secret = sys_user.keys[0].secret_key

        realm_token = RealmToken(realm_name, period.realm_id, ep, sys_access_key, sys_secret)

        logging.info(realm_token.to_json())

        realm_token_b = realm_token.to_json().encode('utf-8')
        return (0, 'Realm Token: %s' % base64.b64encode(realm_token_b).decode('utf-8'), '')

    def realm_rm_zone_creds(self, realm_token_b64):
        if not realm_token_b64:
            raise RGWAMException('missing realm token')

        realm_token = RealmToken.from_base64_str(realm_token_b64)
        try:
            period_info = self.period_op().get(EntityID(realm_token.realm_id))
        except RGWAMException as e:
            raise RGWAMException('failed to fetch period info', e)

        period = RGWPeriod(period_info)
        master_zg = EntityID(period.master_zonegroup)
        master_zone = EntityID(period.master_zone)
        logging.info('Period: ' + period.id)
        logging.info('Master zone: ' + period.master_zone)
        try:
            zone_info = self.zone_op().get(zone=master_zone)
        except RGWAMException as e:
            raise RGWAMException('failed to access master zone', e)

        if period.master_zone != zone_info['id']:
            return (-errno.EINVAL, '', 'Command needs to run on master zone')

        access_key = realm_token.access_key
        try:
            user_info = self.user_op().info(master_zone, master_zg, access_key=access_key)
        except RGWAMException as e:
            raise RGWAMException('failed to get the system user information', e)

        user = RGWUser(user_info)

        only_key = True

        for k in user.keys:
            if k.access_key != access_key:
                only_key = False
                break

        success_message = ''

        if only_key:
            # the only key this user has is the one defined in the token
            # can remove the user completely

            try:
                self.user_op().rm(master_zone, master_zg, uid=user.uid)
            except RGWAMException as e:
                raise RGWAMException('failed removing user ' + user, user.uid, e)

            success_message = 'Removed uid ' + user.uid
        else:
            try:
                self.user_op().rm_key(master_zone, master_zg, access_key=access_key)
            except RGWAMException as e:
                raise RGWAMException('failed removing access key ' +
                                     access_key + '(uid = ' + user.uid + ')', e)

            success_message = 'Removed access key ' + access_key + '(uid = ' + user.uid + ')'

        return (0, success_message, '')

    def zone_modify(self, realm_name, zonegroup_name, zone_name, endpoints, realm_token_b64):

        if not realm_token_b64:
            raise RGWAMException('missing realm access config')
        if zone_name is None:
            raise RGWAMException('Zone name is a mandatory parameter')

        realm_token = RealmToken.from_base64_str(realm_token_b64)
        access_key = realm_token.access_key
        secret = realm_token.secret
        realm_name = realm_token.realm_name
        realm_id = realm_token.realm_id
        logging.info(f'Using realm {realm_name} {realm_id}')

        realm = EntityID(realm_id)
        period_info = self.period_op().get(realm)
        period = RGWPeriod(period_info)
        logging.info('Period: ' + period.id)
        zonegroup = period.find_zonegroup_by_name(zonegroup_name)
        if not zonegroup:
            raise RGWAMException(f'zonegroup {zonegroup_name} not found')

        zg = EntityName(zonegroup.name)
        zone = EntityName(zone_name)
        master_zone_info = self.period_op().get_master_zone(realm, zg)
        success_message = f'Modified zone {realm_name} {zonegroup_name} {zone_name}'
        logging.info(success_message)
        try:
            self.zone_op().modify(zone, zg, access_key=access_key,
                                  secret=secret, endpoints=','.join(endpoints))
            # we only update the zonegroup endpoints if the zone being
            # modified is a master zone
            if zone_name == master_zone_info['name']:
                self.zonegroup_op().modify(realm, zg, endpoints=','.join(endpoints))
        except RGWAMException as e:
            raise RGWAMException('failed to modify zone', e)

        # done, let's update the period
        try:
            period_info = self.period_op().update(realm, zg, zone, True)
        except RGWAMException as e:
            raise RGWAMException('failed to update period', e)

        period = RGWPeriod(period_info)
        logging.debug(period.to_json())

        return (0, success_message, '')

    def zonegroup_modify(self, realm_name, zonegroup_name, zone_name, hostnames):
        if realm_name is None:
            raise RGWAMException('Realm name is a mandatory parameter')
        if zone_name is None:
            raise RGWAMException('Zone name is a mandatory parameter')
        if zonegroup_name is None:
            raise RGWAMException('Zonegroup name is a mandatory parameter')

        realm = EntityName(realm_name)
        zone = EntityName(zone_name)
        period_info = self.period_op().get(realm)
        period = RGWPeriod(period_info)
        logging.info('Period: ' + period.id)
        zonegroup = period.find_zonegroup_by_name(zonegroup_name)
        if not zonegroup:
            raise RGWAMException(f'zonegroup {zonegroup_name} not found')
        zg = EntityName(zonegroup.name)
        zg_json = self.zonegroup_op().get(zg)

        if hostnames:
            zg_json['hostnames'] = hostnames

        try:
            self.zonegroup_op().set(zg, json.dumps(zg_json))
        except RGWAMException as e:
            raise RGWAMException('failed to set zonegroup', e)

        try:
            period_info = self.period_op().update(realm, zg, zone, True)
        except RGWAMException as e:
            raise RGWAMException('failed to update period', e)

        period = RGWPeriod(period_info)
        logging.debug(period.to_json())

        return (0, f'Modified zonegroup {zonegroup_name} of realm {realm_name}', '')

    def get_realms_info(self):
        realms_info = []
        for realm_name in self.realm_op().list():
            realm = self.get_realm(realm_name)
            realm_period = self.period_op().get(realm)
            master_zone_id = realm_period['master_zone']
            master_zone_name = self.get_master_zone_name(realm_period, master_zone_id)
            local_zone_list = self.zone_op().list()

            # Only consider the realm if master_zone_name is in the local zone list
            if master_zone_name in local_zone_list:
                master_zone_inf = self.zone_op().get(EntityID(master_zone_id))
                zone_ep = self.period_op().get_master_zone_ep(realm)

                if master_zone_inf and 'system_key' in master_zone_inf:
                    access_key = master_zone_inf['system_key']['access_key']
                    secret = master_zone_inf['system_key']['secret_key']
                else:
                    access_key = ''
                    secret = ''

                realms_info.append({
                    "realm_name": realm_name,
                    "realm_id": realm.id,
                    "master_zone_id": master_zone_inf['id'] if master_zone_inf else '',
                    "endpoint": zone_ep[0] if zone_ep else None,
                    "access_key": access_key,
                    "secret": secret
                })

        return realms_info

    def get_master_zone_name(self, realm_data, master_zone_id):
        # Find the zonegroups in the period_map
        zonegroups = realm_data.get('period_map', {}).get('zonegroups', [])
        for zonegroup in zonegroups:
            for zone in zonegroup.get('zones', []):
                if zone.get('id') == master_zone_id:
                    return zone.get('name')
        return None

    def zone_create(self, rgw_spec, start_radosgw, secondary_zone_period_retry_limit=5):

        if not rgw_spec.rgw_realm_token:
            raise RGWAMException('missing realm token')
        if rgw_spec.rgw_zone is None:
            raise RGWAMException('Zone name is a mandatory parameter')
        if rgw_spec.rgw_zone in self.zone_op().list():
            raise RGWAMException(f'Zone {rgw_spec.rgw_zone} already exists')

        realm_token = RealmToken.from_base64_str(rgw_spec.rgw_realm_token)
        if realm_token.endpoint is None:
            raise RGWAMException('Provided realm token has no endpoint')

        access_key = realm_token.access_key
        secret = realm_token.secret
        try:
            realm_info = self.realm_op().pull(EntityName(realm_token.realm_name),
                                              realm_token.endpoint, access_key, secret)
        except RGWAMException as e:
            raise RGWAMException('failed to pull realm', e)

        logging.info(f"Pulled realm {realm_info['name']} ({realm_info['id']})")
        realm_name = realm_info['name']
        realm_id = realm_info['id']

        realm = EntityKey(realm_name, realm_id)
        period_info = self.period_op().get(realm)
        period = RGWPeriod(period_info)
        logging.info('Period: ' + period.id)

        zonegroup = period.get_master_zonegroup()
        if not zonegroup:
            raise RGWAMException(f'Cannot find master zonegroup of realm {realm_name}')

        zone = self.create_zone(realm, zonegroup, rgw_spec.rgw_zone,
                                False,  # secondary zone
                                access_key, secret, endpoints=rgw_spec.zone_endpoints)

        # Adding a retry limit for period update in case the default 10s timeout is not sufficient
        rgw_limit = 0

        while rgw_limit != int(secondary_zone_period_retry_limit):
            try:
                self.update_period(realm, zonegroup, zone)
                break
            except RGWAMException as e:
                logging.info(f'Failed to update Period in 10s. Retrying with current limit \
                             & retry-limit values {rgw_limit} {secondary_zone_period_retry_limit}')
                rgw_limit += 1
                if rgw_limit == secondary_zone_period_retry_limit:
                    raise RGWAMException(f'Period Update failed for zone {zone}. \
                                          Exception raised while period update {e.message}')
                continue

        # By default the above operation is expected to be completed in 10s timeout but if we
        # updating this for secondary site it would take some time because of pool creation

        period = RGWPeriod(period_info)
        logging.debug(period.to_json())

        if start_radosgw and rgw_spec.zone_endpoints is None:
            secondary_realm_token = RealmToken(realm_name,
                                               realm_id,
                                               None,   # no endpoint
                                               realm_token.access_key,
                                               realm_token.secret)
            realm_token_b = secondary_realm_token.to_json().encode('utf-8')
            realm_token_s = base64.b64encode(realm_token_b).decode('utf-8')
            rgw_spec.update_endpoints = True
            rgw_spec.rgw_realm_token = realm_token_s
            rgw_spec.rgw_zonegroup = zonegroup.name  # master zonegroup is used
            self.env.mgr.apply_rgw(rgw_spec)

    def _get_daemon_eps(self, realm_name=None, zonegroup_name=None, zone_name=None):
        # get running daemons info
        service_name = None
        if realm_name and zone_name:
            service_name = 'rgw.%s.%s' % (realm_name, zone_name)

        daemon_type = 'rgw'
        daemon_id = None
        hostname = None
        refresh = True

        daemons = self.env.mgr.list_daemons(service_name,
                                            daemon_type,
                                            daemon_id=daemon_id,
                                            host=hostname,
                                            refresh=refresh)

        rep = RealmsEPs()

        for s in daemons:
            for p in s.ports:
                svc_id = s.service_id()
                sp = svc_id.split('.')
                if len(sp) < 2:
                    log.error('ERROR: service id cannot be parsed: (svc_id=%s)' % svc_id)
                    continue

                svc_realm = sp[0]
                svc_zone = sp[1]

                if realm_name and svc_realm != realm_name:
                    log.debug('skipping realm %s' % svc_realm)
                    continue

                if zone_name and svc_zone != zone_name:
                    log.debug('skipping zone %s' % svc_zone)
                    continue

                ep = 'http://%s:%d' % (s.hostname, p)  # ssl?

                rep.add(svc_realm, svc_zone, ep)

        return rep

    def _get_rgw_eps(self, realm_name=None, zonegroup_name=None, zone_name=None):
        rep = RealmsEPs()

        try:
            realms = self.realm_op().list()
        except RGWAMException as e:
            raise RGWAMException('failed to list realms', e)

        zones_map = {}
        for realm in realms:
            if realm_name and realm != realm_name:
                log.debug('skipping realm %s' % realm)
                continue

            period_info = self.period_op().get(EntityName(realm))

            period = RGWPeriod(period_info)

            zones_map[realm] = {}

            for zg in period.iter_zonegroups():
                if zonegroup_name and zg.name != zonegroup_name:
                    log.debug('skipping zonegroup %s' % zg.name)
                    continue

                for zone in zg.iter_zones():
                    if zone_name and zone.name != zone_name:
                        log.debug('skipping zone %s' % zone.name)
                        continue

                    zones_map[realm][zone.name] = zg.name

                    if len(zone.endpoints) == 0:
                        rep.add(realm, zone.name, None)
                        continue

                    for ep in zone.endpoints:
                        rep.add(realm, zone.name, ep)

        return (rep, zones_map)

    def realm_reconcile(self, realm_name=None, zonegroup_name=None, zone_name=None, update=False):

        daemon_rep = self._get_daemon_eps(realm_name, zonegroup_name, zone_name)

        rgw_rep, zones_map = self._get_rgw_eps(realm_name, zonegroup_name, zone_name)

        diff = daemon_rep.diff(rgw_rep)

        diffj = json.dumps(diff)

        if not update:
            return (0, diffj, '')

        for realm, realm_diff in diff.items():
            for zone, endpoints in realm_diff.items():

                zg = zones_map[realm][zone]

                try:
                    self.zone_op().modify(EntityName(zone), EntityName(zg),
                                          endpoints=','.join(diff[realm][zone]))
                except RGWAMException as e:
                    raise RGWAMException('failed to modify zone', e)

            try:
                self.period_op().update(EntityName(realm), EntityName(zg), EntityName(zone), True)
            except RGWAMException as e:
                raise RGWAMException('failed to update period', e)

        return (0, 'Updated: ' + diffj, '')

    def run_radosgw(self, port=None, log_file=None, debug_ms=None, debug_rgw=None):

        fe_cfg = 'beast'
        if port:
            fe_cfg += ' port=%s' % port

        params = ['--rgw-frontends', fe_cfg]

        if log_file:
            params += ['--log-file', log_file]

        if debug_ms:
            params += ['--debug-ms', debug_ms]

        if debug_rgw:
            params += ['--debug-rgw', debug_rgw]

        (retcode, stdout, stderr) = RGWCmd(self.env).run(params)

        return (retcode, stdout, stderr)
