use crate::error::{Result, SyncularError};
use crate::protocol::{
    blob_hash, normalize_blob_mime_type, validate_blob_bytes, validate_blob_size_bytes, BlobRef,
    OperationResult, PullResponse, PushBatchRequest, PushCommitRequest, PushCommitResponse,
    SyncChange, SyncOperation,
};
use argon2::{Algorithm, Argon2, Params, Version};
use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD};
use base64::Engine as _;
use bip39::{Language, Mnemonic};
use chacha20poly1305::aead::{Aead, KeyInit, Payload};
use chacha20poly1305::{XChaCha20Poly1305, XNonce};
use hkdf::Hkdf;
use pbkdf2::pbkdf2_hmac;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use sha2::Sha256;
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::sync::Arc;
use x25519_dalek::{PublicKey, StaticSecret};
use zeroize::Zeroize;

pub const DEFAULT_FIELD_ENCRYPTION_PREFIX: &str = "dgsync:e2ee:1:";
const KEY_WRAP_HKDF_INFO: &[u8] = b"syncular-key-wrap-v1";
const BLOB_CIPHERTEXT_VERSION: u8 = 1;
const BLOB_NONCE_LEN: usize = 24;
const BLOB_CIPHERTEXT_HEADER_LEN: usize = 1 + BLOB_NONCE_LEN;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum FieldDecryptionErrorMode {
    Throw,
    KeepCiphertext,
}

