// Copyright 2023 The Jujutsu Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Generic APIs to work with cryptographic signatures created and verified by
//! various backends.

use std::fmt::Debug;
use std::fmt::Display;
use std::sync::Mutex;

use clru::CLruCache;
use thiserror::Error;

use crate::backend::CommitId;
use crate::config::ConfigGetError;
use crate::gpg_signing::GpgBackend;
use crate::gpg_signing::GpgsmBackend;
use crate::settings::UserSettings;
use crate::ssh_signing::SshBackend;
use crate::store::COMMIT_CACHE_CAPACITY;
#[cfg(feature = "testing")]
use crate::test_signing_backend::TestSigningBackend;

/// A status of the signature, part of the [Verification] type.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SigStatus {
    /// Valid signature that matches the data.
    Good,
    /// Valid signature that could not be verified (e.g. due to an unknown key).
    Unknown,
    /// Valid signature that does not match the signed data.
    Bad,
}

impl Display for SigStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let s = match self {
            Self::Good => "good",
            Self::Unknown => "unknown",
            Self::Bad => "bad",
        };
        write!(f, "{s}")
    }
}

/// The result of a signature verification.
/// Key and display are optional additional info that backends can or can not
/// provide to add additional information for the templater to potentially show.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Verification {
    /// The status of the signature.
    pub status: SigStatus,
    /// The key id representation, if available. For GPG, this will be the key
    /// fingerprint.
    pub key: Option<String>,
    /// A display string, if available. For GPG, this will be formatted primary
    /// user ID.
    pub display: Option<String>,
}

impl Verification {
    /// A shortcut to create an `Unknown` verification with no additional
    /// metadata.
    pub fn unknown() -> Self {
        Self {
            status: SigStatus::Unknown,
            key: None,
            display: None,
        }
    }

    /// Create a new verification
    pub fn new(status: SigStatus, key: Option<String>, display: Option<String>) -> Self {
        Self {
            status,
            key,
            display,
        }
    }
}

/// The backend for signing and verifying cryptographic signatures.
///
/// This allows using different signers, such as GPG or SSH, or different
/// versions of them.
pub trait SigningBackend: Debug + Send + Sync {
    /// Name of the backend, used in the config and for display.
    fn name(&self) -> &str;

    /// Check if the signature can be read and verified by this backend.
    ///
    /// Should check the signature format, usually just looks at the prefix.
    fn can_read(&self, signature: &[u8]) -> bool;

    /// Create a signature for arbitrary data.
    ///
    /// The `key` parameter is what `jj sign` receives as key argument, or what
    /// is configured in the `signing.key` config.
    fn sign(&self, data: &[u8], key: Option<&str>) -> SignResult<Vec<u8>>;

    /// Verify a signature. Should be reflexive with `sign`:
    /// ```rust,ignore
    /// verify(data, sign(data)?)?.status == SigStatus::Good
    /// ```
    fn verify(&self, data: &[u8], signature: &[u8]) -> SignResult<Verification>;
}

