use crate::sync_oauth::{
    FediOAuthError, MastodonOAuthProvider, SyncOAuthProvider, normalize_fedi_instance_url,
    owner_identity_from_mastodon_account,
};
use crate::{
    DeckVisibility, InMemorySyncServer, LocalDeviceIdentity, SqliteSyncError, SqliteSyncServer,
    SyncAccountDeletionOutcome, SyncAuthError, SyncAuthSession, SyncBundlePayload, SyncDeckSummary,
    SyncDeleteError, SyncDownloadError, SyncDownloadKind, SyncOAuthPendingLogin, SyncOAuthStart,
    SyncQuotaStatus, SyncRemoteDeckStatus, SyncServerLimits, SyncSharedDeckStatus, SyncUploadError,
    SyncUploadOutcome, SyncUploadRequest, SyncUuid,
};
use sha2::{Digest, Sha256};
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::io::{self, Read};
use std::path::Path;
use std::sync::{Arc, Mutex};

pub const MAX_HEADER_BYTES: usize = 64 * 1024;
pub const MAX_BODY_BYTES: usize = 512 * 1024 * 1024;
const PROJECT_WIKI_URL: &str = "https://modulus.isonomia.net/cosimo.cgi/wiki?name=home";
const SYNC_STORAGE_BUSY_MESSAGE: &str =
    "sync storage is temporarily busy; another upload or maintenance operation may be in progress";
const SYNC_STORAGE_BUSY_RETRY_AFTER_SECONDS: &str = "15";

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyncServerServiceMode {
    Public,
    Domestic,
}

impl SyncServerServiceMode {
    pub fn from_code(value: &str) -> Option<Self> {
        match value.trim() {
            "public" => Some(Self::Public),
            "domestic" | "personal" => Some(Self::Domestic),
            _ => None,
        }
    }

    pub fn code(self) -> &'static str {
        match self {
            Self::Public => "public",
            Self::Domestic => "domestic",
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SyncServerPrivacyConfig {
    pub mode: SyncServerServiceMode,
    pub controller_name: Option<String>,
    pub controller_contact: Option<String>,
    pub controller_fedi: Option<String>,
    pub dpo_contact: Option<String>,
    pub rights_contact: Option<String>,
    pub notice_version: Option<String>,
    pub data_categories: Option<String>,
    pub purposes: Option<String>,
    pub lawful_basis: Option<String>,
    pub recipients: Option<String>,
    pub international_transfers: Option<String>,
    pub retention: Option<String>,
    pub complaint_contact: Option<String>,
    pub domestic_registration_allowlist: BTreeSet<String>,
}

impl Default for SyncServerPrivacyConfig {
    fn default() -> Self {
        Self {
            mode: SyncServerServiceMode::Public,
            controller_name: None,
            controller_contact: None,
            controller_fedi: None,
            dpo_contact: None,
            rights_contact: None,
            notice_version: None,
            data_categories: None,
            purposes: None,
            lawful_basis: None,
            recipients: None,
            international_transfers: None,
            retention: None,
            complaint_contact: None,
            domestic_registration_allowlist: BTreeSet::new(),
        }
    }
}

impl SyncServerPrivacyConfig {
    pub fn public_for_tests() -> Self {
        Self {
            mode: SyncServerServiceMode::Public,
            controller_name: Some("Cosimo Test Controller".to_string()),
            controller_contact: Some("privacy@example.invalid".to_string()),
            controller_fedi: Some("@cosimo@example.invalid".to_string()),
            dpo_contact: None,
            rights_contact: Some("privacy@example.invalid".to_string()),
            notice_version: Some("test-notice".to_string()),
            data_categories: Some(
                "Fediverse account identity, bearer-session metadata, deck bundles, review-history bundles, and operational logs"
                    .to_string(),
            ),
            purposes: Some("Cosimo deck synchronisation".to_string()),
            lawful_basis: Some("Service operation or explicit consent as configured".to_string()),
            recipients: Some("server operator and configured hosting providers".to_string()),
            international_transfers: Some("none configured for this test server".to_string()),
            retention: Some("logs are retained briefly; account data is kept until deletion".to_string()),
            complaint_contact: Some("local supervisory authority".to_string()),
            domestic_registration_allowlist: BTreeSet::new(),
        }
    }

    pub fn domestic(allowlist: impl IntoIterator<Item = String>) -> Self {
        Self {
            mode: SyncServerServiceMode::Domestic,
            domestic_registration_allowlist: allowlist
                .into_iter()
                .map(|value| value.trim().to_string())
                .filter(|value| !value.is_empty())
                .collect(),
            ..Self::default()
        }
    }

    pub fn registration_problem(&self) -> Option<&'static str> {
        match self.mode {
            SyncServerServiceMode::Domestic => None,
            SyncServerServiceMode::Public => {
                if self.controller_name.as_deref().is_none_or(str::is_empty) {
                    return Some(
                        "sync registration is disabled: controller name is not configured",
                    );
                }
                let Some(contact) = self.controller_contact.as_deref() else {
                    return Some(
                        "sync registration is disabled: controller contact is not configured",
                    );
                };
                if !public_controller_contact_is_robust(contact) {
                    return Some(
                        "sync registration is disabled: public servers need an email, web form, or postal controller contact; Fediverse-only contact is not enough",
                    );
                }
                for (label, value) in [
                    ("privacy notice version", self.notice_version.as_deref()),
                    ("processed data categories", self.data_categories.as_deref()),
                    ("processing purposes", self.purposes.as_deref()),
                    ("lawful basis", self.lawful_basis.as_deref()),
                    (
                        "recipient or processor information",
                        self.recipients.as_deref(),
                    ),
                    (
                        "international transfer statement",
                        self.international_transfers.as_deref(),
                    ),
                    ("retention policy", self.retention.as_deref()),
                    (
                        "rights or complaint contact",
                        self.rights_contact
                            .as_deref()
                            .or(self.complaint_contact.as_deref()),
                    ),
                ] {
                    if value.is_none_or(str::is_empty) {
                        return Some(match label {
                            "privacy notice version" => {
                                "sync registration is disabled: privacy notice version is not configured"
                            }
                            "processed data categories" => {
                                "sync registration is disabled: processed data categories are not configured"
                            }
                            "processing purposes" => {
                                "sync registration is disabled: processing purposes are not configured"
                            }
                            "lawful basis" => {
                                "sync registration is disabled: lawful basis is not configured"
                            }
                            "recipient or processor information" => {
                                "sync registration is disabled: recipient or processor information is not configured"
                            }
                            "international transfer statement" => {
                                "sync registration is disabled: international transfer statement is not configured"
                            }
                            "retention policy" => {
                                "sync registration is disabled: retention policy is not configured"
                            }
                            _ => {
                                "sync registration is disabled: rights or complaint contact is not configured"
                            }
                        });
                    }
                }
                None
            }
        }
    }

    fn owner_registration_problem(&self, owner_identity: &str) -> Option<&'static str> {
        if let Some(problem) = self.registration_problem() {
            return Some(problem);
        }
        if self.mode == SyncServerServiceMode::Domestic
            && !self.domestic_registration_allowlist.is_empty()
            && !self
                .domestic_registration_allowlist
                .contains(owner_identity)
        {
            return Some(
                "sync registration is disabled: this domestic server only allows configured accounts",
            );
        }
        None
    }

    fn registration_available(&self) -> bool {
        self.registration_problem().is_none()
    }
}

fn public_controller_contact_is_robust(contact: &str) -> bool {
    let contact = contact.trim();
    if contact.is_empty() || contact.starts_with('@') || contact.starts_with("acct:") {
        return false;
    }
    contact.contains('@')
        || contact.starts_with("http://")
        || contact.starts_with("https://")
        || contact.contains(char::is_whitespace)
}

#[derive(Debug)]
pub struct ServerState {
    backend: ServerBackend,
    oauth_provider: Arc<dyn SyncOAuthProvider>,
    limits: SyncServerLimits,
    privacy_config: SyncServerPrivacyConfig,
}

impl Default for ServerState {
    fn default() -> Self {
        Self {
            backend: ServerBackend::Memory(Mutex::new(InMemorySyncServer::new())),
            oauth_provider: Arc::new(MastodonOAuthProvider),
            limits: SyncServerLimits::default(),
            privacy_config: SyncServerPrivacyConfig::default(),
        }
    }
}

impl ServerState {
    pub fn memory() -> Self {
        Self::default()
    }

    pub fn memory_with_limits(limits: SyncServerLimits) -> Self {
        Self {
            backend: ServerBackend::Memory(Mutex::new(InMemorySyncServer::new())),
            oauth_provider: Arc::new(MastodonOAuthProvider),
            limits,
            privacy_config: SyncServerPrivacyConfig::default(),
        }
    }

    pub fn sqlite(root: impl AsRef<Path>) -> Self {
        Self::sqlite_with_limits(root, SyncServerLimits::default())
    }

    pub fn sqlite_with_limits(root: impl AsRef<Path>, limits: SyncServerLimits) -> Self {
        Self {
            backend: ServerBackend::Sqlite(SqliteSyncServer::new(root.as_ref())),
            oauth_provider: Arc::new(MastodonOAuthProvider),
            limits,
            privacy_config: SyncServerPrivacyConfig::default(),
        }
    }

    pub fn with_privacy_config(mut self, privacy_config: SyncServerPrivacyConfig) -> Self {
        self.privacy_config = privacy_config;
        self
    }

    #[cfg(test)]
    pub(crate) fn memory_with_oauth_provider(provider: Arc<dyn SyncOAuthProvider>) -> Self {
        Self::memory_with_oauth_provider_and_limits(provider, SyncServerLimits::default())
    }

    #[cfg(test)]
    pub(crate) fn memory_with_oauth_provider_and_limits(
        provider: Arc<dyn SyncOAuthProvider>,
        limits: SyncServerLimits,
    ) -> Self {
        Self {
            backend: ServerBackend::Memory(Mutex::new(InMemorySyncServer::new())),
            oauth_provider: provider,
            limits,
            privacy_config: SyncServerPrivacyConfig::public_for_tests(),
        }
    }

    pub fn storage_label(&self) -> &'static str {
        match &self.backend {
            ServerBackend::Memory(_) => "memory",
            ServerBackend::Sqlite(_) => "sqlite",
        }
    }

    pub fn max_request_body_bytes(&self) -> usize {
        max_request_body_bytes_for_limits(self.limits)
    }

    pub fn privacy_config(&self) -> &SyncServerPrivacyConfig {
        &self.privacy_config
    }

    fn upload(&self, request: SyncUploadRequest) -> Result<SyncUploadOutcome, ServerUploadError> {
        match &self.backend {
            ServerBackend::Memory(sync) => sync
                .lock()
                .map_err(|_| ServerUploadError::Storage("sync state is unavailable".to_string()))?
                .upload(request)
                .map_err(ServerUploadError::Upload),
            ServerBackend::Sqlite(sync) => sync
                .upload_with_limits(request, self.limits)
                .map_err(ServerUploadError::from),
        }
    }

    fn create_auth_session_for_verified_owner(
        &self,
        owner_identity: &str,
    ) -> Result<SyncAuthSession, ServerAuthError> {
        match &self.backend {
            ServerBackend::Memory(sync) => sync
                .lock()
                .map_err(|_| ServerAuthError::Storage("sync state is unavailable".to_string()))?
                .create_auth_session_for_verified_owner(owner_identity)
                .map_err(ServerAuthError::Auth),
            ServerBackend::Sqlite(sync) => sync
                .create_auth_session_for_verified_owner(owner_identity)
                .map_err(ServerAuthError::from),
        }
    }

    fn authenticate_bearer_token(
        &self,
        bearer_token: &str,
    ) -> Result<Option<String>, ServerAuthError> {
        match &self.backend {
            ServerBackend::Memory(sync) => sync
                .lock()
                .map_err(|_| ServerAuthError::Storage("sync state is unavailable".to_string()))
                .map(|sync| sync.authenticate_bearer_token(bearer_token)),
            ServerBackend::Sqlite(sync) => sync
                .authenticate_bearer_token(bearer_token)
                .map_err(ServerAuthError::from),
        }
    }

    fn owner_account_exists(&self, owner_identity: &str) -> Result<bool, ServerAuthError> {
        match &self.backend {
            ServerBackend::Memory(sync) => sync
                .lock()
                .map_err(|_| ServerAuthError::Storage("sync state is unavailable".to_string()))?
                .owner_account_exists(owner_identity)
                .map_err(ServerAuthError::Auth),
            ServerBackend::Sqlite(sync) => sync
                .owner_account_exists(owner_identity)
                .map_err(ServerAuthError::from),
        }
    }

    fn revoke_bearer_token(&self, bearer_token: &str) -> Result<bool, ServerAuthError> {
        match &self.backend {
            ServerBackend::Memory(sync) => sync
                .lock()
                .map_err(|_| ServerAuthError::Storage("sync state is unavailable".to_string()))
                .map(|mut sync| sync.revoke_bearer_token(bearer_token)),
            ServerBackend::Sqlite(sync) => sync
                .revoke_bearer_token(bearer_token)
                .map_err(ServerAuthError::from),
        }
    }

    fn revoke_owner_bearer_tokens(&self, owner_identity: &str) -> Result<usize, ServerAuthError> {
        match &self.backend {
            ServerBackend::Memory(sync) => sync
                .lock()
                .map_err(|_| ServerAuthError::Storage("sync state is unavailable".to_string()))
                .map(|mut sync| sync.revoke_owner_bearer_tokens(owner_identity)),
            ServerBackend::Sqlite(sync) => sync
                .revoke_owner_bearer_tokens(owner_identity)
                .map_err(ServerAuthError::from),
        }
    }

    fn delete_owner_account(
        &self,
        owner_identity: &str,
    ) -> Result<SyncAccountDeletionOutcome, ServerAuthError> {
        match &self.backend {
            ServerBackend::Memory(sync) => sync
                .lock()
                .map_err(|_| ServerAuthError::Storage("sync state is unavailable".to_string()))?
                .delete_owner_account(owner_identity)
                .map_err(ServerAuthError::Auth),
            ServerBackend::Sqlite(sync) => sync
                .delete_owner_account(owner_identity)
                .map_err(ServerAuthError::from),
        }
    }

    fn create_oauth_browser_pending_login(&self) -> Result<SyncOAuthPendingLogin, ServerAuthError> {
        match &self.backend {
            ServerBackend::Memory(sync) => sync
                .lock()
                .map_err(|_| ServerAuthError::Storage("sync state is unavailable".to_string()))?
                .create_oauth_browser_pending_login()
                .map_err(ServerAuthError::Auth),
            ServerBackend::Sqlite(sync) => sync
                .create_oauth_browser_pending_login()
                .map_err(ServerAuthError::from),
        }
    }

    fn create_oauth_account_action_pending_login(
        &self,
        instance_url: &str,
        client_id: &str,
        client_secret: &str,
        redirect_uri: &str,
        expected_owner_identity: &str,
    ) -> Result<SyncOAuthPendingLogin, ServerAuthError> {
        match &self.backend {
            ServerBackend::Memory(sync) => sync
                .lock()
                .map_err(|_| ServerAuthError::Storage("sync state is unavailable".to_string()))?
                .create_oauth_account_action_pending_login_with_limits(
                    instance_url,
                    client_id,
                    client_secret,
                    redirect_uri,
                    expected_owner_identity,
                    self.limits,
                )
                .map_err(ServerAuthError::Auth),
            ServerBackend::Sqlite(sync) => sync
                .create_oauth_account_action_pending_login_with_limits(
                    instance_url,
                    client_id,
                    client_secret,
                    redirect_uri,
                    expected_owner_identity,
                    self.limits,
                )
                .map_err(ServerAuthError::from),
        }
    }

    #[allow(clippy::too_many_arguments)]
    fn configure_oauth_pending_login_for_browser(
        &self,
        login_token: &str,
        instance_url: &str,
        client_id: &str,
        client_secret: &str,
        redirect_uri: &str,
        expected_owner_identity: Option<&str>,
        notice_version: &str,
        notice_hash: &str,
        lawful_basis: &str,
    ) -> Result<Option<SyncOAuthPendingLogin>, ServerAuthError> {
        match &self.backend {
            ServerBackend::Memory(sync) => sync
                .lock()
                .map_err(|_| ServerAuthError::Storage("sync state is unavailable".to_string()))?
                .configure_oauth_pending_login_for_browser(
                    login_token,
                    instance_url,
                    client_id,
                    client_secret,
                    redirect_uri,
                    expected_owner_identity,
                    notice_version,
                    notice_hash,
                    lawful_basis,
                )
                .map_err(ServerAuthError::Auth),
            ServerBackend::Sqlite(sync) => sync
                .configure_oauth_pending_login_for_browser(
                    login_token,
                    instance_url,
                    client_id,
                    client_secret,
                    redirect_uri,
                    expected_owner_identity,
                    notice_version,
                    notice_hash,
                    lawful_basis,
                )
                .map_err(ServerAuthError::from),
        }
    }

    fn configure_oauth_pending_login_for_browser_authorization(
        &self,
        login_token: &str,
        instance_url: &str,
        client_id: &str,
        client_secret: &str,
        redirect_uri: &str,
        expected_owner_identity: Option<&str>,
    ) -> Result<Option<SyncOAuthPendingLogin>, ServerAuthError> {
        match &self.backend {
            ServerBackend::Memory(sync) => sync
                .lock()
                .map_err(|_| ServerAuthError::Storage("sync state is unavailable".to_string()))?
                .configure_oauth_pending_login_for_browser_authorization(
                    login_token,
                    instance_url,
                    client_id,
                    client_secret,
                    redirect_uri,
                    expected_owner_identity,
                )
                .map_err(ServerAuthError::Auth),
            ServerBackend::Sqlite(sync) => sync
                .configure_oauth_pending_login_for_browser_authorization(
                    login_token,
                    instance_url,
                    client_id,
                    client_secret,
                    redirect_uri,
                    expected_owner_identity,
                )
                .map_err(ServerAuthError::from),
        }
    }

    fn record_oauth_start_attempt(
        &self,
        instance_url: &str,
        client_key: &str,
    ) -> Result<(), ServerAuthError> {
        match &self.backend {
            ServerBackend::Memory(sync) => sync
                .lock()
                .map_err(|_| ServerAuthError::Storage("sync state is unavailable".to_string()))?
                .record_oauth_start_attempt_with_limits(instance_url, client_key, self.limits)
                .map_err(ServerAuthError::Auth),
            ServerBackend::Sqlite(sync) => sync
                .record_oauth_start_attempt_with_limits(instance_url, client_key, self.limits)
                .map_err(ServerAuthError::from),
        }
    }

    fn take_oauth_pending_login(
        &self,
        login_token: &str,
    ) -> Result<Option<SyncOAuthPendingLogin>, ServerAuthError> {
        match &self.backend {
            ServerBackend::Memory(sync) => sync
                .lock()
                .map_err(|_| ServerAuthError::Storage("sync state is unavailable".to_string()))
                .map(|mut sync| sync.take_oauth_pending_login(login_token)),
            ServerBackend::Sqlite(sync) => sync
                .take_oauth_pending_login(login_token)
                .map_err(ServerAuthError::from),
        }
    }

    fn record_privacy_acceptance_for_verified_owner(
        &self,
        owner_identity: &str,
        pending: &SyncOAuthPendingLogin,
    ) -> Result<(), ServerAuthError> {
        match &self.backend {
            ServerBackend::Memory(sync) => sync
                .lock()
                .map_err(|_| ServerAuthError::Storage("sync state is unavailable".to_string()))?
                .record_privacy_acceptance_for_verified_owner(owner_identity, pending)
                .map_err(ServerAuthError::Auth),
            ServerBackend::Sqlite(sync) => sync
                .record_privacy_acceptance_for_verified_owner(owner_identity, pending)
                .map_err(ServerAuthError::from),
        }
    }

    fn store_completed_oauth_login(
        &self,
        login_token: &str,
        owner_identity: &str,
    ) -> Result<(), ServerAuthError> {
        match &self.backend {
            ServerBackend::Memory(sync) => sync
                .lock()
                .map_err(|_| ServerAuthError::Storage("sync state is unavailable".to_string()))?
                .store_completed_oauth_login(login_token, owner_identity)
                .map_err(ServerAuthError::Auth),
            ServerBackend::Sqlite(sync) => sync
                .store_completed_oauth_login(login_token, owner_identity)
                .map_err(ServerAuthError::from),
        }
    }

    fn take_completed_oauth_login(
        &self,
        login_token: &str,
    ) -> Result<Option<String>, ServerAuthError> {
        match &self.backend {
            ServerBackend::Memory(sync) => sync
                .lock()
                .map_err(|_| ServerAuthError::Storage("sync state is unavailable".to_string()))
                .map(|mut sync| sync.take_completed_oauth_login(login_token)),
            ServerBackend::Sqlite(sync) => sync
                .take_completed_oauth_login(login_token)
                .map_err(ServerAuthError::from),
        }
    }

    fn download(
        &self,
        deck_uuid: &SyncUuid,
        kind: SyncDownloadKind,
        requester_owner_identity: Option<&str>,
    ) -> Result<crate::SyncDownload, ServerDownloadError> {
        match &self.backend {
            ServerBackend::Memory(sync) => sync
                .lock()
                .map_err(|_| ServerDownloadError::Storage("sync state is unavailable".to_string()))?
                .download(deck_uuid, kind, requester_owner_identity)
                .map_err(ServerDownloadError::Download),
            ServerBackend::Sqlite(sync) => sync
                .download(deck_uuid, kind, requester_owner_identity)
                .map_err(ServerDownloadError::from),
        }
    }

    fn delete_deck(
        &self,
        deck_uuid: &SyncUuid,
        requester_owner_identity: &str,
    ) -> Result<(), ServerDeleteError> {
        match &self.backend {
            ServerBackend::Memory(sync) => sync
                .lock()
                .map_err(|_| ServerDeleteError::Storage("sync state is unavailable".to_string()))?
                .delete(deck_uuid, requester_owner_identity)
                .map_err(ServerDeleteError::Delete),
            ServerBackend::Sqlite(sync) => sync
                .delete(deck_uuid, requester_owner_identity)
                .map_err(ServerDeleteError::from),
        }
    }

    fn inspect_shared_deck(
        &self,
        deck_uuid: &SyncUuid,
    ) -> Result<SyncSharedDeckStatus, ServerDownloadError> {
        match &self.backend {
            ServerBackend::Memory(sync) => sync
                .lock()
                .map_err(|_| ServerDownloadError::Storage("sync state is unavailable".to_string()))?
                .inspect_shared_deck(deck_uuid)
                .map_err(ServerDownloadError::Download),
            ServerBackend::Sqlite(sync) => sync
                .inspect_shared_deck(deck_uuid)
                .map_err(ServerDownloadError::from),
        }
    }

    fn list_public_decks(&self) -> Result<Vec<SyncSharedDeckStatus>, String> {
        match &self.backend {
            ServerBackend::Memory(sync) => Ok(sync
                .lock()
                .map_err(|_| "sync state is unavailable".to_string())?
                .list_public_decks()),
            ServerBackend::Sqlite(sync) => {
                sync.list_public_decks().map_err(|error| error.to_string())
            }
        }
    }

    fn list_owner_decks(
        &self,
        requester_owner_identity: &str,
    ) -> Result<Vec<SyncRemoteDeckStatus>, String> {
        match &self.backend {
            ServerBackend::Memory(sync) => Ok(sync
                .lock()
                .map_err(|_| "sync state is unavailable".to_string())?
                .list_owner_decks(requester_owner_identity)),
            ServerBackend::Sqlite(sync) => sync
                .list_owner_decks(requester_owner_identity)
                .map_err(|error| error.to_string()),
        }
    }

    fn quota_status(&self, owner_identity: Option<&str>) -> Result<SyncQuotaStatus, String> {
        match &self.backend {
            ServerBackend::Memory(sync) => Ok(sync
                .lock()
                .map_err(|_| "sync state is unavailable".to_string())?
                .quota_status(self.limits, owner_identity)),
            ServerBackend::Sqlite(sync) => sync
                .quota_status(self.limits, owner_identity)
                .map_err(|error| error.to_string()),
        }
    }
}

