# Copyright 2018 OpenStack Foundation.
# All Rights Reserved
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.
#

import yaml

from osc_lib.command import command
from osc_lib import utils

from tackerclient.common import exceptions
from tackerclient.i18n import _
from tackerclient.osc import sdk_utils
from tackerclient.osc import utils as tacker_osc_utils
from tackerclient.tacker import v1_0 as tackerV10

_VNFFG = 'vnffg'    # VNF Forwarding Graph
_NFP = 'nfp'        # Network Forwarding Path
_SFC = 'sfc'        # Service Function Chain
_FC = 'classifier'  # Flow Classifier

nfps_path = '/nfps'
fcs_path = '/classifiers'
sfcs_path = '/sfcs'

DEFAULT_ERROR_REASON_LENGTH = 100

_attr_map_vnffg = (
    ('id', 'ID', tacker_osc_utils.LIST_BOTH),
    ('name', 'Name', tacker_osc_utils.LIST_BOTH),
    ('ns_id', 'NS ID', tacker_osc_utils.LIST_BOTH),
    ('vnffgd_id', 'VNFFGD ID', tacker_osc_utils.LIST_BOTH),
    ('status', 'Status', tacker_osc_utils.LIST_BOTH),
    ('description', 'Description', tacker_osc_utils.LIST_LONG_ONLY),
)

_attr_map_nfp = (
    ('id', 'ID', tacker_osc_utils.LIST_BOTH),
    ('name', 'Name', tacker_osc_utils.LIST_BOTH),
    ('status', 'Status', tacker_osc_utils.LIST_BOTH),
    ('vnffg_id', 'VNFFG ID', tacker_osc_utils.LIST_BOTH),
    ('path_id', 'Path ID', tacker_osc_utils.LIST_BOTH),
)

_attr_map_sfc = (
    ('id', 'ID', tacker_osc_utils.LIST_BOTH),
    ('status', 'Status', tacker_osc_utils.LIST_BOTH),
    ('nfp_id', 'NFP ID', tacker_osc_utils.LIST_BOTH),
)

_attr_map_fc = (
    ('id', 'ID', tacker_osc_utils.LIST_BOTH),
    ('name', 'Name', tacker_osc_utils.LIST_BOTH),
    ('status', 'Status', tacker_osc_utils.LIST_BOTH),
    ('nfp_id', 'NFP ID', tacker_osc_utils.LIST_BOTH),
    ('chain_id', 'Chain ID', tacker_osc_utils.LIST_BOTH),
)

_formatters = {
    'attributes': tacker_osc_utils.format_dict_with_indention,
    'match': tacker_osc_utils.format_dict_with_indention,
    'chain': tacker_osc_utils.format_dict_with_indention,
}


def _get_columns(item):
    column_map = {
        'tenant_id': 'project_id',
    }
    return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map)


