# -*- coding: utf8 -*-
"""
.. module:: burpui.api.clients
    :platform: Unix
    :synopsis: Burp-UI clients api module.

.. moduleauthor:: Ziirish <hi+burpui@ziirish.me>

"""
from . import api, cache_key, force_refresh
from ..engines.server import BUIServer  # noqa
from .custom import fields, Resource
from .client import ClientLabels
from ..ext.cache import cache
from ..exceptions import BUIserverException
from ..decorators import browser_cache
from ..filter import mask

from flask import current_app, g
from flask_login import current_user

bui = current_app  # type: BUIServer
ns = api.namespace("clients", "Clients methods")


@ns.route(
    "/running",
    "/<server>/running",
    "/running/<client>",
    "/<server>/running/<client>",
    endpoint="running_clients",
)
@ns.doc(
    params={
        "server": "Which server to collect data from when in multi-agent mode",
        "client": "Client name",
    },
)
class RunningClients(Resource):
    """The :class:`burpui.api.clients.RunningClients` resource allows you to
    retrieve a list of clients that are currently running a backup.

    This resource is part of the :mod:`burpui.api.clients` module.

    An optional ``GET`` parameter called ``serverName`` is supported when running
    in multi-agent mode.
    """

    parser = ns.parser()
    parser.add_argument(
        "serverName", help="Which server to collect data from when in multi-agent mode"
    )

    @ns.expect(parser)
    def get(self, client=None, server=None):
        """Returns a list of clients currently running a backup

        **GET** method provided by the webservice.

        The *JSON* returned is:
        ::

            [
              'client1',
              'client2'
            ]


        The output is filtered by the :mod:`burpui.misc.acl` module so that you
        only see stats about the clients you are authorized to see.

        :param server: Which server to collect data from when in multi-agent mode
        :type server: str

        :param client: Ask a specific client in order to know if it is running a backup
        :type client: str

        :returns: The *JSON* described above.
        """
        server = server or self.parser.parse_args()["serverName"]
        return self._running_clients(None, client, server)

    def _running_clients(self, res, client, server):
        if client:
            if mask.has_filters(current_user):
                if not mask.is_client_allowed(current_user, client):
                    return []

            if bui.client.is_backup_running(client, server):
                return [client]
            else:
                return []

        running = res or bui.client.is_one_backup_running(server)
        # Manage ACL
        if mask.has_filters(current_user):
            if isinstance(running, dict):
                ret = {}

                def __extract_running_clients(serv):
                    try:
                        clients = [
                            x["name"]
                            for x in bui.client.get_all_clients(
                                serv, last_attempt=False
                            )
                        ]
                    except BUIserverException:
                        clients = []
                    allowed = [
                        x
                        for x in clients
                        if mask.is_client_allowed(current_user, x, serv)
                    ]
                    return [x for x in running[serv] if x in allowed]

                if server:
                    return __extract_running_clients(server)

                for serv in bui.client.servers:
                    ret[serv] = __extract_running_clients(serv)
                return ret
            else:
                try:
                    clients = [
                        x["name"]
                        for x in bui.client.get_all_clients(server, last_attempt=False)
                    ]
                except BUIserverException:
                    clients = []
                allowed = [
                    x
                    for x in clients
                    if mask.is_client_allowed(current_user, x, server)
                ]
                running = [x for x in running if x in allowed]
        elif server and isinstance(running, dict):
            return running.get(server, [])
        return running