#[derive(Debug)]
enum ServerBackend {
    Memory(Mutex<InMemorySyncServer>),
    Sqlite(SqliteSyncServer),
}

#[derive(Debug)]
enum ServerUploadError {
    Upload(SyncUploadError),
    Busy,
    Storage(String),
}

impl From<SqliteSyncError> for ServerUploadError {
    fn from(error: SqliteSyncError) -> Self {
        match error {
            SqliteSyncError::Upload(error) => Self::Upload(error),
            error if error.is_database_busy() => Self::Busy,
            error => Self::Storage(error.to_string()),
        }
    }
}

#[derive(Debug)]
enum ServerAuthError {
    Auth(SyncAuthError),
    Busy,
    Storage(String),
}

impl From<SqliteSyncError> for ServerAuthError {
    fn from(error: SqliteSyncError) -> Self {
        match error {
            SqliteSyncError::Auth(error) => Self::Auth(error),
            error if error.is_database_busy() => Self::Busy,
            error => Self::Storage(error.to_string()),
        }
    }
}

#[derive(Debug)]
enum ServerDownloadError {
    Download(SyncDownloadError),
    Busy,
    Storage(String),
}

impl From<SqliteSyncError> for ServerDownloadError {
    fn from(error: SqliteSyncError) -> Self {
        match error {
            SqliteSyncError::Download(error) => Self::Download(error),
            error if error.is_database_busy() => Self::Busy,
            error => Self::Storage(error.to_string()),
        }
    }
}

#[derive(Debug)]
enum ServerDeleteError {
    Delete(SyncDeleteError),
    Busy,
    Storage(String),
}