/// An error type for the signing/verifying operations
#[derive(Debug, Error)]
pub enum SignError {
    /// The verification failed because the signature *format* was invalid.
    #[error("Invalid signature")]
    InvalidSignatureFormat,
    /// A generic error from the backend impl.
    #[error("Signing error")]
    Backend(#[source] Box<dyn std::error::Error + Send + Sync>),
}

/// A result type for the signing/verifying operations
pub type SignResult<T> = Result<T, SignError>;

/// An error type for the signing backend initialization.
#[derive(Debug, Error)]
pub enum SignInitError {
    /// If the backend name specified in the config is not known.
    #[error("Unknown signing backend configured: {0}")]
    UnknownBackend(String),
    /// Failed to load backend configuration.
    #[error("Failed to configure signing backend")]
    BackendConfig(#[source] ConfigGetError),
}

/// A enum that describes if a created/rewritten commit should be signed or not.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum SignBehavior {
    /// Drop existing signatures.
    /// This is what jj did before signing support or does now when a signing
    /// backend is not configured.
    Drop,
    /// Only sign commits that were authored by self and already signed,
    /// "preserving" the signature across rewrites.
    /// This is what jj does when a signing backend is configured.
    Keep,
    /// Sign/re-sign commits that were authored by self and drop them for
    /// others. This is what jj does when configured to always sign.
    Own,
    /// Always sign commits, regardless of who authored or signed them before.
    /// This is what jj does on `jj sign -f`.
    Force,
}

/// Wraps low-level signing backends and adds caching, similar to `Store`.
#[derive(Debug)]
pub struct Signer {
    /// The backend that is used for signing commits.
    /// Optional because signing might not be configured.
    main_backend: Option<Box<dyn SigningBackend>>,
    /// All known backends without the main one - used for verification.
    /// Main backend is also used for verification, but it's not in this list
    /// for ownership reasons.
    backends: Vec<Box<dyn SigningBackend>>,
    cache: Mutex<CLruCache<CommitId, Verification>>,
}

impl Signer {
    /// Creates a signer based on user settings. Uses all known backends, and
    /// chooses one of them to be used for signing depending on the config.
    pub fn from_settings(settings: &UserSettings) -> Result<Self, SignInitError> {
        let mut backends: Vec<Box<dyn SigningBackend>> = vec![
            Box::new(GpgBackend::from_settings(settings).map_err(SignInitError::BackendConfig)?),
            Box::new(GpgsmBackend::from_settings(settings).map_err(SignInitError::BackendConfig)?),
            Box::new(SshBackend::from_settings(settings).map_err(SignInitError::BackendConfig)?),
            #[cfg(feature = "testing")]
            Box::new(TestSigningBackend),
        ];

        let main_backend = settings
            .signing_backend()
            .map_err(SignInitError::BackendConfig)?
            .map(|backend| {
                backends
                    .iter()
                    .position(|b| b.name() == backend)
                    .map(|i| backends.remove(i))
                    .ok_or(SignInitError::UnknownBackend(backend))
            })
            .transpose()?;

        Ok(Self::new(main_backend, backends))
    }

    /// Creates a signer with the given backends.
    pub fn new(
        main_backend: Option<Box<dyn SigningBackend>>,
        other_backends: Vec<Box<dyn SigningBackend>>,
    ) -> Self {
        Self {
            main_backend,
            backends: other_backends,
            cache: Mutex::new(CLruCache::new(COMMIT_CACHE_CAPACITY.try_into().unwrap())),
        }
    }

    /// Checks if the signer can sign, i.e. if a main backend is configured.
    pub fn can_sign(&self) -> bool {
        self.main_backend.is_some()
    }

    /// This is just a pass-through to the main backend that unconditionally
    /// creates a signature.
    pub fn sign(&self, data: &[u8], key: Option<&str>) -> SignResult<Vec<u8>> {
        self.main_backend
            .as_ref()
            .expect("tried to sign without checking can_sign first")
            .sign(data, key)
    }

    /// Looks for backend that can verify the signature and returns the result
    /// of its verification.
    pub fn verify(
        &self,
        commit_id: &CommitId,
        data: &[u8],
        signature: &[u8],
    ) -> SignResult<Verification> {
        let cached = self.cache.lock().unwrap().get(commit_id).cloned();
        if let Some(check) = cached {
            return Ok(check);
        }

        let verification = self
            .main_backend
            .iter()
            .chain(self.backends.iter())
            .filter(|b| b.can_read(signature))
            // skip unknown and invalid sigs to allow other backends that can read to try
            // for example, we might have gpg and sq, both of which could read a PGP signature
            .find_map(|backend| match backend.verify(data, signature) {
                Ok(check) if check.status == SigStatus::Unknown => None,
                Err(SignError::InvalidSignatureFormat) => None,
                e => Some(e),
            })
            .transpose()?;

        if let Some(verification) = verification {
            // a key might get imported before next call?.
            // realistically this is unlikely, but technically
            // it's correct to not cache unknowns here
            if verification.status != SigStatus::Unknown {
                self.cache
                    .lock()
                    .unwrap()
                    .put(commit_id.clone(), verification.clone());
            }
            Ok(verification)
        } else {
            // now here it's correct to cache unknowns, as we don't
            // have a backend that knows how to handle this signature
            //
            // not sure about how much of an optimization this is
            self.cache
                .lock()
                .unwrap()
                .put(commit_id.clone(), Verification::unknown());
            Ok(Verification::unknown())
        }
    }
}
