//! Moderation domain logic — report, ban, unban, list.
//!
//! Pure business logic operating on `Store` trait and domain types.

use std::sync::Arc;

use crate::storage::Store;

use super::types::*;

/// Shared state needed by moderation operations.
pub struct ModerationService {
    pub store: Arc<dyn Store>,
}

impl ModerationService {
    /// Submit an encrypted report for a message.
    pub fn report_message(
        &self,
        req: ReportMessageReq,
    ) -> Result<ReportMessageResp, DomainError> {
        if req.encrypted_report.is_empty() {
            return Err(DomainError::BadParams(
                "encrypted report must not be empty".into(),
            ));
        }

        self.store
            .store_report(
                &req.encrypted_report,
                &req.conversation_id,
                &req.reporter_identity,
            )
            .map_err(DomainError::Storage)?;

        tracing::info!(
            reporter_prefix = %hex_prefix(&req.reporter_identity),
            "audit: message reported"
        );

        Ok(ReportMessageResp { accepted: true })
    }

    /// Ban a user by identity key.
    pub fn ban_user(&self, req: BanUserReq) -> Result<BanUserResp, DomainError> {
        if req.identity_key.len() != 32 {
            return Err(DomainError::InvalidIdentityKey(req.identity_key.len()));
        }

        let expires_at = if req.duration_secs == 0 {
            0 // permanent
        } else {
            now_secs() + req.duration_secs
        };

        self.store
            .ban_user(&req.identity_key, &req.reason, expires_at)
            .map_err(DomainError::Storage)?;

        tracing::info!(
            identity_prefix = %hex_prefix(&req.identity_key),
            reason = %req.reason,
            expires_at,
            "audit: user banned"
        );

        Ok(BanUserResp { success: true })
    }

    /// Unban a user by identity key.
    pub fn unban_user(&self, req: UnbanUserReq) -> Result<UnbanUserResp, DomainError> {
        if req.identity_key.len() != 32 {
            return Err(DomainError::InvalidIdentityKey(req.identity_key.len()));
        }

        let removed = self
            .store
            .unban_user(&req.identity_key)
            .map_err(DomainError::Storage)?;

        if removed {
            tracing::info!(
                identity_prefix = %hex_prefix(&req.identity_key),
                "audit: user unbanned"
            );
        }

        Ok(UnbanUserResp { success: removed })
    }

    /// Check if a user is currently banned.
    pub fn check_ban(&self, identity_key: &[u8]) -> Result<Option<String>, DomainError> {
        self.store
            .is_banned(identity_key)
            .map_err(DomainError::Storage)
    }

    /// List reports with pagination.
    pub fn list_reports(&self, req: ListReportsReq) -> Result<ListReportsResp, DomainError> {
        let raw = self
            .store
            .list_reports(req.limit, req.offset)
            .map_err(DomainError::Storage)?;

        let reports = raw
            .into_iter()
            .map(
                |(id, encrypted_report, conversation_id, reporter_identity, timestamp)| {
                    ReportEntry {
                        id,
                        encrypted_report,
                        conversation_id,
                        reporter_identity,
                        timestamp,
                    }
                },
            )
            .collect();

        Ok(ListReportsResp { reports })
    }

    /// List all currently banned users.
    pub fn list_banned(&self) -> Result<ListBannedResp, DomainError> {
        let raw = self.store.list_banned().map_err(DomainError::Storage)?;

        let users = raw
            .into_iter()
            .map(
                |(identity_key, reason, banned_at, expires_at)| BannedUserEntry {
                    identity_key,
                    reason,
                    banned_at,
                    expires_at,
                },
            )
            .collect();

        Ok(ListBannedResp { users })
    }
}

fn now_secs() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs()
}