impl From<SqliteSyncError> for ServerDeleteError {
    fn from(error: SqliteSyncError) -> Self {
        match error {
            SqliteSyncError::Delete(error) => Self::Delete(error),
            error if error.is_database_busy() => Self::Busy,
            error => Self::Storage(error.to_string()),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Request {
    pub method: String,
    pub target: String,
    pub headers: BTreeMap<String, String>,
    pub body: Vec<u8>,
}

impl Request {
    pub fn header(&self, name: &str) -> Option<&str> {
        self.headers
            .get(&name.to_ascii_lowercase())
            .map(String::as_str)
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Response {
    pub status: Status,
    pub headers: BTreeMap<String, String>,
    pub body: Vec<u8>,
}

impl Response {
    pub fn text(status: Status, text: &str) -> Self {
        let mut headers = no_store_headers();
        headers.insert(
            "content-type".to_string(),
            "text/plain; charset=utf-8".to_string(),
        );
        Self {
            status,
            headers,
            body: text.as_bytes().to_vec(),
        }
    }

    pub fn json(status: Status, json: String) -> Self {
        let mut headers = no_store_headers();
        headers.insert(
            "content-type".to_string(),
            "application/json; charset=utf-8".to_string(),
        );
        Self {
            status,
            headers,
            body: json.into_bytes(),
        }
    }

    pub fn html(status: Status, html: String) -> Self {
        let mut headers = no_store_headers();
        headers.insert(
            "content-type".to_string(),
            "text/html; charset=utf-8".to_string(),
        );
        Self {
            status,
            headers,
            body: html.into_bytes(),
        }
    }

    pub fn see_other(location: String) -> Self {
        let mut headers = no_store_headers();
        headers.insert("location".to_string(), location);
        headers.insert(
            "content-type".to_string(),
            "text/plain; charset=utf-8".to_string(),
        );
        Self {
            status: Status::SeeOther,
            headers,
            body: b"see other".to_vec(),
        }
    }

    fn bundle(download: crate::SyncDownload) -> Self {
        let mut headers = no_store_headers();
        let bundle_bytes = download.bundle().bytes();
        headers.insert(
            "content-type".to_string(),
            "application/octet-stream".to_string(),
        );
        headers.insert(
            "x-cosimo-revision".to_string(),
            download.revision().to_string(),
        );
        headers.insert(
            "x-cosimo-visibility".to_string(),
            download.visibility().code().to_string(),
        );
        headers.insert(
            "x-cosimo-bundle-hash".to_string(),
            download.bundle().bundle_hash().to_string(),
        );
        headers.insert(
            "x-cosimo-includes-review-database".to_string(),
            download.bundle().includes_review_database().to_string(),
        );
        headers.insert(
            "x-cosimo-bundle-length".to_string(),
            bundle_bytes.len().to_string(),
        );
        if let Some(device_name) = download.last_upload_device_name() {
            headers.insert(
                "x-cosimo-last-upload-device-name".to_string(),
                device_name.to_string(),
            );
        }
        Self {
            status: Status::Ok,
            headers,
            body: bundle_bytes.to_vec(),
        }
    }

    pub fn to_http_bytes(&self) -> Vec<u8> {
        let mut response = Vec::new();
        let status_line = format!(
            "HTTP/1.1 {} {}\r\n",
            self.status.code(),
            self.status.reason()
        );
        response.extend_from_slice(status_line.as_bytes());
        response.extend_from_slice(format!("Content-Length: {}\r\n", self.body.len()).as_bytes());
        response.extend_from_slice(b"Connection: close\r\n");
        for (name, value) in &self.headers {
            response.extend_from_slice(format!("{name}: {value}\r\n").as_bytes());
        }
        response.extend_from_slice(b"\r\n");
        response.extend_from_slice(&self.body);
        response
    }

    pub fn to_cgi_bytes(&self) -> Vec<u8> {
        let mut response = Vec::new();
        response.extend_from_slice(
            format!(
                "Status: {} {}\r\n",
                self.status.code(),
                self.status.reason()
            )
            .as_bytes(),
        );
        response.extend_from_slice(format!("Content-Length: {}\r\n", self.body.len()).as_bytes());
        for (name, value) in &self.headers {
            response.extend_from_slice(format!("{name}: {value}\r\n").as_bytes());
        }
        response.extend_from_slice(b"\r\n");
        response.extend_from_slice(&self.body);
        response
    }
}

fn no_store_headers() -> BTreeMap<String, String> {
    BTreeMap::from([("cache-control".to_string(), "no-store".to_string())])
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
    Ok,
    Created,
    Accepted,
    SeeOther,
    BadRequest,
    Unauthorized,
    Forbidden,
    NotFound,
    MethodNotAllowed,
    Conflict,
    PayloadTooLarge,
    TooManyRequests,
    BadGateway,
    ServiceUnavailable,
    InternalServerError,
}

impl Status {
    pub fn code(self) -> u16 {
        match self {
            Self::Ok => 200,
            Self::Created => 201,
            Self::Accepted => 202,
            Self::SeeOther => 303,
            Self::BadRequest => 400,
            Self::Unauthorized => 401,
            Self::Forbidden => 403,
            Self::NotFound => 404,
            Self::MethodNotAllowed => 405,
            Self::Conflict => 409,
            Self::PayloadTooLarge => 413,
            Self::TooManyRequests => 429,
            Self::BadGateway => 502,
            Self::ServiceUnavailable => 503,
            Self::InternalServerError => 500,
        }
    }

    pub fn reason(self) -> &'static str {
        match self {
            Self::Ok => "OK",
            Self::Created => "Created",
            Self::Accepted => "Accepted",
            Self::SeeOther => "See Other",
            Self::BadRequest => "Bad Request",
            Self::Unauthorized => "Unauthorized",
            Self::Forbidden => "Forbidden",
            Self::NotFound => "Not Found",
            Self::MethodNotAllowed => "Method Not Allowed",
            Self::Conflict => "Conflict",
            Self::PayloadTooLarge => "Payload Too Large",
            Self::TooManyRequests => "Too Many Requests",
            Self::BadGateway => "Bad Gateway",
            Self::ServiceUnavailable => "Service Unavailable",
            Self::InternalServerError => "Internal Server Error",
        }
    }
}

pub fn read_http_request(reader: &mut impl Read) -> Result<Request, HttpError> {
    read_http_request_with_max_body_bytes(reader, MAX_BODY_BYTES)
}

pub fn read_http_request_with_max_body_bytes(
    reader: &mut impl Read,
    max_body_bytes: usize,
) -> Result<Request, HttpError> {
    let mut buffer = Vec::new();
    let header_end = loop {
        let mut chunk = [0; 4096];
        let read = reader.read(&mut chunk)?;
        if read == 0 {
            return Err(HttpError::BadRequest(
                "connection closed before headers".to_string(),
            ));
        }
        buffer.extend_from_slice(&chunk[..read]);
        if buffer.len() > MAX_HEADER_BYTES {
            return Err(HttpError::PayloadTooLarge(
                "HTTP headers are too large".to_string(),
            ));
        }
        if let Some(header_end) = find_header_end(&buffer) {
            break header_end;
        }
    };

    let header_text = std::str::from_utf8(&buffer[..header_end])
        .map_err(|_| HttpError::BadRequest("HTTP headers are not UTF-8".to_string()))?;
    let (method, target, headers) = parse_request_headers(header_text)?;
    let content_length = headers
        .get("content-length")
        .map(|value| parse_usize_header("Content-Length", value))
        .transpose()?
        .unwrap_or(0);
    if content_length > max_body_bytes {
        return Err(HttpError::PayloadTooLarge(
            "HTTP body is too large".to_string(),
        ));
    }

    let body_start = header_end + 4;
    let already_read = buffer.len().saturating_sub(body_start);
    let mut body = Vec::with_capacity(content_length);
    body.extend_from_slice(&buffer[body_start..]);
    if already_read > content_length {
        body.truncate(content_length);
    }
    while body.len() < content_length {
        let remaining = content_length - body.len();
        let chunk_len = remaining.min(4096);
        let mut chunk = vec![0; chunk_len];
        reader.read_exact(&mut chunk)?;
        body.extend_from_slice(&chunk);
    }

    Ok(Request {
        method,
        target,
        headers,
        body,
    })
}

pub fn max_request_body_bytes_for_limits(limits: SyncServerLimits) -> usize {
    let mut max_body_bytes = MAX_BODY_BYTES as u64;
    if let Some(max_bundle_bytes) = limits.max_bundle_bytes {
        max_body_bytes = max_body_bytes.min(max_bundle_bytes.saturating_mul(2));
    }
    if let Some(max_storage_bytes) = limits.max_storage_bytes_per_owner {
        max_body_bytes = max_body_bytes.min(max_storage_bytes);
    }
    if let Some(max_storage_bytes) = limits.max_storage_bytes_total {
        max_body_bytes = max_body_bytes.min(max_storage_bytes);
    }
    max_body_bytes.min(usize::MAX as u64) as usize
}

fn find_header_end(buffer: &[u8]) -> Option<usize> {
    buffer.windows(4).position(|window| window == b"\r\n\r\n")
}

fn parse_request_headers(
    header_text: &str,
) -> Result<(String, String, BTreeMap<String, String>), HttpError> {
    let mut lines = header_text.split("\r\n");
    let request_line = lines
        .next()
        .ok_or_else(|| HttpError::BadRequest("missing request line".to_string()))?;
    let mut request_parts = request_line.split_whitespace();
    let method = request_parts
        .next()
        .ok_or_else(|| HttpError::BadRequest("missing request method".to_string()))?;
    let target = request_parts
        .next()
        .ok_or_else(|| HttpError::BadRequest("missing request target".to_string()))?;
    let version = request_parts
        .next()
        .ok_or_else(|| HttpError::BadRequest("missing HTTP version".to_string()))?;
    if request_parts.next().is_some() || !version.starts_with("HTTP/") {
        return Err(HttpError::BadRequest("invalid request line".to_string()));
    }

    let mut headers = BTreeMap::new();
    for line in lines {
        if line.is_empty() {
            continue;
        }
        let Some((name, value)) = line.split_once(':') else {
            return Err(HttpError::BadRequest("invalid HTTP header".to_string()));
        };
        let name = name.trim().to_ascii_lowercase();
        let value = value.trim();
        if name.is_empty() {
            return Err(HttpError::BadRequest("empty HTTP header name".to_string()));
        }
        headers.insert(name, value.to_string());
    }

    Ok((method.to_string(), target.to_string(), headers))
}

#[derive(Debug)]
pub enum HttpError {
    BadRequest(String),
    PayloadTooLarge(String),
    Io(io::Error),
}

impl fmt::Display for HttpError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::BadRequest(message) | Self::PayloadTooLarge(message) => f.write_str(message),
            Self::Io(error) => error.fmt(f),
        }
    }
}

impl From<io::Error> for HttpError {
    fn from(error: io::Error) -> Self {
        Self::Io(error)
    }
}

impl From<HttpHeaderError> for HttpError {
    fn from(error: HttpHeaderError) -> Self {
        Self::BadRequest(error.to_string())
    }
}

pub fn http_error_response(error: HttpError) -> Response {
    match error {
        HttpError::PayloadTooLarge(message) => Response::text(Status::PayloadTooLarge, &message),
        HttpError::BadRequest(message) => Response::text(Status::BadRequest, &message),
        HttpError::Io(error) => Response::text(Status::BadRequest, &error.to_string()),
    }
}

pub fn handle_request(state: &ServerState, request: Request) -> Response {
    let target = request.target.clone();
    let (path, query) = split_target(&target);
    let segments = path_segments(path);
    match (request.method.as_str(), segments.as_slice()) {
        ("GET", []) => sync_root_response(state, &request),
        ("GET", ["api", "v1", "health"]) => Response::json(
            Status::Ok,
            format!(
                "{{\"status\":\"ok\",\"storage\":\"{}\"}}",
                state.storage_label()
            ),
        ),
        ("GET", ["privacy"]) => sync_privacy_response(state),
        ("GET", ["auth", "fedi", "register"]) => sync_registration_form_response(state, query),
        ("POST", ["auth", "fedi", "register"]) => {
            accept_sync_registration_response(state, &request)
        }
        ("GET", ["auth", "fedi", "callback"]) => {
            fedi_oauth_callback_response(state, &request, query)
        }
        ("GET", ["auth", "fedi", "authorize"]) => sync_device_authorization_form_response(query),
        ("POST", ["auth", "fedi", "authorize"]) => {
            accept_sync_device_authorization_response(state, &request)
        }
        ("GET", ["auth", "fedi", "authorize", "callback"]) => {
            fedi_oauth_device_authorization_callback_response(state, query)
        }
        ("GET", ["auth", "fedi", "delete"]) => sync_account_deletion_form_response(),
        ("POST", ["auth", "fedi", "delete"]) => {
            accept_sync_account_deletion_response(state, &request)
        }
        ("GET", ["auth", "fedi", "delete", "callback"]) => {
            fedi_oauth_delete_callback_response(state, query)
        }
        ("GET", ["api", "v1", "privacy"]) => {
            Response::json(Status::Ok, privacy_config_json(state.privacy_config()))
        }
        ("GET", ["api", "v1", "quota"]) => quota_status(state, &request),
        ("POST", ["api", "v1", "auth", "fedi", "browser-start"]) => {
            start_fedi_browser_auth_session(state)
        }
        ("POST", ["api", "v1", "auth", "fedi", "device-browser-start"]) => {
            start_fedi_browser_device_auth_session(state)
        }
        ("POST", ["api", "v1", "auth", "fedi", "poll"]) => {
            poll_fedi_browser_auth_session(state, &request)
        }
        ("POST", ["api", "v1", "auth", "logout"]) => revoke_auth_session(state, &request),
        ("POST", ["api", "v1", "auth", "logout-all"]) => {
            revoke_all_owner_auth_sessions(state, &request)
        }
        ("GET", ["api", "v1", "decks"]) => list_public_decks(state),
        ("GET", ["api", "v1", "me", "decks"]) => list_owner_decks(state, &request),
        ("GET", ["api", "v1", "decks", deck_uuid]) => inspect_shared_deck(state, deck_uuid),
        ("GET", ["api", "v1", "decks", deck_uuid, "download"]) => {
            download_deck(state, &request, deck_uuid, query)
        }
        ("POST", ["api", "v1", "decks", deck_uuid, "upload"]) => {
            upload_deck(state, request, deck_uuid)
        }
        ("DELETE", ["api", "v1", "decks", deck_uuid]) => delete_deck(state, &request, deck_uuid),
        ("GET" | "POST", _) => Response::text(Status::NotFound, "not found"),
        _ => Response::text(Status::MethodNotAllowed, "method not allowed"),
    }
}

fn sync_root_response(state: &ServerState, request: &Request) -> Response {
    let privacy_path = browser_route_path(request, "/privacy");
    let account_deletion_path = browser_route_path(request, "/auth/fedi/delete");
    let health_path = browser_route_path(request, "/api/v1/health");
    Response::html(
        Status::Ok,
        format!(
            "<!doctype html>\n\
             <html lang=\"en\">\n\
             <head>\n\
             <meta charset=\"utf-8\">\n\
             <title>Cosimo Sync Server</title>\n\
             </head>\n\
             <body>\n\
             <main>\n\
             <h1>Cosimo Sync Server</h1>\n\
             <p>This is a Cosimo sync server. Configure Cosimo to use this page's \
             URL as the sync server URL.</p>\n\
             <section aria-labelledby=\"server-links-heading\">\n\
             <h2 id=\"server-links-heading\">Server links</h2>\n\
             <ul>\n\
             <li><strong>Status:</strong> running.</li>\n\
             <li><strong>Storage backend:</strong> {}</li>\n\
             <li><a href=\"{}\">Read this server's privacy notice</a>.</li>\n\
             <li><a href=\"{}\">Delete your Cosimo sync account data</a>.</li>\n\
             <li><a href=\"{}\">Check the API health endpoint</a>.</li>\n\
             <li><a href=\"{}\">Visit the Cosimo project wiki</a> for project \
             information, documentation, and downloads.</li>\n\
             </ul>\n\
             </section>\n\
             </main>\n\
             </body>\n\
             </html>\n",
            state.storage_label(),
            html_escape(&privacy_path),
            html_escape(&account_deletion_path),
            html_escape(&health_path),
            PROJECT_WIKI_URL
        ),
    )
}

fn browser_route_path(request: &Request, path: &str) -> String {
    let Some(script_name) = request.header("x-cosimo-script-name").map(str::trim) else {
        return path.to_string();
    };
    if !safe_cgi_script_name(script_name) {
        return path.to_string();
    }
    let script_name = script_name.trim_end_matches('/');
    format!("{script_name}{path}")
}

fn safe_cgi_script_name(script_name: &str) -> bool {
    !script_name.is_empty()
        && script_name.starts_with('/')
        && !script_name.contains("://")
        && !script_name
            .chars()
            .any(|character| character.is_control() || character.is_whitespace())
}

fn sync_privacy_response(state: &ServerState) -> Response {
    let privacy = state.privacy_config();
    let registration_status = privacy_registration_status_text(privacy);
    Response::html(
        Status::Ok,
        format!(
            "<!doctype html>\n\
             <html lang=\"en\">\n\
             <head>\n\
             <meta charset=\"utf-8\">\n\
             <title>Cosimo Sync Privacy Notice</title>\n\
             </head>\n\
             <body>\n\
             <main>\n\
             <h1>Cosimo Sync Privacy Notice</h1>\n\
             <section aria-labelledby=\"registration-status-heading\">\n\
             <h2 id=\"registration-status-heading\">Registration status</h2>\n\
             <p>{}</p>\n\
             </section>\n\
             {}\n\
             </main>\n\
             </body>\n\
             </html>\n",
            html_escape(&registration_status),
            privacy_notice_fragment(privacy),
        ),
    )
}

fn privacy_registration_status_text(privacy: &SyncServerPrivacyConfig) -> String {
    match privacy.registration_problem() {
        Some(problem) => {
            let detail = problem
                .strip_prefix("sync registration is disabled: ")
                .unwrap_or(problem);
            format!("New account registration is disabled. Reason: {detail}.")
        }
        None => "New account registration is available on this server.".to_string(),
    }
}

fn sync_registration_form_response(state: &ServerState, query: Option<&str>) -> Response {
    if let Some(problem) = state.privacy_config().registration_problem() {
        return Response::html(Status::Forbidden, registration_error_html(problem));
    }
    let login_token = match query_string_value(query, "login_token") {
        Ok(Some(login_token)) => login_token,
        Ok(None) => {
            return Response::html(
                Status::BadRequest,
                registration_error_html("sync login token is missing"),
            );
        }
        Err(message) => {
            return Response::html(Status::BadRequest, registration_error_html(&message));
        }
    };
    Response::html(
        Status::Ok,
        format!(
            "<!doctype html>\n\
             <html lang=\"en\">\n\
             <head>\n\
             <meta charset=\"utf-8\">\n\
             <title>Register with Cosimo Sync</title>\n\
             </head>\n\
             <body>\n\
             <main>\n\
             <h1>Register with Cosimo Sync</h1>\n\
             <p>Registering creates a Cosimo sync account for the Fediverse \
             identity you authorise. This server will store the sync data \
             described below until you delete the account or the retention \
             policy applies.</p>\n\
             <p>Enter your Fediverse instance or full account. Cosimo will send \
             you there to authorise this server.</p>\n\
             {}\n\
             <form method=\"post\">\n\
             <input type=\"hidden\" name=\"login_token\" value=\"{}\">\n\
             <p><label>Fediverse instance or account<br>\n\
             <input type=\"text\" name=\"fedi_instance\" autocomplete=\"username\" required></label></p>\n\
             <p><label><input type=\"checkbox\" name=\"privacy_acceptance\" value=\"yes\"> \
             I have read and accept this Cosimo sync server privacy notice.</label></p>\n\
             <p><button type=\"submit\">Continue to Fediverse authorisation</button></p>\n\
             </form>\n\
             </main>\n\
             </body>\n\
             </html>\n",
            privacy_notice_fragment(state.privacy_config()),
            html_escape(&login_token),
        ),
    )
}

fn accept_sync_registration_response(state: &ServerState, request: &Request) -> Response {
    if let Some(problem) = state.privacy_config().registration_problem() {
        return Response::html(Status::Forbidden, registration_error_html(problem));
    }
    let form = match form_body_values(request) {
        Ok(form) => form,
        Err(message) => {
            return Response::html(Status::BadRequest, registration_error_html(&message));
        }
    };
    let Some(login_token) = form.get("login_token").filter(|value| !value.is_empty()) else {
        return Response::html(
            Status::BadRequest,
            registration_error_html("sync login token is missing"),
        );
    };
    if form.get("privacy_acceptance").map(String::as_str) != Some("yes") {
        return Response::html(
            Status::BadRequest,
            registration_error_html("privacy notice acceptance is required before login"),
        );
    }
    let privacy = state.privacy_config();
    let notice_version = privacy.notice_version.as_deref().unwrap_or("domestic");
    let notice_hash = privacy_notice_hash(privacy);
    let lawful_basis = privacy
        .lawful_basis
        .as_deref()
        .unwrap_or("domestic/personal deployment");
    if let Some(instance_value) = form
        .get("fedi_instance")
        .map(String::as_str)
        .filter(|value| !value.trim().is_empty())
    {
        return accept_browser_sync_registration_response(
            state,
            request,
            login_token,
            instance_value,
            notice_version,
            &notice_hash,
            lawful_basis,
        );
    }
    Response::html(
        Status::BadRequest,
        registration_error_html("enter the Fediverse instance or account for registration"),
    )
}

fn accept_browser_sync_registration_response(
    state: &ServerState,
    request: &Request,
    login_token: &str,
    instance_value: &str,
    notice_version: &str,
    notice_hash: &str,
    lawful_basis: &str,
) -> Response {
    let registration_target = match fedi_registration_target_from_value(instance_value) {
        Ok(registration_target) => registration_target,
        Err(error) => return oauth_error_response(error),
    };
    let client_key = oauth_rate_limit_client_key(request);
    if let Err(error) =
        state.record_oauth_start_attempt(&registration_target.instance_url, &client_key)
    {
        return server_auth_error_response(error);
    }
    let redirect_uri = match oauth_callback_url(request) {
        Ok(redirect_uri) => redirect_uri,
        Err(message) => {
            return Response::html(Status::BadRequest, registration_error_html(&message));
        }
    };
    let application = match state
        .oauth_provider
        .register_application_with_redirect_uri(&registration_target.instance_url, &redirect_uri)
    {
        Ok(application) => application,
        Err(error) => return oauth_error_response(error),
    };
    let pending = match state.configure_oauth_pending_login_for_browser(
        login_token,
        application.instance_url(),
        application.client_id(),
        application.client_secret(),
        &redirect_uri,
        registration_target.expected_owner_identity.as_deref(),
        notice_version,
        notice_hash,
        lawful_basis,
    ) {
        Ok(Some(pending)) => pending,
        Ok(None) => {
            return Response::html(
                Status::BadRequest,
                registration_error_html("sync login token was not found or expired"),
            );
        }
        Err(error) => return server_auth_error_response(error),
    };
    let authorization_url =
        authorization_url_with_state(application.authorization_url(), pending.login_token());
    Response::see_other(authorization_url)
}

fn sync_device_authorization_form_response(query: Option<&str>) -> Response {
    let login_token = match query_string_value(query, "login_token") {
        Ok(Some(login_token)) => login_token,
        Ok(None) => {
            return Response::html(
                Status::BadRequest,
                registration_error_html("sync login token is missing"),
            );
        }
        Err(message) => {
            return Response::html(Status::BadRequest, registration_error_html(&message));
        }
    };
    Response::html(
        Status::Ok,
        format!(
            "<!doctype html>\n\
             <html lang=\"en\">\n\
             <head>\n\
             <meta charset=\"utf-8\">\n\
             <title>Authorize Cosimo Sync Device</title>\n\
             </head>\n\
             <body>\n\
             <main>\n\
             <h1>Authorize Cosimo Sync Device</h1>\n\
             <p>This authorizes this Cosimo installation for an existing sync account. \
             If you do not already have a Cosimo sync account on this server, register \
             an account first.</p>\n\
             <form method=\"post\">\n\
             <input type=\"hidden\" name=\"login_token\" value=\"{}\">\n\
             <p><label>Fediverse instance or account<br>\n\
             <input type=\"text\" name=\"fedi_instance\" autocomplete=\"username\" required></label></p>\n\
             <p><button type=\"submit\">Continue to Fediverse authorisation</button></p>\n\
             </form>\n\
             </main>\n\
             </body>\n\
             </html>\n",
            html_escape(&login_token),
        ),
    )
}

fn accept_sync_device_authorization_response(state: &ServerState, request: &Request) -> Response {
    let form = match form_body_values(request) {
        Ok(form) => form,
        Err(message) => {
            return Response::html(Status::BadRequest, registration_error_html(&message));
        }
    };
    let Some(login_token) = form.get("login_token").filter(|value| !value.is_empty()) else {
        return Response::html(
            Status::BadRequest,
            registration_error_html("sync login token is missing"),
        );
    };
    let Some(instance_value) = form
        .get("fedi_instance")
        .map(String::as_str)
        .filter(|value| !value.trim().is_empty())
    else {
        return Response::html(
            Status::BadRequest,
            registration_error_html(
                "enter the Fediverse instance or account for the existing sync account",
            ),
        );
    };
    let authorization_target = match fedi_registration_target_from_value(instance_value) {
        Ok(registration_target) => registration_target,
        Err(error) => return oauth_error_response(error),
    };
    let client_key = oauth_rate_limit_client_key(request);
    if let Err(error) =
        state.record_oauth_start_attempt(&authorization_target.instance_url, &client_key)
    {
        return server_auth_error_response(error);
    }
    let redirect_uri = match oauth_device_authorization_callback_url(request) {
        Ok(redirect_uri) => redirect_uri,
        Err(message) => {
            return Response::html(Status::BadRequest, registration_error_html(&message));
        }
    };
    let application = match state
        .oauth_provider
        .register_application_with_redirect_uri(&authorization_target.instance_url, &redirect_uri)
    {
        Ok(application) => application,
        Err(error) => return oauth_error_response(error),
    };
    let pending = match state.configure_oauth_pending_login_for_browser_authorization(
        login_token,
        application.instance_url(),
        application.client_id(),
        application.client_secret(),
        &redirect_uri,
        authorization_target.expected_owner_identity.as_deref(),
    ) {
        Ok(Some(pending)) => pending,
        Ok(None) => {
            return Response::html(
                Status::BadRequest,
                registration_error_html("sync login token was not found or expired"),
            );
        }
        Err(error) => return server_auth_error_response(error),
    };
    let authorization_url =
        authorization_url_with_state(application.authorization_url(), pending.login_token());
    Response::see_other(authorization_url)
}

fn sync_account_deletion_form_response() -> Response {
    Response::html(
        Status::Ok,
        "<!doctype html>\n\
         <html lang=\"en\">\n\
         <head>\n\
         <meta charset=\"utf-8\">\n\
         <title>Delete Cosimo Sync Account Data</title>\n\
         </head>\n\
         <body>\n\
         <main>\n\
         <h1>Delete Cosimo Sync Account Data</h1>\n\
         <p>This deletes the Cosimo sync data held by this server for the \
         verified Fediverse account: owned decks, stored deck bundles, review \
         history bundles, Cosimo sync tokens, completed login handoffs, and \
         privacy-notice acceptance records.</p>\n\
         <p>It does not delete your Fediverse account, local Cosimo deck files, \
         local review databases, local audio files, or web-server logs retained \
         by the operator.</p>\n\
         <form method=\"post\">\n\
         <p><label>Fediverse account<br>\n\
         <input type=\"text\" name=\"fedi_account\" autocomplete=\"username\" \
         placeholder=\"@user@example.social\" required></label></p>\n\
         <p><label><input type=\"checkbox\" name=\"deletion_confirmation\" \
         value=\"yes\"> I understand that this will permanently delete this \
         account's Cosimo sync data from this server.</label></p>\n\
         <p><button type=\"submit\">Continue to Fediverse authorisation</button></p>\n\
         </form>\n\
         </main>\n\
         </body>\n\
         </html>\n"
            .to_string(),
    )
}

fn accept_sync_account_deletion_response(state: &ServerState, request: &Request) -> Response {
    let form = match form_body_values(request) {
        Ok(form) => form,
        Err(message) => {
            return Response::html(Status::BadRequest, account_deletion_error_html(&message));
        }
    };
    if form.get("deletion_confirmation").map(String::as_str) != Some("yes") {
        return Response::html(
            Status::BadRequest,
            account_deletion_error_html("account deletion confirmation is required"),
        );
    }
    let Some(account_value) = form
        .get("fedi_account")
        .map(String::as_str)
        .filter(|value| !value.trim().is_empty())
    else {
        return Response::html(
            Status::BadRequest,
            account_deletion_error_html("Fediverse account is required"),
        );
    };
    let deletion_target = match fedi_registration_target_from_value(account_value) {
        Ok(target) => target,
        Err(error) => return oauth_error_response(error),
    };
    let Some(expected_owner_identity) = deletion_target.expected_owner_identity.as_deref() else {
        return Response::html(
            Status::BadRequest,
            account_deletion_error_html(
                "enter the full Fediverse account to delete Cosimo sync account data",
            ),
        );
    };
    let client_key = oauth_rate_limit_client_key(request);
    if let Err(error) = state.record_oauth_start_attempt(&deletion_target.instance_url, &client_key)
    {
        return server_auth_error_response(error);
    }
    let redirect_uri = match oauth_delete_callback_url(request) {
        Ok(redirect_uri) => redirect_uri,
        Err(message) => {
            return Response::html(Status::BadRequest, account_deletion_error_html(&message));
        }
    };
    let application = match state
        .oauth_provider
        .register_application_with_redirect_uri(&deletion_target.instance_url, &redirect_uri)
    {
        Ok(application) => application,
        Err(error) => return oauth_error_response(error),
    };
    let pending = match state.create_oauth_account_action_pending_login(
        application.instance_url(),
        application.client_id(),
        application.client_secret(),
        &redirect_uri,
        expected_owner_identity,
    ) {
        Ok(pending) => pending,
        Err(error) => return server_auth_error_response(error),
    };
    let authorization_url =
        authorization_url_with_state(application.authorization_url(), pending.login_token());
    Response::see_other(authorization_url)
}

fn fedi_oauth_delete_callback_response(state: &ServerState, query: Option<&str>) -> Response {
    let login_token = match query_string_value(query, "state") {
        Ok(Some(value)) => value,
        Ok(None) => {
            return Response::html(
                Status::BadRequest,
                account_deletion_error_html("sync account deletion state is missing"),
            );
        }
        Err(message) => {
            return Response::html(Status::BadRequest, account_deletion_error_html(&message));
        }
    };
    let authorization_code = match query_string_value(query, "code") {
        Ok(Some(value)) => value,
        Ok(None) => {
            return Response::html(
                Status::BadRequest,
                account_deletion_error_html("Fediverse authorisation code is missing"),
            );
        }
        Err(message) => {
            return Response::html(Status::BadRequest, account_deletion_error_html(&message));
        }
    };
    let pending = match state.take_oauth_pending_login(&login_token) {
        Ok(Some(pending)) => pending,
        Ok(None) => {
            return Response::html(
                Status::BadRequest,
                account_deletion_error_html(
                    "sync account deletion token was not found or has already been used",
                ),
            );
        }
        Err(error) => return server_auth_error_response(error),
    };
    let owner_identity = match state
        .oauth_provider
        .exchange_code_for_owner_identity(&pending, &authorization_code)
    {
        Ok(owner_identity) => owner_identity,
        Err(error) => return oauth_error_response(error),
    };
    let Some(expected_owner_identity) = pending.expected_owner_identity() else {
        return Response::html(
            Status::BadRequest,
            account_deletion_error_html("sync account deletion target is missing"),
        );
    };
    if owner_identity != expected_owner_identity {
        return Response::html(
            Status::Forbidden,
            account_deletion_error_html(
                "authenticated Fediverse account does not match the account entered for deletion",
            ),
        );
    }
    match state.delete_owner_account(&owner_identity) {
        Ok(outcome) => Response::html(
            Status::Ok,
            account_deletion_complete_html(&owner_identity, outcome),
        ),
        Err(error) => server_auth_error_response(error),
    }
}

fn account_deletion_error_html(message: &str) -> String {
    format!(
        "<!doctype html>\n\
         <html lang=\"en\">\n\
         <head><meta charset=\"utf-8\"><title>Cosimo Sync Account Deletion Error</title></head>\n\
         <body><main><h1>Cosimo Sync Account Deletion Error</h1><p>{}</p></main></body>\n\
         </html>\n",
        html_escape(message)
    )
}

fn account_deletion_complete_html(
    owner_identity: &str,
    outcome: SyncAccountDeletionOutcome,
) -> String {
    format!(
        "<!doctype html>\n\
         <html lang=\"en\">\n\
         <head><meta charset=\"utf-8\"><title>Cosimo Sync Account Data Deleted</title></head>\n\
         <body><main><h1>Cosimo Sync Account Data Deleted</h1>\
         <p>Deleted Cosimo sync account data for <code>{}</code>.</p>\
         <dl>\
         <dt>Synced decks deleted</dt><dd>{}</dd>\
         <dt>Cosimo sync tokens revoked</dt><dd>{}</dd>\
         </dl>\
         <p>Local Cosimo files and your Fediverse account were not changed.</p>\
         </main></body>\n\
         </html>\n",
        html_escape(owner_identity),
        outcome.decks_deleted(),
        outcome.sessions_revoked(),
    )
}

fn registration_error_html(message: &str) -> String {
    format!(
        "<!doctype html>\n\
         <html lang=\"en\">\n\
         <head><meta charset=\"utf-8\"><title>Cosimo Sync Registration Error</title></head>\n\
         <body><main><h1>Cosimo Sync Registration Error</h1><p>{}</p></main></body>\n\
         </html>\n",
        html_escape(message)
    )
}

fn privacy_notice_fragment(privacy: &SyncServerPrivacyConfig) -> String {
    let mut rows = String::new();
    push_privacy_row(&mut rows, "Controller", privacy.controller_name.as_deref());
    push_privacy_row(
        &mut rows,
        "Controller contact",
        privacy.controller_contact.as_deref(),
    );
    push_privacy_row(
        &mut rows,
        "Fediverse contact",
        privacy.controller_fedi.as_deref(),
    );
    push_privacy_row(&mut rows, "DPO contact", privacy.dpo_contact.as_deref());
    push_privacy_row(
        &mut rows,
        "Data rights contact",
        privacy
            .rights_contact
            .as_deref()
            .or(privacy.controller_contact.as_deref()),
    );
    push_privacy_owned_row(
        &mut rows,
        "Privacy notice version",
        privacy_notice_version_text(privacy.notice_version.as_deref()),
    );
    push_privacy_row(
        &mut rows,
        "Data processed by this server",
        privacy.data_categories.as_deref(),
    );
    push_privacy_row(
        &mut rows,
        "Why this data is processed",
        privacy.purposes.as_deref(),
    );
    push_privacy_row(
        &mut rows,
        "Legal basis for processing",
        privacy.lawful_basis.as_deref(),
    );
    push_privacy_row(
        &mut rows,
        "Recipients and processors",
        privacy.recipients.as_deref(),
    );
    push_privacy_row(
        &mut rows,
        "International transfers",
        privacy.international_transfers.as_deref(),
    );
    push_privacy_row(&mut rows, "Retention", privacy.retention.as_deref());
    push_privacy_row(
        &mut rows,
        "Complaints",
        privacy.complaint_contact.as_deref(),
    );
    let details = if rows.is_empty() {
        String::new()
    } else {
        format!(
            "<dl>\n\
             {}\
             </dl>\n",
            rows
        )
    };

    format!(
        "<section aria-labelledby=\"privacy-notice-heading\">\n\
         <h2 id=\"privacy-notice-heading\">Privacy notice</h2>\n\
         {}\n\
         {}\
         </section>\n",
        privacy_service_mode_html(privacy),
        details,
    )
}

fn privacy_service_mode_html(privacy: &SyncServerPrivacyConfig) -> String {
    match privacy.mode {
        SyncServerServiceMode::Public => {
            "<p>This server is configured for public Cosimo sync registration.</p>\n".to_string()
        }
        SyncServerServiceMode::Domestic => {
            let allowlist = if privacy.domestic_registration_allowlist.is_empty() {
                ""
            } else {
                " Registration is restricted to configured Fediverse accounts."
            };
            format!(
                "<p>This server is configured for domestic or personal Cosimo sync use. \
                 It should not be offered as a public service.{}\
                 </p>\n",
                allowlist
            )
        }
    }
}

fn push_privacy_row(rows: &mut String, label: &str, value: Option<&str>) {
    let Some(value) = value.and_then(non_empty_trimmed) else {
        return;
    };
    rows.push_str(&format!(
        "<dt>{}</dt><dd>{}</dd>\n",
        html_escape(label),
        html_escape(value)
    ));
}

fn push_privacy_owned_row(rows: &mut String, label: &str, value: Option<String>) {
    if let Some(value) = value {
        push_privacy_row(rows, label, Some(&value));
    }
}

fn non_empty_trimmed(value: &str) -> Option<&str> {
    let value = value.trim();
    (!value.is_empty()).then_some(value)
}

fn privacy_notice_version_text(value: Option<&str>) -> Option<String> {
    let value = value.and_then(non_empty_trimmed)?;
    if value.eq_ignore_ascii_case("initial") {
        None
    } else {
        Some(value.to_string())
    }
}

fn privacy_notice_hash(privacy: &SyncServerPrivacyConfig) -> String {
    let mut hasher = Sha256::new();
    for value in [
        privacy.mode.code(),
        privacy.controller_name.as_deref().unwrap_or(""),
        privacy.controller_contact.as_deref().unwrap_or(""),
        privacy.controller_fedi.as_deref().unwrap_or(""),
        privacy.dpo_contact.as_deref().unwrap_or(""),
        privacy
            .rights_contact
            .as_deref()
            .or(privacy.controller_contact.as_deref())
            .unwrap_or(""),
        privacy.notice_version.as_deref().unwrap_or(""),
        privacy.data_categories.as_deref().unwrap_or(""),
        privacy.purposes.as_deref().unwrap_or(""),
        privacy.lawful_basis.as_deref().unwrap_or(""),
        privacy.recipients.as_deref().unwrap_or(""),
        privacy.international_transfers.as_deref().unwrap_or(""),
        privacy.retention.as_deref().unwrap_or(""),
        privacy.complaint_contact.as_deref().unwrap_or(""),
    ] {
        hasher.update(value.as_bytes());
        hasher.update([0]);
    }
    let digest = hasher.finalize();
    let mut output = String::from("sha256:");
    for byte in digest {
        output.push_str(&format!("{byte:02x}"));
    }
    output
}

fn query_string_value(query: Option<&str>, name: &str) -> Result<Option<String>, String> {
    let Some(query) = query else {
        return Ok(None);
    };
    form_values_from_bytes(query.as_bytes()).map(|values| values.get(name).cloned())
}

fn form_body_values(request: &Request) -> Result<BTreeMap<String, String>, String> {
    if request.header("content-type").is_some_and(|content_type| {
        !content_type
            .to_ascii_lowercase()
            .starts_with("application/x-www-form-urlencoded")
    }) {
        return Err("registration form used an unsupported content type".to_string());
    }
    form_values_from_bytes(&request.body)
}

fn form_values_from_bytes(bytes: &[u8]) -> Result<BTreeMap<String, String>, String> {
    let text = std::str::from_utf8(bytes)
        .map_err(|_| "registration form is not valid UTF-8".to_string())?;
    let mut values = BTreeMap::new();
    for pair in text.split('&') {
        if pair.is_empty() {
            continue;
        }
        let (key, value) = pair.split_once('=').unwrap_or((pair, ""));
        values.insert(
            percent_decode_form_value(key)?,
            percent_decode_form_value(value)?,
        );
    }
    Ok(values)
}

fn percent_decode_form_value(value: &str) -> Result<String, String> {
    let bytes = value.as_bytes();
    let mut output = Vec::new();
    let mut index = 0;
    while index < bytes.len() {
        match bytes[index] {
            b'+' => {
                output.push(b' ');
                index += 1;
            }
            b'%' if index + 2 < bytes.len() => {
                let high = hex_value(bytes[index + 1]).ok_or_else(|| {
                    "registration form contains invalid percent encoding".to_string()
                })?;
                let low = hex_value(bytes[index + 2]).ok_or_else(|| {
                    "registration form contains invalid percent encoding".to_string()
                })?;
                output.push((high << 4) | low);
                index += 3;
            }
            b'%' => {
                return Err("registration form contains truncated percent encoding".to_string());
            }
            byte => {
                output.push(byte);
                index += 1;
            }
        }
    }
    String::from_utf8(output).map_err(|_| "registration form is not valid UTF-8".to_string())
}

fn hex_value(byte: u8) -> Option<u8> {
    match byte {
        b'0'..=b'9' => Some(byte - b'0'),
        b'a'..=b'f' => Some(byte - b'a' + 10),
        b'A'..=b'F' => Some(byte - b'A' + 10),
        _ => None,
    }
}

struct FediRegistrationTarget {
    instance_url: String,
    expected_owner_identity: Option<String>,
}

fn fedi_registration_target_from_value(
    value: &str,
) -> Result<FediRegistrationTarget, FediOAuthError> {
    let value = value.trim();
    if let Some((account, host)) = fedi_account_from_registration_value(value) {
        let instance_url = normalize_fedi_instance_url(host)?;
        let expected_owner_identity = owner_identity_from_mastodon_account(&instance_url, account)?;
        Ok(FediRegistrationTarget {
            instance_url,
            expected_owner_identity: Some(expected_owner_identity),
        })
    } else {
        Ok(FediRegistrationTarget {
            instance_url: normalize_fedi_instance_url(value)?,
            expected_owner_identity: None,
        })
    }
}

fn fedi_account_from_registration_value(value: &str) -> Option<(&str, &str)> {
    let mut account = value.trim();
    if account.contains("://") {
        return None;
    }
    if let Some(stripped) = account.strip_prefix("acct:") {
        account = stripped.trim();
    }
    while let Some(stripped) = account.strip_prefix('@') {
        account = stripped;
    }
    let (name, host) = account.rsplit_once('@')?;
    let name = name.trim();
    let host = host.trim();
    if name.is_empty() || host.is_empty() {
        None
    } else {
        Some((name, host))
    }
}

fn oauth_callback_url(request: &Request) -> Result<String, String> {
    oauth_callback_url_for_path(request, "/auth/fedi/callback")
}

fn oauth_device_authorization_callback_url(request: &Request) -> Result<String, String> {
    oauth_callback_url_for_path(request, "/auth/fedi/authorize/callback")
}

fn oauth_delete_callback_url(request: &Request) -> Result<String, String> {
    oauth_callback_url_for_path(request, "/auth/fedi/delete/callback")
}

fn oauth_callback_url_for_path(request: &Request, path: &str) -> Result<String, String> {
    let host = request
        .header("host")
        .map(str::trim)
        .filter(|host| safe_http_host(host))
        .ok_or_else(|| {
            "sync server cannot determine its public host for OAuth callback".to_string()
        })?;
    let scheme = request
        .header("x-cosimo-request-scheme")
        .or_else(|| request.header("x-forwarded-proto"))
        .and_then(|value| value.split(',').next())
        .map(str::trim)
        .filter(|scheme| *scheme == "https" || *scheme == "http")
        .unwrap_or("https");
    Ok(format!(
        "{scheme}://{host}{}",
        browser_route_path(request, path)
    ))
}

fn safe_http_host(host: &str) -> bool {
    !host.is_empty()
        && !host.contains("://")
        && !host
            .chars()
            .any(|character| character.is_control() || character.is_whitespace())
}

fn authorization_url_with_state(authorization_url: &str, login_token: &str) -> String {
    let separator = if authorization_url.contains('?') {
        '&'
    } else {
        '?'
    };
    format!(
        "{authorization_url}{separator}state={}",
        form_url_encode(login_token)
    )
}

fn split_target(target: &str) -> (&str, Option<&str>) {
    target
        .split_once('?')
        .map_or((target, None), |(path, query)| (path, Some(query)))
}

fn path_segments(path: &str) -> Vec<&str> {
    path.split('/')
        .filter(|segment| !segment.is_empty())
        .collect()
}

fn sync_registration_url(login_token: &str) -> String {
    format!(
        "/auth/fedi/register?login_token={}",
        form_url_encode(login_token)
    )
}

fn sync_device_authorization_url(login_token: &str) -> String {
    format!(
        "/auth/fedi/authorize?login_token={}",
        form_url_encode(login_token)
    )
}

fn form_url_encode(value: &str) -> String {
    let mut output = String::new();
    for byte in value.bytes() {
        match byte {
            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
                output.push(byte as char)
            }
            byte => output.push_str(&format!("%{byte:02X}")),
        }
    }
    output
}