@ns.route("/backup-running", "/<server>/backup-running", endpoint="running_backup")
@ns.doc(
    params={
        "server": "Which server to collect data from when in multi-agent mode",
    }
)
class RunningBackup(Resource):
    """The :class:`burpui.api.clients.RunningBackup` resource allows you to
    access the status of the server in order to know if there is a running
    backup currently.

    This resource is part of the :mod:`burpui.api.clients` module.
    """

    running_fields = ns.model(
        "Running",
        {
            "running": fields.Boolean(
                required=True, description="Is there a backup running right now"
            ),
        },
    )

    @ns.marshal_with(running_fields, code=200, description="Success")
    def get(self, server=None):
        """Tells if a backup is running right now

        **GET** method provided by the webservice.

        The *JSON* returned is:
        ::

            {
                "running": false
            }


        The output is filtered by the :mod:`burpui.misc.acl` module so that you
        only see stats about the clients you are authorized to.

        :param server: Which server to collect data from when in multi-agent mode
        :type server: str

        :returns: The *JSON* described above.
        """
        return {
            "running": self._is_one_backup_running(
                bui.client.is_one_backup_running(server), server
            )
        }

    def _is_one_backup_running(self, res, server):
        """Check if a backup is running"""
        # Manage ACL
        if mask.has_filters(current_user):
            if isinstance(res, dict):
                new = {}
                for serv in bui.client.servers:
                    try:
                        clients = [
                            x["name"]
                            for x in bui.client.get_all_clients(
                                serv, last_attempt=False
                            )
                        ]
                    except BUIserverException:
                        clients = []
                    allowed = [
                        x
                        for x in clients
                        if mask.is_client_allowed(current_user, x, serv)
                    ]
                    new[serv] = [x for x in res[serv] if x in allowed]
                res = new
            else:
                try:
                    clients = [
                        x["name"]
                        for x in bui.client.get_all_clients(server, last_attempt=False)
                    ]
                except BUIserverException:
                    clients = []
                allowed = [
                    x
                    for x in clients
                    if mask.is_client_allowed(current_user, x, server)
                ]
                res = [x for x in res if x in allowed]
        running = False
        if isinstance(res, dict):
            for (_, run) in res.items():
                running = running or (len(run) > 0)
                if running:
                    break
        else:
            running = len(res) > 0

        return running