class CreateVNFFG(command.ShowOne):
    _description = _("Create a new VNFFG.")

    def get_parser(self, prog_name):
        parser = super(CreateVNFFG, self).get_parser(prog_name)
        parser.add_argument(
            'name', metavar='NAME',
            help=_('Set a name for the VNFFG'))
        parser.add_argument(
            '--tenant-id', metavar='TENANT_ID',
            help=_('The owner tenant ID'))
        vnffgd_group = parser.add_mutually_exclusive_group(required=True)
        vnffgd_group.add_argument(
            '--vnffgd-id',
            help=_('VNFFGD ID to use as template to create VNFFG'))
        vnffgd_group.add_argument(
            '--vnffgd-name',
            help=_('VNFFGD Name to use as template to create VNFFG'))
        vnffgd_group.add_argument(
            '--vnffgd-template',
            help=_('VNFFGD file to create VNFFG'))
        parser.add_argument(
            '--vnf-mapping',
            help=_('List of logical VNFD name to VNF instance name mapping. '
                   'Example: VNF1:my_vnf1,VNF2:my_vnf2'))
        parser.add_argument(
            '--symmetrical',
            action='store_true',
            default=False,
            help=_('Should a reverse path be created for the NFP '
                   '(True or False)'))
        parser.add_argument(
            '--param-file',
            help=_('YAML file with specific VNFFG parameters'))
        parser.add_argument(
            '--description',
            help=_('Set a description for the VNFFG'))
        return parser

    def args2body(self, parsed_args):
        body = {_VNFFG: {}}
        body[_VNFFG]['attributes'] = {}

        client = self.app.client_manager.tackerclient
        if parsed_args.vnf_mapping:
            _vnf_mapping = dict()
            _vnf_mappings = parsed_args.vnf_mapping.split(",")
            for mapping in _vnf_mappings:
                vnfd_name, vnf = mapping.split(":", 1)
                _vnf_mapping[vnfd_name] = \
                    tackerV10.find_resourceid_by_name_or_id(
                        client, 'vnf', vnf)
            parsed_args.vnf_mapping = _vnf_mapping

        if parsed_args.vnffgd_name:
            _id = tackerV10.find_resourceid_by_name_or_id(
                client, 'vnffgd', parsed_args.vnffgd_name)
            parsed_args.vnffgd_id = _id
        elif parsed_args.vnffgd_template:
            with open(parsed_args.vnffgd_template) as f:
                template = f.read()
                try:
                    template = yaml.load(template, Loader=yaml.SafeLoader)
                except yaml.YAMLError as e:
                    raise exceptions.InvalidInput(reason=e)
                if not template:
                    raise exceptions.InvalidInput(
                        reason='The vnffgd file is empty')
                body[_VNFFG]['vnffgd_template'] = template

        if parsed_args.param_file:
            with open(parsed_args.param_file) as f:
                param_yaml = f.read()
            try:
                param_yaml = yaml.load(
                    param_yaml, Loader=yaml.SafeLoader)
            except yaml.YAMLError as e:
                raise exceptions.InvalidInput(reason=e)
            if not param_yaml:
                raise exceptions.InvalidInput(
                    reason='The parameter file is empty')
            body[_VNFFG]['attributes'] = {'param_values': param_yaml}
        tackerV10.update_dict(parsed_args, body[_VNFFG],
                              ['tenant_id', 'name', 'vnffgd_id',
                               'symmetrical', 'vnf_mapping', 'description'])
        return body

    def take_action(self, parsed_args):
        client = self.app.client_manager.tackerclient
        vnffg = client.create_vnffg(self.args2body(parsed_args))
        display_columns, columns = _get_columns(vnffg[_VNFFG])
        data = utils.get_item_properties(
            sdk_utils.DictModel(vnffg[_VNFFG]),
            columns,
            formatters=_formatters)
        return (display_columns, data)


class DeleteVNFFG(command.Command):
    _description = _("Delete VNFFG(s).")

    def get_parser(self, prog_name):
        parser = super(DeleteVNFFG, self).get_parser(prog_name)
        parser.add_argument(
            _VNFFG,
            metavar="<VNFFG>",
            nargs="+",
            help=_("VNFFG(s) to delete (name or ID)")
        )
        parser.add_argument(
            '--force',
            default=False,
            action='store_true',
            help=_('Force delete VNFFG'))
        return parser

    def args2body(self, parsed_args):
        body = dict()
        if parsed_args.force:
            body[_VNFFG] = dict()
            body[_VNFFG]['attributes'] = dict()
            body[_VNFFG]['attributes']['force'] = True
        return body

    def take_action(self, parsed_args):
        client = self.app.client_manager.tackerclient
        failure = False
        deleted_ids = []
        failed_items = {}
        body = self.args2body(parsed_args)
        for resource_id in parsed_args.vnffg:
            try:
                obj = tackerV10.find_resourceid_by_name_or_id(
                    client, _VNFFG, resource_id)
                client.delete_vnffg(obj, body)
                deleted_ids.append(resource_id)
            except Exception as e:
                failure = True
                failed_items[resource_id] = e
        if failure:
            msg = ''
            if deleted_ids:
                msg = (_('Successfully deleted %(resource)s(s):'
                         ' %(deleted_list)s') % {'deleted_list':
                                                 ', '.join(deleted_ids),
                                                 'resource': _VNFFG})
            err_msg = _("\n\nUnable to delete the below"
                        " %s(s):") % _VNFFG
            for failed_id, error in failed_items.items():
                err_msg += (_('\n Cannot delete %(failed_id)s: %(error)s')
                            % {'failed_id': failed_id,
                               'error': error})
            msg += err_msg
            raise exceptions.CommandError(message=msg)
        else:
            print((_('All specified %(resource)s(s) deleted successfully')
                   % {'resource': _VNFFG}))
        return


