#    Copyright 2014 Mirantis, 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.

from itertools import groupby
from operator import attrgetter

from fuelclient.cli.actions.base import Action
from fuelclient.cli.actions.base import check_all
from fuelclient.cli.actions.base import check_any
import fuelclient.cli.arguments as Args
from fuelclient.cli.arguments import group
from fuelclient.cli import error
from fuelclient.cli.formatting import format_table
from fuelclient.objects.environment import Environment
from fuelclient.objects.node import Node
from fuelclient.objects.node import NodeCollection


class NodeAction(Action):
    """List and assign available nodes to environments
    """
    action_name = "node"
    acceptable_keys = ("id", "status", "name", "cluster", "ip",
                       "mac", "roles", "pending_roles", "online", "group_id")

    def __init__(self):
        super(NodeAction, self).__init__()
        self.args = [
            Args.get_env_arg(),
            group(
                Args.get_list_arg("List all nodes."),
                Args.get_set_arg("Set role for specific node."),
                Args.get_delete_arg("Delete specific node from environment."),
                Args.get_attributes_arg("Node attributes."),
                Args.get_network_arg("Node network configuration."),
                Args.get_disk_arg("Node disk configuration."),
                Args.get_deploy_arg("Deploy specific nodes."),
                Args.get_hostname_arg("Set node hostname."),
                Args.get_node_name_arg("Set node name."),
                Args.get_delete_from_db_arg(
                    "Delete specific nodes only from fuel db.\n"
                    "User should still delete node from cobbler"),
                Args.get_provision_arg("Provision specific nodes."),
            ),
            group(
                Args.get_default_arg(
                    "Get default configuration of some node"),
                Args.get_download_arg(
                    "Download configuration of specific node"),
                Args.get_upload_arg(
                    "Upload configuration to specific node")
            ),
            Args.get_dir_arg(
                "Select directory to which download node attributes"),
            Args.get_node_arg("Node id."),
            Args.get_force_arg("Bypassing parameter validation."),
            Args.get_all_arg("Select all nodes."),
            Args.get_role_arg("Role to assign for node."),
            group(
                Args.get_skip_tasks(),
                Args.get_tasks()
            ),
            Args.get_graph_endpoint(),
            Args.get_graph_startpoint(),
        ]

        self.flag_func_map = (
            ("set", self.set),
            ("delete", self.delete),
            ("network", self.attributes),
            ("disk", self.attributes),
            ("deploy", self.start),
            ("provision", self.start),
            ("hostname", self.set_hostname),
            ("name", self.set_name),
            ("delete-from-db", self.delete_from_db),
            ("tasks", self.execute_tasks),
            ("skip", self.execute_tasks),
            ("end", self.execute_tasks),
            ("start", self.execute_tasks),
            ("attributes", self.node_attributes),
            (None, self.list)
        )

    @check_all("node", "role", "env")
    def set(self, params):
        """Assign some nodes to environment with with specific roles:
                fuel --env 1 node set --node 1 --role controller
                fuel --env 1 node set --node 2,3,4 --role compute,cinder
        """
        env = Environment(params.env)
        nodes = Node.get_by_ids(params.node)
        roles = map(str.lower, params.role)
        env.assign(nodes, roles)
        self.serializer.print_to_output(
            {},
            "Nodes {0} with roles {1} "
            "were added to environment {2}"
            .format(params.node, roles, params.env)
        )

    @check_any("node", "env")
    def delete(self, params):
        """Remove some nodes from environment:
                fuel --env 1 node remove --node 2,3

           Remove nodes no matter to which environment they were assigned:
                fuel node remove --node 2,3,6,7

           Remove all nodes from some environment:
                fuel --env 1 node remove --all
        """
        if params.env:
            env = Environment(params.env)
            if params.node:
                env.unassign(params.node)
                self.serializer.print_to_output(
                    {},
                    "Nodes with ids {0} were removed "
                    "from environment with id {1}."
                    .format(params.node, params.env))
            else:
                if params.all:
                    env.unassign_all()
                else:
                    raise error.ArgumentException(
                        "You have to select which nodes to remove "
                        "with --node-id. Try --all for removing all nodes."
                    )
                self.serializer.print_to_output(
                    {},
                    "All nodes from environment with id {0} were removed."
                    .format(params.env))
        else:
            nodes = map(Node, params.node)
            for env_id, _nodes in groupby(nodes, attrgetter("env_id")):
                list_of_nodes = [n.id for n in _nodes]
                if env_id:
                    Environment(env_id).unassign(list_of_nodes)
                    self.serializer.print_to_output(
                        {},
                        "Nodes with ids {0} were removed "
                        "from environment with id {1}."
                        .format(list_of_nodes, env_id)
                    )
                else:
                    self.serializer.print_to_output(
                        {},
                        "Nodes with ids {0} aren't added to "
                        "any environment.".format(list_of_nodes)
                    )

    @check_all("node")
    @check_any("default", "download", "upload")
    def attributes(self, params):
        """Download current or default disk, network,
           configuration for some node:
                fuel node --node-id 2 --disk --default
                fuel node --node-id 2 --network --download \\
                --dir path/to/directory

           Upload disk, network, configuration for some node:
                fuel node --node-id 2 --network --upload
                fuel node --node-id 2 --disk --upload --dir path/to/directory
        """
        nodes = Node.get_by_ids(params.node)
        attribute_type = "interfaces" if params.network else "disks"
        attributes = []
        files = []
        if params.default:
            for node in nodes:
                default_attribute = node.get_default_attribute(attribute_type)
                file_path = node.write_attribute(
                    attribute_type,
                    default_attribute,
                    params.dir,
                    serializer=self.serializer
                )
                files.append(file_path)
                attributes.append(default_attribute)
            message = "Default node attributes for {0} were written" \
                      " to:\n{1}".format(attribute_type, "\n".join(files))
        elif params.upload:
            for node in nodes:
                attribute = node.read_attribute(
                    attribute_type,
                    params.dir,
                    serializer=self.serializer
                )
                node.upload_node_attribute(
                    attribute_type,
                    attribute
                )
                attributes.append(attribute)
            message = "Node attributes for {0} were uploaded" \
                      " from {1}".format(attribute_type, params.dir)
        else:
            for node in nodes:
                downloaded_attribute = node.get_attribute(attribute_type)
                file_path = node.write_attribute(
                    attribute_type,
                    downloaded_attribute,
                    params.dir,
                    serializer=self.serializer
                )
                attributes.append(downloaded_attribute)
                files.append(file_path)
            message = "Node attributes for {0} were written" \
                      " to:\n{1}".format(attribute_type, "\n".join(files))
        print(message)

    def get_env_id(self, node_collection):
        env_ids = set(n.env_id for n in node_collection)
        if len(env_ids) != 1:
            raise error.ActionException(
                "Inputed nodes assigned to multiple environments!")
        else:
            return env_ids.pop()

    @check_all("node")
    def start(self, params):
        """Deploy/Provision some node:
                fuel node --node-id 2 --provision
                fuel node --node-id 2 --deploy
        """
        node_collection = NodeCollection.init_with_ids(params.node)
        method_type = "deploy" if params.deploy else "provision"
        env_id_to_start = self.get_env_id(node_collection)

        if not env_id_to_start:
            raise error.ActionException(
                "Input nodes are not assigned to any environment!")

        task = Environment(env_id_to_start).install_selected_nodes(
            method_type, node_collection.collection)

        self.serializer.print_to_output(
            task.data,
            "Started {0}ing {1}."
            .format(method_type, node_collection))

    @check_all("node")
    def execute_tasks(self, params):
        """Execute deployment tasks
                fuel node --node 2 --tasks hiera netconfig
                fuel node --node 2 --skip hiera netconfig
                fuel node --node 2 --skip rsync --end pre_deployment_end
                fuel node --node 2 --end netconfig
                fuel node --node 2 --start hiera --end neutron
                fuel node --node 2 --start post_deployment_start
        """
        node_collection = NodeCollection.init_with_ids(params.node)
        env_id_to_start = self.get_env_id(node_collection)

        env = Environment(env_id_to_start)

        tasks = params.tasks or None

        if params.skip or params.end or params.start:
            tasks = env.get_tasks(
                skip=params.skip,
                end=params.end,
                start=params.start,
                include=tasks)

        if not tasks:
            self.serializer.print_to_output({}, "Nothing to run.")
            return

        task = env.execute_tasks(node_collection.collection, tasks=tasks)

        self.serializer.print_to_output(
            task.data,
            "Started tasks {0} for nodes {1}.".format(tasks, node_collection))

    def list(self, params):
        """To list all available nodes:
                fuel node

            To filter them by environment:
                fuel --env-id 1 node

            It's Possible to manipulate nodes with their short mac addresses:
                fuel node --node-id 80:ac
                fuel node remove --node-id 80:ac,5d:a2
        """
        if params.node:
            node_collection = NodeCollection.init_with_ids(params.node)
        else:
            node_collection = NodeCollection.get_all()
        if params.env:
            node_collection.filter_by_env_id(int(params.env))
        self.serializer.print_to_output(
            node_collection.data,
            format_table(
                node_collection.data,
                acceptable_keys=self.acceptable_keys,
                column_to_join=("roles", "pending_roles")
            )
        )

    @check_all("node")
    def delete_from_db(self, params):
        """To delete nodes from fuel db:
                fuel node --node-id 1 --delete-from-db
                fuel node --node-id 1 2 --delete-from-db
            (this works only for offline nodes)
                fuel node --node-id 1 --delete-from-db --force
            (this forces deletion of nodes regardless of their state)
        """
        if not params.force:
            node_collection = NodeCollection.init_with_ids(params.node)

            online_nodes = [node for node in node_collection.data
                            if node['online']]

            if online_nodes:
                raise error.ActionException(
                    "Nodes with ids {0} cannot be deleted from cluster "
                    "because they are online. You might want to use the "
                    "--force option.".format(
                        [node['id'] for node in online_nodes]))

        NodeCollection.delete_by_ids(params.node)

        self.serializer.print_to_output(
            {},
            "Nodes with ids {0} have been deleted from Fuel db.".format(
                params.node)
        )

    @staticmethod
    def _get_one_node(params):
        """Ensures that only one node was passed in the command and returns it.

        :raises ArgumentException: When more than 1 node provided.
        """
        if len(params.node) > 1:
            raise error.ArgumentException(
                "You should select only one node to change.")

        return Node(params.node[0])

    @check_all("node", "name")
    def set_name(self, params):
        """To set node name:
                fuel node --node-id 1 --name NewName
        """
        node = self._get_one_node(params)
        node.set({"name": params.name})
        self.serializer.print_to_output(
            {},
            u"Name for node with id {0} has been changed to {1}."
            .format(node.id, params.name)
        )

    @check_all("node", "hostname")
    def set_hostname(self, params):
        """To set node hostname:
                fuel node --node-id 1 --hostname ctrl-01
        """
        node = self._get_one_node(params)
        node.set({"hostname": params.hostname})
        self.serializer.print_to_output(
            {},
            "Hostname for node with id {0} has been changed to {1}."
            .format(node.id, params.hostname)
        )

    @check_all("node")
    @check_any("upload", "download")
    def node_attributes(self, params):
        """Download node attributes for specified node:
            fuel node --node-id 1 --attributes --download [--dir download-dir]

        Upload node attributes for specified node
            fuel node --node-id 1 --attributes --upload [--dir upload-dir]

        """
        node = self._get_one_node(params)
        if params.upload:
            data = node.read_attribute(
                'attributes', params.dir, serializer=self.serializer)
            node.update_node_attributes(data)
            self.serializer.print_to_output(
                {},
                "Attributes for node {0} were uploaded."
                .format(node.id))
        elif params.download:
            attributes = node.get_node_attributes()
            file_path = node.write_attribute(
                'attributes', attributes,
                params.dir, serializer=self.serializer)
            self.serializer.print_to_output(
                {},
                "Attributes for node {0} were written to {1}"
                .format(node.id, file_path))

        else:
            raise error.ArgumentException(
                "--upload or --download action should be specified.")
