import logging
from datetime import datetime
from urllib.parse import urlparse

from cryptography.hazmat.primitives import serialization

from KL.application_protocol import ApplicationType, MessageSecurityMode, UserTokenType, UserTokenPolicy, \
    ApplicationDescription, EndpointDescription
from KL.types import SecurityPolicyType, NodeId, LocalizedText
from KL.constants import ObjectIds
from common.node import Node
from mainserver.user_manager import UserManager
from mainserver.internal_server import InternalServer
from mainserver.BinaryServer import BinaryServer
from KL.hand_protocol import *
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from Security import security_policies
from KL.shortcuts import Shortcuts


class Server(object):

    def __init__(self, shelffile=None, iserver=None):
        self.logger = logging.getLogger(__name__)
        self.endpoint = urlparse("opc.tcp://localhost:4842/freeopcua/server/")
        self._application_uri = "urn:freeopcua:python:server"
        self.product_uri = "urn:freeopcua.github.io:python:server"
        self.name = "FreeOpcUa Python Server"
        self.manufacturer_name = "FreeOpcUa"
        self.application_type = ApplicationType.ClientAndServer
        self.default_timeout = 3600000
        if iserver is not None:
            self.iserver = iserver
        else:
            self.iserver = InternalServer(shelffile=shelffile)
        self.bserver = None
        self._policies = []
        self.nodes = Shortcuts(self.iserver.isession)

        # setup some expected values
        self.set_application_uri(self._application_uri)
        sa_node = self.get_node(NodeId(ObjectIds.Server_ServerArray))
        sa_node.set_value([self._application_uri])

        # self.set_build_info(self.product_uri, self.manufacturer_name, self.name, "1.0pre", "0", datetime.now())

        # enable all endpoints by default
        self.certificate = None
        self.private_key = None
        self.user_manager = UserManager(parent=self)
        self._security_policy = [
            SecurityPolicyType.NoSecurity,
            SecurityPolicyType.Basic256Sha256_SignAndEncrypt,
            SecurityPolicyType.Basic256Sha256_Sign
        ]
        self._policyIDs = ["Anonymous", "Basic256Sha256", "Username"]

    def set_application_uri(self, uri):
        """
        Set application/server URI.
        This uri is supposed to be unique. If you intent to register
        your server to a discovery server, it really should be unique in
        your system!
        default is : "urn:freeopcua:python:server"
        """
        self._application_uri = uri
        ns_node = self.get_node(NodeId(ObjectIds.Server_NamespaceArray))
        uries = ns_node.get_value()
        if len(uries) > 1:
            uries[1] = uri  # application uri is always namespace 1
        else:
            uries.append(uri)
        ns_node.set_value(uries)

    def get_node(self, nodeid):
        """
        Get a specific node using NodeId object or a string representing a NodeId
        """
        return Node(self.iserver.isession, nodeid)

    def start(self):
        """
        Start to listen on network
        """
        self._setup_server_nodes()
        self.iserver.start()
        try:
            if not self.bserver:
                self.bserver = BinaryServer(self.iserver, self.endpoint.hostname, self.endpoint.port)
            self.bserver.set_policies(self._policies)
            self.bserver.set_loop(self.iserver.loop)
            self.bserver.start()
        except Exception as exp:
            self.iserver.stop()
            raise exp

    def stop(self):
        """
        Stop server
        """
        self.bserver.stop()
        self.iserver.stop()

    def _setup_server_nodes(self):
        # to be called just before starting server since it needs all parameters to be setup
        if SecurityPolicyType.NoSecurity in self._security_policy:
            self._set_endpoints()
            self._policies = [SecurityPolicyFactory()]

        if self._security_policy != [SecurityPolicyType.NoSecurity]:
            if not (self.certificate and self.private_key):
                self.logger.warning("Endpoints other than open requested but private key and certificate are not set.")
                return

            if SecurityPolicyType.NoSecurity in self._security_policy:
                self.logger.warning(
                    "Creating an open endpoint to the server, although encrypted endpoints are enabled.")

            if SecurityPolicyType.Basic256Sha256_SignAndEncrypt in self._security_policy:
                self._set_endpoints(security_policies.SecurityPolicyBasic256Sha256,
                                    MessageSecurityMode.SignAndEncrypt)
                self._policies.append(SecurityPolicyFactory(security_policies.SecurityPolicyBasic256Sha256,
                                                            MessageSecurityMode.SignAndEncrypt,
                                                            self.certificate,
                                                            self.private_key)
                                      )
            if SecurityPolicyType.Basic256Sha256_Sign in self._security_policy:
                self._set_endpoints(security_policies.SecurityPolicyBasic256Sha256,
                                    MessageSecurityMode.Sign)
                self._policies.append(SecurityPolicyFactory(security_policies.SecurityPolicyBasic256Sha256,
                                                            MessageSecurityMode.Sign,
                                                            self.certificate,
                                                            self.private_key)
                                      )

    def _set_endpoints(self, policy=SecurityPolicy, mode=MessageSecurityMode.None_):
        idtokens = []
        if "Anonymous" in self._policyIDs:
            idtoken = UserTokenPolicy()
            idtoken.PolicyId = 'anonymous'
            idtoken.TokenType = UserTokenType.Anonymous
            idtokens.append(idtoken)

        if "Basic256Sha256" in self._policyIDs:
            idtoken = UserTokenPolicy()
            idtoken.PolicyId = 'certificate_basic256sha256'
            idtoken.TokenType = UserTokenType.Certificate
            idtokens.append(idtoken)

        if "Username" in self._policyIDs:
            idtoken = UserTokenPolicy()
            idtoken.PolicyId = 'username'
            idtoken.TokenType = UserTokenType.UserName
            idtokens.append(idtoken)

        appdesc = ApplicationDescription()
        appdesc.ApplicationName = LocalizedText(self.name)
        appdesc.ApplicationUri = self._application_uri
        appdesc.ApplicationType = self.application_type
        appdesc.ProductUri = self.product_uri
        appdesc.DiscoveryUrls.append(self.endpoint.geturl())

        edp = EndpointDescription()
        edp.EndpointUrl = self.endpoint.geturl()
        edp.Server = appdesc
        if self.certificate:
            cert = x509.load_pem_x509_certificate(self.certificate.encode(), default_backend())
            edp.ServerCertificate = cert.public_bytes(serialization.Encoding.DER)
            # edp.ServerCertificate = uacrypto.der_from_x509(self.certificate)
        edp.SecurityMode = mode
        edp.SecurityPolicyUri = policy.URI
        edp.UserIdentityTokens = idtokens
        edp.TransportProfileUri = 'http://opcfoundation.org/UA-Profile/Transport/uatcp-uasc-uabinary'
        edp.SecurityLevel = 0
        self.iserver.add_endpoint(edp)

    def set_endpoint(self, url):
        self.endpoint = urlparse(url)


    def register_namespace(self, uri):
        """
        Register a new namespace. Nodes should in custom namespace, not 0.
        """
        ns_node = self.get_node(NodeId(ObjectIds.Server_NamespaceArray))
        uries = ns_node.get_value()
        if uri in uries:
            return uries.index(uri)
        uries.append(uri)
        ns_node.set_value(uries)
        return len(uries) - 1
