"""
Handlers
========

Entry points called by the web framework.
"""

from abc import ABCMeta
import logging
import os.path
import re
import sys
from traceback import format_exception_only, format_tb

import cherrypy
from cherrypy.lib.cpstats import StatsPage
import pygerduty
import utc

from mekansm.core import Deployment, Node, json_encoder, NodeStatus
import mekansm.errors


FILTER_API_REGEXP = re.compile(r"^filter\[(.*)\]$")


class Handler(object):
    """Base handler class for the Mekansm application"""
    __metaclass__ = ABCMeta

    exposed = True
    URI_TEMPLATE = NotImplemented

    @property
    def context(self):
        """Return the current running application context."""
        return self.req.context

    @property
    def req(self):
        """Return the current HTTP request object"""
        return cherrypy.serving.request

    @property
    def resp(self):
        """Return the current HTTP response object"""
        return cherrypy.serving.response

    @classmethod
    def uri(cls, **kwargs):
        """Return the url for the handler instance"""
        return cherrypy.url(cls.URI_TEMPLATE.format(**kwargs))


class DeploymentNodeStatusHandler(Handler):
    """`/deployments/{deployment_id}/nodes_status/{node_id}`

    Return details for the status of a node in a deployment.
    """

    URI_TEMPLATE = "/deployments/{deployment_id}/nodes_status/{node_id}"

    @cherrypy.tools.cpstats(uriset="GET " + URI_TEMPLATE)
    def GET(self):
        """HTTP GET handler for `/deployments/{deployment_id}/nodes`

        :return: A dictionary with a node status
        :rtype: dict
        """
        return {
            "data": decorate_nodestatus(
                NodeStatus.get(
                    self.context, self.req.deployment_id, self.req.node_id)
            )
        }


class DeploymentNodesStatusesHandler(Handler):
    """`/deployments/{deployment_id}/nodes_status`

    Return details for the status of nodes in a deployment.
    """

    URI_TEMPLATE = "/deployments/{deployment_id}/nodes_status"

    @cherrypy.tools.cpstats(uriset="GET " + URI_TEMPLATE)
    def GET(self):
        """HTTP GET handler for `/deployments/{deployment_id}/nodes`

        :return: A dictionary with a list of nodes statuses.
        :rtype: dict
        """
        return {
            "links": {"self": cherrypy.url()},
            "data": [
                decorate_nodestatus(node_status)
                for node_status in
                Deployment.get(
                    self.context, self.req.deployment_id).instances_statuses
            ]
        }

    def _cp_dispatch(self, vpath):
        """Custom dispatcher for `/deployments/{deployment_id}/nodes_status`
        """
        self.req.node_id = vpath.pop(0)
        return DeploymentNodeStatusHandler()


class DeploymentHandler(Handler):
    """`/deployments/{deployment_id}`

    A deployment resource.
    """

    nodes_status = DeploymentNodesStatusesHandler()

    URI_TEMPLATE = "/deployments/{deployment_id}"

    @cherrypy.tools.cpstats(uriset="GET " + URI_TEMPLATE)
    def GET(self):
        """HTTP GET handler for the `/deployments/{deployment_id}/` resource.

        :return: A dictionary with details on the deployment.
        :rtype: dict
        """
        return {
            "data": decorate_deployment(
                Deployment.get(self.context, self.req.deployment_id))
        }

    @cherrypy.tools.cpstats(uriset="PATCH " + URI_TEMPLATE)
    def PATCH(self):
        """HTTP PATCH handler for the `/deployments/{deployment_id}/` resource.

        Example request body::

            {
                "data": {
                    "type": "deployments",
                    "id": "d-ABCDEF",
                    "attributes": {"status": "Failed"}
                }
            }

        The response is the same as with GET, with the patched deployment.

        :return: A dictionary with details on the deployment.
        :rtype: dict
        """
        data = self.req.json.pop("data")
        assert data["type"] == "deployments"
        assert data["id"] == self.req.deployment_id
        dep = Deployment.get(self.context, self.req.deployment_id)
        dep.set_status(data["attributes"]["status"])
        cherrypy.response.status = 200
        return {"data": decorate_deployment(dep)}


