import click
import falcon
import logging
import json
import hashlib
from asn1crypto import pem
from asn1crypto.csr import CertificationRequest
from pinecrypt.server import const, errors, authority
from pinecrypt.server.decorators import csrf_protection, MyEncoder
from pinecrypt.server.user import DirectoryConnection
from pinecrypt.server.mongolog import LogHandler
from oscrypto import asymmetric
from .utils.firewall import whitelist_subnets, whitelist_content_types, \
    login_required, login_optional, authorize_admin, validate_clock_skew

logger = LogHandler()

"""
openssl genrsa -out test.key 1024
openssl req -new -sha256 -key test.key -out test.csr -subj "/CN=test"
curl -f -L -H "Content-type: application/pkcs10" --data-binary @test.csr \
  http://ca.example.lan/api/request/?wait=yes
"""

class RequestListResource(object):
    @login_optional
    @whitelist_subnets(const.REQUEST_SUBNETS)
    @whitelist_content_types("application/pkcs10")
    @validate_clock_skew
    def on_post(self, req, resp):
        """
        Validate and parse certificate signing request, the RESTful way
        Endpoint urls
        /request/?wait=yes
        /request/autosign=1
        /request
        """
        reasons = []
        body = req.stream.read(req.content_length)

        try:
            header, _, der_bytes = pem.unarmor(body)
            csr = CertificationRequest.load(der_bytes)
        except ValueError:
            logger.info("Malformed certificate signing request submission from %s blocked", req.context["remote"]["addr"])
            raise falcon.HTTPBadRequest(
                "Bad request",
                "Malformed certificate signing request")
        else:
            req_public_key = asymmetric.load_public_key(csr["certification_request_info"]["subject_pk_info"])
            if authority.public_key.algorithm != req_public_key.algorithm:
                logger.info("Attempt to submit %s based request from %s blocked, only %s allowed" % (
                    req_public_key.algorithm.upper(),
                    req.context["remote"]["addr"],
                    authority.public_key.algorithm.upper()))
                raise falcon.HTTPBadRequest(
                    "Bad request",
                    "Unsupported key algorithm %s, expected %s" % (req_public_key.algorithm, authority.public_key.algorithm))

        try:
            common_name = csr["certification_request_info"]["subject"].native["common_name"]
        except KeyError:
            logger.info("Malformed certificate signing request without common name submitted from %s" % req.context["remote"]["addr"])
            raise falcon.HTTPBadRequest(title="Bad request",description="Common name missing from certificate signing request")

        """
        Determine whether autosign is allowed to overwrite already issued
        certificates automatically
        """

        overwrite_allowed = False
        for subnet in const.OVERWRITE_SUBNETS:
            if req.context["remote"]["addr"] in subnet:
                overwrite_allowed = True
                break


        """
        Handle domain computer automatic enrollment
        """
        machine = req.context.get("machine")
        if machine:
            reasons.append("machine enrollment not allowed from %s" % req.context["remote"]["addr"])
            for subnet in const.MACHINE_ENROLLMENT_SUBNETS:
                if req.context["remote"]["addr"] in subnet:
                    if common_name != machine:
                        raise falcon.HTTPBadRequest(
                            "Bad request",
                            "Common name %s differs from Kerberos credential %s!" % (common_name, machine))

                    hit = False
                    with DirectoryConnection() as conn:
                        ft = const.LDAP_COMPUTER_FILTER % ("%s$" % machine)
                        attribs = "cn",
                        r = conn.search_s(const.LDAP_BASE, 2, ft, attribs)
                        for dn, entry in r:
                            if not dn:
                                continue
                            else:
                                hit = True
                                break

                    if hit:
                        # Automatic enroll with Kerberos machine cerdentials
                        resp.set_header("Content-Type", "application/x-pem-file")
                        try:
                            mongo_doc = authority.store_request(body,address=str(req.context["remote"]["addr"]))
                            cert, resp.text = authority.sign(mongo_id=str(mongo_doc["_id"]),
                                profile="Roadwarrior", overwrite=overwrite_allowed) # TODO: handle thrown exception
                            logger.info("Automatically enrolled Kerberos authenticated machine %s (%s) from %s",
                                machine, dn, req.context["remote"]["addr"])
                            return
                        except errors.RequestExists:
                            reasons.append("same request already uploaded exists")
                            # We should still redirect client to long poll URL below
                        except errors.DuplicateCommonNameError:
                            logger.warning("rejected signing request with overlapping common name from %s",
                                req.context["remote"]["addr"])
                            raise falcon.HTTPConflict(
                                "CSR with such CN already exists",
                                 "Will not overwrite existing certificate signing request, explicitly delete CSR and try again")

                    else:
                        logger.error("Kerberos authenticated machine %s didn't fit the 'ldap computer filter' criteria %s" % (machine, ft))


        """
        Process automatic signing if the IP address is whitelisted,
        autosigning was requested and certificate can be automatically signed
        """

        if req.get_param_as_bool("autosign"):
            for subnet in const.AUTOSIGN_SUBNETS:
                if req.context["remote"]["addr"] in subnet:
                    try:
                        resp.set_header("Content-Type", "application/x-pem-file")
                        mongo_doc = authority.store_request(body,address=str(req.context["remote"]["addr"]))
                        _, resp.text = authority.sign(mongo_id=str(mongo_doc["_id"]),
                            overwrite=overwrite_allowed, profile="Roadwarrior")

                        logger.info("Signed %s as %s is whitelisted for autosign", common_name, req.context["remote"]["addr"])
                        return
                    except errors.RequestExists:
                        logger.info("Autosign for %s from %s failed, signed certificate already exists",
                            common_name, req.context["remote"]["addr"])
                        reasons.append("autosign failed, signed certificate already exists")
                    break
            else:
                reasons.append("IP address not whitelisted for autosign")
        else:
            reasons.append("autosign not requested")

        # Attempt to save the request otherwise
        try:
            mongo_doc = authority.store_request(body,
                address=str(req.context["remote"]["addr"]))
        except errors.RequestExists:
            reasons.append("same request already uploaded exists")
            # We should still redirect client to long poll URL below
        except errors.DuplicateCommonNameError:
            logger.warning("rejected signing request with overlapping common name from %s",
                req.context["remote"]["addr"])
            raise falcon.HTTPConflict(
                "CSR with such CN already exists",
                "Will not overwrite existing certificate signing request, explicitly delete CSR and try again")

        # Wait the certificate to be signed if waiting is requested
        logger.info("Signing request %s from %s put on hold,  %s", common_name, req.context["remote"]["addr"], ", ".join(reasons))

        if req.get_param("wait"):
            header, _, der_bytes = pem.unarmor(body)
            url = "https://%s/api/event/request-signed/%s" % (const.AUTHORITY_NAMESPACE, str(mongo_doc["_id"]))
            click.echo("Redirecting to: %s"  % url)
            resp.status = falcon.HTTP_SEE_OTHER
            resp.set_header("Location", url)
        else:
            # Request was accepted, but not processed
            resp.status = falcon.HTTP_202
            resp.text = ". ".join(reasons)

            if req.client_accepts("application/json"):
                resp.text = json.dumps({"title":"Accepted", "description":resp.text, "id":str(mongo_doc["_id"])},
                    cls=MyEncoder)


