import os
import datetime
import hmac
import hashlib
import base58
from OpenSSL.crypto import FILETYPE_PEM
from ssl import create_default_context, SSLContext
from cryptography.hazmat.backends import default_backend
from cryptography import x509
from cryptography.hazmat.primitives import hashes
from cryptography.x509.name import NameOID, NameAttribute
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
from twisted.internet import ssl
import logging
from lbrynet import conf

log = logging.getLogger(__name__)


def sha(x: bytes) -> str:
    h = hashlib.sha256(x).digest()
    return base58.b58encode(h).decode()


def generate_key(x: bytes = None) -> str:
    if not x:
        return sha(os.urandom(256))
    else:
        return sha(x)


class APIKey:
    def __init__(self, secret: str, name: str):
        self.secret = secret
        self.name = name

    @classmethod
    def create(cls, seed=None, name=None):
        secret = generate_key(seed)
        return APIKey(secret, name)

    def _raw_key(self) -> str:
        return base58.b58decode(self.secret)

    def get_hmac(self, message) -> str:
        decoded_key = self._raw_key()
        signature = hmac.new(decoded_key, message.encode(), hashlib.sha256)
        return base58.b58encode(signature.digest())

    def compare_hmac(self, message, token) -> bool:
        decoded_token = base58.b58decode(token)
        target = base58.b58decode(self.get_hmac(message))

        try:
            if len(decoded_token) != len(target):
                return False
            return hmac.compare_digest(decoded_token, target)
        except:
            return False


class Keyring:
    encoding = serialization.Encoding.PEM
    filetype = FILETYPE_PEM

    def __init__(self, api_key: APIKey, public_certificate: str, private_certificate: ssl.PrivateCertificate = None):
        self.api_key: APIKey = api_key
        self.public_certificate: str = public_certificate
        self.private_certificate: (ssl.PrivateCertificate or None) = private_certificate
        self.ssl_context: SSLContext = create_default_context(cadata=self.public_certificate)

    @classmethod
    def load_from_disk(cls):
        api_key_path = os.path.join(conf.settings['data_dir'], 'auth_token')
        api_ssl_cert_path = os.path.join(conf.settings['data_dir'], 'api_ssl_cert.pem')
        if not os.path.isfile(api_key_path) or not os.path.isfile(api_ssl_cert_path):
            return
        with open(api_key_path, 'rb') as f:
            api_key = APIKey(f.read().decode(), "api")
        with open(api_ssl_cert_path, 'rb') as f:
            public_cert = f.read().decode()
        return cls(api_key, public_cert)

    @classmethod
    def generate_and_save(cls):
        dns = conf.settings['api_host']
        private_key = rsa.generate_private_key(
            public_exponent=65537,
            key_size=4096,
            backend=default_backend()
        )
        subject = issuer = x509.Name([
            NameAttribute(NameOID.COUNTRY_NAME, "US"),
            NameAttribute(NameOID.ORGANIZATION_NAME, "LBRY"),
            NameAttribute(NameOID.COMMON_NAME, "LBRY API"),
        ])
        alternative_name = x509.SubjectAlternativeName([x509.DNSName(dns)])
        certificate = x509.CertificateBuilder(
            subject_name=subject,
            issuer_name=issuer,
            public_key=private_key.public_key(),
            serial_number=x509.random_serial_number(),
            not_valid_before=datetime.datetime.utcnow(),
            not_valid_after=datetime.datetime.utcnow() + datetime.timedelta(days=365),
            extensions=[x509.Extension(oid=alternative_name.oid, critical=False, value=alternative_name)]
        ).sign(private_key, hashes.SHA256(), default_backend())
        public_certificate = certificate.public_bytes(cls.encoding).decode()
        private_certificate = ssl.PrivateCertificate.load(
            public_certificate,
            ssl.KeyPair.load(
                private_key.private_bytes(
                    encoding=cls.encoding,
                    format=serialization.PrivateFormat.TraditionalOpenSSL,
                    encryption_algorithm=serialization.NoEncryption()
                ).decode(),
                cls.filetype
            ),
            cls.filetype
        )

        auth_token = APIKey.create(seed=None, name="api")

        with open(os.path.join(conf.settings['data_dir'], 'auth_token'), 'wb') as f:
            f.write(auth_token.secret.encode())

        with open(os.path.join(conf.settings['data_dir'], 'api_ssl_cert.pem'), 'wb') as f:
            f.write(public_certificate.encode())

        return cls(auth_token, public_certificate, private_certificate)