#!/usr/bin/env python3

#
# Licensed to the Apache Software Foundation (ASF) undeugr one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
#

import sys
import json
import re
from collections.abc import Mapping, Sequence

import qpid_dispatch_site
qpid_dispatch_site.populate_pythonpath()

from qpid_dispatch.management.client import Node
from qpid_dispatch_internal.tools.command import (UsageError, _qdmanage_parser, check_args,
                                                  main, opts_ssl_domain, opts_url, opts_sasl)
from qpid_dispatch_internal.management.qdrouter import QdSchema

INTEGER_TYPE = "integer"


def attr_split(attrstr, qd_schema, type):
    """Split an attribute string of the form name=value or name to indicate None"""
    nv = attrstr.split("=", 1)
    if len(nv) == 1:
        return [nv[0], None]
    else:
        if nv[1] == "true":
            nv[1] = True
        elif nv[1] == "false":
            nv[1] = False
        elif type and qd_schema.entity_type(type) and qd_schema.entity_type(type).attribute(nv[0]).type == INTEGER_TYPE:
            nv[1] = int(nv[1])

        return nv


class QdManage():
    def __init__(self):
        self.qd_schema = QdSchema()
        self.prefix = 'org.apache.qpid.dispatch.'
        self.operations = ['QUERY', 'CREATE', 'READ', 'UPDATE', 'DELETE',
                           'GET-TYPES', 'GET-OPERATIONS', 'GET-ATTRIBUTES', 'GET-ANNOTATIONS',
                           'GET-MGMT-NODES', 'GET-SCHEMA', 'GET-LOG']
        self.op = _qdmanage_parser(self.operations)

    def clean_opts(self):
        attr_type = self.opts.type
        if attr_type:
            self.opts.type = self.long_type(attr_type)

    def run(self, argv):
        # Make all args unicode to avoid encoding arg values as AMQP bytes.
        al = [x.decode() if not isinstance(x, str) else x
              for x in argv[1:]]
        self.opts, self.args = self.op.parse_known_args(al)
        self.clean_opts()
        if self.opts.indent == -1:
            self.opts.indent = None
        if len(self.args) == 0:
            raise UsageError("No operation specified")
        self.node = Node.connect(opts_url(self.opts), self.opts.router, self.opts.timeout,
                                 opts_ssl_domain(self.opts),
                                 opts_sasl(self.opts),
                                 edge_router=self.opts.edge_router)

        operation = self.args.pop(0)
        method = operation.lower().replace('-', '_')
        if operation.upper() in self.operations and hasattr(self, method):
            getattr(self, method)()  # Built-in operation
        else:
            self.operation(operation)  # Custom operation

    def main(self, argv):
        return main(self.run, argv, self.op)

    def print_json(self, data):
        """Print data as JSON"""
        print("%s" % json.dumps(data, indent=self.opts.indent))

    def print_result(self, result):
        """Print a string result as-is, else try json dump, else print as-is"""
        if not result:
            return
        if isinstance(result, str):
            print("%s" % result)
        else:
            try:
                self.print_json(result)
            except ValueError:
                print("%s" % result)

    def long_type(self, type):
        if not type or "." in type:
            return type

        if type in ('link', 'node'):
            return self.prefix + "router." + type

        if type in ('linkRoute', 'address', 'autoLink'):
            return self.prefix + "router.config." + type

        return self.prefix + type

    def call_node(self, method, *argnames, **kwargs):
        """Call method on node, use opts named in argnames"""
        names = set(argnames)

        attributes = kwargs.get('attributes')
        if attributes and attributes.get('type'):
            attributes['type'] = self.long_type(attributes['type'])

        for k in self.opts.__dict__:
            if k in names and hasattr(self.opts, k):
                kwargs[k] = getattr(self.opts, k)

        return getattr(self.node, method)(**kwargs)

    def call_bulk(self, func):
        """Call function for attributes from stdin or --attributes option"""
        if self.opts.stdin:
            data = json.load(sys.stdin)
            if isinstance(data, Mapping):
                self.print_json(func(data).attributes)
            elif isinstance(data, Sequence):
                self.print_json([func(attrs).attributes for attrs in data])
            else:
                raise ValueError("stdin is not a JSON map or list")
        else:
            self.print_json(func(self.opts.attributes).attributes)

    def query(self):
        """query [ATTR...]          Print attributes of entities."""
        if self.args:
            self.opts.attribute_names = self.args
        result = self.call_node('query', 'type', 'attribute_names')
        self.print_json(result.get_dicts(clean=True))

    def create(self):
        """create [ATTR=VALUE...]   Create a new entity."""
        if self.args:
            self.opts.attributes = dict(attr_split(arg, self.qd_schema, self.opts.type) for arg in self.args)
        self.call_bulk(lambda attrs: self.call_node('create', 'type', 'name', attributes=attrs))

    def read(self):
        """read                     Print attributes of selected entity."""
        check_args(self.args, 0)
        self.print_json(self.call_node('read', 'type', 'name', 'identity').attributes)

    def update(self):
        """update [ATTR=VALUE...]   Update an entity."""
        if self.args:
            self.opts.attributes = dict(attr_split(arg, self.qd_schema, self.opts.type) for arg in self.args)
        self.call_bulk(
            lambda attrs: self.call_node('update', 'type', 'name', 'identity', attributes=attrs))

    def delete(self):
        """delete                   Delete an entity"""
        check_args(self.args, 0)
        self.call_node('delete', 'type', 'name', 'identity')

    def get_types(self):
        """get-types [TYPE]         List entity types with their base types."""
        if not self.opts.type:
            self.opts.type = check_args(self.args, 1)[0]
        self.print_json(self.call_node('get_types', 'type'))

    def get_annotations(self):
        """get-annotations [TYPE]   List entity types with the annotations they implement."""
        if not self.opts.type:
            self.opts.type = check_args(self.args, 1)[0]
        self.print_json(self.call_node('get_annotations', 'type'))

    def get_attributes(self):
        """get-attributes [TYPE]    List entity types with their attributes."""
        if not self.opts.type:
            self.opts.type = check_args(self.args, 1)[0]
        self.print_json(self.call_node('get_attributes', 'type'))

    def get_operations(self):
        """get-operations [TYPE]    List entity types with their operations."""
        if not self.opts.type:
            self.opts.type = check_args(self.args, 1)[0]
        self.print_json(self.call_node('get_operations', 'type'))

    def get_mgmt_nodes(self):
        """get-mgmt-nodes           List of other known management nodes"""
        check_args(self.args, 0)
        self.print_json(self.call_node('get_mgmt_nodes'))

    def json_arg(self, value):
        try:
            return json.loads(value)
        except ValueError as e:
            if not re.search(r'["{}\[\]]', value):  # Doesn't look like attempted JSON.
                return value    # Just treat as plain string
            raise ValueError("Invalid JSON '%s': %s" % (value, e))

    def operation(self, operation):
        """operation [ATTR=VALUE...]   Call custom operation with ATTR=VALUE as request properties. Use --body and --properties if specified."""
        properties = dict(attr_split(arg, self.qd_schema, self.opts.type) for arg in self.args or [])
        if self.opts.properties:
            properties.update(self.json_arg(self.opts.properties))
        body = None
        if self.opts.body:
            body = self.json_arg(self.opts.body)
        request = self.call_node('request', 'type', 'name', 'identity', operation=operation, body=body, **properties)
        self.print_result(self.node.call(request).body)


if __name__ == "__main__":
    sys.exit(QdManage().main(sys.argv))
