//! Shared authentication leases for interactive frontends.
//!
//! A session owns its secret store, so callers cannot bypass a lease by retaining
//! an independently unlocked store. Frontends explicitly report real user input
//! with [`AuthenticationHandle::touch_user_activity`]; secret reads, Git work,
//! refreshes, timers, and repaints deliberately do not extend the deadline.

use std::{
    collections::BTreeMap,
    error::Error,
    fmt,
    sync::{Arc, Mutex},
    time::{Duration, Instant},
};

use crate::{
    crypto::{KeyInfo, SecretProvider, SecretProviderError},
    git::{GitCredential, GitCredentialProvider, GitError},
    repository::SecretBytes,
    secret_store::{
        NativeSecretBackend, SecretCachePolicy, SecretProtectionPolicy, SecretReference,
        SecretStore, SecretStoreBackend, SecretStoreError,
    },
};

pub const DEFAULT_AUTHENTICATION_TIMEOUT: Duration = Duration::from_secs(120);
pub const MAX_AUTHENTICATION_TIMEOUT: Duration = Duration::from_secs(24 * 60 * 60);

/// A validated inactivity duration shared by every frontend.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AuthenticationTimeout(Duration);

impl AuthenticationTimeout {
    pub fn new(duration: Duration) -> Result<Self, AuthenticationError> {
        if duration.is_zero() || duration > MAX_AUTHENTICATION_TIMEOUT {
            return Err(AuthenticationError::InvalidTimeout);
        }
        Ok(Self(duration))
    }

    pub const fn duration(self) -> Duration {
        self.0
    }
}

impl Default for AuthenticationTimeout {
    fn default() -> Self {
        Self(DEFAULT_AUTHENTICATION_TIMEOUT)
    }
}

/// Monotonic clock boundary used to make expiry deterministic in tests.
pub trait AuthenticationClock: Send + Sync + 'static {
    /// Elapsed monotonic time from an arbitrary, stable origin.
    fn now(&self) -> Duration;
}

/// Process-local monotonic clock used by production sessions.
#[derive(Clone, Debug)]
pub struct SystemAuthenticationClock {
    origin: Arc<Instant>,
}

impl Default for SystemAuthenticationClock {
    fn default() -> Self {
        Self {
            origin: Arc::new(Instant::now()),
        }
    }
}

impl AuthenticationClock for SystemAuthenticationClock {
    fn now(&self) -> Duration {
        self.origin.elapsed()
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AuthenticationError {
    InvalidTimeout,
    Locked,
    Expired,
    Revoked,
    Cancelled,
    ClockOverflow,
    SecretStore(SecretStoreError),
}

impl fmt::Display for AuthenticationError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::InvalidTimeout => formatter.write_str("the authentication timeout is invalid"),
            Self::Locked => formatter.write_str("authentication is required"),
            Self::Expired => formatter.write_str("the authentication lease expired"),
            Self::Revoked => formatter.write_str("the authentication lease was revoked"),
            Self::Cancelled => formatter.write_str("authentication was cancelled"),
            Self::ClockOverflow => formatter.write_str("the authentication deadline overflowed"),
            Self::SecretStore(error) => write!(formatter, "authentication failed: {error}"),
        }
    }
}

impl Error for AuthenticationError {}

