"""Base support for certificate storage backends.

Version Added:
    6.0
"""

from __future__ import annotations

import logging
from abc import ABC, abstractmethod
from typing import Generic, TYPE_CHECKING, TypeVar

from django.utils.text import slugify
from typing_extensions import TypedDict

from reviewboard.certs.cert import CertDataFormat, CertPurpose
from reviewboard.certs.errors import CertificateNotFoundError
from reviewboard.certs.utils import normalize_cert_hostname

if TYPE_CHECKING:
    from collections.abc import Iterator, Mapping

    from typelets.django.strings import StrOrPromise

    from reviewboard.certs.cert import (Certificate,
                                        CertificateBundle,
                                        CertificateFingerprints)
    from reviewboard.site.models import LocalSite, AnyOrAllLocalSites


logger = logging.getLogger(__name__)


class BaseStoredData(ABC):
    """Base class for a stored certificate-related data.

    Version Added:
        6.0
    """

    ######################
    # Instance variables #
    ######################

    #: The Local Site owning this stored certificate.
    local_site: LocalSite | None

    #: The storage backend managing this certificate.
    storage: BaseCertificateStorageBackend

    #: A unique ID for this data in the storage backend.
    #:
    #: This should be considered an opaque value outside of the storage
    #: backend.
    storage_id: str

    def __init__(
        self,
        *,
        storage: BaseCertificateStorageBackend,
        storage_id: (str | None) = None,
        local_site: (LocalSite | None) = None,
    ) -> None:
        """Initialize the stored data.

        Args:
            storage (BaseCertificateStorageBackend):
                The storage backend managing this stored certificate.

            storage_id (str, optional):
                The opaque ID of the stored data in the backend.

                If not provided, :py:meth:`build_storage_id` will be called
                to generate a new ID.

            local_site (reviewboard.site.models.LocalSite, optional):
                The Local Site owning this stored certificate.
        """
        self.storage = storage
        self.local_site = local_site

        # Use the storage ID if provided, or build a new one off of computed
        # state if not.
        self.storage_id = storage_id or self.build_storage_id()

    def build_storage_id(self) -> str:
        """Return a new storage ID for the stored data.

        Version Added:
            8.0

        Returns:
            str:
            The new storage ID.
        """
        raise NotImplementedError