class UpdateVNFFG(command.ShowOne):
    _description = _("Update VNFFG.")

    def get_parser(self, prog_name):
        parser = super(UpdateVNFFG, self).get_parser(prog_name)
        parser.add_argument(
            _VNFFG,
            metavar="<VNFFG>",
            help=_('VNFFG to update (name or ID)'))
        parser.add_argument(
            '--vnffgd-template',
            help=_('VNFFGD file to update VNFFG'))
        parser.add_argument(
            '--vnf-mapping',
            help=_('List of logical VNFD name to VNF instance name mapping.  '
                   'Example: VNF1:my_vnf1,VNF2:my_vnf2'))
        parser.add_argument(
            '--symmetrical',
            action='store_true',
            default=False,
            help=_('Should a reverse path be created for the NFP'))
        parser.add_argument(
            '--description',
            help=_('Set a description for the VNFFG'))
        parser.add_argument(
            '--param-file',
            help=_('YAML file with specific VNFFG parameters'))
        return parser

    def args2body(self, parsed_args):
        body = {_VNFFG: {}}
        body[_VNFFG]['attributes'] = {}

        client = self.app.client_manager.tackerclient

        if parsed_args.vnf_mapping:
            _vnf_mapping = dict()
            _vnf_mappings = parsed_args.vnf_mapping.split(",")
            for mapping in _vnf_mappings:
                vnfd_name, vnf = mapping.split(":", 1)
                _vnf_mapping[vnfd_name] = \
                    tackerV10.find_resourceid_by_name_or_id(
                        client, 'vnf', vnf)
            parsed_args.vnf_mapping = _vnf_mapping

        if parsed_args.vnffgd_template:
            with open(parsed_args.vnffgd_template) as f:
                template = f.read()
                try:
                    template = yaml.load(
                        template, Loader=yaml.SafeLoader)
                except yaml.YAMLError as e:
                    raise exceptions.InvalidInput(reason=e)
                if not template:
                    raise exceptions.InvalidInput(
                        reason='The vnffgd file is empty')
                body[_VNFFG]['vnffgd_template'] = template

        if parsed_args.param_file:
            with open(parsed_args.param_file) as f:
                param_yaml = f.read()
            try:
                param_yaml = yaml.load(
                    param_yaml, Loader=yaml.SafeLoader)
            except yaml.YAMLError as e:
                raise exceptions.InvalidInput(reason=e)
            if not param_yaml:
                raise exceptions.InvalidInput(
                    reason='The parameter file is empty')
            body[_VNFFG]['attributes'] = {'param_values': param_yaml}
        tackerV10.update_dict(parsed_args, body[_VNFFG],
                              ['vnf_mapping', 'symmetrical', 'description'])
        return body

    def take_action(self, parsed_args):
        client = self.app.client_manager.tackerclient
        obj_id = tackerV10.find_resourceid_by_name_or_id(
            client, _VNFFG, parsed_args.vnffg)
        vnffg = client.update_vnffg(obj_id, self.args2body(parsed_args))
        display_columns, columns = _get_columns(vnffg[_VNFFG])
        data = utils.get_item_properties(
            sdk_utils.DictModel(vnffg[_VNFFG]),
            columns,
            formatters=_formatters)
        return (display_columns, data)