@ns.route("/report", "/<server>/report", endpoint="clients_report")
@ns.doc(
    params={
        "server": "Which server to collect data from when in multi-agent mode",
    },
)
class ClientsReport(Resource):
    """The :class:`burpui.api.clients.ClientsReport` resource allows you to
    access general reports about your clients.

    This resource is part of the :mod:`burpui.api.clients` module.

    An optional ``GET`` parameter called ``serverName`` is supported when running
    in multi-agent mode.
    """

    parser = ns.parser()
    parser.add_argument(
        "serverName", help="Which server to collect data from when in multi-agent mode"
    )
    parser.add_argument(
        "limit", type=int, default=8, help="Number of elements to return"
    )
    parser.add_argument(
        "aggregation",
        help="What aggregation to operate",
        default="number",
        choices=("number", "files", "size", "none"),
    )

    translation = {
        "number": "number",
        "files": "total",
        "size": "totsize",
    }

    stats_fields = ns.model(
        "ClientsStats",
        {
            "total": fields.Integer(
                required=True, description="Number of files", default=0
            ),
            "totsize": fields.Integer(
                required=True,
                description="Total size occupied by all the backups of this client",
                default=0,
            ),
            "os": fields.String(
                required=True, description="OS of the client", default="unknown"
            ),
        },
    )
    client_fields = ns.model(
        "ClientsReport",
        {
            "name": fields.String(required=True, description="Client name"),
            "stats": fields.Nested(stats_fields, required=True),
        },
    )
    backup_fields = ns.model(
        "ClientsBackup",
        {
            "name": fields.String(required=True, description="Client name"),
            "number": fields.Integer(
                required=True, description="Number of backups on this client", default=0
            ),
        },
    )
    report_fields = ns.model(
        "Report",
        {
            "backups": fields.Nested(backup_fields, as_list=True, required=True),
            "clients": fields.Nested(client_fields, as_list=True, required=True),
        },
    )

    @cache.cached(timeout=1800, key_prefix=cache_key, unless=force_refresh)
    @ns.marshal_with(report_fields, code=200, description="Success")
    @ns.expect(parser)
    @ns.doc(
        responses={
            403: "Insufficient permissions",
            500: "Internal failure",
        },
    )
    @browser_cache(1800)
    def get(self, server=None):
        """Returns a global report about all the clients of a given server

        **GET** method provided by the webservice.

        The *JSON* returned is:
        ::

            {
              "backups": [
                {
                  "name": "client1",
                  "number": 15
                },
                {
                  "name": "client2",
                  "number": 1
                }
              ],
              "clients": [
                {
                  "name": "client1",
                  "stats": {
                    "total": 296377,
                    "totsize": 57055793698,
                    "os": "unknown"
                  }
                },
                {
                  "name": "client2",
                  "stats": {
                    "total": 3117,
                    "totsize": 5345361,
                    "os": "windows"
                  }
                }
              ]
            }


        The output is filtered by the :mod:`burpui.misc.acl` module so that you
        only see stats about the clients you are authorized to.

        :param server: Which server to collect data from when in multi-agent mode
        :type server: str

        :returns: The *JSON* described above
        """

        server = server or self.parser.parse_args()["serverName"]
        self._check_acl(server)
        return self._get_clients_reports(server=server)

    def _check_acl(self, server):
        # Manage ACL
        if (
            not bui.config["STANDALONE"]
            and not current_user.is_anonymous
            and (
                not current_user.acl.is_admin()
                and not current_user.acl.is_server_allowed(server)
            )
        ):
            self.abort(403, "Sorry, you don't have any rights on this server")

    def _get_clients_reports(self, res=None, server=None):
        args = self.parser.parse_args()
        limit = args["limit"]
        aggregation = self.translation.get(args["aggregation"], "number")
        ret = self._parse_clients_reports(res, server)
        backups = backups_orig = ret.get("backups", [])
        clients = clients_orig = ret.get("clients", [])
        aggregate = False
        if limit > 1:
            limit -= 1
            aggregate = True
        if aggregation == "none":
            aggregate = False
            limit = 0
        # limit the number of elements to return so the graphs stay readable
        if len(backups) > limit and limit > 0:
            if aggregation == "number":
                backups = (
                    sorted(backups, key=lambda x: x.get("number"), reverse=True)
                )[:limit]
            else:
                clients = (
                    sorted(
                        clients,
                        key=lambda x: x.get("stats", {}).get(aggregation),
                        reverse=True,
                    )
                )[:limit]
        else:
            aggregate = False
        if aggregation == "number":
            clients_name = [x.get("name") for x in backups]
            ret["backups"] = backups
            ret["clients"] = [x for x in clients_orig if x.get("name") in clients_name]
        else:
            clients_name = [x.get("name") for x in clients]
            ret["clients"] = clients
            ret["backups"] = [x for x in backups_orig if x.get("name") in clients_name]
        if aggregate:
            backups = {"name": "others", "number": 0}
            for client in backups_orig:
                if client.get("name") not in clients_name:
                    backups["number"] += client.get("number", 0)

            complement = {
                "name": "others",
                "stats": {"total": 0, "totsize": 0, "os": None},
            }
            # TODO: fix OS aggregation
            for client in clients_orig:
                if client.get("name") not in clients_name:
                    complement["stats"]["total"] += client.get("stats", {}).get(
                        "total", 0
                    )
                    complement["stats"]["totsize"] += client.get("stats", {}).get(
                        "totsize", 0
                    )
                    os = client.get("stats", {}).get("os", "unknown")
                    if not complement["stats"]["os"]:
                        complement["stats"]["os"] = os
                    elif os != complement["stats"]["os"]:
                        complement["stats"]["os"] = "unknown"

            ret["clients"].append(complement)
            ret["backups"].append(backups)

        return ret

    def _parse_clients_reports(self, res=None, server=None):
        if not res:
            try:
                clients = bui.client.get_all_clients(agent=server, last_attempt=False)
            except BUIserverException as e:
                self.abort(500, str(e))
            if mask.has_filters(current_user):
                clients = [
                    x
                    for x in clients
                    if mask.is_client_allowed(current_user, x["name"], server)
                ]
            return bui.client.get_clients_report(clients, server)
        if bui.config["STANDALONE"]:
            ret = res
        else:
            ret = res.get(server, {})
        if mask.has_filters(current_user):
            ret["backups"] = [
                x
                for x in ret.get("backups", [])
                if mask.is_client_allowed(current_user, x.get("name"), server)
            ]
            ret["clients"] = [
                x
                for x in ret.get("clients", [])
                if mask.is_client_allowed(current_user, x.get("name"), server)
            ]
        return ret