class BaseStoredCertificate(BaseStoredData):
    """Base class for a stored certificate.

    These represent a certificate stored in the backend, associating
    certificate data with backend-specific data such as a storage ID.

    Backends must subclass this in order to implement the following:

    * :py:meth:`get_cert_file_path`
    * :py:meth:`get_key_file_path`
    * :py:meth:`load_certificate`

    Version Added:
        6.0
    """

    ######################
    # Instance variables #
    ######################

    #: The hostname associated with the certificate.
    #:
    #: Version Added:
    #:     8.0
    hostname: str

    #: The port associated with the certificate.
    #:
    #: Version Added:
    #:     8.0
    port: int

    #: The purpose set for a certificate.
    #:
    #: This defines whether the certificate is used for trusting a remote
    #: server or authenticating Review Board with a service.
    #:
    #: Version Added:
    #:     8.0
    purpose: CertPurpose

    #: The certificate data being stored.
    _certificate: Certificate | None

    def __init__(
        self,
        *,
        storage: BaseCertificateStorageBackend,
        certificate: (Certificate | None) = None,
        hostname: (str | None) = None,
        port: (int | None) = None,
        purpose: (CertPurpose | None) = None,
        storage_id: (str | None) = None,
        local_site: (LocalSite | None) = None,
    ) -> None:
        """Initialize the stored certificate.

        Version Changed:
            8.0:
            Added the ``purpose``, ``hostname``, and ``port`` arguments.

        Args:
            storage (BaseCertificateStorageBackend):
                The storage backend managing this stored certificate.

            certificate (reviewboard.certs.cert.Certificate, optional):
                The certificate data being stored.

            hostname (str, optional):
                The hostname associated with the certificate.

                If ``certificate`` is set, this must either match or not be
                provided. If it's not set, then this must be provided.

                Version Added:
                    8.0

            port (int, optional):
                The port associated with the certificate.

                If ``certificate`` is set, this must either match or not be
                provided. If it's not set, then this must be provided.

                Version Added:
                    8.0

            purpose (reviewboard.certs.cert.CertPurpose):
                The purpose of the certificate.

                This sets whether the certificate should be used to trust
                a remote server or authenticate Review Board.

                If ``certificate`` is set, this must either match or not be
                provided. If it's not set, then this must be provided.

                Version Added:
                    8.0

            storage_id (str, optional):
                The opaque ID of the stored data in the backend.

            local_site (reviewboard.site.models.LocalSite, optional):
                The Local Site owning this stored certificate.

        Raises:
            ValueError:
                One or more arguments were not provided or contained invalid
                values.
        """
        if certificate:
            if not hostname:
                hostname = normalize_cert_hostname(certificate.hostname)
            else:
                hostname = normalize_cert_hostname(hostname)

                if normalize_cert_hostname(certificate.hostname) != hostname:
                    raise ValueError(
                        'The provided hostname= argument must match the '
                        'hostname of the provided certificate=.'
                    )

            if not port:
                port = certificate.port
            elif certificate.port != port:
                raise ValueError(
                    'The provided port= argument must match the port '
                    'of the provided certificate=.'
                )

            if not purpose:
                purpose = certificate.purpose
            elif certificate.purpose != purpose:
                raise ValueError(
                    'The provided purpose= argument must match the purpose '
                    'of the provided certificate=.'
                )
        else:
            if not hostname:
                raise ValueError(
                    'Either certificate= or hostname= must be provided.'
                )

            if not port:
                raise ValueError(
                    'Either certificate= or port= must be provided.'
                )

            if not purpose:
                raise ValueError(
                    'Either certificate= or purpose= must be provided.'
                )

            hostname = normalize_cert_hostname(hostname)

        self.hostname = hostname
        self.port = port
        self.purpose = purpose
        self._certificate = certificate

        super().__init__(storage=storage,
                         storage_id=storage_id,
                         local_site=local_site)

    @property
    def certificate(self) -> Certificate:
        """The Certificate data in storage.

        Type:
            reviewboard.certs.cert.Certificate

        Raises:
            reviewboard.certs.errors.CertificateStorageError:
                There was an error loading the certificate from storage.

                Details are in the error message.
        """
        if self._certificate is None:
            self._certificate = self.load_certificate()

        return self._certificate

    @abstractmethod
    def get_cert_file_path(
        self,
        *,
        data_format: CertDataFormat = CertDataFormat.PEM,
    ) -> str:
        """Return the filesystem path to a certificate.

        This must be implemented by subclasses.

        Args:
            data_format (reviewboard.certs.cert.CertDataFormat, optional):
                The requested certificate data file format.

        Returns:
            str:
            The resulting file path.
        """
        raise NotImplementedError

    @abstractmethod
    def get_key_file_path(
        self,
        *,
        data_format: CertDataFormat = CertDataFormat.PEM,
    ) -> str | None:
        """Return the filesystem path to a certificate private key.

        Not all certificates will have an associated private key. If one is
        not available, this will return ``None``.

        This must be implemented by subclasses.

        Args:
            data_format (reviewboard.certs.cert.CertDataFormat, optional):
                The requested private key data file format.

        Returns:
            str:
            The resulting file path, or ``None`` if a private key is not
            available.
        """
        raise NotImplementedError

    @abstractmethod
    def load_certificate(self) -> Certificate:
        """Load and return a Certificate from storage.

        This must be implemented by subclasses.

        Returns:
            reviewboard.certs.cert.Certificate:
            The resulting loaded certificate.

        Raises:
            reviewboard.certs.errors.CertificateStorageError:
                There was an error loading the certificate from storage.

                Details are in the error message.
        """
        raise NotImplementedError