impl Default for FieldDecryptionErrorMode {
    fn default() -> Self {
        Self::Throw
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FieldEncryptionRule {
    pub scope: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub table: Option<String>,
    pub fields: Vec<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub row_id_field: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldEncryptionContext {
    pub actor_id: String,
    pub client_id: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldEncryptionTarget {
    pub scope: String,
    pub table: String,
    pub row_id: String,
    pub field: String,
}

pub trait FieldEncryptionKeyProvider: Send + Sync {
    fn get_key(&self, kid: &str) -> Result<Vec<u8>>;

    fn encryption_kid(
        &self,
        _ctx: &FieldEncryptionContext,
        _target: &FieldEncryptionTarget,
    ) -> Result<String> {
        Ok("default".to_string())
    }
}

#[derive(Debug, Clone)]
pub struct StaticFieldEncryptionKeys {
    keys: BTreeMap<String, Vec<u8>>,
    encryption_kid: String,
}

impl StaticFieldEncryptionKeys {
    pub fn new(
        keys: impl IntoIterator<Item = (impl Into<String>, impl Into<Vec<u8>>)>,
        encryption_kid: Option<String>,
    ) -> Result<Self> {
        let mut decoded = BTreeMap::new();
        for (kid, key) in keys {
            let kid = kid.into();
            validate_kid(&kid)?;
            let key = key.into();
            validate_32_bytes("encryption key", &key)?;
            decoded.insert(kid, key);
        }

        let encryption_kid = encryption_kid.unwrap_or_else(|| "default".to_string());
        validate_kid(&encryption_kid)?;

        Ok(Self {
            keys: decoded,
            encryption_kid,
        })
    }

    pub fn from_key_material(
        keys: BTreeMap<String, String>,
        encryption_kid: Option<String>,
    ) -> Result<Self> {
        let mut decoded = BTreeMap::new();
        for (kid, material) in keys {
            decoded.insert(kid, decode_key_material(&material)?);
        }
        Self::new(decoded, encryption_kid)
    }
}

impl FieldEncryptionKeyProvider for StaticFieldEncryptionKeys {
    fn get_key(&self, kid: &str) -> Result<Vec<u8>> {
        self.keys.get(kid).cloned().ok_or_else(|| {
            SyncularError::config(format!("Missing encryption key for kid \"{kid}\""))
        })
    }

    fn encryption_kid(
        &self,
        _ctx: &FieldEncryptionContext,
        _target: &FieldEncryptionTarget,
    ) -> Result<String> {
        Ok(self.encryption_kid.clone())
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StaticFieldEncryptionConfig {
    pub rules: Vec<FieldEncryptionRule>,
    pub keys: BTreeMap<String, String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub encryption_kid: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub decryption_error_mode: Option<FieldDecryptionErrorMode>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub envelope_prefix: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StaticBlobEncryptionConfig {
    pub keys: BTreeMap<String, String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub encryption_kid: Option<String>,
}

#[derive(Debug, Clone)]
pub struct EncryptedBlobBody {
    pub blob: BlobRef,
    pub body: Vec<u8>,
}

#[derive(Debug, Clone)]
pub struct BlobEncryption {
    keys: BTreeMap<String, Vec<u8>>,
    encryption_kid: String,
}

impl BlobEncryption {
    pub fn from_static_config(config: StaticBlobEncryptionConfig) -> Result<Self> {
        let mut keys = BTreeMap::new();
        for (kid, material) in config.keys {
            validate_kid(&kid)?;
            keys.insert(kid, decode_key_material(&material)?);
        }

        let encryption_kid = config
            .encryption_kid
            .unwrap_or_else(|| "default".to_string());
        validate_kid(&encryption_kid)?;
        if keys.is_empty() {
            return Err(SyncularError::config(
                "blob encryption requires at least one key",
            ));
        }
        if !keys.contains_key(&encryption_kid) {
            return Err(SyncularError::config(format!(
                "blob encryption key \"{encryption_kid}\" is missing"
            )));
        }

        Ok(Self {
            keys,
            encryption_kid,
        })
    }

    pub fn from_static_config_json(config_json: &str) -> Result<Option<Self>> {
        let trimmed = config_json.trim();
        if trimmed.is_empty() || trimmed == "null" {
            return Ok(None);
        }
        let config: StaticBlobEncryptionConfig = serde_json::from_str(trimmed)?;
        Ok(Some(Self::from_static_config(config)?))
    }

    pub fn encrypt_blob(&self, plaintext: &[u8], mime_type: &str) -> Result<EncryptedBlobBody> {
        let mime_type = normalize_blob_mime_type(mime_type);
        let kid = self.encryption_kid.clone();
        let key = self.key(&kid)?;
        let nonce = random_bytes(BLOB_NONCE_LEN)?;
        let aad = make_blob_aad(&kid, &mime_type);
        let encrypted = xchacha_encrypt(key, &nonce, &aad, plaintext)?;
        let mut body = Vec::with_capacity(BLOB_CIPHERTEXT_HEADER_LEN + encrypted.len());
        body.push(BLOB_CIPHERTEXT_VERSION);
        body.extend_from_slice(&nonce);
        body.extend_from_slice(&encrypted);

        let size = i64::try_from(body.len()).map_err(|_| {
            SyncularError::protocol_message("encrypted blob is too large for SQLite size metadata")
        })?;
        validate_blob_size_bytes(size)?;
        let blob = BlobRef {
            hash: blob_hash(&body),
            size,
            mime_type,
            encrypted: true,
            key_id: Some(kid),
        };
        Ok(EncryptedBlobBody { blob, body })
    }

    pub fn decrypt_blob(&self, blob: &BlobRef, body: &[u8]) -> Result<Vec<u8>> {
        validate_blob_bytes(blob, body)?;
        if !blob.encrypted {
            return Ok(body.to_vec());
        }
        let kid = blob.key_id.as_deref().ok_or_else(|| {
            SyncularError::protocol_message("encrypted blob ref is missing keyId")
        })?;
        validate_kid(kid)?;
        let key = self.key(kid)?;
        if body.len() < BLOB_CIPHERTEXT_HEADER_LEN {
            return Err(SyncularError::protocol_message(
                "encrypted blob body is too short",
            ));
        }
        if body[0] != BLOB_CIPHERTEXT_VERSION {
            return Err(SyncularError::protocol_message(format!(
                "unsupported encrypted blob version {}",
                body[0]
            )));
        }
        let nonce = &body[1..BLOB_CIPHERTEXT_HEADER_LEN];
        let ciphertext = &body[BLOB_CIPHERTEXT_HEADER_LEN..];
        xchacha_decrypt(key, nonce, &make_blob_aad(kid, &blob.mime_type), ciphertext)
    }

    pub fn ensure_can_decrypt(&self, blob: &BlobRef) -> Result<()> {
        if !blob.encrypted {
            return Ok(());
        }
        let kid = blob.key_id.as_deref().ok_or_else(|| {
            SyncularError::protocol_message("encrypted blob ref is missing keyId")
        })?;
        validate_kid(kid)?;
        let _ = self.key(kid)?;
        Ok(())
    }

    fn key(&self, kid: &str) -> Result<&[u8]> {
        self.keys.get(kid).map(Vec::as_slice).ok_or_else(|| {
            SyncularError::config(format!("Missing blob encryption key for kid \"{kid}\""))
        })
    }
}

#[derive(Clone)]
pub struct FieldEncryption {
    rules: Vec<FieldEncryptionRule>,
    index: RuleIndex,
    keys: Arc<dyn FieldEncryptionKeyProvider>,
    prefix: String,
    decryption_error_mode: FieldDecryptionErrorMode,
}

impl FieldEncryption {
    pub fn new(
        rules: Vec<FieldEncryptionRule>,
        keys: Arc<dyn FieldEncryptionKeyProvider>,
    ) -> Result<Self> {
        Self::with_options(rules, keys, None, FieldDecryptionErrorMode::Throw)
    }

    pub fn with_options(
        rules: Vec<FieldEncryptionRule>,
        keys: Arc<dyn FieldEncryptionKeyProvider>,
        envelope_prefix: Option<String>,
        decryption_error_mode: FieldDecryptionErrorMode,
    ) -> Result<Self> {
        let prefix = envelope_prefix.unwrap_or_else(|| DEFAULT_FIELD_ENCRYPTION_PREFIX.to_string());
        if !prefix.ends_with(':') {
            return Err(SyncularError::config(
                "field encryption envelope prefix must end with ':'",
            ));
        }
        let index = RuleIndex::build(&rules)?;
        Ok(Self {
            rules,
            index,
            keys,
            prefix,
            decryption_error_mode,
        })
    }

    pub fn from_static_config(config: StaticFieldEncryptionConfig) -> Result<Self> {
        let keys =
            StaticFieldEncryptionKeys::from_key_material(config.keys, config.encryption_kid)?;
        Self::with_options(
            config.rules,
            Arc::new(keys),
            config.envelope_prefix,
            config.decryption_error_mode.unwrap_or_default(),
        )
    }

    pub fn from_static_config_json(config_json: &str) -> Result<Option<Self>> {
        let trimmed = config_json.trim();
        if trimmed.is_empty() || trimmed == "null" {
            return Ok(None);
        }
        let config: StaticFieldEncryptionConfig = serde_json::from_str(trimmed)?;
        Ok(Some(Self::from_static_config(config)?))
    }

    pub fn rules(&self) -> &[FieldEncryptionRule] {
        &self.rules
    }

    pub fn transform_push_batch_request(
        &self,
        ctx: &FieldEncryptionContext,
        mut request: PushBatchRequest,
    ) -> Result<PushBatchRequest> {
        for commit in &mut request.commits {
            self.transform_push_commit_request_in_place(ctx, commit)?;
        }
        Ok(request)
    }

    pub fn transform_push_commit_request(
        &self,
        ctx: &FieldEncryptionContext,
        mut request: PushCommitRequest,
    ) -> Result<PushCommitRequest> {
        self.transform_push_commit_request_in_place(ctx, &mut request)?;
        Ok(request)
    }

    pub fn transform_operations_for_push(
        &self,
        ctx: &FieldEncryptionContext,
        operations: Vec<SyncOperation>,
    ) -> Result<Vec<SyncOperation>> {
        operations
            .into_iter()
            .map(|operation| self.transform_operation_for_push(ctx, operation))
            .collect()
    }

    pub fn transform_push_response(
        &self,
        ctx: &FieldEncryptionContext,
        outbox_operations: &[SyncOperation],
        mut response: PushCommitResponse,
    ) -> Result<PushCommitResponse> {
        for result in &mut response.results {
            self.transform_operation_result(ctx, outbox_operations, result)?;
        }
        Ok(response)
    }

    pub fn transform_pull_response(
        &self,
        ctx: &FieldEncryptionContext,
        mut response: PullResponse,
    ) -> Result<PullResponse> {
        for sub in &mut response.subscriptions {
            if let Some(snapshots) = &mut sub.snapshots {
                for snapshot in snapshots {
                    for row in &mut snapshot.rows {
                        *row = self.transform_snapshot_row(ctx, &snapshot.table, row.clone())?;
                    }
                }
            }
            for commit in &mut sub.commits {
                for change in &mut commit.changes {
                    self.transform_change_in_place(ctx, change)?;
                }
            }
        }
        Ok(response)
    }

    pub fn transform_snapshot_row(
        &self,
        ctx: &FieldEncryptionContext,
        snapshot_table: &str,
        row: Value,
    ) -> Result<Value> {
        let Value::Object(record) = row else {
            return Ok(row);
        };
        let scope = snapshot_table.to_string();
        let table = self.infer_snapshot_table(&scope, &record)?;
        let Some(config) = self.index.config_for(&scope, &table) else {
            return Ok(Value::Object(record));
        };
        let row_id = snapshot_row_id(&record, &config.row_id_field, &scope, &table)?;
        let transformed = self.transform_record_fields(
            ctx,
            TransformMode::Decrypt,
            FieldRecordTarget {
                scope: &scope,
                table: &table,
                row_id: &row_id,
            },
            record,
        )?;
        Ok(Value::Object(transformed))
    }

    pub fn transform_change(
        &self,
        ctx: &FieldEncryptionContext,
        mut change: SyncChange,
    ) -> Result<SyncChange> {
        self.transform_change_in_place(ctx, &mut change)?;
        Ok(change)
    }

    fn transform_push_commit_request_in_place(
        &self,
        ctx: &FieldEncryptionContext,
        request: &mut PushCommitRequest,
    ) -> Result<()> {
        for operation in &mut request.operations {
            *operation = self.transform_operation_for_push(ctx, operation.clone())?;
        }
        Ok(())
    }

    fn transform_operation_for_push(
        &self,
        ctx: &FieldEncryptionContext,
        mut operation: SyncOperation,
    ) -> Result<SyncOperation> {
        if operation.op != "upsert" {
            return Ok(operation);
        }
        let Some(Value::Object(record)) = operation.payload.take() else {
            return Ok(operation);
        };
        let target = self.index.resolve_scope_and_table(&operation.table);
        let record = self.transform_record_fields(
            ctx,
            TransformMode::Encrypt,
            FieldRecordTarget {
                scope: &target.scope,
                table: &target.table,
                row_id: &operation.row_id,
            },
            record,
        )?;
        operation.payload = Some(Value::Object(record));
        Ok(operation)
    }

    fn transform_operation_result(
        &self,
        ctx: &FieldEncryptionContext,
        outbox_operations: &[SyncOperation],
        result: &mut OperationResult,
    ) -> Result<()> {
        if result.status != "conflict" && result.status != "error" {
            return Ok(());
        }
        let Some(Value::Object(record)) = result.server_row.take() else {
            return Ok(());
        };
        let Some(operation) = outbox_operations.get(result.op_index as usize) else {
            result.server_row = Some(Value::Object(record));
            return Ok(());
        };
        let target = self.index.resolve_scope_and_table(&operation.table);
        let record = self.transform_record_fields(
            ctx,
            TransformMode::Decrypt,
            FieldRecordTarget {
                scope: &target.scope,
                table: &target.table,
                row_id: &operation.row_id,
            },
            record,
        )?;
        result.server_row = Some(Value::Object(record));
        Ok(())
    }

    fn transform_change_in_place(
        &self,
        ctx: &FieldEncryptionContext,
        change: &mut SyncChange,
    ) -> Result<()> {
        if change.op != "upsert" {
            return Ok(());
        }
        let Some(Value::Object(record)) = change.row_json.take() else {
            return Ok(());
        };
        let target = self.index.resolve_scope_and_table(&change.table);
        let record = self.transform_record_fields(
            ctx,
            TransformMode::Decrypt,
            FieldRecordTarget {
                scope: &target.scope,
                table: &target.table,
                row_id: &change.row_id,
            },
            record,
        )?;
        change.row_json = Some(Value::Object(record));
        Ok(())
    }

    fn infer_snapshot_table(&self, scope: &str, row: &Map<String, Value>) -> Result<String> {
        if let Some(table) = row.get("table_name").and_then(Value::as_str) {
            if !table.is_empty() {
                return Ok(table.to_string());
            }
        }
        if let Some(table) = row.get("__table").and_then(Value::as_str) {
            if !table.is_empty() {
                return Ok(table.to_string());
            }
        }
        if let Some(table) = self.index.only_table_for_scope(scope) {
            return Ok(table.to_string());
        }
        Ok(scope.to_string())
    }

    fn transform_record_fields(
        &self,
        ctx: &FieldEncryptionContext,
        mode: TransformMode,
        target: FieldRecordTarget<'_>,
        mut record: Map<String, Value>,
    ) -> Result<Map<String, Value>> {
        let Some(config) = self.index.config_for(target.scope, target.table) else {
            return Ok(record);
        };

        for field in &config.fields {
            let Some(value) = record.remove(field) else {
                continue;
            };
            let transformed = match mode {
                TransformMode::Encrypt => self.encrypt_value(ctx, &target, field, value)?,
                TransformMode::Decrypt => self.decrypt_value(&target, field, value)?,
            };
            record.insert(field.clone(), transformed);
        }

        Ok(record)
    }

    fn encrypt_value(
        &self,
        ctx: &FieldEncryptionContext,
        target: &FieldRecordTarget<'_>,
        field: &str,
        value: Value,
    ) -> Result<Value> {
        if value.is_null() {
            return Ok(value);
        }
        if value
            .as_str()
            .and_then(|value| decode_envelope(&self.prefix, value).ok().flatten())
            .is_some()
        {
            return Ok(value);
        }

        let field_target = FieldEncryptionTarget {
            scope: target.scope.to_string(),
            table: target.table.to_string(),
            row_id: target.row_id.to_string(),
            field: field.to_string(),
        };
        let kid = self.keys.encryption_kid(ctx, &field_target)?;
        validate_kid(&kid)?;
        let key = self.keys.get_key(&kid)?;
        validate_32_bytes("encryption key", &key)?;
        let nonce = random_bytes(24)?;
        let aad = make_aad(target.scope, target.table, target.row_id, field);
        let plaintext = serde_json::to_vec(&value)?;
        let ciphertext = xchacha_encrypt(&key, &nonce, &aad, &plaintext)?;
        Ok(Value::String(encode_envelope(
            &self.prefix,
            &kid,
            &nonce,
            &ciphertext,
        )))
    }

    fn decrypt_value(
        &self,
        target: &FieldRecordTarget<'_>,
        field: &str,
        value: Value,
    ) -> Result<Value> {
        let Some(raw) = value.as_str() else {
            return Ok(value);
        };
        let Some(envelope) = decode_envelope(&self.prefix, raw)? else {
            return Ok(value);
        };

        let decrypt = || -> Result<Value> {
            let key = self.keys.get_key(&envelope.kid)?;
            validate_32_bytes("encryption key", &key)?;
            let aad = make_aad(target.scope, target.table, target.row_id, field);
            let plaintext = xchacha_decrypt(&key, &envelope.nonce, &aad, &envelope.ciphertext)?;
            Ok(serde_json::from_slice(&plaintext)?)
        };

        match decrypt() {
            Ok(value) => Ok(value),
            Err(_) if self.decryption_error_mode == FieldDecryptionErrorMode::KeepCiphertext => {
                Ok(value)
            }
            Err(error) => Err(SyncularError::protocol_message(format!(
                "Failed to decrypt {}.{}.{} row={}: {}",
                target.scope,
                target.table,
                field,
                target.row_id,
                error.message_text()
            ))),
        }
    }
}

#[derive(Debug, Clone)]
struct RuleConfig {
    fields: BTreeSet<String>,
    row_id_field: String,
}

#[derive(Clone)]
struct RuleIndex {
    by_scope_table: HashMap<(String, String), RuleConfig>,
    tables_by_scope: HashMap<String, BTreeSet<String>>,
    scopes_by_table: HashMap<String, BTreeSet<String>>,
}

impl RuleIndex {
    fn build(rules: &[FieldEncryptionRule]) -> Result<Self> {
        let mut by_scope_table: HashMap<(String, String), RuleConfig> = HashMap::new();
        let mut tables_by_scope: HashMap<String, BTreeSet<String>> = HashMap::new();
        let mut scopes_by_table: HashMap<String, BTreeSet<String>> = HashMap::new();

        for rule in rules {
            if rule.scope.trim().is_empty() {
                return Err(SyncularError::config(
                    "field encryption rule scope cannot be empty",
                ));
            }
            let table = rule.table.clone().unwrap_or_else(|| "*".to_string());
            if table.trim().is_empty() {
                return Err(SyncularError::config(
                    "field encryption rule table cannot be empty",
                ));
            }
            if rule.fields.is_empty() {
                return Err(SyncularError::config(format!(
                    "field encryption rule {}/{} has no fields",
                    rule.scope, table
                )));
            }
            for field in &rule.fields {
                if field.trim().is_empty() {
                    return Err(SyncularError::config(format!(
                        "field encryption rule {}/{} has an empty field",
                        rule.scope, table
                    )));
                }
            }

            let row_id_field = rule
                .row_id_field
                .clone()
                .unwrap_or_else(|| "id".to_string());
            let key = (rule.scope.clone(), table.clone());
            let entry = by_scope_table.entry(key).or_insert_with(|| RuleConfig {
                fields: BTreeSet::new(),
                row_id_field: row_id_field.clone(),
            });
            if entry.row_id_field != row_id_field {
                return Err(SyncularError::config(format!(
                    "conflicting rowIdField for field encryption rule {}/{}",
                    rule.scope, table
                )));
            }
            for field in &rule.fields {
                entry.fields.insert(field.clone());
            }

            if table != "*" {
                tables_by_scope
                    .entry(rule.scope.clone())
                    .or_default()
                    .insert(table.clone());
                scopes_by_table
                    .entry(table)
                    .or_default()
                    .insert(rule.scope.clone());
            }
        }

        Ok(Self {
            by_scope_table,
            tables_by_scope,
            scopes_by_table,
        })
    }

    fn config_for(&self, scope: &str, table: &str) -> Option<&RuleConfig> {
        self.by_scope_table
            .get(&(scope.to_string(), table.to_string()))
            .or_else(|| {
                self.by_scope_table
                    .get(&(scope.to_string(), "*".to_string()))
            })
    }

    fn only_table_for_scope(&self, scope: &str) -> Option<&str> {
        let tables = self.tables_by_scope.get(scope)?;
        if tables.len() == 1 {
            tables.iter().next().map(String::as_str)
        } else {
            None
        }
    }

    fn resolve_scope_and_table(&self, identifier: &str) -> ResolvedScopeTable {
        if self.config_for(identifier, identifier).is_some() {
            return ResolvedScopeTable {
                scope: identifier.to_string(),
                table: identifier.to_string(),
            };
        }

        if let Some(table) = self.only_table_for_scope(identifier) {
            return ResolvedScopeTable {
                scope: identifier.to_string(),
                table: table.to_string(),
            };
        }

        if let Some(scopes) = self.scopes_by_table.get(identifier) {
            if scopes.len() == 1 {
                return ResolvedScopeTable {
                    scope: scopes.iter().next().expect("one scope").clone(),
                    table: identifier.to_string(),
                };
            }
        }

        ResolvedScopeTable {
            scope: identifier.to_string(),
            table: identifier.to_string(),
        }
    }
}

struct ResolvedScopeTable {
    scope: String,
    table: String,
}

#[derive(Debug, Clone, Copy)]
enum TransformMode {
    Encrypt,
    Decrypt,
}

#[derive(Debug, Clone, Copy)]
struct FieldRecordTarget<'a> {
    scope: &'a str,
    table: &'a str,
    row_id: &'a str,
}

#[derive(Debug, Clone)]
struct DecodedEnvelope {
    kid: String,
    nonce: Vec<u8>,
    ciphertext: Vec<u8>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct X25519KeyPair {
    pub public_key: String,
    pub private_key: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WrappedKey {
    pub ephemeral_public: Vec<u8>,
    pub ciphertext: Vec<u8>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WrappedKeyJson {
    pub ephemeral_public: String,
    pub ciphertext: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "type")]
pub enum ParsedKeyShare {
    #[serde(rename = "symmetric")]
    Symmetric { key: String, kid: Option<String> },
    #[serde(rename = "publicKey")]
    PublicKey { public_key: String },
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Argon2idKeyDerivationParams {
    pub memory_kib: u32,
    pub iterations: u32,
    pub parallelism: u32,
}

impl Default for Argon2idKeyDerivationParams {
    fn default() -> Self {
        Self {
            memory_kib: 19 * 1024,
            iterations: 2,
            parallelism: 1,
        }
    }
}

pub fn generate_symmetric_key() -> Result<Vec<u8>> {
    random_bytes(32)
}

pub fn key_to_base64url(key: &[u8]) -> Result<String> {
    validate_32_bytes("key", key)?;
    Ok(URL_SAFE_NO_PAD.encode(key))
}

pub fn base64url_to_key(encoded: &str) -> Result<Vec<u8>> {
    let key = URL_SAFE_NO_PAD
        .decode(encoded)
        .map_err(|err| SyncularError::config(format!("invalid base64url key: {err}")))?;
    validate_32_bytes("key", &key)?;
    Ok(key)
}

pub fn key_to_mnemonic(key: &[u8]) -> Result<String> {
    validate_32_bytes("key", key)?;
    let mnemonic = Mnemonic::from_entropy_in(Language::English, key)
        .map_err(|err| SyncularError::config(format!("encode mnemonic: {err}")))?;
    Ok(mnemonic.to_string())
}

pub fn mnemonic_to_key(phrase: &str) -> Result<Vec<u8>> {
    let normalized = normalize_mnemonic_input(phrase);
    let mnemonic = Mnemonic::parse_in_normalized(Language::English, &normalized)
        .map_err(|err| SyncularError::config(format!("decode mnemonic: {err}")))?;
    let key = mnemonic.to_entropy();
    validate_32_bytes("mnemonic entropy", &key)?;
    Ok(key)
}

pub fn generate_x25519_keypair() -> Result<X25519KeyPair> {
    let private_key = random_array_32()?;
    let public_key = PublicKey::from(&StaticSecret::from(private_key));
    Ok(X25519KeyPair {
        public_key: URL_SAFE_NO_PAD.encode(public_key.as_bytes()),
        private_key: URL_SAFE_NO_PAD.encode(private_key),
    })
}

pub fn public_key_to_mnemonic(public_key: &[u8]) -> Result<String> {
    key_to_mnemonic(public_key)
}

pub fn mnemonic_to_public_key(phrase: &str) -> Result<Vec<u8>> {
    mnemonic_to_key(phrase)
}

pub fn wrap_key_for_recipient(
    recipient_public_key: &[u8],
    symmetric_key: &[u8],
) -> Result<WrappedKey> {
    let recipient_public_key =
        PublicKey::from(expect_32("recipient public key", recipient_public_key)?);
    validate_32_bytes("symmetric key", symmetric_key)?;

    let mut ephemeral_private = random_array_32()?;
    let ephemeral_secret = StaticSecret::from(ephemeral_private);
    let ephemeral_public = PublicKey::from(&ephemeral_secret);
    let shared_secret = ephemeral_secret.diffie_hellman(&recipient_public_key);
    validate_shared_secret(shared_secret.as_bytes())?;

    let mut wrapping_key = [0u8; 32];
    Hkdf::<Sha256>::new(Some(ephemeral_public.as_bytes()), shared_secret.as_bytes())
        .expand(KEY_WRAP_HKDF_INFO, &mut wrapping_key)
        .map_err(|err| SyncularError::protocol_message(format!("derive wrapping key: {err}")))?;
    let nonce = random_bytes(24)?;
    let encrypted = xchacha_encrypt(&wrapping_key, &nonce, &[], symmetric_key)?;
    wrapping_key.zeroize();
    ephemeral_private.zeroize();

    let mut ciphertext = Vec::with_capacity(24 + encrypted.len());
    ciphertext.extend_from_slice(&nonce);
    ciphertext.extend_from_slice(&encrypted);
    Ok(WrappedKey {
        ephemeral_public: ephemeral_public.as_bytes().to_vec(),
        ciphertext,
    })
}

pub fn unwrap_key(my_private_key: &[u8], wrapped: &WrappedKey) -> Result<Vec<u8>> {
    let my_private_key = StaticSecret::from(expect_32("private key", my_private_key)?);
    let ephemeral_public = PublicKey::from(expect_32(
        "ephemeral public key",
        &wrapped.ephemeral_public,
    )?);
    if wrapped.ciphertext.len() != 72 {
        return Err(SyncularError::protocol_message(format!(
            "wrapped key ciphertext must be 72 bytes, got {}",
            wrapped.ciphertext.len()
        )));
    }
    let shared_secret = my_private_key.diffie_hellman(&ephemeral_public);
    validate_shared_secret(shared_secret.as_bytes())?;

    let mut wrapping_key = [0u8; 32];
    Hkdf::<Sha256>::new(Some(&wrapped.ephemeral_public), shared_secret.as_bytes())
        .expand(KEY_WRAP_HKDF_INFO, &mut wrapping_key)
        .map_err(|err| SyncularError::protocol_message(format!("derive wrapping key: {err}")))?;
    let nonce = &wrapped.ciphertext[..24];
    let encrypted = &wrapped.ciphertext[24..];
    let key = xchacha_decrypt(&wrapping_key, nonce, &[], encrypted)?;
    wrapping_key.zeroize();
    validate_32_bytes("unwrapped key", &key)?;
    Ok(key)
}

pub fn encode_wrapped_key(wrapped: &WrappedKey) -> String {
    let mut combined =
        Vec::with_capacity(wrapped.ephemeral_public.len() + wrapped.ciphertext.len());
    combined.extend_from_slice(&wrapped.ephemeral_public);
    combined.extend_from_slice(&wrapped.ciphertext);
    URL_SAFE_NO_PAD.encode(combined)
}

pub fn decode_wrapped_key(encoded: &str) -> Result<WrappedKey> {
    let combined = URL_SAFE_NO_PAD
        .decode(encoded)
        .map_err(|err| SyncularError::protocol_message(format!("decode wrapped key: {err}")))?;
    if combined.len() != 104 {
        return Err(SyncularError::protocol_message(format!(
            "wrapped key must be 104 bytes, got {}",
            combined.len()
        )));
    }
    Ok(WrappedKey {
        ephemeral_public: combined[..32].to_vec(),
        ciphertext: combined[32..].to_vec(),
    })
}

pub fn wrapped_key_to_json(wrapped: &WrappedKey) -> WrappedKeyJson {
    WrappedKeyJson {
        ephemeral_public: URL_SAFE_NO_PAD.encode(&wrapped.ephemeral_public),
        ciphertext: URL_SAFE_NO_PAD.encode(&wrapped.ciphertext),
    }
}

pub fn wrapped_key_from_json(json: &WrappedKeyJson) -> Result<WrappedKey> {
    let ephemeral_public = base64url_to_key(&json.ephemeral_public)?;
    let ciphertext = URL_SAFE_NO_PAD.decode(&json.ciphertext).map_err(|err| {
        SyncularError::protocol_message(format!("decode wrapped key ciphertext: {err}"))
    })?;
    Ok(WrappedKey {
        ephemeral_public,
        ciphertext,
    })
}

pub fn key_to_share_url(key: &[u8], kid: Option<&str>) -> Result<String> {
    validate_32_bytes("key", key)?;
    if let Some(kid) = kid {
        validate_kid(kid)?;
    }
    let kid_part = kid
        .map(|kid| format!("/{}", percent_encode(kid)))
        .unwrap_or_default();
    Ok(format!(
        "sync://k/1/{}{}",
        URL_SAFE_NO_PAD.encode(key),
        kid_part
    ))
}

pub fn public_key_to_share_url(public_key: &[u8]) -> Result<String> {
    validate_32_bytes("public key", public_key)?;
    Ok(format!(
        "sync://pk/1/{}",
        URL_SAFE_NO_PAD.encode(public_key)
    ))
}

pub fn parse_share_url(url: &str) -> Result<ParsedKeyShare> {
    let Some(rest) = url.strip_prefix("sync://") else {
        return Err(SyncularError::config("share URL must start with sync://"));
    };
    let mut parts = rest.split('/');
    let share_type = parts
        .next()
        .ok_or_else(|| SyncularError::config("missing share URL type"))?;
    let version = parts
        .next()
        .ok_or_else(|| SyncularError::config("missing share URL version"))?;
    let encoded = parts
        .next()
        .ok_or_else(|| SyncularError::config("missing share URL key data"))?;
    if version != "1" {
        return Err(SyncularError::config(format!(
            "unsupported share URL version: {version}"
        )));
    }
    match share_type {
        "k" => {
            let key = base64url_to_key(encoded)?;
            let kid = parts.next().map(percent_decode).transpose()?;
            if let Some(kid) = kid.as_deref() {
                validate_kid(kid)?;
            }
            Ok(ParsedKeyShare::Symmetric {
                key: URL_SAFE_NO_PAD.encode(key),
                kid,
            })
        }
        "pk" => {
            let public_key = base64url_to_key(encoded)?;
            Ok(ParsedKeyShare::PublicKey {
                public_key: URL_SAFE_NO_PAD.encode(public_key),
            })
        }
        _ => Err(SyncularError::config(format!(
            "unknown share URL type: {share_type}"
        ))),
    }
}

pub fn derive_scoped_passphrase_key_pbkdf2(
    passphrase: &str,
    scope: &str,
    iterations: u32,
) -> Result<Vec<u8>> {
    if iterations == 0 {
        return Err(SyncularError::config(
            "PBKDF2 iteration count must be greater than zero",
        ));
    }
    let mut key = [0u8; 32];
    let salt = format!("sync-e2ee:{scope}");
    pbkdf2_hmac::<Sha256>(passphrase.as_bytes(), salt.as_bytes(), iterations, &mut key);
    Ok(key.to_vec())
}

pub fn derive_passphrase_key_argon2id(
    passphrase: &str,
    salt: &[u8],
    params: Argon2idKeyDerivationParams,
) -> Result<Vec<u8>> {
    if salt.len() < 8 {
        return Err(SyncularError::config(
            "Argon2id salt must be at least 8 bytes",
        ));
    }
    let params = Params::new(
        params.memory_kib,
        params.iterations,
        params.parallelism,
        Some(32),
    )
    .map_err(|err| SyncularError::config(format!("invalid Argon2id params: {err}")))?;
    let argon = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
    let mut key = [0u8; 32];
    argon
        .hash_password_into(passphrase.as_bytes(), salt, &mut key)
        .map_err(|err| SyncularError::config(format!("derive Argon2id key: {err}")))?;
    Ok(key.to_vec())
}

pub fn encryption_helpers_json(method: &str, args_json: &str) -> Result<String> {
    match method {
        "generateSymmetricKey" => Ok(serde_json::to_string(&key_to_base64url(
            &generate_symmetric_key()?,
        )?)?),
        "keyToMnemonic" => {
            let args: KeyMaterialArgs = serde_json::from_str(args_json)?;
            Ok(serde_json::to_string(&key_to_mnemonic(
                &decode_key_material(&args.key)?,
            )?)?)
        }
        "mnemonicToKey" => {
            let args: MnemonicArgs = serde_json::from_str(args_json)?;
            Ok(serde_json::to_string(&key_to_base64url(
                &mnemonic_to_key(&args.phrase)?,
            )?)?)
        }
        "generateKeypair" => Ok(serde_json::to_string(&generate_x25519_keypair()?)?),
        "wrapKeyForRecipient" => {
            let args: WrapKeyArgs = serde_json::from_str(args_json)?;
            let wrapped = wrap_key_for_recipient(
                &decode_key_material(&args.recipient_public_key)?,
                &decode_key_material(&args.symmetric_key)?,
            )?;
            Ok(serde_json::to_string(&encode_wrapped_key(&wrapped))?)
        }
        "unwrapKey" => {
            let args: UnwrapKeyArgs = serde_json::from_str(args_json)?;
            let wrapped = decode_wrapped_key(&args.wrapped_key)?;
            Ok(serde_json::to_string(&key_to_base64url(&unwrap_key(
                &decode_key_material(&args.private_key)?,
                &wrapped,
            )?)?)?)
        }
        "keyToShareUrl" => {
            let args: KeyShareUrlArgs = serde_json::from_str(args_json)?;
            Ok(serde_json::to_string(&key_to_share_url(
                &decode_key_material(&args.key)?,
                args.kid.as_deref(),
            )?)?)
        }
        "publicKeyToShareUrl" => {
            let args: PublicKeyArgs = serde_json::from_str(args_json)?;
            Ok(serde_json::to_string(&public_key_to_share_url(
                &decode_key_material(&args.public_key)?,
            )?)?)
        }
        "parseShareUrl" => {
            let args: ShareUrlArgs = serde_json::from_str(args_json)?;
            Ok(serde_json::to_string(&parse_share_url(&args.url)?)?)
        }
        "deriveScopedPassphraseKeyPbkdf2" => {
            let args: Pbkdf2Args = serde_json::from_str(args_json)?;
            Ok(serde_json::to_string(&key_to_base64url(
                &derive_scoped_passphrase_key_pbkdf2(
                    &args.passphrase,
                    &args.scope,
                    args.iterations.unwrap_or(100_000),
                )?,
            )?)?)
        }
        "derivePassphraseKeyArgon2id" => {
            let args: Argon2idArgs = serde_json::from_str(args_json)?;
            Ok(serde_json::to_string(&key_to_base64url(
                &derive_passphrase_key_argon2id(
                    &args.passphrase,
                    &decode_key_material(&args.salt)?,
                    args.params.unwrap_or_default(),
                )?,
            )?)?)
        }
        _ => Err(SyncularError::config(format!(
            "unknown encryption helper method: {method}"
        ))),
    }
}

fn encode_envelope(prefix: &str, kid: &str, nonce: &[u8], ciphertext: &[u8]) -> String {
    format!(
        "{prefix}{kid}:{}:{}",
        URL_SAFE_NO_PAD.encode(nonce),
        URL_SAFE_NO_PAD.encode(ciphertext)
    )
}

fn decode_envelope(prefix: &str, value: &str) -> Result<Option<DecodedEnvelope>> {
    let Some(rest) = value.strip_prefix(prefix) else {
        return Ok(None);
    };
    let mut parts = rest.split(':');
    let kid = parts.next().unwrap_or_default();
    let nonce = parts.next().unwrap_or_default();
    let ciphertext = parts.next().unwrap_or_default();
    if parts.next().is_some() || kid.is_empty() || nonce.is_empty() || ciphertext.is_empty() {
        return Ok(None);
    }
    let nonce = URL_SAFE_NO_PAD.decode(nonce).map_err(|err| {
        SyncularError::protocol_message(format!("decode encryption nonce: {err}"))
    })?;
    let ciphertext = URL_SAFE_NO_PAD
        .decode(ciphertext)
        .map_err(|err| SyncularError::protocol_message(format!("decode ciphertext: {err}")))?;
    Ok(Some(DecodedEnvelope {
        kid: kid.to_string(),
        nonce,
        ciphertext,
    }))
}

pub(crate) fn xchacha_encrypt(
    key: &[u8],
    nonce: &[u8],
    aad: &[u8],
    plaintext: &[u8],
) -> Result<Vec<u8>> {
    validate_32_bytes("XChaCha20-Poly1305 key", key)?;
    if nonce.len() != 24 {
        return Err(SyncularError::protocol_message(format!(
            "XChaCha20-Poly1305 nonce must be 24 bytes, got {}",
            nonce.len()
        )));
    }
    let cipher = XChaCha20Poly1305::new_from_slice(key)
        .map_err(|_| SyncularError::protocol_message("invalid XChaCha20-Poly1305 key"))?;
    cipher
        .encrypt(
            XNonce::from_slice(nonce),
            Payload {
                msg: plaintext,
                aad,
            },
        )
        .map_err(|_| SyncularError::protocol_message("XChaCha20-Poly1305 encryption failed"))
}

pub(crate) fn xchacha_decrypt(
    key: &[u8],
    nonce: &[u8],
    aad: &[u8],
    ciphertext: &[u8],
) -> Result<Vec<u8>> {
    validate_32_bytes("XChaCha20-Poly1305 key", key)?;
    if nonce.len() != 24 {
        return Err(SyncularError::protocol_message(format!(
            "XChaCha20-Poly1305 nonce must be 24 bytes, got {}",
            nonce.len()
        )));
    }
    let cipher = XChaCha20Poly1305::new_from_slice(key)
        .map_err(|_| SyncularError::protocol_message("invalid XChaCha20-Poly1305 key"))?;
    cipher
        .decrypt(
            XNonce::from_slice(nonce),
            Payload {
                msg: ciphertext,
                aad,
            },
        )
        .map_err(|_| SyncularError::protocol_message("XChaCha20-Poly1305 decryption failed"))
}

fn make_aad(scope: &str, table: &str, row_id: &str, field: &str) -> Vec<u8> {
    format!("{scope}\u{1f}{table}\u{1f}{row_id}\u{1f}{field}").into_bytes()
}

fn make_blob_aad(kid: &str, mime_type: &str) -> Vec<u8> {
    format!(
        "syncular:blob:v1\u{1f}{kid}\u{1f}{}",
        normalize_blob_mime_type(mime_type)
    )
    .into_bytes()
}

fn snapshot_row_id(
    row: &Map<String, Value>,
    row_id_field: &str,
    scope: &str,
    table: &str,
) -> Result<String> {
    let row_id = row
        .get(row_id_field)
        .and_then(|value| match value {
            Value::String(value) => Some(value.clone()),
            Value::Number(value) => Some(value.to_string()),
            _ => None,
        })
        .unwrap_or_default();
    if row_id.is_empty() {
        return Err(SyncularError::protocol_message(format!(
            "snapshot row for {scope}/{table} is missing row id field \"{row_id_field}\""
        )));
    }
    Ok(row_id)
}

pub(crate) fn random_bytes(length: usize) -> Result<Vec<u8>> {
    let mut bytes = vec![0u8; length];
    getrandom::getrandom(&mut bytes)
        .map_err(|err| SyncularError::config(format!("secure random generator failed: {err}")))?;
    Ok(bytes)
}

fn random_array_32() -> Result<[u8; 32]> {
    let mut bytes = [0u8; 32];
    getrandom::getrandom(&mut bytes)
        .map_err(|err| SyncularError::config(format!("secure random generator failed: {err}")))?;
    Ok(bytes)
}

pub(crate) fn validate_32_bytes(label: &str, bytes: &[u8]) -> Result<()> {
    if bytes.len() != 32 {
        return Err(SyncularError::config(format!(
            "{label} must be 32 bytes, got {}",
            bytes.len()
        )));
    }
    Ok(())
}

fn expect_32(label: &str, bytes: &[u8]) -> Result<[u8; 32]> {
    validate_32_bytes(label, bytes)?;
    let mut out = [0u8; 32];
    out.copy_from_slice(bytes);
    Ok(out)
}

fn validate_kid(kid: &str) -> Result<()> {
    if kid.is_empty() {
        return Err(SyncularError::config("encryption key id cannot be empty"));
    }
    if kid.contains(':') {
        return Err(SyncularError::config(
            "encryption key id must not contain ':'",
        ));
    }
    Ok(())
}

fn validate_shared_secret(shared_secret: &[u8; 32]) -> Result<()> {
    if shared_secret.iter().all(|byte| *byte == 0) {
        return Err(SyncularError::protocol_message(
            "X25519 shared secret is all zeros",
        ));
    }
    Ok(())
}

fn decode_key_material(material: &str) -> Result<Vec<u8>> {
    let trimmed = material.trim();
    let decoded = if let Some(rest) = trimmed.strip_prefix("hex:") {
        hex::decode(rest).map_err(|err| SyncularError::config(format!("invalid hex key: {err}")))?
    } else if let Some(rest) = trimmed.strip_prefix("base64:") {
        STANDARD
            .decode(rest)
            .map_err(|err| SyncularError::config(format!("invalid base64 key: {err}")))?
    } else if let Some(rest) = trimmed.strip_prefix("base64url:") {
        URL_SAFE_NO_PAD
            .decode(rest)
            .map_err(|err| SyncularError::config(format!("invalid base64url key: {err}")))?
    } else if trimmed.len() == 64 && trimmed.bytes().all(|byte| byte.is_ascii_hexdigit()) {
        hex::decode(trimmed)
            .map_err(|err| SyncularError::config(format!("invalid hex key: {err}")))?
    } else {
        URL_SAFE_NO_PAD
            .decode(trimmed)
            .map_err(|err| SyncularError::config(format!("invalid base64url key: {err}")))?
    };
    validate_32_bytes("key material", &decoded)?;
    Ok(decoded)
}

fn normalize_mnemonic_input(phrase: &str) -> String {
    phrase
        .trim()
        .to_lowercase()
        .split_whitespace()
        .collect::<Vec<_>>()
        .join(" ")
}

fn percent_encode(value: &str) -> String {
    let mut out = String::new();
    for byte in value.bytes() {
        if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~') {
            out.push(byte as char);
        } else {
            out.push_str(&format!("%{byte:02X}"));
        }
    }
    out
}

fn percent_decode(value: &str) -> Result<String> {
    let bytes = value.as_bytes();
    let mut out = Vec::with_capacity(bytes.len());
    let mut index = 0usize;
    while index < bytes.len() {
        if bytes[index] != b'%' {
            out.push(bytes[index]);
            index += 1;
            continue;
        }
        if index + 2 >= bytes.len() {
            return Err(SyncularError::config("invalid percent-encoded key id"));
        }
        let hex = std::str::from_utf8(&bytes[index + 1..index + 3])
            .map_err(|_| SyncularError::config("invalid percent-encoded key id"))?;
        let byte = u8::from_str_radix(hex, 16)
            .map_err(|_| SyncularError::config("invalid percent-encoded key id"))?;
        out.push(byte);
        index += 3;
    }
    String::from_utf8(out).map_err(|_| SyncularError::config("invalid UTF-8 key id"))
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct KeyMaterialArgs {
    key: String,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MnemonicArgs {
    phrase: String,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct WrapKeyArgs {
    recipient_public_key: String,
    symmetric_key: String,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct UnwrapKeyArgs {
    private_key: String,
    wrapped_key: String,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct KeyShareUrlArgs {
    key: String,
    kid: Option<String>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PublicKeyArgs {
    public_key: String,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ShareUrlArgs {
    url: String,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Pbkdf2Args {
    passphrase: String,
    scope: String,
    iterations: Option<u32>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Argon2idArgs {
    passphrase: String,
    salt: String,
    params: Option<Argon2idKeyDerivationParams>,
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::protocol::{PullResponse, SubscriptionResponse, SyncCommit, SyncSnapshot};
    use serde_json::json;

    fn encryption() -> FieldEncryption {
        let mut keys = BTreeMap::new();
        keys.insert("default".to_string(), URL_SAFE_NO_PAD.encode([7u8; 32]));
        FieldEncryption::from_static_config(StaticFieldEncryptionConfig {
            rules: vec![FieldEncryptionRule {
                scope: "tasks".to_string(),
                table: Some("tasks".to_string()),
                fields: vec!["title".to_string()],
                row_id_field: None,
            }],
            keys,
            encryption_kid: None,
            decryption_error_mode: None,
            envelope_prefix: None,
        })
        .expect("encryption")
    }

    fn ctx() -> FieldEncryptionContext {
        FieldEncryptionContext {
            actor_id: "user-rust".to_string(),
            client_id: "client-rust".to_string(),
        }
    }

    fn blob_encryption() -> BlobEncryption {
        let mut keys = BTreeMap::new();
        keys.insert("default".to_string(), URL_SAFE_NO_PAD.encode([9u8; 32]));
        BlobEncryption::from_static_config(StaticBlobEncryptionConfig {
            keys,
            encryption_kid: None,
        })
        .expect("blob encryption")
    }

    #[test]
    fn encrypts_push_and_decrypts_pull_rows() -> Result<()> {
        let encryption = encryption();
        let op = SyncOperation {
            table: "tasks".to_string(),
            row_id: "t1".to_string(),
            op: "upsert".to_string(),
            payload: Some(json!({ "title": "Secret", "completed": 0 })),
            base_version: None,
        };
        let encrypted = encryption.transform_operations_for_push(&ctx(), vec![op.clone()])?;
        let payload = encrypted[0].payload.as_ref().expect("payload");
        let title = payload["title"].as_str().expect("encrypted title");
        assert!(title.starts_with(DEFAULT_FIELD_ENCRYPTION_PREFIX));
        assert_ne!(title, "Secret");

        let pull = PullResponse {
            ok: true,
            subscriptions: vec![SubscriptionResponse {
                id: "sub-tasks".to_string(),
                status: "active".to_string(),
                scopes: Map::new(),
                bootstrap: true,
                bootstrap_state: None,
                next_cursor: 1,
                integrity: None,
                commits: Vec::new(),
                snapshots: Some(vec![SyncSnapshot {
                    table: "tasks".to_string(),
                    rows: vec![json!({ "id": "t1", "title": title, "completed": 0 })],
                    chunks: None,
                    artifacts: None,
                    manifest: None,
                    is_first_page: true,
                    is_last_page: true,
                    bootstrap_state_after: None,
                }]),
            }],
        };
        let decrypted = encryption.transform_pull_response(&ctx(), pull)?;
        let row = &decrypted.subscriptions[0].snapshots.as_ref().unwrap()[0].rows[0];
        assert_eq!(row["title"], "Secret");
        Ok(())
    }

    #[test]
    fn encrypted_blob_body_roundtrips_with_ciphertext_ref() -> Result<()> {
        let encryption = blob_encryption();
        let plaintext = b"top secret blob payload";
        let encrypted = encryption.encrypt_blob(plaintext, "text/plain")?;
        assert!(encrypted.blob.encrypted);
        assert_eq!(encrypted.blob.key_id.as_deref(), Some("default"));
        assert_eq!(encrypted.blob.hash, blob_hash(&encrypted.body));
        assert_ne!(encrypted.blob.hash, blob_hash(plaintext));
        assert_ne!(encrypted.body, plaintext);

        let decrypted = encryption.decrypt_blob(&encrypted.blob, &encrypted.body)?;
        assert_eq!(decrypted, plaintext);
        Ok(())
    }

    #[test]
    fn encrypted_blob_decryption_authenticates_metadata() -> Result<()> {
        let encryption = blob_encryption();
        let encrypted = encryption.encrypt_blob(b"payload", "text/plain")?;

        let mut tampered_mime = encrypted.blob.clone();
        tampered_mime.mime_type = "application/json".to_string();
        let error = encryption
            .decrypt_blob(&tampered_mime, &encrypted.body)
            .unwrap_err();
        assert!(error.to_string().contains("decryption failed"));

        let mut tampered_key = encrypted.blob.clone();
        tampered_key.key_id = Some("missing".to_string());
        let error = encryption
            .decrypt_blob(&tampered_key, &encrypted.body)
            .unwrap_err();
        assert!(error.to_string().contains("Missing blob encryption key"));
        Ok(())
    }

    #[test]
    fn decrypts_incremental_changes() -> Result<()> {
        let encryption = encryption();
        let encrypted = encryption.transform_operations_for_push(
            &ctx(),
            vec![SyncOperation {
                table: "tasks".to_string(),
                row_id: "t2".to_string(),
                op: "upsert".to_string(),
                payload: Some(json!({ "title": "Incremental" })),
                base_version: None,
            }],
        )?;
        let title = encrypted[0].payload.as_ref().unwrap()["title"]
            .as_str()
            .unwrap()
            .to_string();
        let pull = PullResponse {
            ok: true,
            subscriptions: vec![SubscriptionResponse {
                id: "sub-tasks".to_string(),
                status: "active".to_string(),
                scopes: Map::new(),
                bootstrap: false,
                bootstrap_state: None,
                next_cursor: 2,
                integrity: None,
                snapshots: None,
                commits: vec![SyncCommit {
                    commit_seq: 2,
                    created_at: "2026-05-10T00:00:00.000Z".to_string(),
                    actor_id: "other".to_string(),
                    changes: vec![SyncChange {
                        table: "tasks".to_string(),
                        row_id: "t2".to_string(),
                        op: "upsert".to_string(),
                        row_json: Some(json!({ "id": "t2", "title": title })),
                        row_version: Some(1),
                        scopes: Map::new(),
                    }],
                }],
            }],
        };
        let decrypted = encryption.transform_pull_response(&ctx(), pull)?;
        let change = &decrypted.subscriptions[0].commits[0].changes[0];
        assert_eq!(change.row_json.as_ref().unwrap()["title"], "Incremental");
        Ok(())
    }

    #[test]
    fn key_wrapping_roundtrips() -> Result<()> {
        let alice = generate_x25519_keypair()?;
        let key = generate_symmetric_key()?;
        let wrapped = wrap_key_for_recipient(&decode_key_material(&alice.public_key)?, &key)?;
        let unwrapped = unwrap_key(&decode_key_material(&alice.private_key)?, &wrapped)?;
        assert_eq!(unwrapped, key);
        Ok(())
    }

    #[test]
    fn static_reader_keys_do_not_require_default_encryption_kid() -> Result<()> {
        let mut keys = BTreeMap::new();
        keys.insert("k1".to_string(), URL_SAFE_NO_PAD.encode([1u8; 32]));
        let provider = StaticFieldEncryptionKeys::from_key_material(keys, None)?;
        assert!(provider.get_key("k1").is_ok());
        assert_eq!(
            provider.encryption_kid(
                &ctx(),
                &FieldEncryptionTarget {
                    scope: "tasks".to_string(),
                    table: "tasks".to_string(),
                    row_id: "t1".to_string(),
                    field: "title".to_string(),
                }
            )?,
            "default"
        );
        Ok(())
    }

    #[test]
    fn key_share_url_roundtrips() -> Result<()> {
        let key = [9u8; 32];
        let url = key_to_share_url(&key, Some("scope~patient%3Ap1"))?;
        let parsed = parse_share_url(&url)?;
        assert_eq!(
            parsed,
            ParsedKeyShare::Symmetric {
                key: URL_SAFE_NO_PAD.encode(key),
                kid: Some("scope~patient%3Ap1".to_string())
            }
        );
        Ok(())
    }

    #[test]
    fn mnemonic_roundtrips_32_byte_keys() -> Result<()> {
        let key = [3u8; 32];
        let phrase = key_to_mnemonic(&key)?;
        let decoded = mnemonic_to_key(&phrase)?;
        assert_eq!(decoded, key);
        Ok(())
    }
}