fn oauth_rate_limit_client_key(request: &Request) -> String {
    request
        .header("x-cosimo-client-address")
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .unwrap_or("unknown")
        .to_string()
}

fn start_fedi_browser_auth_session(state: &ServerState) -> Response {
    if let Some(problem) = state.privacy_config().registration_problem() {
        return Response::text(Status::Forbidden, problem);
    }
    let pending = match state.create_oauth_browser_pending_login() {
        Ok(pending) => pending,
        Err(error) => return server_auth_error_response(error),
    };
    let registration_url = sync_registration_url(pending.login_token());
    let start = match SyncOAuthStart::from_parts_with_registration_url(
        pending.login_token(),
        &registration_url,
        Some(&registration_url),
    ) {
        Ok(start) => start,
        Err(error) => return Response::text(Status::InternalServerError, &error.to_string()),
    };
    Response::json(Status::Created, oauth_start_json(&start))
}

fn start_fedi_browser_device_auth_session(state: &ServerState) -> Response {
    let pending = match state.create_oauth_browser_pending_login() {
        Ok(pending) => pending,
        Err(error) => return server_auth_error_response(error),
    };
    let authorization_url = sync_device_authorization_url(pending.login_token());
    let start = match SyncOAuthStart::from_parts_with_registration_url(
        pending.login_token(),
        &authorization_url,
        Some(&authorization_url),
    ) {
        Ok(start) => start,
        Err(error) => return Response::text(Status::InternalServerError, &error.to_string()),
    };
    Response::json(Status::Created, oauth_start_json(&start))
}

fn fedi_oauth_callback_response(
    state: &ServerState,
    _request: &Request,
    query: Option<&str>,
) -> Response {
    let login_token = match query_string_value(query, "state") {
        Ok(Some(value)) => value,
        Ok(None) => {
            return Response::html(
                Status::BadRequest,
                registration_error_html("sync login state is missing"),
            );
        }
        Err(message) => {
            return Response::html(Status::BadRequest, registration_error_html(&message));
        }
    };
    let authorization_code = match query_string_value(query, "code") {
        Ok(Some(value)) => value,
        Ok(None) => {
            return Response::html(
                Status::BadRequest,
                registration_error_html("Fediverse authorisation code is missing"),
            );
        }
        Err(message) => {
            return Response::html(Status::BadRequest, registration_error_html(&message));
        }
    };
    let pending = match state.take_oauth_pending_login(&login_token) {
        Ok(Some(pending)) => pending,
        Ok(None) => {
            return Response::html(
                Status::BadRequest,
                registration_error_html("sync login token was not found or has already been used"),
            );
        }
        Err(error) => return server_auth_error_response(error),
    };
    if !pending.has_privacy_acceptance() {
        return Response::html(
            Status::Forbidden,
            registration_error_html(
                "sync privacy notice must be accepted before login can complete",
            ),
        );
    }
    let owner_identity = match state
        .oauth_provider
        .exchange_code_for_owner_identity(&pending, &authorization_code)
    {
        Ok(owner_identity) => owner_identity,
        Err(error) => return oauth_error_response(error),
    };
    if let Some(expected_owner_identity) = pending.expected_owner_identity()
        && owner_identity != expected_owner_identity
    {
        return Response::html(
            Status::Forbidden,
            registration_error_html(
                "authenticated Fediverse account does not match the account entered for sync login",
            ),
        );
    }
    if let Some(problem) = state
        .privacy_config()
        .owner_registration_problem(&owner_identity)
    {
        return Response::html(Status::Forbidden, registration_error_html(problem));
    }
    if let Err(error) =
        state.record_privacy_acceptance_for_verified_owner(&owner_identity, &pending)
    {
        return server_auth_error_response(error);
    }
    if let Err(error) = state.store_completed_oauth_login(pending.login_token(), &owner_identity) {
        return server_auth_error_response(error);
    }
    Response::html(
        Status::Ok,
        "<!doctype html>\n\
         <html lang=\"en\">\n\
         <head><meta charset=\"utf-8\"><title>Cosimo Sync Login Complete</title></head>\n\
         <body><main><h1>Cosimo Sync Login Complete</h1>\
         <p>You can now return to Cosimo to finish login.</p></main></body>\n\
         </html>\n"
            .to_string(),
    )
}

fn fedi_oauth_device_authorization_callback_response(
    state: &ServerState,
    query: Option<&str>,
) -> Response {
    let login_token = match query_string_value(query, "state") {
        Ok(Some(value)) => value,
        Ok(None) => {
            return Response::html(
                Status::BadRequest,
                registration_error_html("sync login state is missing"),
            );
        }
        Err(message) => {
            return Response::html(Status::BadRequest, registration_error_html(&message));
        }
    };
    let authorization_code = match query_string_value(query, "code") {
        Ok(Some(value)) => value,
        Ok(None) => {
            return Response::html(
                Status::BadRequest,
                registration_error_html("Fediverse authorisation code is missing"),
            );
        }
        Err(message) => {
            return Response::html(Status::BadRequest, registration_error_html(&message));
        }
    };
    let pending = match state.take_oauth_pending_login(&login_token) {
        Ok(Some(pending)) => pending,
        Ok(None) => {
            return Response::html(
                Status::BadRequest,
                registration_error_html("sync login token was not found or has already been used"),
            );
        }
        Err(error) => return server_auth_error_response(error),
    };
    let owner_identity = match state
        .oauth_provider
        .exchange_code_for_owner_identity(&pending, &authorization_code)
    {
        Ok(owner_identity) => owner_identity,
        Err(error) => return oauth_error_response(error),
    };
    if let Some(expected_owner_identity) = pending.expected_owner_identity()
        && owner_identity != expected_owner_identity
    {
        return Response::html(
            Status::Forbidden,
            registration_error_html(
                "authenticated Fediverse account does not match the account entered for device authorization",
            ),
        );
    }
    match state.owner_account_exists(&owner_identity) {
        Ok(true) => {}
        Ok(false) => {
            return Response::html(
                Status::Forbidden,
                registration_error_html(
                    "no Cosimo sync account exists for the authenticated Fediverse account; register a sync account before authorizing another device",
                ),
            );
        }
        Err(error) => return server_auth_error_response(error),
    }
    if let Err(error) = state.store_completed_oauth_login(pending.login_token(), &owner_identity) {
        return server_auth_error_response(error);
    }
    Response::html(
        Status::Ok,
        "<!doctype html>\n\
         <html lang=\"en\">\n\
         <head><meta charset=\"utf-8\"><title>Cosimo Sync Device Authorized</title></head>\n\
         <body><main><h1>Cosimo Sync Device Authorized</h1>\
         <p>You can now return to Cosimo to finish login on this device.</p></main></body>\n\
         </html>\n"
            .to_string(),
    )
}

fn poll_fedi_browser_auth_session(state: &ServerState, request: &Request) -> Response {
    let login_token = match required_header(request, "x-cosimo-login-token") {
        Ok(login_token) => login_token,
        Err(response) => return response,
    };
    let owner_identity = match state.take_completed_oauth_login(login_token) {
        Ok(Some(owner_identity)) => owner_identity,
        Ok(None) => {
            return Response::json(Status::Accepted, "{\"status\":\"pending\"}".to_string());
        }
        Err(error) => return server_auth_error_response(error),
    };
    match state.create_auth_session_for_verified_owner(&owner_identity) {
        Ok(session) => Response::json(Status::Created, auth_session_json(&session)),
        Err(error) => server_auth_error_response(error),
    }
}

fn revoke_auth_session(state: &ServerState, request: &Request) -> Response {
    let bearer_token = match authorization_bearer_token(request) {
        Ok(Some(token)) => token,
        Ok(None) => {
            return Response::text(
                Status::Unauthorized,
                "sync authentication token is required",
            );
        }
        Err(response) => return response,
    };
    match state.revoke_bearer_token(bearer_token) {
        Ok(true) => Response::json(Status::Ok, "{\"status\":\"logged-out\"}".to_string()),
        Ok(false) => Response::text(
            Status::Unauthorized,
            "sync authentication token is not valid",
        ),
        Err(error) => server_auth_error_response(error),
    }
}