class BaseStoredCertificateBundle(BaseStoredData):
    """Base class for a stored CA bundle.

    These represent a root CA bundle in the backend, associating root and
    intermediary certificate data with backend-specific data such as a
    storage ID.

    Backends must subclass this in order to implement the following:

    * :py:meth:`load_bundle`

    Version Added:
        6.0
    """

    ######################
    # Instance variables #
    ######################

    #: The associated name of the CA bundle.
    #:
    #: This is in :term:`slug` format.
    #:
    #: Version Added:
    #:     8.0
    name: str

    #: The certificate bundle data being stored.
    _bundle: CertificateBundle | None

    def __init__(
        self,
        *,
        storage: BaseCertificateStorageBackend,
        name: (str | None) = None,
        bundle: (CertificateBundle | None) = None,
        storage_id: (str | None) = None,
        local_site: (LocalSite | None) = None,
    ) -> None:
        """Initialize the stored certificate.

        Version Changed:
            8.0:
            * Added the ``name`` argument.

        Args:
            storage (BaseCertificateStorageBackend):
                The storage backend managing this stored certificate.

            name (str, optional):
                The associated name of the CA bundle.

                If ``bundle`` is set, this must either match or not be
                provided. If it's not set, then this must be provided.

                Version Added:
                    8.0

            bundle (reviewboard.certs.cert.CertificateBundle, optional):
                The certificate bundle data being stored.

                Either this or ``name`` must be provided.

            storage_id (str, optional):
                The opaque ID of the stored data in the backend.

            local_site (reviewboard.site.models.LocalSite, optional):
                The Local Site owning this stored certificate.

        Raises:
            ValueError:
                One or more arguments were invalid or missing.
        """
        if bundle:
            if not name:
                name = bundle.name
            elif bundle.name != name:
                raise ValueError(
                    'The provided name= argument must match the name '
                    'of the provided bundle=.'
                )
        else:
            if not name:
                raise ValueError('Either bundle= or name= must be provided.')

        if name != slugify(name):
            raise ValueError('The name must be in slug format.')

        self.name = name
        self._bundle = bundle

        super().__init__(storage=storage,
                         storage_id=storage_id,
                         local_site=local_site)

    @property
    def bundle(self) -> CertificateBundle:
        """The CA bundle data in storage.

        Type:
            reviewboard.certs.cert.CertificateBundle

        Raises:
            reviewboard.certs.errors.CertificateStorageError:
                There was an error loading the CA bundle from storage.

                Details are in the error message.
        """
        if self._bundle is None:
            self._bundle = self.load_bundle()

        return self._bundle

    @abstractmethod
    def get_bundle_file_path(
        self,
        *,
        data_format: CertDataFormat = CertDataFormat.PEM,
    ) -> str:
        """Return the filesystem path to a certificate bundle.

        This must be implemented by subclasses.

        Args:
            data_format (reviewboard.certs.cert.CertDataFormat, optional):
                The requested certificate bundle data file format.

        Returns:
            str:
            The resulting file path.
        """
        raise NotImplementedError

    @abstractmethod
    def load_bundle(self) -> CertificateBundle:
        """Load and return the CA bundle data from storage.

        Returns:
            reviewboard.certs.cert.CertificateBundle:
            The resulting loaded CA bundle.

        Raises:
            reviewboard.certs.errors.CertificateStorageError:
                There was an error loading the CA bundle from storage.

                Details are in the error message.
        """
        raise NotImplementedError