class DeploymentsHandler(Handler):
    """/deployments

    Handler for a collection of deployment resources.
    """

    URI_TEMPLATE = "/deployments"
    SEARCH_FILTERS = {
        "owner", "repo", "environment", "datacenter", "deployment_status",
        "node"}

    @cherrypy.tools.cpstats(uriset="GET " + URI_TEMPLATE)
    def GET(self, **kwargs):
        """HTTP GET handler for the `/deployments/` resource.

        Can be filtered by:

        - `filter[owner]={owner}`
        - `filter[repo]={repo}`
        - `filter[environment]={name}`
        - `filter[datacenter]={name}`
        - `filter[deployment_status]={status}`
        - `filter[node]={node}`

        :param dict kwargs: Query parameters
        :return: A dictionary with a list of deployments.
        :rtype: dict
        """
        return {
            "links": {"self": cherrypy.url()},
            "data": [
                decorate_deployment(deployment)
                for deployment in
                Deployment.find_by(
                    self.context,
                    kwargs_to_filter_obj(self.SEARCH_FILTERS, **kwargs)
                )
            ]
        }

    @cherrypy.tools.cpstats(uriset="POST " + URI_TEMPLATE)
    def POST(self):
        """Creates new deployment resources.

        Example request body:

        .. code-block:: json

            {
                "data": {
                    "type": "deployments",
                    "attributes": {
                        "nodes": [
                            {"dc": "SFO", "nodes": ["server-1", "server-2"]},
                            {"dc": "LIM", "nodes": ["slow-server"]},
                            {"dc": "SEA", "nodes": [""]}
                        ],
                        "repository": "mekansm",
                        "owner": "dta",
                        "triggered_by": "Kappa",
                        "environment": "staging",
                        "sha": "KappaKappaKappa",
                        "account": "aws-account-id",
                        "application": "dta-mekansm",
                        "group": "dta-mekansm-staging",
                        "s3location": "myartifacts/dta/mekansm",
                        "bundletype": "tgz",
                        "config": "CodeDeployDefault.AllAtOnce"
                    }
                }
            }

        If succesful, the response will have HTTP status 201 with
        the deployments resource, and the URL of the new resource
        if the `Location` header.

        :return: A dictionary with the deployment resource that was created.
        :rtype: dict
        """
        body = self.req.json["data"]
        assert body["type"] == "deployments"
        payload = body["attributes"].copy()
        self._decorate_payload_nodes(payload)
        depobj = Deployment.create(self.context, **payload)
        self.resp.headers["Location"] = cherrypy.url(
            "/deployments/{}".format(depobj["id"]))
        cherrypy.response.status = 201
        return {"data": decorate_deployment(depobj)}

    def _decorate_payload_nodes(self, payload):
        """Turn the given nodes arg passed on POST into something usable.

        :param dict payload: The payload to be passed to Deployment creation
        :return: None
        """
        nodes_data = payload.pop("nodes", [])
        nodes_list = []
        for dc in nodes_data:
            for node in dc["nodes"]:
                nodes_list.append({"datacenter": dc["dc"], "id": node})
        payload["nodes"] = nodes_list

    def _cp_dispatch(self, vpath):
        """Custom dispatcher for the `/deployments/` resource.

        Handle a Deployment resource like  `/deployments/{deployment_id}`
        """
        self.req.deployment_id = vpath.pop(0)
        return DeploymentHandler()


class NodeDeploymentsHandler(Handler):
    """`/nodes/{node_id}/deployments`

    Handler for deployments resources for a node
    """

    URI_TEMPLATE = "/nodes/{node_id}/deployments"
    SEARCH_FILTERS = {
        "deployment", "owner", "repo", "environment", "deployment_status"}

    @cherrypy.tools.cpstats(uriset="GET " + URI_TEMPLATE)
    def GET(self, **kwargs):
        """HTTP GET handler for the `/nodes/{node_id}/deployments` resource.
        Additional filters:

        - `filter[deployment]={deployment_id}`
        - `filter[owner]={owner}`
        - `filter[repo]={repo}`
        - `filter[environment]={name}`
        - `filter[deployment_status]={status}`

        :param dict kwargs: Query parameters
        :return: A dictionary with a list of deployments in the node.
        :rtype: dict
        """
        filter_obj = kwargs_to_filter_obj(self.SEARCH_FILTERS, **kwargs)
        filter_obj["node"] = self.req.node_id
        return {
            "links": {"self": cherrypy.url()},
            "data": [
                decorate_deployment(deployment)
                for deployment in
                Deployment.find_by(self.context, filter_obj)
            ]
        }