impl From<SecretStoreError> for AuthenticationError {
    fn from(error: SecretStoreError) -> Self {
        match error {
            SecretStoreError::Cancelled => Self::Cancelled,
            error => Self::SecretStore(error),
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum RevocationReason {
    Expired,
    Manual,
    Cancelled,
    AuthenticationFailed,
}

struct ActiveLease {
    generation: u64,
    deadline: Duration,
    /// One zeroizing passphrase per key actually requested by this lease.
    unlock_material: BTreeMap<String, SecretBytes>,
}

struct LeaseState {
    next_generation: u64,
    active: Option<ActiveLease>,
    last_revocation: Option<(u64, RevocationReason)>,
}

struct Shared<B: SecretStoreBackend, C: AuthenticationClock> {
    store: SecretStore<B>,
    timeout: AuthenticationTimeout,
    clock: C,
    /// Serializes access checks with relock, eliminating check-then-use races.
    operation: Mutex<()>,
    state: Mutex<LeaseState>,
}

impl<B: SecretStoreBackend, C: AuthenticationClock> Drop for Shared<B, C> {
    fn drop(&mut self) {
        if let Ok(state) = self.state.get_mut() {
            state.active = None;
        }
        let _ = self.store.lock();
    }
}

/// Owner of an OS-backed secret store and its single shared authentication lease.
pub struct AuthenticationSession<
    B: SecretStoreBackend,
    C: AuthenticationClock = SystemAuthenticationClock,
> {
    shared: Arc<Shared<B, C>>,
}

impl<B: SecretStoreBackend, C: AuthenticationClock> Clone for AuthenticationSession<B, C> {
    fn clone(&self) -> Self {
        Self {
            shared: Arc::clone(&self.shared),
        }
    }
}

impl<B: SecretStoreBackend, C: AuthenticationClock> fmt::Debug for AuthenticationSession<B, C> {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("AuthenticationSession")
            .field("timeout", &self.shared.timeout)
            .field("unlock_material", &"[REDACTED]")
            .finish()
    }
}

impl<B: SecretStoreBackend> AuthenticationSession<B, SystemAuthenticationClock> {
    pub fn new(
        backend: B,
        protections: SecretProtectionPolicy,
        timeout: AuthenticationTimeout,
    ) -> Self {
        Self::with_clock(
            backend,
            protections,
            timeout,
            SystemAuthenticationClock::default(),
        )
    }
}

impl<B: SecretStoreBackend, C: AuthenticationClock> AuthenticationSession<B, C> {
    pub fn with_clock(
        backend: B,
        protections: SecretProtectionPolicy,
        timeout: AuthenticationTimeout,
        clock: C,
    ) -> Self {
        Self {
            shared: Arc::new(Shared {
                // The lease is the only cache. This prevents a second lifetime
                // policy in SecretStore from retaining duplicate records.
                store: SecretStore::new(backend, SecretCachePolicy::Disabled, protections),
                timeout,
                clock,
                operation: Mutex::new(()),
                state: Mutex::new(LeaseState {
                    next_generation: 0,
                    active: None,
                    last_revocation: None,
                }),
            }),
        }
    }

    /// Authenticate through the OS secret store and cache only this key's passphrase.
    ///
    /// Unprotected secret keys still receive a lease, but cache no unlock bytes.
    pub fn authenticate(
        &self,
        key: &KeyInfo,
    ) -> Result<AuthenticationHandle<B, C>, AuthenticationError> {
        if !key.has_secret() {
            return Err(AuthenticationError::Locked);
        }
        let _operation = self.shared.operation()?;
        self.shared.expire_if_needed()?;
        let generation = self.shared.begin_lease()?;

        if key.requires_passphrase()
            && let Err(error) = self.shared.cache_key_passphrase(generation, key)
        {
            self.shared
                .revoke_local(generation, RevocationReason::AuthenticationFailed)?;
            let _ = self.shared.store.lock();
            return Err(error);
        }

        Ok(AuthenticationHandle {
            shared: Arc::clone(&self.shared),
            generation,
        })
    }

    /// Establish a lease from a manually entered passphrase without persisting it.
    pub fn authenticate_with_passphrase(
        &self,
        key: &KeyInfo,
        passphrase: SecretBytes,
    ) -> Result<AuthenticationHandle<B, C>, AuthenticationError> {
        if !key.has_secret() || !key.requires_passphrase() || passphrase.expose().is_empty() {
            return Err(AuthenticationError::Locked);
        }
        let _operation = self.shared.operation()?;
        self.shared.expire_if_needed()?;
        let generation = self.shared.begin_lease()?;
        let fingerprint = key.fingerprint().as_str().to_owned();
        self.shared.with_active(generation, |active| {
            active.unlock_material.insert(fingerprint, passphrase);
        })?;
        Ok(AuthenticationHandle {
            shared: Arc::clone(&self.shared),
            generation,
        })
    }

    /// Remove this key's OS-protected passphrase. A missing item is already disabled.
    pub fn delete_key_passphrase(&self, key: &KeyInfo) -> Result<(), AuthenticationError> {
        let _operation = self.shared.operation()?;
        self.shared.expire_if_needed()?;
        self.shared.store.unlock()?;
        let reference = SecretReference::openpgp_passphrase(key.fingerprint().as_str())?;
        match self.shared.store.delete_openpgp_passphrase(&reference) {
            Ok(()) | Err(SecretStoreError::Missing) => Ok(()),
            Err(error) => Err(error.into()),
        }
    }

    /// Remaining active time. Calling this from a timer never extends the lease.
    pub fn remaining_time(&self) -> Result<Option<Duration>, AuthenticationError> {
        let _operation = self.shared.operation()?;
        self.shared.expire_if_needed()?;
        let now = self.shared.clock.now();
        Ok(self
            .shared
            .state()?
            .active
            .as_ref()
            .map(|active| active.deadline.saturating_sub(now)))
    }

    /// Poll a frontend timer. Returns true only when this call performed expiry.
    pub fn expire(&self) -> Result<bool, AuthenticationError> {
        let _operation = self.shared.operation()?;
        self.shared.expire_if_needed()
    }

    /// Revoke every outstanding handle immediately.
    pub fn manual_lock(&self) -> Result<(), AuthenticationError> {
        let _operation = self.shared.operation()?;
        self.shared.revoke(RevocationReason::Manual)
    }

    /// Cancel authentication and revoke any lease created by the pending interaction.
    pub fn cancel(&self) -> Result<(), AuthenticationError> {
        let _operation = self.shared.operation()?;
        self.shared.revoke(RevocationReason::Cancelled)
    }
}

/// Generation-bound secret access. Clones are revoked together.
pub struct AuthenticationHandle<
    B: SecretStoreBackend,
    C: AuthenticationClock = SystemAuthenticationClock,
> {
    shared: Arc<Shared<B, C>>,
    generation: u64,
}

impl<B: SecretStoreBackend, C: AuthenticationClock> Clone for AuthenticationHandle<B, C> {
    fn clone(&self) -> Self {
        Self {
            shared: Arc::clone(&self.shared),
            generation: self.generation,
        }
    }
}

impl<B: SecretStoreBackend, C: AuthenticationClock> fmt::Debug for AuthenticationHandle<B, C> {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("AuthenticationHandle")
            .field("generation", &"[REDACTED]")
            .field("unlock_material", &"[REDACTED]")
            .finish()
    }
}

impl<B: SecretStoreBackend, C: AuthenticationClock> AuthenticationHandle<B, C> {
    pub fn ensure_active(&self) -> Result<(), AuthenticationError> {
        let _operation = self.shared.operation()?;
        self.shared.expire_if_needed()?;
        self.shared.with_active(self.generation, |_| ())
    }