class RequestDetailResource(object):
    def on_get(self, req, resp, id):
        """
        Fetch certificate signing request as PEM
        """

        try:
            csr, csr_doc, buf = authority.get_request(id)
        except errors.RequestDoesNotExist:
            logger.warning("Failed to serve non-existant request %s to %s",
                id, req.context["remote"]["addr"])
            raise falcon.HTTPNotFound()

        resp.set_header("Content-Type", "application/pkcs10")
        logger.debug("Signing request %s was downloaded by %s",
            csr_doc["common_name"], req.context["remote"]["addr"])

        preferred_type = req.client_prefers(("application/json", "application/x-pem-file"))

        if preferred_type == "application/x-pem-file":
            # For certidude client, curl scripts etc
            resp.set_header("Content-Type", "application/x-pem-file")
            resp.set_header("Content-Disposition", ("attachment; filename=%s.pem" % csr_doc["common_name"]))
            resp.text = buf
        elif preferred_type == "application/json":
            # For web interface events
            resp.set_header("Content-Type", "application/json")
            resp.set_header("Content-Disposition", ("attachment; filename=%s.json" % csr_doc["common_name"]))
            resp.text = json.dumps(dict(
                submitted=csr_doc["submitted"],
                common_name=csr_doc["common_name"],
                id=str(csr_doc["_id"]),
                address=csr_doc["user"]["request_addresss"],
                md5sum=hashlib.md5(buf).hexdigest(),
                sha1sum=hashlib.sha1(buf).hexdigest(),
                sha256sum=hashlib.sha256(buf).hexdigest(),
                sha512sum=hashlib.sha512(buf).hexdigest()), cls=MyEncoder)
        else:
            raise falcon.HTTPUnsupportedMediaType(
                "Client did not accept application/json or application/x-pem-file")


    @csrf_protection
    @login_required
    @authorize_admin
    def on_post(self, req, resp, id):
        """
        Sign a certificate signing request
        """
        try:
            cert, buf = authority.sign(mongo_id=id,
                profile=req.get_param("profile", default="Roadwarrior"),
                overwrite=True,
                signer=req.context.get("user").name)  #if user is cached in browser then there is no name
            # Mailing and long poll publishing implemented in the function above
        except EnvironmentError: # no such CSR
            raise falcon.HTTPNotFound(title="Not found",description="CSR not found with id %s" %  id)

        resp.text = "Certificate successfully signed"
        resp.status = falcon.HTTP_201
        resp.location = req.forwarded_uri.replace("request","sign")

        cn = cert.subject.native.get("common_name")
        logger.info("Signing request %s signed by %s from %s", cn,
            req.context.get("user"), req.context["remote"]["addr"])


    @csrf_protection
    @login_required
    @authorize_admin
    def on_delete(self, req, resp, id):
        try:
            authority.delete_request(id, user=req.context.get("user"))
            # Logging implemented in the function above
        except errors.RequestDoesNotExist as e:
            resp.text = "No certificate signing request for with id %s not found" % id
            logger.warning("User %s failed to delete signing request %s from %s, reason: %s",
                req.context["user"], id, req.context["remote"]["addr"], e)
            raise falcon.HTTPNotFound()
        except ValueError as e:
            resp.text = "No ID specified %s" % id
            logger.warning("User %s wanted to delete invalid signing request %s from %s, reason: %s",
                req.context["user"], id, req.context["remote"]["addr"], e)
            raise falcon.HTTPBadRequest()
