# Copyright 2015 Hewlett-Packard Development Company, L.P.
#
# Author: Kiall Mac Innes <kiall@hpe.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_log import log as logging
import oslo_messaging as messaging
import stevedore.exception
import yaml

from designate.backend import base as backend_base
from designate.central import rpcapi as central_rpcapi
import designate.conf
from designate import exceptions
from designate.manage import base
from designate import objects
from designate.objects.adapters import DesignateAdapter
from designate import policy
from designate import rpc
from designate import utils


CONF = designate.conf.CONF
LOG = logging.getLogger(__name__)


class PoolCommands(base.Commands):
    def __init__(self):
        super().__init__()
        self.central_api = None
        self.dry_run = False
        self.skip_verify_drivers = False

    def _setup(self, dry_run=False, skip_verify_drivers=False):
        self.dry_run = dry_run
        self.skip_verify_drivers = skip_verify_drivers
        rpc.init(CONF)
        self.central_api = central_rpcapi.CentralAPI()

    @base.args('--file', help='The path to the file the yaml output should be '
                              'written to',
               default='/etc/designate/pools.yaml')
    def generate_file(self, file):
        self._setup()

        try:
            pools = self.central_api.find_pools(self.context)
            data = DesignateAdapter.render('YAML', pools)
            self._write_config_to_file(file, data)

        except messaging.exceptions.MessagingTimeout:
            LOG.critical(
                'No response received from designate-central. '
                'Check it is running, and retry'
            )
            raise SystemExit(1)

    @base.args('--pool_id', help='ID of the pool to be examined',
               default=CONF['service:central'].default_pool_id)
    def show_config(self, pool_id):
        self._setup()

        self.output_message.append('Pool Configuration:')
        self.output_message.append('-------------------')

        try:
            if not utils.is_uuid_like(pool_id):
                self.output_message.append('Not a valid uuid: %s' % pool_id)
                raise SystemExit(1)

            pool = self.central_api.find_pool(self.context, {'id': pool_id})

            self.output_message.append(
                yaml.dump(
                    DesignateAdapter.render('YAML', pool),
                    default_flow_style=False
                )
            )

        except exceptions.PoolNotFound:
            self.output_message.append('Pool not found')
            raise SystemExit(1)
        except messaging.exceptions.MessagingTimeout:
            LOG.critical(
                'No response received from designate-central. '
                'Check it is running, and retry'
            )
            raise SystemExit(1)
        finally:
            self._print_result()

    @base.args('--file', help='The path to the yaml file describing the pools',
               default='/etc/designate/pools.yaml')
    @base.args(
        '--delete',
        help='Any Pools not listed in the config file will be deleted. '
             ' WARNING: This will delete any zones left in this pool',
        action='store_true',
        default=False)
    @base.args(
        '--dry-run',
        help='This will simulate what will happen when you run this command',
        action='store_true',
        default=False)
    @base.args(
        '--skip-verify-drivers',
        help='Don\'t verify the designate backend drivers',
        action='store_true',
        default=False)
    def update(self, file, delete, dry_run=False, skip_verify_drivers=False):
        self._setup(dry_run, skip_verify_drivers)

        try:
            self.output_message.append('Updating Pools Configuration')
            self.output_message.append('****************************')

            pools_data = self._load_config(file)

            if dry_run:
                self.output_message.append('The following changes will occur:')
                self.output_message.append('*********************************')

            for pool_data in pools_data:
                self._create_or_update_pool(pool_data)

            if delete:
                pools = self.central_api.find_pools(self.context)
                pools_in_db = {pool.name for pool in pools}
                pools_in_yaml = {pool_data['name'] for pool_data in pools_data}
                pools_to_delete = pools_in_db - pools_in_yaml
                for pool_name in pools_to_delete:
                    self._delete_pool(pool_name)

        except exceptions.InvalidObject as e:
            self.output_message.append(str(e))
            raise SystemExit(1)
        except messaging.exceptions.MessagingTimeout:
            LOG.critical(
                'No response received from designate-central. '
                'Check it is running, and retry'
            )
            raise SystemExit(1)
        finally:
            self._print_result()

    def _create_or_update_pool(self, pool_data):
        try:
            pool = self._get_pool(pool_data)
            self._update_pool(pool_data, pool)

        except exceptions.PoolNotFound:
            self._create_pool(pool_data)

    def _get_pool(self, pool_data):
        if 'id' in pool_data:
            pool_id = pool_data['id']
            if not utils.is_uuid_like(pool_id):
                self.output_message.append('Not a valid uuid: %s' % pool_id)
                raise SystemExit(1)

            pool = self.central_api.get_pool(
                self.context, pool_id
            )
        else:
            pool = self.central_api.find_pool(
                self.context, {'name': pool_data['name']}
            )

        return pool

    def _create_pool(self, pool_data):
        pool = DesignateAdapter.parse('YAML', pool_data, objects.Pool())
        self._validate_pool(pool)

        if self.dry_run:
            self.output_message.append('Create Pool: %s' % pool)
        else:
            LOG.info('Creating new pool: %s', pool)
            self.central_api.create_pool(self.context, pool)

        return pool

    def _update_pool(self, pool_data, pool):
        pool = DesignateAdapter.parse('YAML', pool_data, pool)
        self._validate_pool(pool)

        if self.dry_run:
            self.output_message.append('Update Pool: %s' % pool)
        else:
            pool = self.central_api.update_pool(self.context, pool)
            self._update_zones(pool)

    def _delete_pool(self, pool_name):
        pool = self.central_api.find_pool(
            self.context, criterion={'name': pool_name}
        )

        if self.dry_run:
            self.output_message.append('Delete Pool: %s' % pool_name)
        else:
            LOG.info('Deleting %s', pool_name)
            self.central_api.delete_pool(self.context, pool.id)

    def _update_zones(self, pool):
        LOG.info('Updating zone masters for pool: %s', pool.id)

        policy.init()
        self.context.all_tenants = True
        zones = self.central_api.find_zones(
            self.context, criterion={'pool_id': pool.id}
        )

        for zone in zones:
            zone.masters = objects.ZoneMasterList().from_list(
                self._get_masters_from_pool(pool)
            )
            self.central_api.update_zone(self.context, zone)

    def _validate_pool(self, pool):
        for ns_record in pool.ns_records:
            ns_record.validate()

        if not self.skip_verify_drivers:
            for target in pool.targets:
                try:
                    backend_base.Backend.get_driver(target.type)
                except stevedore.exception.NoMatches:
                    self.output_message.append(
                        'Unable to find designate backend driver type: '
                        '%s' % target.type
                    )
                    if not self.dry_run:
                        raise SystemExit(1)

    @staticmethod
    def _get_masters_from_pool(pool):
        masters = []
        for target in pool.targets:
            for master in target.get('masters', []):
                master = {'host': master['host'], 'port': master['port']}
                found = False
                for existing_master in masters:
                    if master == existing_master:
                        found = True
                if not found:
                    masters.append(master)
        return masters

    @staticmethod
    def _load_config(filename):
        with open(filename) as stream:
            return yaml.safe_load(stream)

    @staticmethod
    def _write_config_to_file(filename, data):
        with open(filename, 'w') as stream:
            yaml.dump(data, stream, default_flow_style=False)