class NodeCurrent(Handler):
    """`/nodes/{node_id}/current`

    Return the deployments and statuses for a node.
    """

    URI_TEMPLATE = "/nodes/{node_id}/current"

    @cherrypy.tools.cpstats(uriset="GET " + URI_TEMPLATE)
    def GET(self):
        return {
            "links": {"self": cherrypy.url()},
            "data": Node.get(
                self.context, self.req.node_id
            ).deployments_and_statuses()
        }


class NodeHandler(Handler):
    """`/nodes/{node_id}`

    Handler for a single Node resource.
    """

    deployments = NodeDeploymentsHandler()
    current = NodeCurrent()

    URI_TEMPLATE = "/nodes/{node_id}"

    @cherrypy.tools.cpstats(uriset="GET " + URI_TEMPLATE)
    def GET(self):
        """HTTP GET handler for the `/nodes/{node_id}/` resource.
        """
        node = Node.get(self.context, self.req.node_id)
        return {"data": decorate_node(node)}


class NodesHandler(Handler):
    """`/nodes`

    Handler for a collection of Node resources.
    """

    URI_TEMPLATE = "/nodes"
    SEARCH_FILTERS = {
        "deployment", "owner", "repo", "environment", "datacenter",
        "deployment_status"}

    @cherrypy.tools.cpstats(uriset="GET " + URI_TEMPLATE)
    def GET(self, **kwargs):
        """HTTP GET handler for the `/nodes/` resource.

        Additional filters:

        - `filter[deployment]={deployment_id}`
        - `filter[owner]={owner}`
        - `filter[repo]={repo}`
        - `filter[environment]={name}`
        - `filter[datacenter]={name}`
        - `filter[deployment_status]={status}`

        :param dict kwargs: Query parameters
        :return: A dictionary with a list of nodes.
        :rtype: dict
        """

        nodes = Node.find_by(
            self.context,
            kwargs_to_filter_obj(self.SEARCH_FILTERS, **kwargs)
        )
        return {
            "links": {"self": cherrypy.url()},
            "data": [
                decorate_node(node)
                for node in nodes
            ]
        }

    def _cp_dispatch(self, vpath):
        """Custom dispatcher for the `/nodes/` resource.

        Handle a Node resource like  `/nodes/{node_id}`
        """
        self.req.node_id = vpath.pop(0)
        return NodeHandler()


class HealthCheck(Handler):
    """`/health`"""

    URI_TEMPLATE = "/health"
    LAST_SUCCESS_AGO_WARNING = 60 * 5

    def _threads_health(self):
        queue_size = cherrypy.server.httpserver.requests.qsize
        if queue_size:
            msg = "{} connections waiting for HTTP workers".format(queue_size)
            if queue_size > 10:
                return "CRITICAL " + msg
            return "WARNING " + msg
        return "OK Pool is healthy, no connections piling up."

    @cherrypy.tools.cpstats(uriset="GET " + URI_TEMPLATE)
    def GET(self):
        res = self.context.health_check()
        res["http thread pool"] = self._threads_health()
        last_success = logging.statistics["Health"].get("Last Success")
        if last_success:
            seconds = (utc.now() - last_success).total_seconds()
            msg = "Last success was served {} ago.".format(seconds)
            if seconds > self.LAST_SUCCESS_AGO_WARNING:
                msg = "WARNING " + msg
            else:
                msg = "OK " + msg
            res["last success"] = msg
        else:
            res["last success"] = "OK No succesful response yet."
        return res


class RootHandler(Handler):
    """`/`"""

    deployments = DeploymentsHandler()
    nodes = NodesHandler()
    cpstats = StatsPage()
    health = HealthCheck()

    URI_TEMPLATE = "/"

    @staticmethod
    def _is_jsonapi_request():
        ranges = cherrypy.request.headers.elements("Accept")
        json_mime_types = ("application/json", "application/vnd.api+json")
        if ranges:
            for element in ranges:
                if element.value in json_mime_types:
                    return True
        return False

    @cherrypy.tools.cpstats(uriset="GET " + URI_TEMPLATE)
    def GET(self):
        """HTTP GET handler for the root resource

        :return: A dictionary with links to internal resources.
        :rtype: dict
        """
        if self._is_jsonapi_request():
            return {
                "meta": {
                    "welcome": "Welcome to Mekansm"
                },
                "links": {
                    "deployments": DeploymentsHandler.uri(),
                    "nodes": NodesHandler.uri(),
                    "internal statistics": cherrypy.url("/cpstats/"),
                    "user interface": cherrypy.url("/swagger/index.html")
                }
            }
        raise cherrypy.HTTPRedirect("/swagger/index.html")


