//! Paired-device session use cases.

use std::sync::Arc;

use async_trait::async_trait;
use domain::events::DomainEvent;
use domain::ports::{Clock, DeviceSessionStore, EventBus, IdGenerator, StoreError};
use domain::{AuthenticatedDevice, DeviceId, DeviceName, PairedDevice, SessionTokenHash};
use subtle::ConstantTimeEq;

use crate::error::AppError;

const LAST_SEEN_TOUCH_THROTTLE_MS: u64 = 5 * 60 * 1000;

/// Input for [`PairDevice`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PairDeviceInput {
    /// Name supplied by the new device.
    pub name: String,
    /// Hash of the freshly issued session token.
    pub session_token_hash: SessionTokenHash,
}

/// Output for [`PairDevice`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PairDeviceOutput {
    /// Created paired device.
    pub device: PairedDevice,
}

/// Validates a pairing request and persists the new paired device.
pub struct PairDevice {
    store: Arc<dyn DeviceSessionStore>,
    ids: Arc<dyn IdGenerator>,
    clock: Arc<dyn Clock>,
}

impl PairDevice {
    /// Builds the use case from injected ports.
    #[must_use]
    pub fn new(
        store: Arc<dyn DeviceSessionStore>,
        ids: Arc<dyn IdGenerator>,
        clock: Arc<dyn Clock>,
    ) -> Self {
        Self { store, ids, clock }
    }

    /// Executes device pairing.
    ///
    /// # Errors
    /// Returns [`AppError::Invalid`] for an invalid name and [`AppError::Store`] for
    /// persistence failures.
    pub async fn execute(&self, input: PairDeviceInput) -> Result<PairDeviceOutput, AppError> {
        let now = self.clock.now_millis().max(0) as u64;
        let device = PairedDevice {
            device_id: DeviceId::from_uuid(self.ids.new_uuid()),
            name: DeviceName::new(input.name).map_err(|err| AppError::Invalid(err.to_string()))?,
            paired_at_ms: now,
            last_seen_at_ms: now,
            session_token_hash: input.session_token_hash,
        };
        let mut devices = self.store.load_devices().await?;
        devices.push(device.clone());
        self.store.save_devices(&devices).await?;
        Ok(PairDeviceOutput { device })
    }
}

/// Rate-limit key for pairing attempts.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RateLimitKey {
    /// Resolved origin identity, typically the peer IP after transport-layer proxy handling.
    pub origin: String,
    /// Logical route being limited.
    pub route: String,
}

/// Pairing-attempt limiter port.
#[async_trait]
pub trait PairAttemptLimiter: Send + Sync {
    /// Returns whether a new failed pairing attempt may be processed.
    async fn check(&self, key: RateLimitKey) -> Result<PairAttemptDecision, StoreError>;

    /// Records one failed pairing attempt for the key and the global bucket.
    async fn record_failure(&self, key: RateLimitKey) -> Result<(), StoreError>;
}

/// Result of a rate-limit check.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PairAttemptDecision {
    /// Attempt can proceed.
    Allowed,
    /// Attempt is blocked by one of the buckets.
    RateLimited,
}

/// Input for [`AuthenticateSession`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthenticateSessionInput {
    /// Raw token bytes decoded from the session cookie.
    pub token_bytes: Vec<u8>,
}

/// Authenticates a persisted device session.
pub struct AuthenticateSession {
    store: Arc<dyn DeviceSessionStore>,
}

impl AuthenticateSession {
    /// Builds the use case from injected ports.
    #[must_use]
    pub fn new(store: Arc<dyn DeviceSessionStore>) -> Self {
        Self { store }
    }

    /// Returns the authenticated device, including its token hash, when valid.
    ///
    /// # Errors
    /// Returns [`AppError::Store`] for persistence failures.
    pub async fn execute(
        &self,
        input: AuthenticateSessionInput,
    ) -> Result<Option<AuthenticatedDevice>, AppError> {
        Ok(self.store.authenticate_token(&input.token_bytes).await?)
    }
}

/// Input for [`TouchDevice`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TouchDeviceInput {
    /// Device to mark as seen.
    pub device_id: DeviceId,
}

/// Output for [`TouchDevice`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TouchDeviceOutput {
    /// Whether the store was rewritten.
    pub updated: bool,
}

/// Device row exposed to presentation layers.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeviceView {
    /// Persistent device id.
    pub device_id: DeviceId,
    /// User-facing name.
    pub name: String,
    /// Pairing timestamp as epoch milliseconds.
    pub paired_at_ms: u64,
    /// Last successful authenticated access timestamp as epoch milliseconds.
    pub last_seen_at_ms: u64,
    /// Whether this row is the authenticated current device.
    pub is_current_device: bool,
}

/// Input for [`ListDevices`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListDevicesInput {
    /// Current device id, when the driving adapter has an authenticated device.
    pub current_device_id: Option<DeviceId>,
}

/// Output for [`ListDevices`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListDevicesOutput {
    /// Paired devices.
    pub devices: Vec<DeviceView>,
}