fn revoke_all_owner_auth_sessions(state: &ServerState, request: &Request) -> Response {
    let owner_identity = match authenticated_owner_identity(state, request) {
        Ok(owner_identity) => owner_identity,
        Err(response) => return response,
    };
    match state.revoke_owner_bearer_tokens(&owner_identity) {
        Ok(revoked_count) => Response::json(
            Status::Ok,
            format!("{{\"status\":\"all-tokens-invalidated\",\"revoked\":{revoked_count}}}"),
        ),
        Err(error) => server_auth_error_response(error),
    }
}

fn list_public_decks(state: &ServerState) -> Response {
    match state.list_public_decks() {
        Ok(decks) => Response::json(Status::Ok, shared_status_list_json(&decks)),
        Err(error) => Response::text(Status::InternalServerError, &error),
    }
}

fn authenticated_owner_identity(
    state: &ServerState,
    request: &Request,
) -> Result<String, Response> {
    let Some(bearer_token) = authorization_bearer_token(request)? else {
        return Err(Response::text(
            Status::Unauthorized,
            "sync authentication token is required",
        ));
    };
    match state.authenticate_bearer_token(bearer_token) {
        Ok(Some(owner_identity)) => Ok(owner_identity),
        Ok(None) => Err(Response::text(
            Status::Unauthorized,
            "sync authentication token is not valid",
        )),
        Err(error) => Err(server_auth_error_response(error)),
    }
}

fn optional_authenticated_owner_identity(state: &ServerState, request: &Request) -> Option<String> {
    authenticated_owner_identity(state, request).ok()
}

fn authorization_bearer_token(request: &Request) -> Result<Option<&str>, Response> {
    let Some(authorization) = request.header("authorization") else {
        return Ok(None);
    };
    let Some(token) = authorization.strip_prefix("Bearer ") else {
        return Err(Response::text(
            Status::Unauthorized,
            "sync authorization must use a bearer token",
        ));
    };
    let token = token.trim();
    if token.is_empty() || token.bytes().any(|byte| byte.is_ascii_whitespace()) {
        return Err(Response::text(
            Status::Unauthorized,
            "sync bearer token is malformed",
        ));
    }
    Ok(Some(token))
}

fn list_owner_decks(state: &ServerState, request: &Request) -> Response {
    let owner = match authenticated_owner_identity(state, request) {
        Ok(owner) => owner,
        Err(response) => return response,
    };
    match state.list_owner_decks(&owner) {
        Ok(decks) => Response::json(Status::Ok, owner_status_list_json(&decks)),
        Err(error) => Response::text(Status::InternalServerError, &error),
    }
}

fn quota_status(state: &ServerState, request: &Request) -> Response {
    let owner = match optional_quota_owner_identity(state, request) {
        Ok(owner) => owner,
        Err(response) => return response,
    };
    match state.quota_status(owner.as_deref()) {
        Ok(status) => Response::json(Status::Ok, quota_status_json(&status)),
        Err(error) => Response::text(Status::InternalServerError, &error),
    }
}

fn optional_quota_owner_identity(
    state: &ServerState,
    request: &Request,
) -> Result<Option<String>, Response> {
    let Some(bearer_token) = authorization_bearer_token(request)? else {
        return Ok(None);
    };
    match state.authenticate_bearer_token(bearer_token) {
        Ok(Some(owner)) => Ok(Some(owner)),
        Ok(None) => Err(Response::text(
            Status::Unauthorized,
            "sync authentication token is not valid",
        )),
        Err(error) => Err(server_auth_error_response(error)),
    }
}

fn inspect_shared_deck(state: &ServerState, deck_uuid: &str) -> Response {
    let deck_uuid = match deck_uuid.parse::<SyncUuid>() {
        Ok(uuid) => uuid,
        Err(error) => {
            return Response::text(Status::BadRequest, &format!("invalid deck UUID: {error}"));
        }
    };
    match state.inspect_shared_deck(&deck_uuid) {
        Ok(status) => Response::json(Status::Ok, shared_status_json(&status)),
        Err(error) => server_download_error_response(error),
    }
}

fn download_deck(
    state: &ServerState,
    request: &Request,
    deck_uuid: &str,
    query: Option<&str>,
) -> Response {
    let deck_uuid = match deck_uuid.parse::<SyncUuid>() {
        Ok(uuid) => uuid,
        Err(error) => {
            return Response::text(Status::BadRequest, &format!("invalid deck UUID: {error}"));
        }
    };
    let kind = match query_parameter(query, "kind").unwrap_or("shared") {
        "shared" => SyncDownloadKind::SharedContent,
        "owner-study-history" => SyncDownloadKind::OwnerStudyHistory,
        _ => return Response::text(Status::BadRequest, "unknown download kind"),
    };
    let owner = match kind {
        SyncDownloadKind::SharedContent => optional_authenticated_owner_identity(state, request),
        SyncDownloadKind::OwnerStudyHistory => match authenticated_owner_identity(state, request) {
            Ok(owner) => Some(owner),
            Err(response) => return response,
        },
    };
    match state.download(&deck_uuid, kind, owner.as_deref()) {
        Ok(download) => Response::bundle(download),
        Err(error) => server_download_error_response(error),
    }
}

fn delete_deck(state: &ServerState, request: &Request, deck_uuid: &str) -> Response {
    let deck_uuid = match deck_uuid.parse::<SyncUuid>() {
        Ok(uuid) => uuid,
        Err(error) => {
            return Response::text(Status::BadRequest, &format!("invalid deck UUID: {error}"));
        }
    };
    let owner = match authenticated_owner_identity(state, request) {
        Ok(owner) => owner,
        Err(response) => return response,
    };
    match state.delete_deck(&deck_uuid, &owner) {
        Ok(()) => Response::json(Status::Ok, "{\"status\":\"deleted\"}".to_string()),
        Err(error) => server_delete_error_response(error),
    }
}

fn upload_deck(state: &ServerState, request: Request, deck_uuid: &str) -> Response {
    let deck_uuid = match deck_uuid.parse::<SyncUuid>() {
        Ok(uuid) => uuid,
        Err(error) => {
            return Response::text(Status::BadRequest, &format!("invalid deck UUID: {error}"));
        }
    };
    let owner = match authenticated_owner_identity(state, &request) {
        Ok(owner) => owner,
        Err(response) => return response,
    };
    let visibility = match required_header(&request, "x-cosimo-visibility").and_then(|value| {
        DeckVisibility::from_code(value)
            .ok_or_else(|| Response::text(Status::BadRequest, "unknown deck visibility"))
    }) {
        Ok(visibility) => visibility,
        Err(response) => return response,
    };
    let expected_revision = request.header("x-cosimo-expected-revision");
    let content_hash = request.header("x-cosimo-content-hash");
    let content_length = match optional_usize_header(&request, "x-cosimo-content-length") {
        Ok(length) => length,
        Err(response) => return response,
    };
    if content_hash.is_some() != content_length.is_some() {
        return Response::text(
            Status::BadRequest,
            "content hash and length must be supplied together",
        );
    }
    let history_hash = request.header("x-cosimo-study-history-hash");
    let history_length = match optional_usize_header(&request, "x-cosimo-study-history-length") {
        Ok(length) => length,
        Err(response) => return response,
    };
    if history_hash.is_some() != history_length.is_some() {
        return Response::text(
            Status::BadRequest,
            "study-history hash and length must be supplied together",
        );
    }
    if content_length.is_none() && history_length.is_none() {
        return Response::text(
            Status::BadRequest,
            "sync upload must include a content or study-history bundle",
        );
    }
    let Some(total_length) = content_length
        .unwrap_or(0)
        .checked_add(history_length.unwrap_or(0))
    else {
        return Response::text(
            Status::BadRequest,
            "declared sync payload lengths are too large",
        );
    };
    if request.body.len() != total_length {
        return Response::text(
            Status::BadRequest,
            "request body length does not match declared sync payload lengths",
        );
    }
    if let Some(response) =
        declared_upload_lengths_limit_error(content_length, history_length, state.limits)
    {
        return response;
    }

    let content_length = content_length.unwrap_or(0);
    let content_bytes = if content_length == 0 {
        None
    } else {
        Some(request.body[..content_length].to_vec())
    };
    let history_bytes =
        history_length.map(|length| request.body[content_length..][..length].to_vec());
    let content_bundle = match (content_bytes, content_hash) {
        (Some(bytes), Some(hash)) => match SyncBundlePayload::from_parts(bytes, hash, false) {
            Ok(bundle) => Some(bundle),
            Err(error) => return Response::text(Status::BadRequest, &error.to_string()),
        },
        _ => None,
    };
    let history_bundle = match (history_bytes, history_hash) {
        (Some(bytes), Some(hash)) => match SyncBundlePayload::from_parts(bytes, hash, true) {
            Ok(bundle) => Some(bundle),
            Err(error) => return Response::text(Status::BadRequest, &error.to_string()),
        },
        _ => None,
    };
    let request = match SyncUploadRequest::from_parts(
        deck_uuid,
        &owner,
        visibility,
        expected_revision,
        LocalDeviceIdentity::new(request.header("x-cosimo-device-name").map(str::to_string)),
        content_bundle,
        history_bundle,
    ) {
        Ok(request) => request,
        Err(error) => return Response::text(Status::BadRequest, &error.to_string()),
    };

    let upload_log =
        SyncUploadLogContext::from_request(&request, content_length, history_length.unwrap_or(0));
    let upload_started = std::time::Instant::now();
    log_sync_upload_started(&upload_log);
    let upload_result = state.upload(request);
    match &upload_result {
        Ok(outcome) => log_sync_upload_finished(
            &upload_log,
            upload_started,
            outcome.revision(),
            outcome.bundle_hash(),
        ),
        Err(error) => log_sync_upload_failed(&upload_log, upload_started, error),
    }

    match upload_result {
        Ok(outcome) => Response::json(
            Status::Created,
            format!(
                "{{\"revision\":\"{}\",\"bundle_hash\":\"{}\"}}",
                json_escape(outcome.revision()),
                json_escape(outcome.bundle_hash())
            ),
        ),
        Err(error) => server_upload_error_response(error),
    }
}

struct SyncUploadLogContext {
    deck_uuid: String,
    owner_identity: String,
    visibility: String,
    expected_revision: Option<String>,
    content_length: usize,
    study_history_length: usize,
}

impl SyncUploadLogContext {
    fn from_request(
        request: &SyncUploadRequest,
        content_length: usize,
        study_history_length: usize,
    ) -> Self {
        Self {
            deck_uuid: request.deck_uuid().as_str().to_string(),
            owner_identity: request.owner_identity().to_string(),
            visibility: request.visibility().code().to_string(),
            expected_revision: request.expected_revision().map(str::to_string),
            content_length,
            study_history_length,
        }
    }
}

fn log_sync_upload_started(context: &SyncUploadLogContext) {
    eprintln!(
        "cosimo-sync-server phase=upload-start deck_uuid={} owner_identity={} visibility={} expected_revision={} content_length={} study_history_length={}",
        sanitize_sync_log_value(&context.deck_uuid),
        sanitize_sync_log_value(&context.owner_identity),
        sanitize_sync_log_value(&context.visibility),
        optional_sync_log_value(context.expected_revision.as_deref()),
        context.content_length,
        context.study_history_length
    );
}

fn log_sync_upload_finished(
    context: &SyncUploadLogContext,
    started: std::time::Instant,
    revision: &str,
    bundle_hash: &str,
) {
    eprintln!(
        "cosimo-sync-server phase=upload-finish deck_uuid={} owner_identity={} visibility={} expected_revision={} content_length={} study_history_length={} status=created revision={} bundle_hash={} elapsed_ms={}",
        sanitize_sync_log_value(&context.deck_uuid),
        sanitize_sync_log_value(&context.owner_identity),
        sanitize_sync_log_value(&context.visibility),
        optional_sync_log_value(context.expected_revision.as_deref()),
        context.content_length,
        context.study_history_length,
        sanitize_sync_log_value(revision),
        sanitize_sync_log_value(bundle_hash),
        started.elapsed().as_millis()
    );
}

fn log_sync_upload_failed(
    context: &SyncUploadLogContext,
    started: std::time::Instant,
    error: &ServerUploadError,
) {
    eprintln!(
        "cosimo-sync-server phase=upload-finish deck_uuid={} owner_identity={} visibility={} expected_revision={} content_length={} study_history_length={} status=error error={} elapsed_ms={}",
        sanitize_sync_log_value(&context.deck_uuid),
        sanitize_sync_log_value(&context.owner_identity),
        sanitize_sync_log_value(&context.visibility),
        optional_sync_log_value(context.expected_revision.as_deref()),
        context.content_length,
        context.study_history_length,
        sanitize_sync_log_value(&sync_upload_error_log_message(error)),
        started.elapsed().as_millis()
    );
}

fn sync_upload_error_log_message(error: &ServerUploadError) -> String {
    match error {
        ServerUploadError::Upload(SyncUploadError::Conflict { expected, actual }) => {
            format!(
                "revision_conflict expected={} actual={}",
                optional_sync_log_value(expected.as_deref()),
                optional_sync_log_value(actual.as_deref())
            )
        }
        ServerUploadError::Upload(SyncUploadError::Forbidden) => "forbidden".to_string(),
        ServerUploadError::Upload(SyncUploadError::ReservedDeckIdentity) => {
            "reserved_deck_identity".to_string()
        }
        ServerUploadError::Upload(SyncUploadError::RetiredDeckIdentity) => {
            "retired_deck_identity".to_string()
        }
        ServerUploadError::Upload(SyncUploadError::QuotaExceeded(message)) => {
            format!("quota_exceeded {}", sanitize_sync_log_value(message))
        }
        ServerUploadError::Upload(SyncUploadError::InvalidBundle(message)) => {
            format!("invalid_bundle {}", sanitize_sync_log_value(message))
        }
        ServerUploadError::Busy => "storage_busy".to_string(),
        ServerUploadError::Storage(error) => {
            format!("storage {}", sanitize_sync_log_value(error))
        }
    }
}

fn optional_sync_log_value(value: Option<&str>) -> String {
    value
        .map(sanitize_sync_log_value)
        .unwrap_or_else(|| "none".to_string())
}

fn sanitize_sync_log_value(value: &str) -> String {
    value
        .chars()
        .map(|character| {
            if character.is_control() || character.is_whitespace() {
                ' '
            } else {
                character
            }
        })
        .collect()
}

fn declared_upload_lengths_limit_error(
    content_length: Option<usize>,
    history_length: Option<usize>,
    limits: SyncServerLimits,
) -> Option<Response> {
    let max_bundle_bytes = usize::try_from(limits.max_bundle_bytes?).unwrap_or(usize::MAX);
    for (label, length) in [
        ("content bundle", content_length),
        ("study-history bundle", history_length),
    ] {
        if let Some(length) = length
            && length > max_bundle_bytes
        {
            return Some(Response::text(
                Status::PayloadTooLarge,
                &format!("{label} is {length} bytes, limit is {max_bundle_bytes} bytes"),
            ));
        }
    }
    None
}

fn required_header<'a>(request: &'a Request, name: &str) -> Result<&'a str, Response> {
    request
        .header(name)
        .filter(|value| !value.trim().is_empty())
        .ok_or_else(|| Response::text(Status::BadRequest, &format!("missing {name} header")))
}

fn optional_usize_header(request: &Request, name: &str) -> Result<Option<usize>, Response> {
    request
        .header(name)
        .map(|value| {
            parse_usize_header(name, value)
                .map_err(|error| Response::text(Status::BadRequest, &error.to_string()))
        })
        .transpose()
}

fn parse_usize_header(name: &str, value: &str) -> Result<usize, HttpHeaderError> {
    value
        .parse::<usize>()
        .map_err(|_| HttpHeaderError::InvalidNumber {
            header: name.to_string(),
        })
}

#[derive(Debug)]
enum HttpHeaderError {
    InvalidNumber { header: String },
}

impl fmt::Display for HttpHeaderError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::InvalidNumber { header } => write!(f, "{header} must be an unsigned integer"),
        }
    }
}

fn query_parameter<'a>(query: Option<&'a str>, name: &str) -> Option<&'a str> {
    query?.split('&').find_map(|pair| {
        let (key, value) = pair.split_once('=')?;
        (key == name).then_some(value)
    })
}

fn server_download_error_response(error: ServerDownloadError) -> Response {
    match error {
        ServerDownloadError::Download(SyncDownloadError::NotFound) => {
            Response::text(Status::NotFound, "remote deck was not found")
        }
        ServerDownloadError::Download(SyncDownloadError::Forbidden) => {
            Response::text(Status::Forbidden, "sync download is not allowed")
        }
        ServerDownloadError::Busy => sync_storage_busy_response(),
        ServerDownloadError::Storage(error) => Response::text(Status::InternalServerError, &error),
    }
}

fn server_delete_error_response(error: ServerDeleteError) -> Response {
    match error {
        ServerDeleteError::Delete(SyncDeleteError::NotFound) => {
            Response::text(Status::NotFound, "remote deck was not found")
        }
        ServerDeleteError::Delete(SyncDeleteError::Forbidden) => {
            Response::text(Status::Forbidden, "sync deletion is not allowed")
        }
        ServerDeleteError::Busy => sync_storage_busy_response(),
        ServerDeleteError::Storage(error) => Response::text(Status::InternalServerError, &error),
    }
}

fn server_upload_error_response(error: ServerUploadError) -> Response {
    match error {
        ServerUploadError::Upload(SyncUploadError::Conflict { expected, actual }) => {
            Response::json(
                Status::Conflict,
                format!(
                    "{{\"error\":\"revision_conflict\",\"message\":\"remote revision conflict: expected {}, found {}\",\"expected_revision\":{},\"actual_revision\":{}}}",
                    json_escape(expected.as_deref().unwrap_or("none")),
                    json_escape(actual.as_deref().unwrap_or("none")),
                    optional_string_json(expected.as_deref()),
                    optional_string_json(actual.as_deref())
                ),
            )
        }
        ServerUploadError::Upload(SyncUploadError::Forbidden) => Response::text(
            Status::Forbidden,
            "sync upload is not allowed for this owner",
        ),
        ServerUploadError::Upload(SyncUploadError::ReservedDeckIdentity) => Response::text(
            Status::Forbidden,
            "sync upload is not allowed because this deck UUID is reserved for another owner",
        ),
        ServerUploadError::Upload(SyncUploadError::RetiredDeckIdentity) => Response::text(
            Status::Forbidden,
            "sync upload is not allowed because this deck UUID has been retired",
        ),
        ServerUploadError::Upload(SyncUploadError::QuotaExceeded(message)) => Response::text(
            Status::Forbidden,
            &format!("sync upload quota exceeded: {message}"),
        ),
        ServerUploadError::Upload(SyncUploadError::InvalidBundle(message)) => {
            Response::text(Status::BadRequest, &message)
        }
        ServerUploadError::Busy => sync_storage_busy_response(),
        ServerUploadError::Storage(error) => Response::text(Status::InternalServerError, &error),
    }
}

fn server_auth_error_response(error: ServerAuthError) -> Response {
    match error {
        ServerAuthError::Auth(SyncAuthError::RateLimited(message)) => {
            Response::text(Status::TooManyRequests, &message)
        }
        ServerAuthError::Auth(error) => Response::text(Status::BadRequest, &error.to_string()),
        ServerAuthError::Busy => sync_storage_busy_response(),
        ServerAuthError::Storage(error) => Response::text(Status::InternalServerError, &error),
    }
}

fn sync_storage_busy_response() -> Response {
    let mut response = Response::text(Status::ServiceUnavailable, SYNC_STORAGE_BUSY_MESSAGE);
    response.headers.insert(
        "retry-after".to_string(),
        SYNC_STORAGE_BUSY_RETRY_AFTER_SECONDS.to_string(),
    );
    response
}