def decorate_deployment(deployment):
    """Decorates a mekansm.code.Deployment object with jsonapi boilerplate

    :param mekansm.core.Deployment deployment: The deployment to decorate
    :return: A dictionary with a decorated deployment
    :rtype: dict
    """
    res = deployment.as_dict()
    deployment_id = deployment["id"]
    res.update(
        {
            "links":  {
                "self": DeploymentHandler.uri(deployment_id=deployment_id)
            },
            "relationships": {
                "nodes_status": {
                    "links": {
                        "related": DeploymentNodesStatusesHandler.uri(
                            deployment_id=deployment_id)
                    }
                }
            }
        }
    )
    return res


def decorate_node(node):
    """Decorates a mekansm.code.Node object with jsonapi boilerplate

    :param mekansm.core.Node node: The node status to decorate
    :return: A dictionary with a decorated node
    :rtype: dict
    """
    res = node.as_dict()
    node_id = node["id"]
    res.update(
        {
            "links": {
                "self": NodeHandler.uri(node_id=node_id)
            },
            "relationships": {
                "deployments": {
                    "links": {
                        "related": NodeDeploymentsHandler.uri(node_id=node_id)
                    }
                },
                "current": {
                    "links": {
                        "related": NodeCurrent.uri(node_id=node_id)
                    }
                }
            }
        }
    )
    return res


def decorate_nodestatus(node_status):
    """Decorates a mekansm.code.NodeStatus object with jsonapi boilerplate

    :param mekansm.core.NodeStatus node_status: The node status to decorate
    :return: A dictionary with a decorated node status
    :rtype: dict
    """
    res = node_status.as_dict()
    deployment_id = res["id"]["deployment"]
    node_id = res["id"]["node"]
    res.update(
        {
            "links": {
                "self": DeploymentNodeStatusHandler.uri(
                    deployment_id=deployment_id, node_id=node_id)
            },
            "relationships": {
                "deployment": {
                    "links": {
                        "related": DeploymentHandler.uri(
                            deployment_id=deployment_id)
                    }
                },
                "node": {
                    "links": {
                        "related": NodeHandler.uri(node_id=node_id)
                    }
                },
            }
        }
    )
    return res


def error_page_handler(status, message, traceback, version):
    """Handle known (4XX) errors.

    Used by CherryPy's 'error_page.default'

    :param int status:
    :param message: Error message
    :param traceback: The exception traceback as a list
    :param version: Running version of CherryPy
    :return: A dictified representation of the error
    :rtype: dict
    """
    errname = "{} {}".format(status, message)
    logging.statistics["Mekansm"]["Errors"][errname]["Events"] += 1
    return json_encoder.encode(
        error_handler(status, [message], traceback.split("\n"), message))


def error_response_handler():
    """Handle unexpected (500) errors.

    Used by CherryPy's 'request.error_response'
    """
    cherrypy.serving.response.body = json_encoder.encode(
        handle_exception(sys.exc_info())).encode("utf-8")


def error_handler(status, exc, traceback, message=None):
    """Set headers and create the json string to be used as a response.

    Also changes the response status to the given status.

    :param int status: HTTP status code
    :param exc: Exception
    :param traceback: Traceback
    :param message: Error message
    :return: A dictified representation of the exception
    :rtype: dict
    """
    cherrypy.serving.response.status = status
    return {
        "errors": [
            {
                "status": status,
                "code": exc,
                "detail": traceback,
                "title": message if message else exc
            }
        ]
    }


def handle_exception(exc_info):
    """Handle any exception received as an argument

    Also changes the response status to the given status.

    :param exc_info: An exception
    :return: A dictified representation of the exception
    :rtype: dict
    """
    exc_type, exc_value, exc_traceback = exc_info
    if issubclass(exc_type, cherrypy.HTTPError):
        status, message = exc_value.status, exc_value._message
    elif issubclass(exc_type, mekansm.errors.MekansmException):
        message = exc_value.message
        if issubclass(exc_type, mekansm.errors.MekansmNotFound):
            status = 404
        else:
            status = 409
    else:
        status = 500
        message = exc_value.message
        pagerduty_alert(exc_traceback, message)
    return error_handler(
        status,
        format_exception_only(exc_type, exc_value),
        format_tb(exc_traceback),
        message
    )


