"""

    AUTHOR:

        Khushal P Soonderji

    DATE:

        Friday, 30th May, 2025

    OBJECTIVE:

        To provide a base class for common behaviour of Google's APIs.

    REFERENCES:

        1. Quickstart: https://developers.google.com/gmail/api/quickstart/python

    DOWNLOADS:

        N/A

"""


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                                   IMPORT                                                      ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


# To make sibling directories accessible for imports:
import sys
sys.path.append(".")
sys.path.append("..")

# System-level activities:
import io

# To check certs through cryptographic algorithms:
from cryptography import x509
from cryptography.hazmat.backends import default_backend

# To fetch remote certs:
import ssl
import socket
from urllib.parse import urlparse

# To work with date and time:
import datetime
import humanize

# For debugging:
from icecream import IceCreamDebugger


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                           MACROS / ONE-TIME INIT                                              ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


# --- Nothing Yet


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                                VARIABLES                                                      ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


# --- Nothing Yet


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                                  FUNCTIONS                                                    ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


# --- Nothing Yet


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                                   CLASSES                                                     ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


class CertExpiryCheck:

    # Certificate variables:
    certificate: str | None = None
    not_valid_before: datetime.datetime | None = None
    not_valid_after: datetime.datetime | None = None
    exception: Exception | None = None
    exception_str: str | None = None

    # ┏┓
    # ┃ ┏┓┏┓╋┏┓┓┏┏╋┏┓┏┓
    # ┗┛┗┛┛┗┗┛ ┗┻┗┗┗┛┛

    def __init__(
            self,
            debug: bool = False,
            debug_prefix: str = "Certs Exp. | "
    ):

        """
        This simple class just checks if a provided certificate is valid or not and what is the date range in which it
        is valid.
        """

        # Prepare the debugging utility:
        self._debug_prefix = debug_prefix
        self._printer = IceCreamDebugger(prefix = debug_prefix, includeContext = True)
        if not debug: self._printer.disable()

    # ┓┏  ┓
    # ┣┫┏┓┃┏┓┏┓┏┓┏
    # ┛┗┗ ┗┣┛┗ ┛ ┛
    #      ┛

    def enable_debug(self):
        self._printer.enable()

    def disable_debug(self):
        self._printer.disable()

    def _clear(self):
        self.not_valid_before = None
        self.not_valid_after = None
        self.exception = None
        self.exception_str = None

    # ┏┓           •
    # ┃┃┏┓┏┓┏┓┏┓┏┓╋┓┏┓┏
    # ┣┛┛ ┗┛┣┛┗ ┛ ┗┗┗ ┛
    #       ┛

    @property
    def is_valid(self) -> bool | None:

        """
        Quickly tests if a loaded certificate is valid or not.
        :return: True if the certificate is valid, False if it isn't. Returns None if no certificate was loaded.
        """

        # Check if the certificate was loaded correctly:
        if self.not_valid_after is None or self.not_valid_before is None:
            self._printer("Please load a certificate first.")
            return None

        # Check if the cert is within the given time limits:
        now = datetime.datetime.now(datetime.timezone.utc)
        if self.not_valid_before <= now <= self.not_valid_after: return True
        else: return False

    @property
    def ttl(self) -> float | None:

        """
        Gives the time for which the certificate is valid in seconds.
        :return: Time for which the certificate is valid in seconds. None if the certificate was not loaded.
        """

        # Check if the certificate was loaded correctly:
        if self.not_valid_after is None or self.not_valid_before is None:
            self._printer("Please load a certificate first.")
            return None

        # Calculate the no. of seconds left till expiry:
        now = datetime.datetime.now(datetime.timezone.utc)
        ttl_seconds = (self.not_valid_after - now).total_seconds()
        return ttl_seconds

    @property
    def ttl_human_readable(self) -> str | None:

        """
        Gives human-readable validity period for certificates.
        e.g.: 1. valid for a month
              2. expired 3 days ago
              3. valid for 2 months
        :return: Human-readable validity period for certificates. None if the certificate was not loaded.
        """

        # Check if the certificate was loaded correctly:
        if self.not_valid_after is None or self.not_valid_before is None:
            self._printer("Please load a certificate first.")
            return None

        # Calculate the no. of seconds left till expiry:
        now = datetime.datetime.now(datetime.timezone.utc)
        ttl_seconds = (self.not_valid_after - now).total_seconds()
        ttl_readable = humanize.naturaldelta(datetime.timedelta(seconds = ttl_seconds))
        if ttl_seconds <= 0: ttl_readable = f"expired {ttl_readable} ago"
        else: ttl_readable = f"valid for {ttl_readable}"
        return ttl_readable

    # ┓      ┓  ┏┓
    # ┃ ┏┓┏┏┓┃  ┃ ┏┓┏┓╋┏
    # ┗┛┗┛┗┗┻┗  ┗┛┗ ┛ ┗┛

    def load_local(self, path: str | io.BytesIO) -> bool:

        """
        Loads a certificate stored on a local file path and checks for its validity.
        :param path: local file path where the certificate is stored.
        :return: True if the certificate was loaded correctly, False if the certificate wasn't loaded.
        """

        # Note down the certificate that is being tested:
        self.certificate = path

        # Clear any previous assessment:
        self._clear()

        try:

            # Read the contents of the certificate file:
            if isinstance(path, str):
                with open(path, 'rb') as f:
                    cert_data = f.read()
            elif isinstance(path, io.BytesIO):
                path.seek(0)
                cert_data = path.read()
            else: cert_data = None

            # Load the certificate:
            cert = x509.load_pem_x509_certificate(cert_data, default_backend())

            # Get the not valid before and not valid after dates:
            self.not_valid_before = cert.not_valid_before_utc
            self.not_valid_after = cert.not_valid_after_utc

            # All done successfully:
            return True

        except Exception as exception:
            self._printer(path, exception)
            self.exception = exception
            self.exception_str = str(exception)
            return False

    # ┳┓            ┏┓
    # ┣┫┏┓┏┳┓┏┓╋┏┓  ┃ ┏┓┏┓╋┏
    # ┛┗┗ ┛┗┗┗┛┗┗   ┗┛┗ ┛ ┗┛

    def load_remote(self, hostname: str, port: int = 443) -> bool:

        """
        Loads the certificate of a remote host and checks for its validity.
        :param hostname: The remote host URL.
        :param port: The remote host's port on which the service is running.
        :return: True if the certificate was loaded correctly, False if the certificate wasn't loaded.
        """

        # Parse the URL's hostname:
        hostname = urlparse(hostname).hostname or hostname

        # Note down the certificate that is being tested:
        self.certificate = f"{hostname}:{port}"

        # Clear any previous assessment:
        self._clear()

        try:

            # Fetch the remote certificate:
            context = ssl.create_default_context()
            with socket.create_connection((hostname, port)) as sock:
                with context.wrap_socket(sock, server_hostname = hostname) as ssock:

                    # Attempt to retrieve the certificate in DER format
                    try:
                        der_cert = ssock.getpeercert(binary_form = True)
                        cert = x509.load_der_x509_certificate(der_cert, default_backend())

                    # If DER parsing fails, retrieve the certificate in PEM format:
                    except ValueError:
                        pem_cert = ssl.DER_cert_to_PEM_cert(ssock.getpeercert(binary_form = True))
                        cert = x509.load_pem_x509_certificate(pem_cert.encode(), default_backend())

                    # Get the not valid before and not valid after dates:
                    self.not_valid_before = cert.not_valid_before_utc
                    self.not_valid_after = cert.not_valid_after_utc

            # All done successfully:
            return True

        except Exception as exception:
            self._printer(hostname, port, exception)
            self.exception = exception
            self.exception_str = str(exception)
            return False


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                               MAIN PROGRAM                                                    ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