fn oauth_error_response(error: FediOAuthError) -> Response {
    match error {
        FediOAuthError::InvalidInstanceUrl(_) | FediOAuthError::Validation(_) => {
            Response::text(Status::BadRequest, &error.to_string())
        }
        FediOAuthError::Network(_)
        | FediOAuthError::Remote { .. }
        | FediOAuthError::Json { .. }
        | FediOAuthError::MissingJsonField(_) => {
            Response::text(Status::BadGateway, &error.to_string())
        }
    }
}

fn oauth_start_json(start: &SyncOAuthStart) -> String {
    let registration_url = start
        .registration_url()
        .map(|url| format!("\"{}\"", json_escape(url)))
        .unwrap_or_else(|| "null".to_string());
    format!(
        "{{\"login_token\":\"{}\",\"authorization_url\":\"{}\",\"registration_url\":{registration_url}}}",
        json_escape(start.login_token()),
        json_escape(start.authorization_url())
    )
}

fn auth_session_json(session: &SyncAuthSession) -> String {
    format!(
        "{{\"owner_identity\":\"{}\",\"access_token\":\"{}\",\"token_type\":\"Bearer\"}}",
        json_escape(session.owner_identity()),
        json_escape(session.bearer_token())
    )
}

fn shared_status_list_json(statuses: &[SyncSharedDeckStatus]) -> String {
    let entries = statuses
        .iter()
        .map(shared_status_json)
        .collect::<Vec<_>>()
        .join(",");
    format!("[{entries}]")
}

fn owner_status_list_json(statuses: &[SyncRemoteDeckStatus]) -> String {
    let entries = statuses
        .iter()
        .map(owner_status_json)
        .collect::<Vec<_>>()
        .join(",");
    format!("[{entries}]")
}

fn shared_status_json(status: &SyncSharedDeckStatus) -> String {
    format!(
        "{{\"deck_uuid\":\"{}\",\"revision\":\"{}\",\"visibility\":\"{}\",\"owner_identity\":\"{}\",\"content_bundle_hash\":\"{}\",{}}}",
        json_escape(status.deck_uuid().as_str()),
        json_escape(status.revision()),
        json_escape(status.visibility().code()),
        json_escape(status.owner_identity()),
        json_escape(status.content_bundle_hash()),
        summary_json_fields(status.summary())
    )
}

fn owner_status_json(status: &SyncRemoteDeckStatus) -> String {
    let study_history = status
        .study_history_bundle_hash()
        .map(|hash| format!("\"{}\"", json_escape(hash)))
        .unwrap_or_else(|| "null".to_string());
    let device_name = status
        .last_upload_device_name()
        .map(|name| format!("\"{}\"", json_escape(name)))
        .unwrap_or_else(|| "null".to_string());
    format!(
        "{{\"deck_uuid\":\"{}\",\"revision\":\"{}\",\"visibility\":\"{}\",\"owner_identity\":\"{}\",\"content_bundle_hash\":\"{}\",\"study_history_bundle_hash\":{},\"last_upload_device_name\":{},{}}}",
        json_escape(status.deck_uuid().as_str()),
        json_escape(status.revision()),
        json_escape(status.visibility().code()),
        json_escape(status.owner_identity()),
        json_escape(status.content_bundle_hash()),
        study_history,
        device_name,
        summary_json_fields(status.summary())
    )
}

fn summary_json_fields(summary: &SyncDeckSummary) -> String {
    format!(
        "\"title\":{},\"deck_name\":{},\"card_count\":{},\"audio_file_count\":{},\"managed_audio_file_count\":{},\"audio_formats\":{}",
        optional_string_json(summary.title()),
        optional_string_json(summary.deck_name()),
        optional_usize_json(summary.card_count()),
        optional_usize_json(summary.audio_file_count()),
        optional_usize_json(summary.managed_audio_file_count()),
        string_array_json(summary.audio_formats())
    )
}

fn quota_status_json(status: &SyncQuotaStatus) -> String {
    format!(
        "{{\"max_decks_per_owner\":{},\"max_storage_bytes_per_owner\":{},\"max_storage_bytes_total\":{},\"owner_deck_count\":{},\"owner_storage_bytes\":{}}}",
        optional_usize_json(status.max_decks_per_owner()),
        optional_u64_json(status.max_storage_bytes_per_owner()),
        optional_u64_json(status.max_storage_bytes_total()),
        optional_usize_json(status.owner_deck_count()),
        optional_u64_json(status.owner_storage_bytes()),
    )
}

fn privacy_config_json(privacy: &SyncServerPrivacyConfig) -> String {
    let allowlist = privacy
        .domestic_registration_allowlist
        .iter()
        .map(|owner| format!("\"{}\"", json_escape(owner)))
        .collect::<Vec<_>>()
        .join(",");
    format!(
        "{{\"service_mode\":\"{}\",\"registration_available\":{},\"registration_problem\":{},\"controller_name\":{},\"controller_contact\":{},\"controller_fedi\":{},\"dpo_contact\":{},\"rights_contact\":{},\"notice_version\":{},\"data_categories\":{},\"purposes\":{},\"lawful_basis\":{},\"recipients\":{},\"international_transfers\":{},\"retention\":{},\"complaint_contact\":{},\"domestic_registration_allowlist\":[{}]}}",
        json_escape(privacy.mode.code()),
        privacy.registration_available(),
        optional_string_json(privacy.registration_problem()),
        optional_string_json(privacy.controller_name.as_deref()),
        optional_string_json(privacy.controller_contact.as_deref()),
        optional_string_json(privacy.controller_fedi.as_deref()),
        optional_string_json(privacy.dpo_contact.as_deref()),
        optional_string_json(
            privacy
                .rights_contact
                .as_deref()
                .or(privacy.controller_contact.as_deref())
        ),
        optional_string_json(privacy.notice_version.as_deref()),
        optional_string_json(privacy.data_categories.as_deref()),
        optional_string_json(privacy.purposes.as_deref()),
        optional_string_json(privacy.lawful_basis.as_deref()),
        optional_string_json(privacy.recipients.as_deref()),
        optional_string_json(privacy.international_transfers.as_deref()),
        optional_string_json(privacy.retention.as_deref()),
        optional_string_json(privacy.complaint_contact.as_deref()),
        allowlist
    )
}

fn optional_usize_json(value: Option<usize>) -> String {
    value
        .map(|value| value.to_string())
        .unwrap_or_else(|| "null".to_string())
}

fn optional_string_json(value: Option<&str>) -> String {
    value
        .map(|value| format!("\"{}\"", json_escape(value)))
        .unwrap_or_else(|| "null".to_string())
}

fn string_array_json(values: &[String]) -> String {
    let values = values
        .iter()
        .map(|value| format!("\"{}\"", json_escape(value)))
        .collect::<Vec<_>>()
        .join(",");
    format!("[{values}]")
}

fn optional_u64_json(value: Option<u64>) -> String {
    value
        .map(|value| value.to_string())
        .unwrap_or_else(|| "null".to_string())
}

fn json_escape(value: &str) -> String {
    let mut escaped = String::new();
    for character in value.chars() {
        match character {
            '"' => escaped.push_str("\\\""),
            '\\' => escaped.push_str("\\\\"),
            '\n' => escaped.push_str("\\n"),
            '\r' => escaped.push_str("\\r"),
            '\t' => escaped.push_str("\\t"),
            character if character.is_control() => {
                escaped.push_str(&format!("\\u{:04x}", character as u32));
            }
            character => escaped.push(character),
        }
    }
    escaped
}