@ns.route("/stats", "/<server>/stats", endpoint="clients_stats")
@ns.doc(
    params={
        "server": "Which server to collect data from when in multi-agent mode",
    },
)
class ClientsStats(Resource):
    """The :class:`burpui.api.clients.ClientsStats` resource allows you to
    access general statistics about your clients.

    This resource is part of the :mod:`burpui.api.clients` module.

    An optional ``GET`` parameter called ``serverName`` is supported when running
    in multi-agent mode.
    """

    parser = ns.parser()
    parser.add_argument(
        "serverName", help="Which server to collect data from when in multi-agent mode"
    )
    client_fields = ns.model(
        "ClientsStatsSingle",
        {
            "last": fields.DateTime(
                required=True, dt_format="iso8601", description="Date of last backup"
            ),
            "last_attempt": fields.DateTime(
                dt_format="iso8601", description="Date of last backup attempt"
            ),
            "name": fields.String(required=True, description="Client name"),
            "state": fields.LocalizedString(
                required=True,
                description="Current state of the client (idle, backup, etc.)",
            ),
            "phase": fields.String(description="Phase of the current running backup"),
            "percent": fields.Integer(description="Percentage done", default=0),
            "labels": fields.List(fields.String, description="List of labels"),
        },
    )

    @cache.cached(timeout=1800, key_prefix=cache_key, unless=force_refresh)
    @ns.marshal_list_with(client_fields, code=200, description="Success")
    @ns.expect(parser)
    @ns.doc(
        responses={
            403: "Insufficient permissions",
            500: "Internal failure",
        },
    )
    @browser_cache(1800)
    def get(self, server=None):
        """Returns a list of clients with their states

        **GET** method provided by the webservice.

        The *JSON* returned is:
        ::

            {
              [
                {
                  "last": "2015-05-17 11:40:02",
                  "last_attempt": "2015-05-17 11:40:02",
                  "name": "client1",
                  "state": "idle",
                  "phase": "phase1",
                  "percent": 12,
                  "labels": [
                    "toto"
                  ]
                },
                {
                  "last": "never",
                  "last_attempt": "never",
                  "name": "client2",
                  "state": "idle",
                  "phase": "phase2",
                  "percent": 42,
                  "labels": [
                    "titi"
                  ]
                }
              ]
            }


        The output is filtered by the :mod:`burpui.misc.acl` module so that you
        only see stats about the clients you are authorized to.

        :param server: Which server to collect data from when in multi-agent mode
        :type server: str

        :returns: The *JSON* described above
        """

        server = server or self.parser.parse_args()["serverName"]
        try:
            if (
                not bui.config["STANDALONE"]
                and not current_user.is_anonymous
                and (
                    not current_user.acl.is_admin()
                    and not current_user.acl.is_server_allowed(server)
                )
            ):
                self.abort(403, "Sorry, you don't have any rights on this server")
            jso = bui.client.get_all_clients(agent=server)
            if mask.has_filters(current_user):
                jso = [
                    x
                    for x in jso
                    if mask.is_client_allowed(current_user, x["name"], server)
                ]
        except BUIserverException as e:
            self.abort(500, str(e))
        ret = []
        for client in jso:
            tmp_client = client
            try:
                labels = ClientLabels._get_labels(client["name"], server)
            except BUIserverException as exp:
                self.abort(500, str(exp))
            tmp_client["labels"] = labels
            ret.append(tmp_client)
            if tmp_client["state"] != "idle":
                g.DONOTCACHE = True

        return ret