if __name__ == "__main__":

    def show_details(my_cert: CertExpiryCheck):
        print("\n---\n")
        print("Certificate   :", my_cert.certificate)
        print("Not Before    :", my_cert.not_valid_before)
        print("Not After     :", my_cert.not_valid_after)
        print("Is Valid      :", my_cert.is_valid)
        print("TTL (seconds) :", my_cert.ttl)
        print("TTL (human)   :", my_cert.ttl_human_readable)
        print("Exception     :", my_cert.exception_str)

    # Create an instance:
    my_cert = CertExpiryCheck(debug = False)

    # Test a local path:
    my_cert.load_local(r"C:\Users\Khushal P Soonderji\Desktop\Dktp Files\certs\mongo\mongo_data_cert.pem")
    show_details(my_cert)

    # Test a local path:
    my_cert.load_local(r"C:\Users\Khushal P Soonderji\Desktop\Dktp Files\certs\kafka\privkey.pem")
    show_details(my_cert)

    # Test a local path:
    my_cert.load_local(r"C:\Users\Khushal P Soonderji\Desktop\Dktp Files\certs\kafka\fullchain.pem")
    show_details(my_cert)

    # Test a remote path:
    my_cert.load_remote("https://thecaoffice.com/", port = 443)
    show_details(my_cert)