    /// Extend the lease only for an input event the frontend classified as user activity.
    pub fn touch_user_activity(&self) -> Result<(), AuthenticationError> {
        let _operation = self.shared.operation()?;
        self.shared.expire_if_needed()?;
        let now = self.shared.clock.now();
        let deadline = now
            .checked_add(self.shared.timeout.duration())
            .ok_or(AuthenticationError::ClockOverflow)?;
        self.shared
            .with_active(self.generation, |active| active.deadline = deadline)
    }

    pub fn remaining_time(&self) -> Result<Duration, AuthenticationError> {
        let _operation = self.shared.operation()?;
        self.shared.expire_if_needed()?;
        let now = self.shared.clock.now();
        self.shared.with_active(self.generation, |active| {
            active.deadline.saturating_sub(now)
        })
    }

    /// Persist the passphrase already verified by a successful crypto operation.
    pub fn persist_passphrase(&self, key: &KeyInfo) -> Result<(), AuthenticationError> {
        let _operation = self.shared.operation()?;
        self.shared.expire_if_needed()?;
        let fingerprint = key.fingerprint().as_str();
        let value = self.shared.with_active(self.generation, |active| {
            active
                .unlock_material
                .get(fingerprint)
                .map(|value| SecretBytes::new(value.expose().to_vec()))
        })?;
        let value = value.ok_or(AuthenticationError::Locked)?;
        let reference = SecretReference::openpgp_passphrase(fingerprint)?;
        self.shared
            .store
            .store_openpgp_passphrase(&reference, value)
            .map_err(Into::into)
    }
}

impl<B: SecretStoreBackend, C: AuthenticationClock> SecretProvider for AuthenticationHandle<B, C> {
    fn secret_for(&mut self, key: &KeyInfo) -> Result<SecretBytes, SecretProviderError> {
        self.shared
            .secret_for(self.generation, key)
            .map_err(|error| match error {
                AuthenticationError::Cancelled => SecretProviderError::Cancelled,
                _ => SecretProviderError::Unavailable,
            })
    }
}

impl<B: SecretStoreBackend, C: AuthenticationClock> GitCredentialProvider
    for AuthenticationHandle<B, C>
{
    fn credential(
        &self,
        server: &crate::config::ServerId,
        application: &crate::config::ApplicationId,
    ) -> Result<GitCredential, GitError> {
        let _operation = self
            .shared
            .operation()
            .map_err(|_| GitError::CredentialsUnavailable)?;
        self.shared
            .expire_if_needed()
            .map_err(|_| GitError::CredentialsUnavailable)?;
        self.shared
            .with_active(self.generation, |_| ())
            .map_err(|_| GitError::CredentialsUnavailable)?;
        self.shared.store.credential(server, application)
    }
}

impl<B: SecretStoreBackend, C: AuthenticationClock> Shared<B, C> {
    fn operation(&self) -> Result<std::sync::MutexGuard<'_, ()>, AuthenticationError> {
        self.operation
            .lock()
            .map_err(|_| AuthenticationError::SecretStore(SecretStoreError::Unavailable))
    }