class ListVNFFG(command.Lister):
    _description = ("List VNFFG(s) that belong to a given tenant.")

    def get_parser(self, prog_name):
        parser = super(ListVNFFG, self).get_parser(prog_name)
        parser.add_argument(
            '--long',
            action='store_true',
            help=_('List additional fields in output')
        )
        return parser

    def take_action(self, parsed_args):
        client = self.app.client_manager.tackerclient
        data = client.list_vnffgs()
        headers, columns = tacker_osc_utils.get_column_definitions(
            _attr_map_vnffg, long_listing=parsed_args.long)
        return (headers,
                (utils.get_dict_properties(
                    s, columns,
                ) for s in data[_VNFFG + 's']))


class ShowVNFFG(command.ShowOne):
    _description = _("Display VNFFG details")

    def get_parser(self, prog_name):
        parser = super(ShowVNFFG, self).get_parser(prog_name)
        parser.add_argument(
            _VNFFG,
            metavar="<VNFFG>",
            help=_('VNFFG to display (name or ID)')
        )
        return parser

    def take_action(self, parsed_args):
        client = self.app.client_manager.tackerclient
        obj_id = tackerV10.find_resourceid_by_name_or_id(
            client, _VNFFG, parsed_args.vnffg)
        obj = client.show_vnffg(obj_id)
        display_columns, columns = _get_columns(obj[_VNFFG])
        data = utils.get_item_properties(
            sdk_utils.DictModel(obj[_VNFFG]),
            columns,
            formatters=_formatters)
        return (display_columns, data)


class ListNFP(command.Lister):
    _description = ("List NFP(s) that belong to a given tenant.")

    def get_parser(self, prog_name):
        parser = super(ListNFP, self).get_parser(prog_name)
        parser.add_argument(
            '--vnffg-id',
            help=_('List NFP(s) with specific VNFFG ID'))
        return parser

    def take_action(self, parsed_args):
        client = self.app.client_manager.tackerclient
        _params = {}
        if parsed_args.vnffg_id:
            _params['vnffg_id'] = parsed_args.vnffg_id
        nfps = client.list('nfps', nfps_path, True, **_params)
        for nfp in nfps['nfps']:
            error_reason = nfp.get('error_reason', None)
            if error_reason and \
                    len(error_reason) > DEFAULT_ERROR_REASON_LENGTH:
                nfp['error_reason'] = error_reason[
                    :DEFAULT_ERROR_REASON_LENGTH]
                nfp['error_reason'] += '...'
        data = {}
        data['nfps'] = nfps['nfps']
        data = client.list_nfps()
        headers, columns = tacker_osc_utils.get_column_definitions(
            _attr_map_nfp, long_listing=None)
        return (headers,
                (utils.get_dict_properties(
                    s, columns,
                ) for s in data[_NFP + 's']))


class ShowNFP(command.ShowOne):
    _description = _("Display NFP details")

    def get_parser(self, prog_name):
        parser = super(ShowNFP, self).get_parser(prog_name)
        parser.add_argument(
            _NFP,
            metavar="<NFP>",
            help=_('NFP to display (name or ID)')
        )
        return parser

    def take_action(self, parsed_args):
        client = self.app.client_manager.tackerclient
        obj_id = tackerV10.find_resourceid_by_name_or_id(
            client, _NFP, parsed_args.nfp)
        obj = client.show_nfp(obj_id)
        display_columns, columns = _get_columns(obj[_NFP])
        data = utils.get_item_properties(
            sdk_utils.DictModel(obj[_NFP]),
            columns)
        return (display_columns, data)