fn html_escape(value: &str) -> String {
    let mut escaped = String::new();
    for character in value.chars() {
        match character {
            '&' => escaped.push_str("&amp;"),
            '<' => escaped.push_str("&lt;"),
            '>' => escaped.push_str("&gt;"),
            '"' => escaped.push_str("&quot;"),
            '\'' => escaped.push_str("&#39;"),
            character => escaped.push(character),
        }
    }
    escaped
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::sync_bundle_hash;
    use proptest::prelude::*;
    use std::fs;
    use std::io::{Cursor, Write};
    use std::path::PathBuf;
    use std::process;
    use std::sync::Arc;
    use std::sync::atomic::{AtomicUsize, Ordering};
    use std::time::{SystemTime, UNIX_EPOCH};
    use zip::write::SimpleFileOptions;
    use zip::{CompressionMethod, ZipWriter};

    const DECK_UUID: &str = "550e8400-e29b-41d4-a716-446655440000";

    fn unique_temp_dir(prefix: &str) -> PathBuf {
        let unique = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let path = std::env::temp_dir().join(format!(
            "cosimo-sync-server-{prefix}-{}-{unique}",
            process::id()
        ));
        fs::create_dir_all(&path).unwrap();
        path
    }

    fn sqlite_state(root: impl Into<PathBuf>) -> ServerState {
        ServerState::sqlite(root.into())
    }

    fn study_history_zip_bytes(review_bytes: &[u8]) -> Vec<u8> {
        let mut zip = ZipWriter::new(Cursor::new(Vec::new()));
        let options = SimpleFileOptions::default()
            .compression_method(CompressionMethod::Deflated)
            .compression_level(Some(1))
            .unix_permissions(0o644);
        zip.start_file("deck.cosimo-deck", options).unwrap();
        zip.write_all(b"prompt\n---\nresponse\n===").unwrap();
        zip.start_file("deck.reviews.sqlite3", options).unwrap();
        zip.write_all(review_bytes).unwrap();
        zip.finish().unwrap().into_inner()
    }

    fn request(method: &str, target: &str) -> Request {
        Request {
            method: method.to_string(),
            target: target.to_string(),
            headers: BTreeMap::new(),
            body: Vec::new(),
        }
    }

    fn upload_request(
        deck_uuid: &str,
        _owner: &str,
        visibility: &str,
        expected_revision: Option<&str>,
        content: &[u8],
        history: Option<&[u8]>,
    ) -> Request {
        let mut request = request("POST", &format!("/api/v1/decks/{deck_uuid}/upload"));
        request
            .headers
            .insert("x-cosimo-visibility".to_string(), visibility.to_string());
        request
            .headers
            .insert("x-cosimo-device-name".to_string(), "laptop".to_string());
        request.headers.insert(
            "x-cosimo-content-hash".to_string(),
            sync_bundle_hash(content),
        );
        request.headers.insert(
            "x-cosimo-content-length".to_string(),
            content.len().to_string(),
        );
        request.body.extend_from_slice(content);
        if let Some(history) = history {
            request.headers.insert(
                "x-cosimo-study-history-hash".to_string(),
                sync_bundle_hash(history),
            );
            request.headers.insert(
                "x-cosimo-study-history-length".to_string(),
                history.len().to_string(),
            );
            request.body.extend_from_slice(history);
        }
        if let Some(expected_revision) = expected_revision {
            request.headers.insert(
                "x-cosimo-expected-revision".to_string(),
                expected_revision.to_string(),
            );
        }
        request
    }

    fn authorize_request(state: &ServerState, request: &mut Request, owner: &str) {
        let session = state.create_auth_session_for_verified_owner(owner).unwrap();
        request
            .headers
            .insert("authorization".to_string(), authorization_header(&session));
    }

    fn authenticated_upload_request(
        state: &ServerState,
        deck_uuid: &str,
        owner: &str,
        visibility: &str,
        expected_revision: Option<&str>,
        content: &[u8],
        history: Option<&[u8]>,
    ) -> Request {
        let mut request = upload_request(
            deck_uuid,
            owner,
            visibility,
            expected_revision,
            content,
            history,
        );
        authorize_request(state, &mut request, owner);
        request
    }

    fn authorization_header(session: &SyncAuthSession) -> String {
        format!("Bearer {}", session.bearer_token())
    }

    #[derive(Debug)]
    struct TestOAuthProvider;

    impl SyncOAuthProvider for TestOAuthProvider {
        fn register_application_with_redirect_uri(
            &self,
            instance_url: &str,
            redirect_uri: &str,
        ) -> Result<crate::sync_oauth::FediOAuthApplication, FediOAuthError> {
            crate::sync_oauth::FediOAuthApplication::from_parts_with_redirect_uri(
                instance_url,
                "test-client-id",
                "test-client-secret",
                redirect_uri,
            )
        }

        fn exchange_code_for_owner_identity(
            &self,
            pending: &SyncOAuthPendingLogin,
            authorization_code: &str,
        ) -> Result<String, FediOAuthError> {
            assert_eq!(pending.instance_url(), "https://example.invalid");
            assert_eq!(pending.client_id(), "test-client-id");
            assert_eq!(pending.client_secret(), "test-client-secret");
            if authorization_code == "good-code" {
                Ok("acct:modulus@example.invalid".to_string())
            } else {
                Err(FediOAuthError::Validation(
                    crate::SyncValidationError::EmptyValue {
                        field: "authorization_code",
                    },
                ))
            }
        }
    }

    #[derive(Debug)]
    struct CountingOAuthProvider {
        registrations: Arc<AtomicUsize>,
    }

    impl SyncOAuthProvider for CountingOAuthProvider {
        fn register_application_with_redirect_uri(
            &self,
            instance_url: &str,
            redirect_uri: &str,
        ) -> Result<crate::sync_oauth::FediOAuthApplication, FediOAuthError> {
            let count = self.registrations.fetch_add(1, Ordering::SeqCst) + 1;
            crate::sync_oauth::FediOAuthApplication::from_parts_with_redirect_uri(
                instance_url,
                &format!("test-client-id-{count}"),
                &format!("test-client-secret-{count}"),
                redirect_uri,
            )
        }

        fn exchange_code_for_owner_identity(
            &self,
            _pending: &SyncOAuthPendingLogin,
            _authorization_code: &str,
        ) -> Result<String, FediOAuthError> {
            Ok("acct:modulus@example.invalid".to_string())
        }
    }

    fn json_string_field(text: &str, field: &str) -> String {
        let marker = format!("\"{field}\":\"");
        let start = text.find(&marker).unwrap() + marker.len();
        let rest = &text[start..];
        let end = rest.find('"').unwrap();
        rest[..end].to_string()
    }

    #[test]
    fn health_endpoint_reports_memory_storage() {
        let state = ServerState::default();
        let response = handle_request(&state, request("GET", "/api/v1/health"));

        assert_eq!(response.status, Status::Ok);
        assert_eq!(
            std::str::from_utf8(&response.body).unwrap(),
            "{\"status\":\"ok\",\"storage\":\"memory\"}"
        );
    }

    #[test]
    fn health_endpoint_reports_sqlite_storage() {
        let state = sqlite_state(unique_temp_dir("health"));
        let response = handle_request(&state, request("GET", "/api/v1/health"));

        assert_eq!(response.status, Status::Ok);
        assert_eq!(
            std::str::from_utf8(&response.body).unwrap(),
            "{\"status\":\"ok\",\"storage\":\"sqlite\"}"
        );
    }

    #[test]
    fn root_endpoint_reports_sync_server_status_for_browsers() {
        let state = ServerState::default();
        let response = handle_request(&state, request("GET", "/"));
        let body = std::str::from_utf8(&response.body).unwrap();

        assert_eq!(response.status, Status::Ok);
        assert_eq!(
            response.headers.get("content-type").map(String::as_str),
            Some("text/html; charset=utf-8")
        );
        assert!(body.contains("<h1>Cosimo Sync Server</h1>"));
        assert!(body.contains("This is a Cosimo sync server."));
        assert!(body.contains("<strong>Storage backend:</strong> memory"));
        assert!(body.contains("/privacy"));
        assert!(body.contains("/api/v1/health"));
        assert!(body.contains("Read this server's privacy notice"));
        assert!(body.contains("Check the API health endpoint"));
        assert!(body.contains("https://modulus.isonomia.net/cosimo.cgi/wiki?name=home"));
        assert!(body.contains("Visit the Cosimo project wiki"));

        let mut cgi_request = request("GET", "/");
        cgi_request
            .headers
            .insert("x-cosimo-script-name".to_string(), "/test.cgi".to_string());
        let cgi_response = handle_request(&state, cgi_request);
        let cgi_body = std::str::from_utf8(&cgi_response.body).unwrap();
        assert!(cgi_body.contains("href=\"/test.cgi/privacy\">Read this server's privacy notice"));
        assert!(
            cgi_body.contains("href=\"/test.cgi/api/v1/health\">Check the API health endpoint")
        );
        assert!(!cgi_body.contains(">/test.cgi/privacy<"));
    }

    #[test]
    fn privacy_endpoints_report_public_registration_gate() {
        let state = ServerState::default();

        let html = handle_request(&state, request("GET", "/privacy"));
        let json = handle_request(&state, request("GET", "/api/v1/privacy"));
        let html_body = std::str::from_utf8(&html.body).unwrap();
        let json_body = std::str::from_utf8(&json.body).unwrap();

        assert_eq!(html.status, Status::Ok);
        assert!(html_body.contains("<h1>Cosimo Sync Privacy Notice</h1>"));
        assert!(html_body.contains("controller name is not configured"));
        assert!(!html_body.contains("domestic or personal"));
        assert_eq!(json.status, Status::Ok);
        assert!(json_body.contains("\"service_mode\":\"public\""));
        assert!(json_body.contains("\"registration_available\":false"));
        assert!(json_body.contains("controller name is not configured"));
    }

    #[test]
    fn public_server_rejects_registration_without_privacy_config() {
        let state = ServerState::default();

        let response = handle_request(&state, request("POST", "/api/v1/auth/fedi/browser-start"));

        assert_eq!(response.status, Status::Forbidden);
        assert!(
            std::str::from_utf8(&response.body)
                .unwrap()
                .contains("controller name is not configured")
        );
    }

    #[test]
    fn public_registration_accepts_rights_contact_without_complaint_contact() {
        let state = ServerState::default().with_privacy_config(SyncServerPrivacyConfig {
            mode: SyncServerServiceMode::Public,
            controller_name: Some("Example Controller".to_string()),
            controller_contact: Some("privacy@example.invalid".to_string()),
            rights_contact: Some("rights@example.invalid".to_string()),
            notice_version: Some("initial".to_string()),
            data_categories: Some("account and sync data".to_string()),
            purposes: Some("deck synchronisation".to_string()),
            lawful_basis: Some("Explicit consent".to_string()),
            recipients: Some("server operator".to_string()),
            international_transfers: Some("none".to_string()),
            retention: Some("until account deletion".to_string()),
            ..SyncServerPrivacyConfig::default()
        });

        assert!(state.privacy_config().registration_problem().is_none());
    }

    #[test]
    fn privacy_html_presents_public_notice_without_unconfigured_or_domestic_filler() {
        let privacy_config = SyncServerPrivacyConfig {
            mode: SyncServerServiceMode::Public,
            controller_name: Some("Example Controller".to_string()),
            controller_contact: Some("privacy@example.invalid".to_string()),
            rights_contact: Some("rights@example.invalid".to_string()),
            notice_version: Some("initial".to_string()),
            data_categories: Some("Fediverse account, bearer sessions, deck bundles".to_string()),
            purposes: Some("Synchronise Cosimo decks and study history".to_string()),
            lawful_basis: Some("Explicit consent".to_string()),
            recipients: Some("Server operator".to_string()),
            international_transfers: Some("No intentional international transfers".to_string()),
            retention: Some(
                "Until account deletion, subject to short operational logs".to_string(),
            ),
            ..SyncServerPrivacyConfig::default()
        };
        let state = ServerState::memory_with_oauth_provider(Arc::new(TestOAuthProvider))
            .with_privacy_config(privacy_config);

        let privacy = handle_request(&state, request("GET", "/privacy"));
        let privacy_body = std::str::from_utf8(&privacy.body).unwrap();
        let start = handle_request(&state, request("POST", "/api/v1/auth/fedi/browser-start"));
        let start_body = std::str::from_utf8(&start.body).unwrap();
        let login_token = json_string_field(start_body, "login_token");
        let registration = handle_request(
            &state,
            request(
                "GET",
                &format!("/auth/fedi/register?login_token={login_token}"),
            ),
        );
        let registration_body = std::str::from_utf8(&registration.body).unwrap();

        assert_eq!(privacy.status, Status::Ok);
        assert_eq!(registration.status, Status::Ok);
        for body in [privacy_body, registration_body] {
            assert!(body.contains("public Cosimo sync registration"));
            assert!(body.contains("Data rights contact"));
            assert!(!body.contains(">initial<"));
            assert!(!body.contains("Privacy notice identifier"));
            assert!(!body.contains("Current configured notice"));
            assert!(!body.contains("not configured"));
            assert!(!body.contains("DPO contact"));
            assert!(!body.contains("Complaints"));
            assert!(!body.contains("domestic or personal"));
            assert!(!body.contains("It should not be offered as a public service"));
        }
    }

    #[test]
    fn privacy_html_reports_domestic_mode_only_for_domestic_servers() {
        let state =
            ServerState::default().with_privacy_config(SyncServerPrivacyConfig::domestic([
                "acct:modulus@example.invalid".to_string(),
            ]));

        let response = handle_request(&state, request("GET", "/privacy"));
        let body = std::str::from_utf8(&response.body).unwrap();

        assert_eq!(response.status, Status::Ok);
        assert!(body.contains("domestic or personal Cosimo sync use"));
        assert!(body.contains("Registration is restricted to configured Fediverse accounts"));
    }

    #[test]
    fn fedi_browser_oauth_callback_flow_issues_bearer_token() {
        let state = ServerState::memory_with_oauth_provider(Arc::new(TestOAuthProvider));

        let start = handle_request(&state, request("POST", "/api/v1/auth/fedi/browser-start"));
        let start_body = std::str::from_utf8(&start.body).unwrap();
        let login_token = json_string_field(start_body, "login_token");
        assert_eq!(start.status, Status::Created);
        assert!(start_body.contains("/auth/fedi/register?login_token="));

        let registration = handle_request(
            &state,
            request(
                "GET",
                &format!("/auth/fedi/register?login_token={login_token}"),
            ),
        );
        let registration_body = std::str::from_utf8(&registration.body).unwrap();
        assert_eq!(registration.status, Status::Ok);
        assert!(registration_body.contains("name=\"fedi_instance\""));
        assert!(registration_body.contains("name=\"privacy_acceptance\""));

        let mut accept = request("POST", "/auth/fedi/register");
        accept
            .headers
            .insert("host".to_string(), "sync.example.invalid".to_string());
        accept
            .headers
            .insert("x-cosimo-script-name".to_string(), "/sync.cgi".to_string());
        accept
            .headers
            .insert("x-cosimo-request-scheme".to_string(), "https".to_string());
        accept.headers.insert(
            "content-type".to_string(),
            "application/x-www-form-urlencoded".to_string(),
        );
        accept.body = format!(
            "login_token={login_token}&fedi_instance=%40modulus%40example.invalid&privacy_acceptance=yes"
        )
        .into_bytes();
        let accept = handle_request(&state, accept);
        assert_eq!(accept.status, Status::SeeOther);
        let location = accept.headers.get("location").unwrap();
        assert!(location.contains("https://example.invalid/oauth/authorize?"));
        assert!(location.contains(
            "redirect_uri=https%3A%2F%2Fsync.example.invalid%2Fsync.cgi%2Fauth%2Ffedi%2Fcallback"
        ));
        assert!(location.contains(&format!("state={login_token}")));

        let callback = handle_request(
            &state,
            request(
                "GET",
                &format!("/auth/fedi/callback?state={login_token}&code=good-code"),
            ),
        );
        assert_eq!(callback.status, Status::Ok);

        let mut poll = request("POST", "/api/v1/auth/fedi/poll");
        poll.headers
            .insert("x-cosimo-login-token".to_string(), login_token.clone());
        let poll = handle_request(&state, poll);
        let poll_body = std::str::from_utf8(&poll.body).unwrap();
        assert_eq!(poll.status, Status::Created);
        assert!(poll_body.contains("\"owner_identity\":\"acct:modulus@example.invalid\""));

        let mut second_poll = request("POST", "/api/v1/auth/fedi/poll");
        second_poll
            .headers
            .insert("x-cosimo-login-token".to_string(), login_token);
        assert_eq!(handle_request(&state, second_poll).status, Status::Accepted);
    }

    #[test]
    fn fedi_browser_device_authorization_requires_existing_account() {
        let state = ServerState::memory_with_oauth_provider(Arc::new(TestOAuthProvider));

        let start = handle_request(
            &state,
            request("POST", "/api/v1/auth/fedi/device-browser-start"),
        );
        let start_body = std::str::from_utf8(&start.body).unwrap();
        let login_token = json_string_field(start_body, "login_token");
        assert_eq!(start.status, Status::Created);
        assert!(start_body.contains("/auth/fedi/authorize?login_token="));

        let mut accept = request("POST", "/auth/fedi/authorize");
        accept
            .headers
            .insert("host".to_string(), "sync.example.invalid".to_string());
        accept
            .headers
            .insert("x-cosimo-request-scheme".to_string(), "https".to_string());
        accept.headers.insert(
            "content-type".to_string(),
            "application/x-www-form-urlencoded".to_string(),
        );
        accept.body =
            format!("login_token={login_token}&fedi_instance=example.invalid").into_bytes();
        let accept = handle_request(&state, accept);
        assert_eq!(accept.status, Status::SeeOther);

        let callback = handle_request(
            &state,
            request(
                "GET",
                &format!("/auth/fedi/authorize/callback?state={login_token}&code=good-code"),
            ),
        );

        assert_eq!(callback.status, Status::Forbidden);
        assert!(
            std::str::from_utf8(&callback.body)
                .unwrap()
                .contains("register a sync account before authorizing another device")
        );

        let mut poll = request("POST", "/api/v1/auth/fedi/poll");
        poll.headers
            .insert("x-cosimo-login-token".to_string(), login_token);
        assert_eq!(handle_request(&state, poll).status, Status::Accepted);
    }

    #[test]
    fn fedi_browser_device_authorization_issues_token_for_existing_account() {
        let state = ServerState::memory_with_oauth_provider(Arc::new(TestOAuthProvider));
        state
            .create_auth_session_for_verified_owner("acct:modulus@example.invalid")
            .unwrap();

        let start = handle_request(
            &state,
            request("POST", "/api/v1/auth/fedi/device-browser-start"),
        );
        let start_body = std::str::from_utf8(&start.body).unwrap();
        let login_token = json_string_field(start_body, "login_token");

        let mut accept = request("POST", "/auth/fedi/authorize");
        accept
            .headers
            .insert("host".to_string(), "sync.example.invalid".to_string());
        accept
            .headers
            .insert("x-cosimo-request-scheme".to_string(), "https".to_string());
        accept.headers.insert(
            "content-type".to_string(),
            "application/x-www-form-urlencoded".to_string(),
        );
        accept.body =
            format!("login_token={login_token}&fedi_instance=%40modulus%40example.invalid")
                .into_bytes();
        let accept = handle_request(&state, accept);
        assert_eq!(accept.status, Status::SeeOther);
        assert!(accept.headers.get("location").unwrap().contains(
            "redirect_uri=https%3A%2F%2Fsync.example.invalid%2Fauth%2Ffedi%2Fauthorize%2Fcallback"
        ));

        let callback = handle_request(
            &state,
            request(
                "GET",
                &format!("/auth/fedi/authorize/callback?state={login_token}&code=good-code"),
            ),
        );
        assert_eq!(callback.status, Status::Ok);

        let mut poll = request("POST", "/api/v1/auth/fedi/poll");
        poll.headers
            .insert("x-cosimo-login-token".to_string(), login_token);
        let poll = handle_request(&state, poll);
        let poll_body = std::str::from_utf8(&poll.body).unwrap();
        assert_eq!(poll.status, Status::Created);
        assert!(poll_body.contains("\"owner_identity\":\"acct:modulus@example.invalid\""));
    }

    #[test]
    fn fedi_browser_oauth_rejects_account_mismatch() {
        let state = ServerState::memory_with_oauth_provider(Arc::new(TestOAuthProvider));

        let start = handle_request(&state, request("POST", "/api/v1/auth/fedi/browser-start"));
        let login_token =
            json_string_field(std::str::from_utf8(&start.body).unwrap(), "login_token");

        let mut accept = request("POST", "/auth/fedi/register");
        accept
            .headers
            .insert("host".to_string(), "sync.example.invalid".to_string());
        accept
            .headers
            .insert("x-cosimo-request-scheme".to_string(), "https".to_string());
        accept.headers.insert(
            "content-type".to_string(),
            "application/x-www-form-urlencoded".to_string(),
        );
        accept.body = format!(
            "login_token={login_token}&fedi_instance=%40other%40example.invalid&privacy_acceptance=yes"
        )
        .into_bytes();
        let accept = handle_request(&state, accept);
        assert_eq!(accept.status, Status::SeeOther);

        let callback = handle_request(
            &state,
            request(
                "GET",
                &format!("/auth/fedi/callback?state={login_token}&code=good-code"),
            ),
        );

        assert_eq!(callback.status, Status::Forbidden);
        assert!(
            std::str::from_utf8(&callback.body)
                .unwrap()
                .contains("does not match the account entered")
        );

        let mut poll = request("POST", "/api/v1/auth/fedi/poll");
        poll.headers
            .insert("x-cosimo-login-token".to_string(), login_token);
        assert_eq!(handle_request(&state, poll).status, Status::Accepted);
    }

    #[test]
    fn fedi_oauth_account_deletion_removes_only_verified_owner_data() {
        let state = ServerState::memory_with_oauth_provider(Arc::new(TestOAuthProvider));
        let owner_session = state
            .create_auth_session_for_verified_owner("acct:modulus@example.invalid")
            .unwrap();
        let other_session = state
            .create_auth_session_for_verified_owner("acct:other@example.invalid")
            .unwrap();
        let mut owner_upload = upload_request(
            DECK_UUID,
            "acct:ignored@example.invalid",
            "public",
            None,
            b"owner content bundle",
            Some(b"owner history bundle"),
        );
        owner_upload.headers.insert(
            "authorization".to_string(),
            authorization_header(&owner_session),
        );
        assert_eq!(handle_request(&state, owner_upload).status, Status::Created);

        let other_uuid = "650e8400-e29b-41d4-a716-446655440000";
        let mut other_upload = upload_request(
            other_uuid,
            "acct:ignored@example.invalid",
            "public",
            None,
            b"other content bundle",
            None,
        );
        other_upload.headers.insert(
            "authorization".to_string(),
            authorization_header(&other_session),
        );
        assert_eq!(handle_request(&state, other_upload).status, Status::Created);

        let mut delete_start = request("POST", "/auth/fedi/delete");
        delete_start
            .headers
            .insert("host".to_string(), "sync.example.invalid".to_string());
        delete_start
            .headers
            .insert("x-cosimo-request-scheme".to_string(), "https".to_string());
        delete_start.headers.insert(
            "content-type".to_string(),
            "application/x-www-form-urlencoded".to_string(),
        );
        delete_start.body =
            b"fedi_account=%40modulus%40example.invalid&deletion_confirmation=yes".to_vec();
        let delete_start = handle_request(&state, delete_start);
        assert_eq!(delete_start.status, Status::SeeOther);
        let location = delete_start.headers.get("location").unwrap();
        assert!(location.contains("https://example.invalid/oauth/authorize?"));
        assert!(location.contains(
            "redirect_uri=https%3A%2F%2Fsync.example.invalid%2Fauth%2Ffedi%2Fdelete%2Fcallback"
        ));
        let deletion_token = location
            .split("state=")
            .nth(1)
            .unwrap()
            .split('&')
            .next()
            .unwrap()
            .to_string();

        let callback = handle_request(
            &state,
            request(
                "GET",
                &format!("/auth/fedi/delete/callback?state={deletion_token}&code=good-code"),
            ),
        );
        let callback_body = std::str::from_utf8(&callback.body).unwrap();
        assert_eq!(callback.status, Status::Ok);
        assert!(callback_body.contains("Cosimo Sync Account Data Deleted"));
        assert!(callback_body.contains("<dt>Synced decks deleted</dt><dd>1</dd>"));
        assert!(callback_body.contains("<dt>Cosimo sync tokens revoked</dt><dd>1</dd>"));

        let mut old_owner_list = request("GET", "/api/v1/me/decks");
        old_owner_list.headers.insert(
            "authorization".to_string(),
            authorization_header(&owner_session),
        );
        assert_eq!(
            handle_request(&state, old_owner_list).status,
            Status::Unauthorized
        );
        assert_eq!(
            handle_request(
                &state,
                request("GET", &format!("/api/v1/decks/{DECK_UUID}"))
            )
            .status,
            Status::NotFound
        );
        assert_eq!(
            handle_request(
                &state,
                request("GET", &format!("/api/v1/decks/{other_uuid}"))
            )
            .status,
            Status::Ok
        );
    }

    #[test]
    fn fedi_browser_registration_rate_limits_before_remote_registration() {
        let registrations = Arc::new(AtomicUsize::new(0));
        let limits = SyncServerLimits {
            max_oauth_starts_per_client_per_hour: Some(1),
            ..SyncServerLimits::default()
        };
        let state = ServerState::memory_with_oauth_provider_and_limits(
            Arc::new(CountingOAuthProvider {
                registrations: Arc::clone(&registrations),
            }),
            limits,
        );

        for expected_status in [Status::SeeOther, Status::TooManyRequests] {
            let start = handle_request(&state, request("POST", "/api/v1/auth/fedi/browser-start"));
            let login_token =
                json_string_field(std::str::from_utf8(&start.body).unwrap(), "login_token");
            let mut register = request("POST", "/auth/fedi/register");
            register.headers.insert(
                "x-cosimo-client-address".to_string(),
                "203.0.113.10".to_string(),
            );
            register
                .headers
                .insert("host".to_string(), "sync.example.invalid".to_string());
            register.headers.insert(
                "content-type".to_string(),
                "application/x-www-form-urlencoded".to_string(),
            );
            register.body = format!(
                "login_token={login_token}&fedi_instance=example.invalid&privacy_acceptance=yes"
            )
            .into_bytes();

            let response = handle_request(&state, register);

            assert_eq!(response.status, expected_status);
        }

        assert_eq!(registrations.load(Ordering::SeqCst), 1);
    }

    #[test]
    fn domestic_allowlist_is_enforced_after_fedi_identity_is_verified() {
        let state = ServerState::memory_with_oauth_provider(Arc::new(TestOAuthProvider))
            .with_privacy_config(SyncServerPrivacyConfig::domestic([
                "acct:other@example.invalid".to_string(),
            ]));
        let start = handle_request(&state, request("POST", "/api/v1/auth/fedi/browser-start"));
        let login_token =
            json_string_field(std::str::from_utf8(&start.body).unwrap(), "login_token");
        let mut register = request("POST", "/auth/fedi/register");
        register
            .headers
            .insert("host".to_string(), "sync.example.invalid".to_string());
        register.headers.insert(
            "content-type".to_string(),
            "application/x-www-form-urlencoded".to_string(),
        );
        register.body = format!(
            "login_token={login_token}&fedi_instance=example.invalid&privacy_acceptance=yes"
        )
        .into_bytes();

        assert_eq!(handle_request(&state, register).status, Status::SeeOther);

        let complete = handle_request(
            &state,
            request(
                "GET",
                &format!("/auth/fedi/callback?state={login_token}&code=good-code"),
            ),
        );

        assert_eq!(complete.status, Status::Forbidden);
        assert!(
            std::str::from_utf8(&complete.body)
                .unwrap()
                .contains("only allows configured accounts")
        );
    }

    #[test]
    fn logout_revokes_bearer_token() {
        let state = ServerState::default();
        let session = state
            .create_auth_session_for_verified_owner("acct:modulus@example.invalid")
            .unwrap();
        let mut logout = request("POST", "/api/v1/auth/logout");
        logout
            .headers
            .insert("authorization".to_string(), authorization_header(&session));

        let logout = handle_request(&state, logout);

        assert_eq!(logout.status, Status::Ok);

        let mut owner_list = request("GET", "/api/v1/me/decks");
        owner_list
            .headers
            .insert("authorization".to_string(), authorization_header(&session));
        let owner_list = handle_request(&state, owner_list);
        assert_eq!(owner_list.status, Status::Unauthorized);
    }

    #[test]
    fn logout_all_revokes_only_the_authenticated_owners_tokens() {
        let state = ServerState::default();
        let first = state
            .create_auth_session_for_verified_owner("acct:modulus@example.invalid")
            .unwrap();
        let second = state
            .create_auth_session_for_verified_owner("acct:modulus@example.invalid")
            .unwrap();
        let other = state
            .create_auth_session_for_verified_owner("acct:other@example.invalid")
            .unwrap();
        let mut logout = request("POST", "/api/v1/auth/logout-all");
        logout
            .headers
            .insert("authorization".to_string(), authorization_header(&first));

        let logout = handle_request(&state, logout);

        assert_eq!(logout.status, Status::Ok);
        assert!(String::from_utf8_lossy(&logout.body).contains("\"revoked\":2"));
        for session in [&first, &second] {
            let mut owner_list = request("GET", "/api/v1/me/decks");
            owner_list
                .headers
                .insert("authorization".to_string(), authorization_header(session));
            assert_eq!(
                handle_request(&state, owner_list).status,
                Status::Unauthorized
            );
        }
        let mut other_owner_list = request("GET", "/api/v1/me/decks");
        other_owner_list
            .headers
            .insert("authorization".to_string(), authorization_header(&other));
        assert_eq!(handle_request(&state, other_owner_list).status, Status::Ok);
    }

    #[test]
    fn bearer_token_owner_is_authoritative_for_uploads_and_downloads() {
        let state = ServerState::default();
        let session = state
            .create_auth_session_for_verified_owner("acct:modulus@example.invalid")
            .unwrap();
        let mut upload = upload_request(
            DECK_UUID,
            "acct:other@example.invalid",
            "private",
            None,
            b"content bundle",
            Some(b"history bundle"),
        );
        upload
            .headers
            .insert("authorization".to_string(), authorization_header(&session));

        let upload = handle_request(&state, upload);
        assert_eq!(upload.status, Status::Created);

        let mut owner_list = request("GET", "/api/v1/me/decks");
        owner_list
            .headers
            .insert("authorization".to_string(), authorization_header(&session));
        let owner_list = handle_request(&state, owner_list);
        let body = std::str::from_utf8(&owner_list.body).unwrap();
        assert_eq!(owner_list.status, Status::Ok);
        assert!(body.contains(DECK_UUID));
        assert!(body.contains("\"owner_identity\":\"acct:modulus@example.invalid\""));

        let other_session = state
            .create_auth_session_for_verified_owner("acct:other@example.invalid")
            .unwrap();
        let mut wrong_owner_download = request(
            "GET",
            &format!("/api/v1/decks/{DECK_UUID}/download?kind=owner-study-history"),
        );
        wrong_owner_download.headers.insert(
            "authorization".to_string(),
            authorization_header(&other_session),
        );
        let wrong_owner_download = handle_request(&state, wrong_owner_download);
        assert_eq!(wrong_owner_download.status, Status::Forbidden);
    }

    #[test]
    fn quota_endpoint_reports_limits_and_authenticated_owner_usage() {
        let limits = SyncServerLimits {
            max_decks_per_owner: Some(5),
            max_storage_bytes_per_owner: Some(10_000),
            max_storage_bytes_total: Some(50_000),
            ..SyncServerLimits::default()
        };
        let state =
            ServerState::memory_with_oauth_provider_and_limits(Arc::new(TestOAuthProvider), limits);
        let owner_session = state
            .create_auth_session_for_verified_owner("acct:modulus@example.invalid")
            .unwrap();
        let other_session = state
            .create_auth_session_for_verified_owner("acct:other@example.invalid")
            .unwrap();
        let mut upload = upload_request(
            DECK_UUID,
            "acct:ignored@example.invalid",
            "private",
            None,
            b"content bundle",
            Some(b"history bundle"),
        );
        upload.headers.insert(
            "authorization".to_string(),
            authorization_header(&owner_session),
        );
        assert_eq!(handle_request(&state, upload).status, Status::Created);

        let public_quota = handle_request(&state, request("GET", "/api/v1/quota"));
        let public_body = std::str::from_utf8(&public_quota.body).unwrap();
        assert_eq!(public_quota.status, Status::Ok);
        assert!(public_body.contains("\"max_decks_per_owner\":5"));
        assert!(public_body.contains("\"max_storage_bytes_per_owner\":10000"));
        assert!(public_body.contains("\"max_storage_bytes_total\":50000"));
        assert!(public_body.contains("\"owner_deck_count\":null"));
        assert!(public_body.contains("\"owner_storage_bytes\":null"));

        let mut owner_quota = request("GET", "/api/v1/quota");
        owner_quota.headers.insert(
            "authorization".to_string(),
            authorization_header(&owner_session),
        );
        let owner_quota = handle_request(&state, owner_quota);
        let owner_body = std::str::from_utf8(&owner_quota.body).unwrap();
        assert_eq!(owner_quota.status, Status::Ok);
        assert!(owner_body.contains("\"max_decks_per_owner\":5"));
        assert!(owner_body.contains("\"max_storage_bytes_per_owner\":10000"));
        assert!(owner_body.contains("\"max_storage_bytes_total\":50000"));
        assert!(owner_body.contains("\"owner_deck_count\":1"));
        assert!(owner_body.contains("\"owner_storage_bytes\":14"));

        let mut other_quota = request("GET", "/api/v1/quota");
        other_quota.headers.insert(
            "authorization".to_string(),
            authorization_header(&other_session),
        );
        let other_quota = handle_request(&state, other_quota);
        let other_body = std::str::from_utf8(&other_quota.body).unwrap();
        assert_eq!(other_quota.status, Status::Ok);
        assert!(other_body.contains("\"owner_deck_count\":0"));
        assert!(other_body.contains("\"owner_storage_bytes\":0"));
    }

    #[test]
    fn delete_deck_requires_authenticated_owner_and_removes_remote_deck() {
        let state = ServerState::default();
        let owner_session = state
            .create_auth_session_for_verified_owner("acct:modulus@example.invalid")
            .unwrap();
        let other_session = state
            .create_auth_session_for_verified_owner("acct:other@example.invalid")
            .unwrap();
        let mut upload = upload_request(
            DECK_UUID,
            "acct:ignored@example.invalid",
            "public",
            None,
            b"content bundle",
            Some(b"history bundle"),
        );
        upload.headers.insert(
            "authorization".to_string(),
            authorization_header(&owner_session),
        );
        assert_eq!(handle_request(&state, upload).status, Status::Created);

        let mut wrong_owner_delete = request("DELETE", &format!("/api/v1/decks/{DECK_UUID}"));
        wrong_owner_delete.headers.insert(
            "authorization".to_string(),
            authorization_header(&other_session),
        );
        assert_eq!(
            handle_request(&state, wrong_owner_delete).status,
            Status::Forbidden
        );
        assert_eq!(
            handle_request(
                &state,
                request("GET", &format!("/api/v1/decks/{DECK_UUID}"))
            )
            .status,
            Status::Ok
        );

        let mut owner_delete = request("DELETE", &format!("/api/v1/decks/{DECK_UUID}"));
        owner_delete.headers.insert(
            "authorization".to_string(),
            authorization_header(&owner_session),
        );
        let owner_delete = handle_request(&state, owner_delete);

        assert_eq!(owner_delete.status, Status::Ok);
        assert_eq!(
            handle_request(
                &state,
                request("GET", &format!("/api/v1/decks/{DECK_UUID}"))
            )
            .status,
            Status::NotFound
        );
    }

    #[test]
    fn owner_routes_reject_invalid_bearer_token() {
        let state = ServerState::default();
        let mut request = request("GET", "/api/v1/me/decks");
        request
            .headers
            .insert("authorization".to_string(), "Bearer missing".to_string());

        let response = handle_request(&state, request);

        assert_eq!(response.status, Status::Unauthorized);
    }

    #[test]
    fn owner_routes_require_bearer_token() {
        let state = ServerState::default();

        let response = handle_request(&state, request("GET", "/api/v1/me/decks"));
        let body = std::str::from_utf8(&response.body).unwrap();

        assert_eq!(response.status, Status::Unauthorized);
        assert!(body.contains("sync authentication token is required"));
    }

    #[test]
    fn upload_and_owner_download_round_trip_study_history() {
        let state = ServerState::default();
        let upload = handle_request(
            &state,
            authenticated_upload_request(
                &state,
                DECK_UUID,
                "acct:modulus@example.invalid",
                "private",
                None,
                b"content bundle",
                Some(b"history bundle"),
            ),
        );

        assert_eq!(upload.status, Status::Created);

        let mut download = request(
            "GET",
            &format!("/api/v1/decks/{DECK_UUID}/download?kind=owner-study-history"),
        );
        authorize_request(&state, &mut download, "acct:modulus@example.invalid");
        let download = handle_request(&state, download);

        assert_eq!(download.status, Status::Ok);
        assert_eq!(download.body, b"history bundle");
        assert_eq!(
            download.headers.get("x-cosimo-includes-review-database"),
            Some(&"true".to_string())
        );
        assert_eq!(
            download.headers.get("x-cosimo-bundle-length"),
            Some(&"14".to_string())
        );
        assert_eq!(
            download.headers.get("x-cosimo-last-upload-device-name"),
            Some(&"laptop".to_string())
        );
    }

    #[test]
    fn upload_accepts_history_only_payloads() {
        let state = ServerState::default();
        let history_bundle = study_history_zip_bytes(b"history bundle");
        let mut upload = request("POST", &format!("/api/v1/decks/{DECK_UUID}/upload"));
        authorize_request(&state, &mut upload, "acct:modulus@example.invalid");
        upload
            .headers
            .insert("x-cosimo-visibility".to_string(), "public".to_string());
        upload.headers.insert(
            "x-cosimo-study-history-hash".to_string(),
            sync_bundle_hash(&history_bundle),
        );
        upload.headers.insert(
            "x-cosimo-study-history-length".to_string(),
            history_bundle.len().to_string(),
        );
        upload.body = history_bundle;

        let upload = handle_request(&state, upload);

        assert_eq!(upload.status, Status::Created);
    }

    #[test]
    fn upload_rejects_mismatched_declared_bundle_hash() {
        let state = ServerState::default();
        let history_bundle = study_history_zip_bytes(b"history bundle");
        let mut upload = request("POST", &format!("/api/v1/decks/{DECK_UUID}/upload"));
        authorize_request(&state, &mut upload, "acct:modulus@example.invalid");
        upload
            .headers
            .insert("x-cosimo-visibility".to_string(), "public".to_string());
        upload.headers.insert(
            "x-cosimo-study-history-hash".to_string(),
            "sha256:0000000000000000000000000000000000000000000000000000000000000000".to_string(),
        );
        upload.headers.insert(
            "x-cosimo-study-history-length".to_string(),
            history_bundle.len().to_string(),
        );
        upload.body = history_bundle;

        let upload = handle_request(&state, upload);
        let body = std::str::from_utf8(&upload.body).unwrap();

        assert_eq!(upload.status, Status::BadRequest);
        assert!(body.contains("sync bundle hash mismatch"));
    }

    #[test]
    fn sqlite_storage_persists_across_server_state_instances() {
        let root = unique_temp_dir("persist");
        let state = sqlite_state(&root);
        let history_bundle = study_history_zip_bytes(b"history bundle");
        let upload = handle_request(
            &state,
            authenticated_upload_request(
                &state,
                DECK_UUID,
                "acct:modulus@example.invalid",
                "public",
                None,
                b"content bundle",
                Some(&history_bundle),
            ),
        );
        assert_eq!(upload.status, Status::Created);
        drop(state);

        let reopened = sqlite_state(&root);
        let mut download = request(
            "GET",
            &format!("/api/v1/decks/{DECK_UUID}/download?kind=owner-study-history"),
        );
        authorize_request(&reopened, &mut download, "acct:modulus@example.invalid");
        let download = handle_request(&reopened, download);

        assert_eq!(download.status, Status::Ok);
        assert_eq!(download.body, history_bundle);
    }

    #[test]
    fn public_listing_omits_unlisted_and_private_decks() {
        let state = ServerState::default();
        handle_request(
            &state,
            authenticated_upload_request(
                &state,
                DECK_UUID,
                "acct:modulus@example.invalid",
                "public",
                None,
                b"public",
                None,
            ),
        );
        handle_request(
            &state,
            authenticated_upload_request(
                &state,
                "650e8400-e29b-41d4-a716-446655440000",
                "acct:modulus@example.invalid",
                "unlisted",
                None,
                b"unlisted",
                None,
            ),
        );
        handle_request(
            &state,
            authenticated_upload_request(
                &state,
                "750e8400-e29b-41d4-a716-446655440000",
                "acct:modulus@example.invalid",
                "private",
                None,
                b"private",
                None,
            ),
        );

        let response = handle_request(&state, request("GET", "/api/v1/decks"));
        let body = std::str::from_utf8(&response.body).unwrap();

        assert_eq!(response.status, Status::Ok);
        assert!(body.contains(DECK_UUID));
        assert!(!body.contains("650e8400-e29b-41d4-a716-446655440000"));
        assert!(!body.contains("750e8400-e29b-41d4-a716-446655440000"));
        assert!(!body.contains("last_upload_device"));
    }

    #[test]
    fn shared_download_rejects_private_decks() {
        let state = ServerState::default();
        handle_request(
            &state,
            authenticated_upload_request(
                &state,
                DECK_UUID,
                "acct:modulus@example.invalid",
                "private",
                None,
                b"content bundle",
                Some(b"history bundle"),
            ),
        );

        let response = handle_request(
            &state,
            request(
                "GET",
                &format!("/api/v1/decks/{DECK_UUID}/download?kind=shared"),
            ),
        );

        assert_eq!(response.status, Status::Forbidden);
    }

    #[test]
    fn shared_inspection_allows_unlisted_decks() {
        let state = ServerState::default();
        let unlisted_uuid = "650e8400-e29b-41d4-a716-446655440000";
        handle_request(
            &state,
            authenticated_upload_request(
                &state,
                unlisted_uuid,
                "acct:modulus@example.invalid",
                "unlisted",
                None,
                b"content bundle",
                Some(b"history bundle"),
            ),
        );

        let response = handle_request(
            &state,
            request("GET", &format!("/api/v1/decks/{unlisted_uuid}")),
        );
        let body = std::str::from_utf8(&response.body).unwrap();

        assert_eq!(response.status, Status::Ok);
        assert!(body.contains("\"visibility\":\"unlisted\""));
        assert!(!body.contains("study_history"));
        assert!(!body.contains("last_upload_device"));
    }

    #[test]
    fn blind_second_upload_reports_conflict() {
        let state = ServerState::default();
        let first = handle_request(
            &state,
            authenticated_upload_request(
                &state,
                DECK_UUID,
                "acct:modulus@example.invalid",
                "public",
                None,
                b"content one",
                None,
            ),
        );
        let second = handle_request(
            &state,
            authenticated_upload_request(
                &state,
                DECK_UUID,
                "acct:modulus@example.invalid",
                "public",
                None,
                b"content two",
                None,
            ),
        );

        assert_eq!(first.status, Status::Created);
        assert_eq!(second.status, Status::Conflict);
        assert_eq!(
            second.headers.get("content-type").map(String::as_str),
            Some("application/json; charset=utf-8")
        );
        let body = std::str::from_utf8(&second.body).unwrap();
        assert!(body.contains("\"error\":\"revision_conflict\""));
        assert!(body.contains("\"expected_revision\":null"));
        assert!(body.contains("\"actual_revision\":\"1\""));
    }

    #[test]
    fn owner_listing_includes_owner_only_fields() {
        let state = ServerState::default();
        handle_request(
            &state,
            authenticated_upload_request(
                &state,
                DECK_UUID,
                "acct:modulus@example.invalid",
                "private",
                None,
                b"content bundle",
                Some(b"history bundle"),
            ),
        );

        let mut list = request("GET", "/api/v1/me/decks");
        authorize_request(&state, &mut list, "acct:modulus@example.invalid");
        let response = handle_request(&state, list);
        let body = std::str::from_utf8(&response.body).unwrap();

        assert_eq!(response.status, Status::Ok);
        assert!(body.contains(DECK_UUID));
        assert!(body.contains("study_history_bundle_hash"));
        assert!(body.contains("last_upload_device_name"));
    }

    #[test]
    fn upload_rejects_mismatched_declared_lengths() {
        let state = ServerState::default();
        let mut upload = upload_request(
            DECK_UUID,
            "acct:modulus@example.invalid",
            "public",
            None,
            b"content",
            None,
        );
        authorize_request(&state, &mut upload, "acct:modulus@example.invalid");
        upload
            .headers
            .insert("x-cosimo-content-length".to_string(), "999".to_string());

        let response = handle_request(&state, upload);

        assert_eq!(response.status, Status::BadRequest);
    }

    #[test]
    fn upload_rejects_declared_length_overflow() {
        let state = ServerState::default();
        let mut upload = upload_request(
            DECK_UUID,
            "acct:modulus@example.invalid",
            "public",
            None,
            b"content",
            Some(b"history"),
        );
        authorize_request(&state, &mut upload, "acct:modulus@example.invalid");
        upload.headers.insert(
            "x-cosimo-content-length".to_string(),
            usize::MAX.to_string(),
        );
        upload
            .headers
            .insert("x-cosimo-study-history-length".to_string(), "1".to_string());

        let response = handle_request(&state, upload);

        assert_eq!(response.status, Status::BadRequest);
    }

    #[test]
    fn upload_rejects_declared_bundle_over_configured_limit() {
        let state = ServerState::memory_with_limits(SyncServerLimits {
            max_bundle_bytes: Some(3),
            ..SyncServerLimits::default()
        });
        let mut upload = upload_request(
            DECK_UUID,
            "acct:modulus@example.invalid",
            "public",
            None,
            b"content",
            None,
        );
        authorize_request(&state, &mut upload, "acct:modulus@example.invalid");

        let response = handle_request(&state, upload);

        assert_eq!(response.status, Status::PayloadTooLarge);
        assert!(
            std::str::from_utf8(&response.body)
                .unwrap()
                .contains("content bundle")
        );
    }

    #[test]
    fn read_http_request_rejects_content_length_over_configured_body_limit_before_body() {
        let mut bytes = b"POST /api/v1/decks/550e8400-e29b-41d4-a716-446655440000/upload HTTP/1.1\r\nContent-Length: 10\r\n\r\n"
            .as_slice();

        let error = read_http_request_with_max_body_bytes(&mut bytes, 9).unwrap_err();

        assert!(matches!(error, HttpError::PayloadTooLarge(_)));
    }

    #[test]
    fn json_escape_handles_control_characters() {
        assert_eq!(
            json_escape("quote \" slash \\ newline\n"),
            "quote \\\" slash \\\\ newline\\n"
        );
    }

    fn http_token() -> impl Strategy<Value = String> {
        "[A-Za-z][A-Za-z0-9_-]{0,20}".prop_map(|value| value)
    }

    fn header_name() -> impl Strategy<Value = String> {
        "[A-Za-z][A-Za-z0-9-]{0,20}".prop_map(|value| value)
    }

    fn header_value() -> impl Strategy<Value = String> {
        "[A-Za-z0-9 _./:@=-]{0,40}".prop_map(|value| value)
    }

    fn request_target() -> impl Strategy<Value = String> {
        "(/[A-Za-z0-9._~!$&'()*+,;=:@%-]{0,20}){0,4}(\\?[A-Za-z0-9._~!$&'()*+,;=:@/?%-]{0,40})?"
            .prop_map(|value| {
                if value.is_empty() {
                    "/".to_string()
                } else {
                    value
                }
            })
    }

    fn arbitrary_request() -> impl Strategy<Value = Request> {
        (
            http_token(),
            request_target(),
            prop::collection::btree_map(header_name(), header_value(), 0..8),
            prop::collection::vec(any::<u8>(), 0..128),
        )
            .prop_map(|(method, target, headers, body)| Request {
                method,
                target,
                headers: headers
                    .into_iter()
                    .map(|(name, value)| (name.to_ascii_lowercase(), value))
                    .collect(),
                body,
            })
    }

    proptest! {
        #![proptest_config(ProptestConfig {
            cases: 96,
            failure_persistence: None,
            ..ProptestConfig::default()
        })]

        #[test]
        fn valid_raw_http_requests_round_trip_through_parser(
            method in http_token(),
            target in request_target(),
            headers in prop::collection::vec((header_name(), header_value()), 0..8),
            body in prop::collection::vec(any::<u8>(), 0..256),
        ) {
            let headers = headers
                .into_iter()
                .filter(|(name, _)| !name.eq_ignore_ascii_case("content-length"))
                .collect::<Vec<_>>();
            let mut raw = format!("{method} {target} HTTP/1.1\r\n");
            for (name, value) in &headers {
                raw.push_str(&format!("{name}: {value}\r\n"));
            }
            raw.push_str(&format!("Content-Length: {}\r\n\r\n", body.len()));
            let mut bytes = raw.into_bytes();
            bytes.extend_from_slice(&body);

            let parsed = read_http_request(&mut bytes.as_slice()).unwrap();
            let expected_headers = headers
                .into_iter()
                .map(|(name, value)| (name.to_ascii_lowercase(), value.trim().to_string()))
                .collect::<BTreeMap<_, _>>();
            let content_length = body.len().to_string();

            prop_assert_eq!(&parsed.method, &method);
            prop_assert_eq!(&parsed.target, &target);
            prop_assert_eq!(&parsed.body, &body);
            prop_assert_eq!(parsed.header("content-length"), Some(content_length.as_str()));
            for (name, value) in expected_headers {
                prop_assert_eq!(parsed.header(&name), Some(value.as_str()));
            }
        }

        #[test]
        fn request_routing_serializes_responses_consistently(request in arbitrary_request()) {
            let state = ServerState::default();
            let response = handle_request(&state, request);
            let http = response.to_http_bytes();
            let cgi = response.to_cgi_bytes();
            let http_text = std::str::from_utf8(&http).unwrap();
            let cgi_text = std::str::from_utf8(&cgi).unwrap();
            let expected_http_status = format!(
                "HTTP/1.1 {} {}",
                response.status.code(),
                response.status.reason()
            );
            let expected_cgi_status = format!(
                "Status: {} {}",
                response.status.code(),
                response.status.reason()
            );
            let expected_content_length = format!("Content-Length: {}\r\n", response.body.len());

            prop_assert!(http_text.starts_with(&expected_http_status));
            prop_assert!(http_text.contains(&expected_content_length));
            prop_assert!(cgi_text.starts_with(&expected_cgi_status));
            prop_assert!(cgi_text.contains(&expected_content_length));
            prop_assert!(http.ends_with(&response.body));
            prop_assert!(cgi.ends_with(&response.body));
        }

        #[test]
        fn json_escape_output_contains_no_unescaped_control_characters(value in "\\PC{0,128}") {
            let escaped = json_escape(&value);

            prop_assert!(!escaped.chars().any(|character| character.is_control()));
        }
    }
}
