import datetime
import enum

import peewee
from playhouse import hybrid
from OpenSSL import crypto
from OpenSSL.SSL import FILETYPE_PEM

from .utils import ask_private_key_passphrase, decrypt_from_b64, encrypt_to_b64
from .masterkey import MasterKeyHelper

"""
The database filename
"""
DATABASE_FILENAME = 'ca.db'  # ca.vault

"""
The database filename backup
"""
DATABASE_FILENAME_BACKUP = f'{DATABASE_FILENAME}.bak'

"""
The database object
"""
database = peewee.SqliteDatabase(None)


class BaseModel(peewee.Model):
    """
    Base model
    """
    class Meta:
        database = database


class BaseCertKeyModel(BaseModel):

    """
    Base certificate model which may have a key.

    The certificate must be stored into the "cert" attribute in the PEM format.
    The optional private key must be stored into the "key" attribute in the PEM format.

    The OpenSSL.crypto.X509 and OpenSSL.crypto.Pkey can be loaded as self.cert_obj and self.key_obj. If the key is
    passphrase protected, the user is asking for this passphrase.
    """
    cert = peewee.TextField()
    encrypted_key = peewee.TextField(null=True)
    _key = None

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.cert_obj = None
        self.key_obj = None

    def load_cert(self) -> None:
        """
        Load the certificate as OpenSSL.crypto.X509 object into the attribute "cert_obj".
        """
        self.cert_obj = crypto.load_certificate(FILETYPE_PEM, self.cert.encode('utf8'))

    def load_key(self) -> None:
        """
        Load the private key as OpenSSL.crypto.Pkey object into the attribute "key_obj".

        If the key is passphrase protected, the user is asking for this passphrase (prompt to stdout, read from stdin).

        If no key is available (self.key is None), a RuntimeError is raised.

        :except: RunTime
        """
        if not self.key:
            raise RuntimeError('No key available')

        self.key_obj = crypto.load_privatekey(FILETYPE_PEM, self.key,
                                              passphrase=ask_private_key_passphrase)

    def clear_key(self) -> None:
        """
        Clear the attribute "key_obj".
        """
        self.key_obj = None

    def _get_key(self):
        if self.encrypted_key is None:
            return None

        return MasterKeyHelper().decrypt_from_b64(self.encrypted_key).decode('utf8')

    def _set_key(self, data: bytes):
        if data is None:
            self.encrypted_key = None
        else:
            if isinstance(data, str):
                data = data.encode('utf8')

            self.encrypted_key = MasterKeyHelper().encrypt_to_b64(data)

    key = property(fget=_get_key, fset=_set_key, doc="Private Key (PEM)")


class CA(BaseCertKeyModel):
    """
    A Certificate Authority model.

    The certificate and the key are stored in the PEM format.
    """
    id = peewee.IntegerField(primary_key=True)
    name = peewee.CharField(unique=True)


class CertificateRevokedReason(enum.Enum):
    """
    The certificate revoked reason
    """
    UNSPECIFIED = 'unspecified'
    KEY_COMPRISE = 'keyCompromise'
    CESSATION_OF_OPERATION = 'cessationOfOperation'

    @staticmethod
    def from_string(s: str):
        s = s.lower()

        if s in ['keycompromise', 'key compromise', 'compromise', 'compromised']:
            return CertificateRevokedReason.KEY_COMPRISE
        elif s in ['cessationOfOperation', 'cessation of operation', 'eol']:
            return CertificateRevokedReason.CESSATION_OF_OPERATION
        else:
            return CertificateRevokedReason.UNSPECIFIED


class Certificate(BaseCertKeyModel):
    """
    A certificate model.

    The created_timestamp and not_after are precise to the second.
    The serial is provided in the official X509 output, so in hex format using ":" separator in the big endian
    representation of the 64 bits integer.
    The certificate and the key (optional) are stored in the PEM format.
    """
    id = peewee.IntegerField(primary_key=True)
    cn = peewee.CharField()
    san = peewee.CharField(null=True)
    created_timestamp = peewee.DateTimeField()
    not_after = peewee.DateTimeField()
    serial = peewee.CharField()
    is_revoked = peewee.BooleanField()
    revoked_timestamp = peewee.DateTimeField(null=True)
    revoked_reason = peewee.TextField(null=True)
    revoked_comment = peewee.TextField(null=True)
    is_renewed = peewee.BooleanField()

    def __str__(self):
        now = datetime.datetime.now()

        s = f'#{self.id}. CN:{self.cn}'\

        if self.san:
            s += f' / {self.san}'

        s += ' ('

        if self.not_after <= now:
            s += 'expired'
        elif self.is_revoked:
            s += 'revoked'

            if self.revoked_comment:
                s += f' [comment: {self.revoked_comment}]'
        elif self.is_renewed:
            s += 'renewed [must not be used anymore]'
        else:
            s += f'expire on {self.not_after.strftime("%d/%m/%Y %H:%M:%S")}'

        s += f', serial : {self.serial}, '
        s += 'no private key stored' if not self.encrypted_key else 'private key available'
        s += ')'

        return s


class CRL(BaseModel):
    """
    A Certificate Revoked List model

    The crl_timestamp is precise to the second.
    """
    id = peewee.IntegerField(primary_key=True)
    crl_version = peewee.IntegerField()
    crl_timestamp = peewee.DateTimeField()
    crl = peewee.TextField()


class ConfigType(enum.IntEnum):
    """
    The configuration value type
    """
    INT = 1
    STRING = 2
    BINARY = 3
    EPOCH = 4


class Config(BaseModel):
    """
    A Configuration model
    """
    name = peewee.CharField(primary_key=True)
    type = peewee.IntegerField()
    value = peewee.CharField()