    fn state(&self) -> Result<std::sync::MutexGuard<'_, LeaseState>, AuthenticationError> {
        self.state
            .lock()
            .map_err(|_| AuthenticationError::SecretStore(SecretStoreError::Unavailable))
    }

    fn begin_lease(&self) -> Result<u64, AuthenticationError> {
        let now = self.clock.now();
        let deadline = now
            .checked_add(self.timeout.duration())
            .ok_or(AuthenticationError::ClockOverflow)?;
        let (generation, was_active) = {
            let mut state = self.state()?;
            match state.active.as_mut() {
                Some(active) => {
                    active.deadline = deadline;
                    (active.generation, true)
                }
                None => {
                    state.next_generation = state.next_generation.wrapping_add(1);
                    let generation = state.next_generation;
                    state.active = Some(ActiveLease {
                        generation,
                        deadline,
                        unlock_material: BTreeMap::new(),
                    });
                    (generation, false)
                }
            }
        };
        if !was_active && let Err(error) = self.store.unlock() {
            self.revoke_local(generation, RevocationReason::AuthenticationFailed)?;
            return Err(error.into());
        }
        Ok(generation)
    }

    fn expire_if_needed(&self) -> Result<bool, AuthenticationError> {
        let now = self.clock.now();
        let expired = self
            .state()?
            .active
            .as_ref()
            .is_some_and(|active| now >= active.deadline);
        if !expired {
            return Ok(false);
        }
        self.revoke(RevocationReason::Expired)?;
        Ok(true)
    }

    fn revoke(&self, reason: RevocationReason) -> Result<(), AuthenticationError> {
        let generation = self
            .state()?
            .active
            .as_ref()
            .map(|active| active.generation);
        if let Some(generation) = generation {
            self.revoke_local(generation, reason)?;
        }
        self.store.lock().map_err(AuthenticationError::from)
    }

    fn revoke_local(
        &self,
        generation: u64,
        reason: RevocationReason,
    ) -> Result<(), AuthenticationError> {
        let mut state = self.state()?;
        if state
            .active
            .as_ref()
            .is_some_and(|active| active.generation == generation)
        {
            // Dropping the map here zeroizes every cached passphrase before return.
            state.active = None;
            state.last_revocation = Some((generation, reason));
        }
        Ok(())
    }

    fn with_active<T>(
        &self,
        generation: u64,
        operation: impl FnOnce(&mut ActiveLease) -> T,
    ) -> Result<T, AuthenticationError> {
        let mut state = self.state()?;
        if let Some(active) = state
            .active
            .as_mut()
            .filter(|active| active.generation == generation)
        {
            return Ok(operation(active));
        }
        Err(match state.last_revocation {
            Some((revoked, RevocationReason::Expired)) if revoked == generation => {
                AuthenticationError::Expired
            }
            Some((revoked, RevocationReason::Cancelled)) if revoked == generation => {
                AuthenticationError::Cancelled
            }
            Some((revoked, _)) if revoked == generation => AuthenticationError::Revoked,
            _ if state.active.is_none() => AuthenticationError::Locked,
            _ => AuthenticationError::Revoked,
        })
    }

    fn cache_key_passphrase(
        &self,
        generation: u64,
        key: &KeyInfo,
    ) -> Result<(), AuthenticationError> {
        let fingerprint = key.fingerprint().as_str().to_owned();
        if self.with_active(generation, |active| {
            active.unlock_material.contains_key(&fingerprint)
        })? {
            return Ok(());
        }
        let reference = SecretReference::openpgp_passphrase(&fingerprint)?;
        let passphrase = self.store.retrieve(&reference)?;
        self.with_active(generation, |active| {
            active.unlock_material.insert(fingerprint, passphrase);
        })?;
        Ok(())
    }

    fn secret_for(
        &self,
        generation: u64,
        key: &KeyInfo,
    ) -> Result<SecretBytes, AuthenticationError> {
        let _operation = self.operation()?;
        self.expire_if_needed()?;
        self.cache_key_passphrase(generation, key)?;
        let fingerprint = key.fingerprint().as_str();
        self.with_active(generation, |active| {
            active
                .unlock_material
                .get(fingerprint)
                .map(|secret| SecretBytes::new(secret.expose().to_vec()))
        })?
        .ok_or(AuthenticationError::SecretStore(SecretStoreError::Missing))
    }
}

pub type NativeAuthenticationSession =
    AuthenticationSession<NativeSecretBackend, SystemAuthenticationClock>;
pub type NativeAuthenticationHandle =
    AuthenticationHandle<NativeSecretBackend, SystemAuthenticationClock>;

impl NativeAuthenticationSession {
    pub fn system(
        protections: SecretProtectionPolicy,
        timeout: AuthenticationTimeout,
    ) -> Result<Self, AuthenticationError> {
        Ok(Self::new(NativeSecretBackend::new()?, protections, timeout))
    }
}