/// Lists paired devices without exposing transport-sensitive fields.
pub struct ListDevices {
    store: Arc<dyn DeviceSessionStore>,
}

impl ListDevices {
    /// Builds the use case from injected ports.
    #[must_use]
    pub fn new(store: Arc<dyn DeviceSessionStore>) -> Self {
        Self { store }
    }

    /// Executes the listing.
    ///
    /// # Errors
    /// Returns [`AppError::Store`] for persistence failures.
    pub async fn execute(&self, input: ListDevicesInput) -> Result<ListDevicesOutput, AppError> {
        let devices = self
            .store
            .load_devices()
            .await?
            .into_iter()
            .map(|device| DeviceView {
                device_id: device.device_id,
                name: device.name.as_str().to_owned(),
                paired_at_ms: device.paired_at_ms,
                last_seen_at_ms: device.last_seen_at_ms,
                is_current_device: Some(device.device_id) == input.current_device_id,
            })
            .collect();
        Ok(ListDevicesOutput { devices })
    }
}

/// Input for [`RenameDevice`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenameDeviceInput {
    /// Device to rename.
    pub device_id: DeviceId,
    /// New user-facing name.
    pub name: String,
}

/// Renames a paired device.
pub struct RenameDevice {
    store: Arc<dyn DeviceSessionStore>,
}

impl RenameDevice {
    /// Builds the use case from injected ports.
    #[must_use]
    pub fn new(store: Arc<dyn DeviceSessionStore>) -> Self {
        Self { store }
    }

    /// Executes the rename.
    ///
    /// # Errors
    /// Returns [`AppError::Invalid`] for an invalid name, [`AppError::NotFound`]
    /// when the device does not exist, and [`AppError::Store`] for persistence failures.
    pub async fn execute(&self, input: RenameDeviceInput) -> Result<(), AppError> {
        let new_name =
            DeviceName::new(input.name).map_err(|err| AppError::Invalid(err.to_string()))?;
        let mut devices = self.store.load_devices().await?;
        let Some(device) = devices
            .iter_mut()
            .find(|device| device.device_id == input.device_id)
        else {
            return Err(AppError::NotFound("device not found".to_owned()));
        };
        device.name = new_name;
        self.store.save_devices(&devices).await?;
        Ok(())
    }
}

/// Input for [`RevokeDevice`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RevokeDeviceInput {
    /// Device to revoke.
    pub device_id: DeviceId,
}

/// Revokes one paired device and publishes a revocation event after persistence.
pub struct RevokeDevice {
    store: Arc<dyn DeviceSessionStore>,
    events: Arc<dyn EventBus>,
}

impl RevokeDevice {
    /// Builds the use case from injected ports.
    #[must_use]
    pub fn new(store: Arc<dyn DeviceSessionStore>, events: Arc<dyn EventBus>) -> Self {
        Self { store, events }
    }

    /// Executes the revocation.
    ///
    /// # Errors
    /// Returns [`AppError::NotFound`] when the device does not exist and
    /// [`AppError::Store`] for persistence failures.
    pub async fn execute(&self, input: RevokeDeviceInput) -> Result<(), AppError> {
        if !self.store.remove_device(input.device_id).await? {
            return Err(AppError::NotFound("device not found".to_owned()));
        }
        self.events.publish(DomainEvent::DeviceRevoked {
            device_id: input.device_id,
        });
        Ok(())
    }
}

/// Revokes every paired device and publishes a global revocation event.
pub struct RevokeAllDevices {
    store: Arc<dyn DeviceSessionStore>,
    events: Arc<dyn EventBus>,
}

impl RevokeAllDevices {
    /// Builds the use case from injected ports.
    #[must_use]
    pub fn new(store: Arc<dyn DeviceSessionStore>, events: Arc<dyn EventBus>) -> Self {
        Self { store, events }
    }

    /// Executes global revocation.
    ///
    /// # Errors
    /// Returns [`AppError::Store`] for persistence failures.
    pub async fn execute(&self) -> Result<(), AppError> {
        self.store.save_devices(&[]).await?;
        self.events.publish(DomainEvent::AllDevicesRevoked);
        Ok(())
    }
}

/// Updates `lastSeenAtMs`, throttled to avoid rewriting the JSON on every request.
pub struct TouchDevice {
    store: Arc<dyn DeviceSessionStore>,
    clock: Arc<dyn Clock>,
}

impl TouchDevice {
    /// Builds the use case from injected ports.
    #[must_use]
    pub fn new(store: Arc<dyn DeviceSessionStore>, clock: Arc<dyn Clock>) -> Self {
        Self { store, clock }
    }