fn hex_prefix(bytes: &[u8]) -> String {
    let len = bytes.len().min(4);
    let hex: String = bytes[..len].iter().map(|b| format!("{b:02x}")).collect();
    format!("{hex}...")
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use super::*;
    use crate::storage::FileBackedStore;

    fn test_service() -> (tempfile::TempDir, ModerationService) {
        let dir = tempfile::tempdir().unwrap();
        let store = Arc::new(FileBackedStore::open(dir.path()).unwrap());
        let svc = ModerationService { store };
        (dir, svc)
    }

    #[test]
    fn report_store_and_list() {
        let (_dir, svc) = test_service();

        let resp = svc
            .report_message(ReportMessageReq {
                encrypted_report: vec![1, 2, 3],
                conversation_id: vec![10; 16],
                reporter_identity: vec![20; 32],
            })
            .unwrap();
        assert!(resp.accepted);

        let reports = svc
            .list_reports(ListReportsReq {
                limit: 10,
                offset: 0,
            })
            .unwrap();
        assert_eq!(reports.reports.len(), 1);
        assert_eq!(reports.reports[0].encrypted_report, vec![1, 2, 3]);
        assert_eq!(reports.reports[0].conversation_id, vec![10; 16]);
        assert_eq!(reports.reports[0].reporter_identity, vec![20; 32]);
    }

    #[test]
    fn report_empty_rejected() {
        let (_dir, svc) = test_service();
        let result = svc.report_message(ReportMessageReq {
            encrypted_report: vec![],
            conversation_id: vec![10; 16],
            reporter_identity: vec![20; 32],
        });
        assert!(result.is_err());
    }

    #[test]
    fn ban_unban_lifecycle() {
        let (_dir, svc) = test_service();
        let ik = vec![1u8; 32];

        // Not banned initially.
        assert!(svc.check_ban(&ik).unwrap().is_none());

        // Ban permanently.
        let resp = svc
            .ban_user(BanUserReq {
                identity_key: ik.clone(),
                reason: "spam".into(),
                duration_secs: 0,
            })
            .unwrap();
        assert!(resp.success);

        // Now banned.
        let reason = svc.check_ban(&ik).unwrap();
        assert_eq!(reason, Some("spam".to_string()));

        // Listed in banned users.
        let banned = svc.list_banned().unwrap();
        assert_eq!(banned.users.len(), 1);
        assert_eq!(banned.users[0].identity_key, ik);
        assert_eq!(banned.users[0].reason, "spam");
        assert_eq!(banned.users[0].expires_at, 0); // permanent

        // Unban.
        let resp = svc.unban_user(UnbanUserReq { identity_key: ik.clone() }).unwrap();
        assert!(resp.success);

        // No longer banned.
        assert!(svc.check_ban(&ik).unwrap().is_none());
        assert!(svc.list_banned().unwrap().users.is_empty());
    }

    #[test]
    fn ban_invalid_identity_key() {
        let (_dir, svc) = test_service();
        let result = svc.ban_user(BanUserReq {
            identity_key: vec![1u8; 16], // wrong length
            reason: "test".into(),
            duration_secs: 0,
        });
        assert!(result.is_err());
    }

    #[test]
    fn list_reports_pagination() {
        let (_dir, svc) = test_service();

        for i in 0..5u8 {
            svc.report_message(ReportMessageReq {
                encrypted_report: vec![i],
                conversation_id: vec![10; 16],
                reporter_identity: vec![20; 32],
            })
            .unwrap();
        }

        let page1 = svc
            .list_reports(ListReportsReq {
                limit: 2,
                offset: 0,
            })
            .unwrap();
        assert_eq!(page1.reports.len(), 2);
        assert_eq!(page1.reports[0].encrypted_report, vec![0]);

        let page2 = svc
            .list_reports(ListReportsReq {
                limit: 2,
                offset: 2,
            })
            .unwrap();
        assert_eq!(page2.reports.len(), 2);
        assert_eq!(page2.reports[0].encrypted_report, vec![2]);

        let page3 = svc
            .list_reports(ListReportsReq {
                limit: 2,
                offset: 4,
            })
            .unwrap();
        assert_eq!(page3.reports.len(), 1);
    }

    #[test]
    fn unban_nonexistent_returns_false() {
        let (_dir, svc) = test_service();
        let resp = svc
            .unban_user(UnbanUserReq {
                identity_key: vec![99u8; 32],
            })
            .unwrap();
        assert!(!resp.success);
    }
}