class ListFC(command.Lister):
    _description = ("List flow classifier(s) that belong to a given tenant.")

    def get_parser(self, prog_name):
        parser = super(ListFC, self).get_parser(prog_name)
        parser.add_argument(
            '--nfp-id',
            help=_('List flow classifier(s) with specific nfp id'))
        parser.add_argument(
            '--tenant-id', metavar='TENANT_ID',
            help=_('The owner tenant ID or project ID'))
        return parser

    def take_action(self, parsed_args):
        client = self.app.client_manager.tackerclient
        _params = {}
        if parsed_args.nfp_id:
            _params['nfp_id'] = parsed_args.nfp_id
        if parsed_args.tenant_id:
            _params['tenant_id'] = parsed_args.tenant_id
        classifiers = client.list('classifiers', fcs_path, True,
                                  **_params)
        for classifier in classifiers['classifiers']:
            error_reason = classifier.get('error_reason', None)
            if error_reason and \
                    len(error_reason) > DEFAULT_ERROR_REASON_LENGTH:
                classifier['error_reason'] = error_reason[
                    :DEFAULT_ERROR_REASON_LENGTH]
                classifier['error_reason'] += '...'
        data = {}
        data['classifiers'] = classifiers['classifiers']
        data = client.list_classifiers()
        headers, columns = tacker_osc_utils.get_column_definitions(
            _attr_map_fc, long_listing=None)
        return (headers,
                (utils.get_dict_properties(
                    s, columns,
                ) for s in data[_FC + 's']))


class ShowFC(command.ShowOne):
    _description = _("Display flow classifier details")

    def get_parser(self, prog_name):
        parser = super(ShowFC, self).get_parser(prog_name)
        parser.add_argument(
            _FC,
            metavar="<Classifier ID>",
            help=_('Flow Classifier to display (name or ID)')
        )
        return parser

    def take_action(self, parsed_args):
        client = self.app.client_manager.tackerclient
        obj_id = tackerV10.find_resourceid_by_name_or_id(
            client, _FC, parsed_args.classifier)
        obj = client.show_classifier(obj_id)
        display_columns, columns = _get_columns(obj[_FC])
        data = utils.get_item_properties(
            sdk_utils.DictModel(obj[_FC]),
            columns,
            formatters=_formatters)
        return (display_columns, data)


class ListSFC(command.Lister):
    _description = ("List SFC(s) that belong to a given tenant.")

    def get_parser(self, prog_name):
        parser = super(ListSFC, self).get_parser(prog_name)
        parser.add_argument(
            '--nfp-id',
            help=_('List SFC(s) with specific nfp id'))
        parser.add_argument(
            '--tenant-id', metavar='TENANT_ID',
            help=_('The owner tenant ID or project ID'))
        return parser

    def take_action(self, parsed_args):
        client = self.app.client_manager.tackerclient
        _params = {}
        if parsed_args.nfp_id:
            _params['nfp_id'] = parsed_args.nfp_id
        if parsed_args.tenant_id:
            _params['tenant_id'] = parsed_args.tenant_id
        sfcs = client.list('sfcs', sfcs_path, True, **_params)
        for chain in sfcs['sfcs']:
            error_reason = chain.get('error_reason', None)
            if error_reason and \
                    len(error_reason) > DEFAULT_ERROR_REASON_LENGTH:
                chain['error_reason'] = error_reason[
                    :DEFAULT_ERROR_REASON_LENGTH]
                chain['error_reason'] += '...'
        data = {}
        data['sfcs'] = sfcs['sfcs']
        headers, columns = tacker_osc_utils.get_column_definitions(
            _attr_map_sfc, long_listing=None)
        return (headers,
                (utils.get_dict_properties(
                    s, columns,
                ) for s in data[_SFC + 's']))


class ShowSFC(command.ShowOne):
    _description = _("Display SFC details")

    def get_parser(self, prog_name):
        parser = super(ShowSFC, self).get_parser(prog_name)
        parser.add_argument(
            _SFC,
            metavar="<SFC>",
            help=_('SFC to display (name or ID)')
        )
        return parser

    def take_action(self, parsed_args):
        client = self.app.client_manager.tackerclient
        obj_id = tackerV10.find_resourceid_by_name_or_id(
            client, _SFC, parsed_args.sfc)
        obj = client.show_sfc(obj_id)
        display_columns, columns = _get_columns(obj[_SFC])
        data = utils.get_item_properties(
            sdk_utils.DictModel(obj[_SFC]),
            columns,
            formatters=_formatters)
        return (display_columns, data)