    /// Executes the touch.
    ///
    /// # Errors
    /// Returns [`AppError::Store`] for persistence failures.
    pub async fn execute(&self, input: TouchDeviceInput) -> Result<TouchDeviceOutput, AppError> {
        let now = self.clock.now_millis().max(0) as u64;
        let mut devices = self.store.load_devices().await?;
        let Some(device) = devices
            .iter_mut()
            .find(|device| device.device_id == input.device_id)
        else {
            return Ok(TouchDeviceOutput { updated: false });
        };

        if now.saturating_sub(device.last_seen_at_ms) < LAST_SEEN_TOUCH_THROTTLE_MS {
            return Ok(TouchDeviceOutput { updated: false });
        }

        device.last_seen_at_ms = now;
        self.store.save_devices(&devices).await?;
        Ok(TouchDeviceOutput { updated: true })
    }
}

/// Normalizes user-entered pairing codes server-side.
#[must_use]
pub fn normalize_pairing_code(code: &str) -> String {
    code.chars()
        .filter(|ch| !ch.is_whitespace() && *ch != '-')
        .flat_map(char::to_uppercase)
        .collect()
}

/// Compares pairing codes after normalization without early-exit byte comparison.
#[must_use]
pub fn pairing_codes_equal(candidate: &str, expected: &str) -> bool {
    let candidate = normalize_pairing_code(candidate);
    let expected = normalize_pairing_code(expected);
    let bytes_equal: bool = candidate.as_bytes().ct_eq(expected.as_bytes()).into();
    bytes_equal && candidate.len() == expected.len()
}

#[cfg(test)]
mod tests {
    use super::*;
    use async_trait::async_trait;
    use domain::ports::StoreError;
    use std::sync::Mutex;
    use uuid::Uuid;

    #[derive(Default)]
    struct FakeStore {
        devices: Mutex<Vec<PairedDevice>>,
        saves: Mutex<usize>,
    }

    #[async_trait]
    impl DeviceSessionStore for FakeStore {
        async fn load_devices(&self) -> Result<Vec<PairedDevice>, StoreError> {
            Ok(self.devices.lock().unwrap().clone())
        }

        async fn save_devices(&self, devices: &[PairedDevice]) -> Result<(), StoreError> {
            *self.devices.lock().unwrap() = devices.to_vec();
            *self.saves.lock().unwrap() += 1;
            Ok(())
        }
    }

    struct FixedClock(i64);

    impl domain::ports::Clock for FixedClock {
        fn now_millis(&self) -> i64 {
            self.0
        }
    }

    struct FixedIds;

    impl domain::ports::IdGenerator for FixedIds {
        fn new_uuid(&self) -> Uuid {
            Uuid::from_u128(7)
        }
    }

    #[test]
    fn pairing_code_normalization_removes_spaces_hyphens_and_uppercases() {
        assert_eq!(normalize_pairing_code(" ab12-cd34 "), "AB12CD34");
        assert!(pairing_codes_equal(" ab12-cd34 ", "AB12CD34"));
        assert!(!pairing_codes_equal("AB12CD35", "AB12CD34"));
    }

    #[tokio::test]
    async fn pair_device_validates_name_and_persists_device() {
        let store = Arc::new(FakeStore::default());
        let use_case = PairDevice::new(
            Arc::clone(&store) as Arc<dyn DeviceSessionStore>,
            Arc::new(FixedIds),
            Arc::new(FixedClock(1234)),
        );

        let output = use_case
            .execute(PairDeviceInput {
                name: "Phone".to_owned(),
                session_token_hash: SessionTokenHash::from_token_bytes(&[1; 32]),
            })
            .await
            .unwrap();

        assert_eq!(output.device.name.as_str(), "Phone");
        assert_eq!(store.load_devices().await.unwrap().len(), 1);
    }

    #[tokio::test]
    async fn pair_device_rejects_empty_name() {
        let use_case = PairDevice::new(
            Arc::new(FakeStore::default()),
            Arc::new(FixedIds),
            Arc::new(FixedClock(1234)),
        );

        let err = use_case
            .execute(PairDeviceInput {
                name: " ".to_owned(),
                session_token_hash: SessionTokenHash::from_token_bytes(&[1; 32]),
            })
            .await
            .unwrap_err();

        assert_eq!(err.code(), "INVALID");
    }

    #[tokio::test]
    async fn touch_device_is_throttled_to_five_minutes() {
        let device = PairedDevice {
            device_id: DeviceId::from_uuid(Uuid::from_u128(7)),
            name: DeviceName::new("Phone").unwrap(),
            paired_at_ms: 1000,
            last_seen_at_ms: 1000,
            session_token_hash: SessionTokenHash::from_token_bytes(&[1; 32]),
        };
        let store = Arc::new(FakeStore::default());
        store.save_devices(&[device]).await.unwrap();
        *store.saves.lock().unwrap() = 0;
        let use_case = TouchDevice::new(
            Arc::clone(&store) as Arc<dyn DeviceSessionStore>,
            Arc::new(FixedClock((1000 + LAST_SEEN_TOUCH_THROTTLE_MS - 1) as i64)),
        );

        let output = use_case
            .execute(TouchDeviceInput {
                device_id: DeviceId::from_uuid(Uuid::from_u128(7)),
            })
            .await
            .unwrap();

        assert!(!output.updated);
        assert_eq!(*store.saves.lock().unwrap(), 0);
    }
}