class BaseStoredCertificateFingerprints(BaseStoredData):
    """Base class for stored certificate fingerprints.

    These represent fingerprints that can be used to verify a server-issued
    certificate, associating them with backend-specific data such as a
    storage ID.

    Backends must subclass this in order to implement the following:

    * :py:meth:`load_fingerprints`

    Version Added:
        6.0
    """

    ######################
    # Instance variables #
    ######################

    #: The hostname associated with the certificate.
    #:
    #: Version Added:
    #:     8.0
    hostname: str

    #: The port associated with the certificate.
    #:
    #: Version Added:
    #:     8.0
    port: int

    #: The fingerprints data being stored.
    _fingerprints: CertificateFingerprints | None

    def __init__(
        self,
        *,
        storage: BaseCertificateStorageBackend,
        hostname: str,
        port: int,
        fingerprints: (CertificateFingerprints | None) = None,
        storage_id: (str | None) = None,
        local_site: (LocalSite | None) = None,
    ) -> None:
        """Initialize the stored certificate.

        Version Changed:
            8.0:
            * Added the required ``hostname`` and ``port`` arguments.

        Args:
            storage (BaseCertificateStorageBackend):
                The storage backend managing this stored certificate.

            hostname (str):
                The hostname associated with the fingerprints.

                Version Added:
                    8.0

            port (int):
                The port associated with the fingerprints.

                Version Added:
                    8.0

            fingerprints (reviewboard.certs.cert.CertificateFingerprints):
                The certificate fingerprints data being stored.

            storage_id (str, optional):
                The opaque ID of the stored data in the backend.

            local_site (reviewboard.site.models.LocalSite, optional):
                The Local Site owning this stored certificate.
        """
        self.hostname = normalize_cert_hostname(hostname)
        self.port = port
        self._fingerprints = fingerprints

        super().__init__(storage=storage,
                         storage_id=storage_id,
                         local_site=local_site)

    @property
    def fingerprints(self) -> CertificateFingerprints:
        """The certificate fingerprint data in storage.

        Type:
            reviewboard.certs.cert.CertificateFingerprints

        Raises:
            reviewboard.certs.errors.CertificateStorageError:
                There was an error loading the fingerprints from storage.

                Details are in the error message.
        """
        if self._fingerprints is None:
            self._fingerprints = self.load_fingerprints()

        return self._fingerprints

    @abstractmethod
    def load_fingerprints(self) -> CertificateFingerprints:
        """Load and return the certificate fingerprint data from storage.

        Returns:
            reviewboard.certs.cert.CertificateFingerprints:
            The resulting loaded fingerprints.

        Raises:
            reviewboard.certs.errors.CertificateStorageError:
                There was an error loading the fingerprints from storage.

                Details are in the error message.
        """
        raise NotImplementedError


class StorageStats(TypedDict):
    """Statistics about the content in a storage backend.

    This contains counts for stored CA bundles, certificates, and verified
    fingerprints. Along with this, it contains a UUID representing the current
    generation of the stats, which is useful for caching.

    Version Added:
        6.0
    """

    #: The number of stored CA bundles.
    ca_bundle_count: int

    #: The total number of stored certificates.
    #:
    #: This must be a sum of all the values in
    #: :py:attr:`cert_counts_by_purpose`.
    cert_count: int

    #: The number of stored certificates by purpose.
    #:
    #: Version Added:
    #:     8.0
    cert_counts_by_purpose: Mapping[CertPurpose, int]

    #: The number of stored verified fingerprints.
    fingerprint_count: int

    #: A UUID specific to the current state of the content in the backend.
    #:
    #: This can be used to help with caching and invalidation of other state.
    state_uuid: str


_StoredCertT = TypeVar(
    '_StoredCertT',
    bound=BaseStoredCertificate,
)
_StoredCertBundleT = TypeVar(
    '_StoredCertBundleT',
    bound=BaseStoredCertificateBundle,
)
_StoredCertFingerprintsT = TypeVar(
    '_StoredCertFingerprintsT',
    bound=BaseStoredCertificateFingerprints,
)