def pagerduty_alert(exc_traceback, message):
    """Triggers a pagerduty alert.

    :param exc_traceback: a list of strings representing an exception traceback
    :param str message: A brief message describing the problem
    :return: None
    """
    req = cherrypy.serving.request
    key = req.context.config.get("pagerduty", {}).get("key")
    msg = "Mekansm: " + message
    if key is not None:
        details = {
            "message": msg,
            "base": req.base,
            "method": req.method,
            "path_info": req.path_info,
            "remote": "{}:{}".format(req.remote.ip, req.remote.port),
            "traceback": format_tb(exc_traceback)
        }
        # We pass dummy args to pygerduty, they'll be unused in our use case
        pagerduty = pygerduty.PagerDuty('LITERALLY', 'NOTHING')
        pagerduty.trigger_incident(key, msg, details=details)
    else:
        cherrypy.log(
            "Pagerduty not configured, alert not delivered: {}".format(msg))


def kwargs_to_filter_obj(valid_filters, **kwargs):
    """Take raw query parameters and turn them into a search dict object.

    :param set valid_filters: Accepted search filters for the resource.
    :param dict kwargs: All the HTTP args passed
    :return: A search dict object that only has valid filters.
    :rtype: dict
    """
    res = {}
    invalid_filters = []
    for key, value in kwargs.items():
        match = FILTER_API_REGEXP.match(key)
        if match:
            name = match.group(1)
            if name in valid_filters:
                res[name] = value
            else:
                invalid_filters.append(name)
    if invalid_filters:
        raise mekansm.errors.MekansmInvalidFilters(invalid_filters)
    return res


def json_handler(*args, **kwargs):
    """A JSON handler for CherryPy that uses Mekansm's encoder."""
    return json_encoder.iterencode(
        cherrypy.serving.request._json_inner_handler(*args, **kwargs))


def context_tool(value):
    """Tool that makes the given value available as a context in requests.

    :param mekansm.atapters.Context value: The running application context
    """
    cherrypy.request.context = value


cherrypy.tools.context = cherrypy.Tool("before_handler", context_tool)


def last_success_tool():
    if int(cherrypy.response.status.split(" ").pop(0)) < 300:
        logging.statistics["Health"] = {"Last Success": utc.now()}

cherrypy.tools.last_success = cherrypy.Tool(
    "on_end_request", last_success_tool)


def wsgi_app(context):
    """Creates a CherryPy application for Mekansm.

    :param mekansm.adapters.Context context: The application context
    :return: The WSGI application object.
    """
    currentdir = os.path.dirname(os.path.abspath(__file__))
    staticpath = os.path.normpath(os.path.join(currentdir, "..", "static"))
    staticfile = os.path.join(staticpath, "swagger.yaml")
    app_conf = {
        "/": {
            "tools.cpstats.on": True,
            "request.dispatch": cherrypy.dispatch.MethodDispatcher(),
            "request.methods_with_bodies": ("POST", "PUT", "PATCH"),
            "tools.json_in.on": True,
            "tools.json_out.on": True,
            "tools.json_out.handler": json_handler,
            "tools.context.on": True,
            "tools.context.value": context,
            "tools.last_success.on": True,
            "request.error_response": error_response_handler,
            "error_page.default": error_page_handler,
            "tools.encode.on": True,
            "tools.encode.encoding": "utf-8",
            "tools.encode.text_only": False,
            "tools.expires.on": True,
            "tools.expires.debug": True,
        },
        "/cpstats": {
            "tools.cpstats.on": True,
            "tools.json_out.on": False,
            "request.dispatch": cherrypy.dispatch.Dispatcher(),
        },
        "/swagger.yaml": {
            "tools.staticfile.on": True,
            "tools.staticfile.filename": staticfile,
            "tools.staticfile.content_types": {
                "yaml": "application/yaml"
            }
        },
        "/swagger": {
            "tools.staticdir.on": True,
            "tools.staticdir.dir": os.path.join(staticpath, "swagger-ui"),
        }
    }
    root = RootHandler()
    return cherrypy.tree.mount(root, "/", app_conf)