@ns.route("/all", "/<server>/all", endpoint="clients_all")
@ns.doc(
    params={
        "server": "Which server to collect data from when in multi-agent mode",
    },
)
class AllClients(Resource):
    """The :class:`burpui.api.clients.AllClients` resource allows you to
    retrieve a list of all clients with their associated server (if any).

    An optional ``GET`` parameter called ``serverName`` is supported when
    running in multi-agent mode.
    """

    parser = ns.parser()
    parser.add_argument(
        "serverName", help="Which server to collect data from when in multi-agent mode"
    )
    parser.add_argument(
        "user", help="For which user do we want the data (only works for admins"
    )
    client_fields = ns.model(
        "AllClients",
        {
            "name": fields.String(required=True, description="Client name"),
            "agent": fields.String(
                required=False, default=None, description="Associated Agent name"
            ),
        },
    )

    @cache.cached(timeout=1800, key_prefix=cache_key, unless=force_refresh)
    @ns.marshal_list_with(client_fields, code=200, description="Success")
    @ns.expect(parser)
    @ns.doc(
        responses={
            200: "Success",
            403: "Insufficient permissions",
        },
    )
    @browser_cache(1800)
    def get(self, server=None):
        """Returns a list of all clients with their associated Agent if any

        **GET** method provided by the webservice.

        The *JSON* returned is:

        ::

            [
              {
                "name": "client1",
                "agent": "agent1"
              },
              {
                "name": "client2",
                "agent": "agent1"
              },
              {
                "name": "client3",
                "agent": "agent2"
              }
            ]


        The output is filtered by the :mod:`burpui.misc.acl` module so that you
        only see stats about the clients you are authorized to.

        :param server: Which server to collect data from when in multi-agent mode
        :type server: str

        :returns: The *JSON* described above
        """
        ret = []
        is_admin = True
        args = self.parser.parse_args()
        server = server or args["serverName"]

        is_admin = current_user.is_anonymous or current_user.acl.is_admin()
        is_moderator = current_user.is_anonymous or current_user.acl.is_moderator()

        user = (
            (args.get("user", current_user.name) or current_user.name)
            if is_admin or is_moderator
            else current_user.name
        )

        # drop privileges when switching user
        if user != current_user.name:
            is_admin = False
            is_moderator = False

        if (
            server
            and not is_admin
            and not is_moderator
            and not current_user.acl.is_server_allowed(server)
        ):
            self.abort(403, "You are not allowed to view this server infos")

        if server:
            try:
                clients = [
                    x["name"]
                    for x in bui.client.get_all_clients(
                        agent=server, last_attempt=False
                    )
                ]
            except BUIserverException:
                clients = []
            if not is_admin:
                # use the bui.acl module since we impersonalized the user
                ret = [
                    {"name": x, "agent": server}
                    for x in clients
                    if bui.acl.is_client_allowed(user, x, server)
                ]
            else:
                ret = [{"name": x, "agent": server} for x in clients]
            return ret

        if bui.config["STANDALONE"]:
            try:
                clients = [
                    x["name"] for x in bui.client.get_all_clients(last_attempt=False)
                ]
            except BUIserverException:
                clients = []
            if not is_admin:
                ret = [
                    {"name": x} for x in clients if bui.acl.is_client_allowed(user, x)
                ]
            else:
                ret = [{"name": x} for x in clients]
        else:
            grants = {}
            clients_cache = {}
            for serv in bui.client.servers:
                try:
                    clients = [
                        x["name"]
                        for x in bui.client.get_all_clients(serv, last_attempt=False)
                    ]
                    clients_cache[serv] = clients
                except BUIserverException:
                    clients = []
            if not is_admin:
                for serv in bui.client.servers:
                    grants[serv] = [
                        x
                        for x in clients_cache.get(serv, [])
                        if bui.acl.is_client_allowed(user, x, serv)
                    ]
            else:
                for serv in bui.client.servers:
                    grants[serv] = "all"
            for (serv, clients) in grants.items():
                if not isinstance(clients, list):
                    clients = clients_cache.get(serv, [])
                ret += [{"name": x, "agent": serv} for x in clients]

        return ret