class BaseCertificateStorageBackend(
    ABC,
    Generic[_StoredCertT, _StoredCertBundleT, _StoredCertFingerprintsT],
):
    """Base class for a certificate storage backend.

    Store backends are responsible for storing certificate and private key
    data, CA bundles, and fingerprint verification information.

    Backends must be able to persist certificates, private keys, and CA bundles
    on the filesystem for the lifetime of the web server process in order to
    allow consumers to access them on demand.

    Subclasses must implement the following attributes:

    * :py:attr:`backend_id`
    * :py:attr:`name`

    And the following methods:

    * :py:meth:`add_ca_bundle`
    * :py:meth:`add_certificate`
    * :py:meth:`add_fingerprints`
    * :py:meth:`delete_ca_bundle`
    * :py:meth:`delete_certificate`
    * :py:meth:`delete_fingerprints`
    * :py:meth:`get_stats`
    * :py:meth:`get_stored_ca_bundle_by_id`
    * :py:meth:`get_stored_ca_bundle`
    * :py:meth:`get_stored_certificate_by_id`
    * :py:meth:`get_stored_certificate`
    * :py:meth:`get_stored_fingerprints_by_id`
    * :py:meth:`get_stored_fingerprints`
    * :py:meth:`iter_ca_bundles`
    * :py:meth:`iter_stored_ca_bundles`
    * :py:meth:`iter_stored_certificates`
    * :py:meth:`iter_stored_fingerprints`

    Version Changed:
        8.0:
        All certificate storage methods have been updated to take a
        ``purpose`` parameter, which is now considered mandatory.

    Version Added:
        6.0
    """

    #: The ID of this storage backend.
    #:
    #: This must be provided by subclasses, and must be unique.
    backend_id: (str | None) = None

    #: The display name of the storage backend.
    #:
    #: This must be provided by subclasses.
    name: (StrOrPromise | None) = ''

    ######################
    # Instance variables #
    ######################

    #: The path to the local filesystem storage usable by this backend.
    #:
    #: This is set when constructing the backend, and should not be changed.
    #: All filesystem operations must be limited to directories under this
    #: path.
    storage_path: str

    def __init__(
        self,
        storage_path: str,
    ) -> None:
        """Initialize the storage backend."""
        assert self.backend_id, 'backend_id must be set.'
        assert self.name, 'name must be set.'

        self.storage_path = storage_path

    @abstractmethod
    def get_stats(
        self,
        *,
        local_site: AnyOrAllLocalSites = None,
    ) -> StorageStats:
        """Return statistics on the certificates managed by the backend.

        This will include the total number of stored certificates, CA bundles,
        and verified fingerprints across zero, one, or all Local Sites.

        It also includes state UUIDs that represent the current states of the
        backend.

        Subclasses must ensure that any modifications to storage result in
        updates to this UUID. They may also want to ensure information is
        properly cached for a period of time, and invalidated upon
        modification.

        Args:
            local_site (reviewboard.site.models.LocalSite or
                        reviewboard.site.models.LocalSite.ALL, optional):
                An optional LocalSite bound to the stats.

                If ``None``, the global site will be used.

        Returns:
            StorageStats:
            The computed or cached storage statistics.

        Raises:
            reviewboard.cert.errors.CertificateStorageError:
                There was an error looking up cetificate stats.
        """
        raise NotImplementedError

    @abstractmethod
    def add_ca_bundle(
        self,
        bundle: CertificateBundle,
        *,
        local_site: (LocalSite | None) = None,
    ) -> _StoredCertBundleT:
        """Add a root CA bundle to storage.

        The certificate bundle's name along with ``local_site`` are considered
        unique within the storage backend. If there's an existing bundle with
        this information, it will be replaced.

        Args:
            bundle (reviewboard.certs.cert.CertificateBundle):
                The bundle to add.

            local_site (reviewboard.site.models.LocalSite, optional):
                An optional LocalSite that owns the certificate.

                If ``None``, the global site will be used.

        Returns:
            BaseStoredCertificateBundle:
            The resulting stored certificate bundle.

        Raises:
            reviewboard.cert.errors.CertificateStorageError:
                There was an error storing this CA bundle.
        """
        raise NotImplementedError

    def delete_ca_bundle(
        self,
        *,
        name: str,
        local_site: (LocalSite | None) = None,
    ) -> None:
        """Delete a root CA bundle from storage.

        Args:
            name (str):
                The unique name of the CA bundle in storage.

            local_site (reviewboard.site.models.LocalSite, optional):
                An optional LocalSite that owns the certificate.

                If ``None``, the global site will be used.

        Raises:
            reviewboard.cert.errors.CertificateNotFoundError:
                The CA bundle was not found.

            reviewboard.cert.errors.CertificateStorageError:
                There was an error deleting this CA bundle.
        """
        stored_bundle = self.get_stored_ca_bundle(
            name=name,
            local_site=local_site)

        if stored_bundle is None:
            raise CertificateNotFoundError()

        assert stored_bundle.storage_id is not None

        self.delete_ca_bundle_by_id(stored_bundle.storage_id)

    @abstractmethod
    def delete_ca_bundle_by_id(
        self,
        storage_id: str,
    ) -> None:
        """Delete a root CA bundle from storage identified by ID.

        Args:
            storage_id (str):
                The ID of the CA bundle to delete.

        Raises:
            reviewboard.cert.errors.CertificateNotFoundError:
                The fingerprints were not found.

            reviewboard.cert.errors.CertificateStorageError:
                There was an error deleting this CA bundle.
        """
        raise NotImplementedError

    @abstractmethod
    def get_stored_ca_bundle(
        self,
        *,
        name: str,
        local_site: (LocalSite | None) = None,
    ) -> _StoredCertBundleT | None:
        """Return a root CA bundle in storage.

        Args:
            name (str):
                The unique name of the CA bundle in storage.

            local_site (reviewboard.site.models.LocalSite, optional):
                An optional LocalSite that owns the certificate.

                If ``None``, the global site will be used.

        Returns:
            BaseStoredCertificateBundle:
            The stored certificate bundle in storage, or ``None`` if not found.

        Raises:
            reviewboard.cert.errors.CertificateStorageError:
                There was an error retrieving this CA bundle.
        """
        raise NotImplementedError

    @abstractmethod
    def get_stored_ca_bundle_by_id(
        self,
        storage_id: str,
    ) -> _StoredCertBundleT | None:
        """Return a root CA bundle in storage identified by ID.

        Args:
            storage_id (str):
                The ID of the CA bundle in storage.

        Returns:
            BaseStoredCertificateBundle:
            The stored certificate bundle in storage, or ``None`` if not found.

        Raises:
            reviewboard.cert.errors.CertificateStorageError:
                There was an error retrieving this CA bundle.
        """
        raise NotImplementedError

    @abstractmethod
    def iter_stored_ca_bundles(
        self,
        *,
        local_site: AnyOrAllLocalSites = None,
        start: int = 0,
    ) -> Iterator[_StoredCertBundleT]:
        """Iterate through all root CA bundles in storage.

        Args:
            local_site (reviewboard.site.models.LocalSite, optional):
                An optional LocalSite that owns the certificate.

                If ``None``, the global site will be used.

            start (int, optional):
                The 0-based index within the list of root CA bundles to start
                iterating at.

        Yields:
            BaseStoredCertificateBundle:
            Each stored certificate bundle in storage.

        Raises:
            reviewboard.cert.errors.CertificateStorageError:
                There was an error iterating through CA bundles.
        """
        raise NotImplementedError

    def get_ca_bundles_dir(
        self,
        *,
        local_site: (LocalSite | None) = None,
    ) -> str:
        """Return a path containing all CA bundle files.

        Args:
            local_site (reviewboard.site.models.LocalSite, optional):
                An optional LocalSite that owns the bundles.

                If ``None``, the global site will be used.

        Returns:
            str:
            The path to the bundles directory.
        """
        raise NotImplementedError

    @abstractmethod
    def add_certificate(
        self,
        certificate: Certificate,
        *,
        local_site: (LocalSite | None) = None,
    ) -> _StoredCertT:
        """Add a certificate to storage.

        The certificate's hostname and port along with ``local_site`` are
        considered unique within the storage backend. If there's an existing
        certificate with this information, it will be replaced.

        Args:
            certificate (reviewboard.certs.cert.Certificate):
                The certificate to add.

            local_site (reviewboard.site.models.LocalSite, optional):
                An optional LocalSite that owns the certificate.

                If ``None``, the global site will be used.

        Returns:
            BaseStoredCertificate:
            The resulting stored certificate.

        Raises:
            reviewboard.cert.errors.CertificateStorageError:
                There was an error storing this certificate.
        """
        raise NotImplementedError

    def delete_certificate(
        self,
        *,
        hostname: str,
        port: int,
        purpose: CertPurpose,
        local_site: (LocalSite | None) = None,
    ) -> None:
        """Delete a certificate from storage.

        Version Changed:
            8.0:
            Added the ``purpose`` argument.

        Args:
            hostname (str):
                The hostname of the certificate in storage.

            port (int):
                The port on the host serving the certificate.

            purpose (reviewboard.certs.cert.CertPurpose):
                The purpose of the certificate to delete.

                This indicates whether the certificate to match is used
                to trust a remote server or authenticate Review Board.

                Version Added:
                    8.0

            local_site (reviewboard.site.models.LocalSite, optional):
                An optional LocalSite that owns the certificate.

                If ``None``, the global site will be used.

        Raises:
            reviewboard.cert.errors.CertificateNotFoundError:
                The fingerprints were not found.

            reviewboard.cert.errors.CertificateStorageError:
                There was an error deleting this certificate.
        """
        stored_certificate = self.get_stored_certificate(
            hostname=hostname,
            port=port,
            local_site=local_site,
            purpose=purpose,
        )

        if stored_certificate is None:
            raise CertificateNotFoundError()

        assert stored_certificate.storage_id

        self.delete_certificate_by_id(stored_certificate.storage_id)

    @abstractmethod
    def delete_certificate_by_id(
        self,
        storage_id: str,
    ) -> None:
        """Delete a certificate from storage.

        Args:
            storage_id (str):
                The ID of the certificate to delete.

        Raises:
            reviewboard.cert.errors.CertificateNotFoundError:
                The fingerprints were not found.

            reviewboard.cert.errors.CertificateStorageError:
                There was an error deleting this certificate.
        """
        raise NotImplementedError

    @abstractmethod
    def get_stored_certificate(
        self,
        *,
        hostname: str,
        port: int,
        purpose: CertPurpose,
        local_site: (LocalSite | None) = None,
    ) -> _StoredCertT | None:
        """Return a certificate from storage.

        Version Changed:
            8.0:
            Added the ``purpose`` argument.

        Args:
            hostname (str):
                The hostname of the certificate in storage.

            port (int):
                The port on the host serving the certificate.

            purpose (reviewboard.certs.cert.CertPurpose):
                The purpose of the certificate to fetch.

                This indicates whether the certificate is used to trust a
                remote server or authenticate Review Board.

                Version Added:
                    8.0

            local_site (reviewboard.site.models.LocalSite, optional):
                An optional LocalSite that owns the certificate.

                If ``None``, the global site will be used.

        Returns:
            BaseStoredCertificate:
            The stored certificate in storage, or ``None`` if not found.

        Raises:
            reviewboard.cert.errors.CertificateStorageError:
                There was an error retrieving this certificate.
        """
        raise NotImplementedError

    @abstractmethod
    def get_stored_certificate_by_id(
        self,
        storage_id: str,
    ) -> _StoredCertT | None:
        """Return a certificate from storage identified by ID.

        Args:
            storage_id (str):
                The ID of the certificate in storage.

        Returns:
            BaseStoredCertificate:
            The stored certificate in storage, or ``None`` if not found.

        Raises:
            reviewboard.cert.errors.CertificateStorageError:
                There was an error retrieving this certificate.
        """
        raise NotImplementedError

    @abstractmethod
    def iter_stored_certificates(
        self,
        *,
        purpose: CertPurpose,
        local_site: AnyOrAllLocalSites = None,
        start: int = 0,
    ) -> Iterator[_StoredCertT]:
        """Iterate through all certificates in storage.

        Version Changed:
            8.0:
            Added the ``purpose`` argument.

        Args:
            purpose (reviewboard.certs.cert.CertPurpose):
                The purpose of the certificates to filter for.

                This will limit results to certificates that match the given
                purpose.

                Version Added:
                    8.0

            local_site (reviewboard.site.models.LocalSite or
                        reviewboard.site.models.LocalSite.ALL, optional):
                An optional LocalSite that owns the certificates.

                If ``None``, the global site will be used.

            start (int, optional):
                The 0-based index within the list of root CA bundles to start
                iterating at.

        Yields:
            BaseStoredCertificate:
            Each stored certificate in storage.

        Raises:
            reviewboard.cert.errors.CertificateStorageError:
                There was an error iterating through stored certificates.
        """
        raise NotImplementedError

    @abstractmethod
    def add_fingerprints(
        self,
        fingerprints: CertificateFingerprints,
        *,
        hostname: str,
        port: int,
        local_site: (LocalSite | None) = None,
    ) -> _StoredCertFingerprintsT:
        """Add verified certificate fingerprints to storage.

        The ``hostname``, ``port``, and ``local_site`` values taken together
        must be unique within a storage backend.

        Backends may store this together with the certificates or separately.

        Args:
            hostname (str):
                The hostname serving the certificate.

            port (int):
                The port on the host serving the certificate.

            fingerprints (reviewboard.certs.cert.CertificateFingerprints):
                The certificate fingerprints to add.

            local_site (reviewboard.site.models.LocalSite, optional):
                An optional LocalSite that owns the certificate.

                If ``None``, the global site will be used.

        Returns:
            BaseStoredCertificateFingerprints:
            The resulting stored certificate fingerprints.

        Raises:
            reviewboard.cert.errors.CertificateStorageError:
                There was an error storing these fingerprints.
        """
        raise NotImplementedError

    def delete_fingerprints(
        self,
        *,
        hostname: str,
        port: int,
        local_site: (LocalSite | None) = None,
    ) -> None:
        """Delete certificate fingerprints from storage.

        Args:
            hostname (str):
                The hostname of the certificate in storage.

            port (int):
                The port on the host serving the certificate.

            local_site (reviewboard.site.models.LocalSite, optional):
                An optional LocalSite that owns the certificate.

                If ``None``, the global site will be used.

        Raises:
            reviewboard.cert.errors.CertificateNotFoundError:
                The fingerprints were not found.

            reviewboard.cert.errors.CertificateStorageError:
                There was an error deleting these fingerprints.
        """
        stored_fingerprints = self.get_stored_fingerprints(
            hostname=hostname,
            port=port,
            local_site=local_site)

        if stored_fingerprints is None:
            raise CertificateNotFoundError()

        assert stored_fingerprints.storage_id is not None

        self.delete_fingerprints_by_id(
            stored_fingerprints.storage_id)

    @abstractmethod
    def delete_fingerprints_by_id(
        self,
        storage_id: str,
    ) -> None:
        """Delete certificate fingerprints from storage identified by ID.

        Args:
            storage_id (str):
                The ID of the certificate fingerprints to delete.

        Raises:
            reviewboard.cert.errors.CertificateNotFoundError:
                The fingerprints were not found.

            reviewboard.cert.errors.CertificateStorageError:
                There was an error deleting these fingerprints.
        """
        raise NotImplementedError

    @abstractmethod
    def iter_stored_fingerprints(
        self,
        *,
        local_site: AnyOrAllLocalSites = None,
        start: int = 0,
    ) -> Iterator[_StoredCertFingerprintsT]:
        """Iterate through all certificate fingerprints in storage.

        Args:
            local_site (reviewboard.site.models.LocalSite or
                        reviewboard.site.models.LocalSite.ALL, optional):
                An optional LocalSite that owns the certificate fingerprints.

                If ``None``, the global site will be used.

            start (int, optional):
                The 0-based index within the list of root CA bundles to start
                iterating at.

        Yields:
            BaseStoredCertificateFingerprints:
            Each stored certificate fingerprints in storage.

        Raises:
            reviewboard.cert.errors.CertificateStorageError:
                There was an error iterating through these fingerprints.
        """
        raise NotImplementedError

    @abstractmethod
    def get_stored_fingerprints(
        self,
        *,
        hostname: str,
        port: int,
        local_site: (LocalSite | None) = None,
    ) -> _StoredCertFingerprintsT | None:
        """Return certificate fingerprints from storage.

        Args:
            hostname (str):
                The hostname of the certificate in storage.

            port (int):
                The port on the host serving the certificate.

            local_site (reviewboard.site.models.LocalSite, optional):
                An optional LocalSite that owns the certificate.

                If ``None``, the global site will be used.

        Returns:
            BaseStoredCertificateFingerprints:
            The stored certificate fingerprints, or ``None`` if not found.

        Raises:
            reviewboard.cert.errors.CertificateStorageError:
                There was an error retrieving these fingerprints.
        """
        raise NotImplementedError

    @abstractmethod
    def get_stored_fingerprints_by_id(
        self,
        storage_id: str,
    ) -> _StoredCertFingerprintsT | None:
        """Return certificate fingerpritns from storage identified by ID.

        Args:
            storage_id (str):
                The ID of the certificate fingerprints in storage.

        Returns:
            CertificateFingerprints:
            The stored certificate fingerprints, or ``None`` if not found.

        Raises:
            reviewboard.cert.errors.CertificateStorageError:
                There was an error retrieving these fingerprints.
        """
        raise NotImplementedError
