use crate::crypto;
use crate::sync::{
    SyncConflict, SyncConflictResolution, SyncConflictResolutionResult, SyncError, SyncOutcome,
    SyncPlanConflict, SyncResult, SyncUploadConflict,
};
use crate::{
    client::{HcfsClient, HcfsClientConfig},
    sync::{
        FetchProgressFn, ScanProgressFn, SyncConflictInfo, SyncConflictType, SyncMode, SyncPlan,
        SyncProgress, SyncProgressFn, SyncState,
    },
};
use bip39::Mnemonic;
use blake3::Hasher as Blake3Hasher;
use bytes::Bytes;
use chacha20poly1305::{
    XChaCha20Poly1305,
    aead::{Aead, AeadCore, AeadInPlace, KeyInit, OsRng},
};
use ed25519_dalek::{Signer, SigningKey};
use hcfs_shared::network::Manifest;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::io::{BufReader, BufWriter, Write};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::sync::mpsc;
use tracing::{debug, info, warn};

// Re-import from crypto module for local use
use crate::crypto::ENCRYPTION_CHUNK_SIZE;

/// Number of bytes to use when creating short file ID hex strings for logging/temp files
const FILE_ID_SHORT_BYTES: usize = 8;

/// Maximum concurrent file uploads
const MAX_CONCURRENT_UPLOADS: usize = 4;

/// Maximum concurrent file downloads
const MAX_CONCURRENT_DOWNLOADS: usize = 4;

/// Shared context for concurrent file operations.
/// All fields are cheaply cloneable (Arc or Copy).
#[derive(Clone)]
struct FileOpCtx {
    client: Arc<HcfsClient>,
    signing_key: SigningKey,
    encryption_key: [u8; 32],
    root_path: PathBuf,
    config_directory: PathBuf,
    encrypt_progress: Option<SyncProgressFn>,
    upload_progress: Option<SyncProgressFn>,
    download_progress: Option<SyncProgressFn>,
    decrypt_progress: Option<SyncProgressFn>,
}

/// Returns true if path should be skipped during directory walk (hidden or config)
fn should_skip_path(path: &Path) -> bool {
    let Some(file_name) = path.file_name().and_then(|n| n.to_str()) else {
        return false;
    };
    // Skip .hippius config directory and hidden files (starting with .)
    file_name == ".hippius" || file_name.starts_with('.')
}

/// Unique identifier for a file (BLAKE3 hash of relative path)
pub type FileId = [u8; 32];

/// Result of a successful file upload, containing metadata and the Arion CID.
struct UploadedFile {
    metadata: FileMetadata,
    arion_hash: String,
}

/// Unique revision identifier (random UUID-like bytes)
pub type RevisionId = [u8; 32];

/// Placeholder nonce for metadata retrieved from server (actual nonce regenerated on upload)
pub const PLACEHOLDER_NONCE: [u8; 24] = [0u8; 24];

/// File metadata stored in each tree
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct FileMetadata {
    /// BLAKE3 hash of relative path (for privacy - plaintext paths never leave client)
    pub path_hash: FileId,
    /// BLAKE3(user_id || plaintext) - salted content hash for privacy-preserving dedup
    pub salted_hash: [u8; 32],
    /// File size in bytes (for progress reporting)
    pub size_bytes: u64,
    /// Per-file revision sequence number (monotonic, for replay protection)
    pub revision_seq: u64,
    /// Unique revision ID for this version
    pub revision_id: RevisionId,
    /// Encryption nonce for this file version (24 bytes for XChaCha20-Poly1305)
    pub encryption_nonce: [u8; 24],
}

impl FileMetadata {
    /// Check if two file metadata entries represent the same content
    #[must_use]
    pub fn content_matches(&self, other: &FileMetadata) -> bool {
        self.salted_hash == other.salted_hash
    }
}

/// Represents one of the three trees in the sync model
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct FileTree {
    /// Version number for this tree snapshot
    pub version: u64,
    /// Files indexed by path_hash (FileId)
    #[serde(default, with = "hcfs_shared::utils::hex_key_map")]
    pub files: HashMap<FileId, FileMetadata>,
}

impl FileTree {
    pub fn get(&self, path_hash: &FileId) -> Option<&FileMetadata> {
        self.files.get(path_hash)
    }

    pub fn insert(&mut self, metadata: FileMetadata) {
        self.files.insert(metadata.path_hash, metadata);
    }

    pub fn remove(&mut self, path_hash: &FileId) -> Option<FileMetadata> {
        self.files.remove(path_hash)
    }

    #[must_use]
    pub fn contains(&self, path_hash: &FileId) -> bool {
        self.files.contains_key(path_hash)
    }

    #[must_use]
    pub fn len(&self) -> usize {
        self.files.len()
    }

    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.files.is_empty()
    }
}

#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
pub struct Drive {
    root_path: PathBuf,
    config_directory: PathBuf,
    encrypted_mnemonic_path: PathBuf,
    sync_state_path: PathBuf,
    sync_state_backup_path: PathBuf,
    /// Optional HCFS client for network operations (Arc for concurrent task sharing)
    client: Option<Arc<HcfsClient>>,
    /// Cached signing key (after unlock)
    signing_key: Option<SigningKey>,
    /// Cached encryption key (derived from mnemonic seed)
    encryption_key: Option<[u8; 32]>,
    /// SS58 address for network operations (from config, or hex fallback)
    ss58_address: Option<String>,
    /// 16-char hex folder hash
    folder_hash: Option<String>,
    /// Optional progress callback for uploads
    upload_progress: Option<SyncProgressFn>,
    /// Optional progress callback for downloads
    download_progress: Option<SyncProgressFn>,
    /// Optional progress callback for encryption
    encrypt_progress: Option<SyncProgressFn>,
    /// Optional progress callback for decryption
    decrypt_progress: Option<SyncProgressFn>,
    /// Optional progress callback for directory scanning
    scan_progress: Option<ScanProgressFn>,
    /// Optional progress callback for fetching remote state
    fetch_state_progress: Option<FetchProgressFn>,
}

#[cfg(feature = "python")]
#[pyo3::prelude::pymethods]
impl Drive {
    #[new]
    #[pyo3(signature = (path, config_dir=None))]
    fn py_new(path: &str, config_dir: Option<&str>) -> Self {
        match config_dir {
            Some(cd) => Self::with_config_dir(path, cd),
            None => Self::new(path),
        }
    }

    #[pyo3(name = "init")]
    #[pyo3(signature = (password, existing_mnemonic=None))]
    fn py_init(
        &mut self,
        password: &str,
        existing_mnemonic: Option<&str>,
    ) -> pyo3::PyResult<String> {
        self.init(password, existing_mnemonic)
            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
    }

    #[pyo3(name = "unlock")]
    fn py_unlock(&mut self, password: &str) -> pyo3::PyResult<()> {
        self.unlock(password)
            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
    }

    #[pyo3(name = "set_config")]
    fn py_set_config(&mut self, config: HcfsClientConfig) -> pyo3::PyResult<()> {
        self.set_config(config)
            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
    }

    #[pyo3(name = "is_initialized")]
    fn py_is_initialized(&self) -> bool {
        self.is_initialized()
    }

    #[pyo3(name = "is_unlocked")]
    fn py_is_unlocked(&self) -> bool {
        self.is_unlocked()
    }

    #[pyo3(name = "user_id")]
    fn py_user_id(&self) -> Option<String> {
        self.user_id()
    }

    #[pyo3(name = "sync")]
    #[pyo3(signature = (resolver=None))]
    fn py_sync(&mut self, resolver: Option<pyo3::PyObject>) -> pyo3::PyResult<SyncOutcome> {
        let rt = tokio::runtime::Runtime::new()
            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;

        match resolver {
            Some(callback) => {
                let outcome =
                    rt.block_on(self.sync_with_resolver(SyncMode::Interactive, |conflict| {
                        pyo3::Python::with_gil(|py| {
                            let py_conflict = crate::sync::PySyncConflict::from(conflict);
                            match callback.call1(py, (py_conflict,)) {
                                Ok(result) => result
                                    .extract::<SyncConflictResolution>(py)
                                    .unwrap_or(SyncConflictResolution::Skip),
                                Err(_) => SyncConflictResolution::Skip,
                            }
                        })
                    }));
                outcome.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
            }
            None => self
                .sync(SyncMode::NonInteractive)
                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string())),
        }
    }

    /// Returns (SyncPlan, local_count, remote_count, synced_count, path_index)
    /// path_index maps hex file_id to relative path string
    #[pyo3(name = "stage")]
    fn py_stage(
        &self,
    ) -> pyo3::PyResult<(
        SyncPlan,
        usize,
        usize,
        usize,
        std::collections::HashMap<String, String>,
    )> {
        let mut state = self
            .load_sync_state()
            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
        self.scan_local_files(&mut state)
            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;

        let local_count = state.local.len();
        let remote_count = state.remote.len();
        let synced_count = state.synced.len();

        let path_index: std::collections::HashMap<String, String> = state
            .path_index
            .iter()
            .map(|(k, v)| (hex::encode(k), v.display().to_string()))
            .collect();

        let plan = SyncPlan::build(&state.local, &state.remote, &state.synced);

        Ok((plan, local_count, remote_count, synced_count, path_index))
    }

    #[pyo3(name = "config_directory")]
    fn py_config_directory(&self) -> String {
        self.config_directory().display().to_string()
    }

    /// Set progress handlers from Python callables.
    ///
    /// Upload/download/encrypt/decrypt callbacks receive: (bytes_completed: int, total_bytes: int, file_path: str | None)
    /// Scan callback receives: (files_scanned: int, current_path: str | None)
    /// Fetch state callback receives: (files_fetched: int, total_estimated: int)
    #[pyo3(name = "set_progress_handlers")]
    #[pyo3(signature = (
        on_upload=None,
        on_download=None,
        on_encrypt=None,
        on_decrypt=None,
        on_scan=None,
        on_fetch_state=None,
    ))]
    fn py_set_progress_handlers(
        &mut self,
        on_upload: Option<pyo3::PyObject>,
        on_download: Option<pyo3::PyObject>,
        on_encrypt: Option<pyo3::PyObject>,
        on_decrypt: Option<pyo3::PyObject>,
        on_scan: Option<pyo3::PyObject>,
        on_fetch_state: Option<pyo3::PyObject>,
    ) {
        fn wrap_sync_progress(cb: pyo3::PyObject) -> SyncProgressFn {
            Arc::new(move |bytes: u64, total: u64, path: Option<&str>| {
                pyo3::Python::with_gil(|py| {
                    let _ = cb.call1(py, (bytes, total, path));
                });
            })
        }

        fn wrap_scan_progress(cb: pyo3::PyObject) -> ScanProgressFn {
            Arc::new(move |files: u64, path: Option<&str>| {
                pyo3::Python::with_gil(|py| {
                    let _ = cb.call1(py, (files, path));
                });
            })
        }

        fn wrap_fetch_progress(cb: pyo3::PyObject) -> FetchProgressFn {
            Arc::new(move |fetched: u64, total: u64| {
                pyo3::Python::with_gil(|py| {
                    let _ = cb.call1(py, (fetched, total));
                });
            })
        }

        let progress = SyncProgress {
            on_upload_progress: on_upload.map(wrap_sync_progress),
            on_download_progress: on_download.map(wrap_sync_progress),
            on_encrypt_progress: on_encrypt.map(wrap_sync_progress),
            on_decrypt_progress: on_decrypt.map(wrap_sync_progress),
            on_scan_progress: on_scan.map(wrap_scan_progress),
            on_fetch_state_progress: on_fetch_state.map(wrap_fetch_progress),
        };
        self.set_progress_handlers(progress);
    }

    #[pyo3(name = "clear_progress_handlers")]
    fn py_clear_progress_handlers(&mut self) {
        self.clear_progress_handlers();
    }
}

impl Drive {
    pub fn new<P: AsRef<Path>>(path: P) -> Self {
        let root_path = path.as_ref();
        let config_directory = root_path.join(".hippius");
        Self::build(root_path.into(), config_directory)
    }

    /// Create a Drive with an explicit config directory separate from the sync folder.
    ///
    /// This keeps the sync folder clean — no `.hippius/` directory visible to the user.
    /// The config directory stores the encrypted mnemonic, sync state, and temp files.
    pub fn with_config_dir<P: AsRef<Path>, Q: AsRef<Path>>(sync_path: P, config_dir: Q) -> Self {
        Self::build(sync_path.as_ref().into(), config_dir.as_ref().into())
    }

    fn build(root_path: PathBuf, config_directory: PathBuf) -> Self {
        let encrypted_mnemonic_path = config_directory.join("enc_mnemonic.json");
        let sync_state_path = config_directory.join("sync_state.json");
        let sync_state_backup_path = config_directory.join("sync_state.json.bak");
        Self {
            root_path,
            config_directory,
            encrypted_mnemonic_path,
            sync_state_path,
            sync_state_backup_path,
            client: None,
            signing_key: None,
            encryption_key: None,
            ss58_address: None,
            folder_hash: None,
            upload_progress: None,
            download_progress: None,
            encrypt_progress: None,
            decrypt_progress: None,
            scan_progress: None,
            fetch_state_progress: None,
        }
    }

    /// Configure the HCFS client
    pub fn set_config(&mut self, config: HcfsClientConfig) -> SyncResult<()> {
        self.ss58_address = if config.ss58_address.is_empty() {
            None
        } else {
            Some(config.ss58_address.clone())
        };
        self.folder_hash = if config.folder_hash.is_empty() {
            None
        } else {
            Some(config.folder_hash.clone())
        };
        self.client = Some(Arc::new(HcfsClient::new(config)?));
        Ok(())
    }

    /// Set progress handlers for all sync operations
    /// The callbacks receive (bytes_completed, total_bytes, file_path)
    pub fn set_progress_handlers(&mut self, progress: SyncProgress) {
        self.upload_progress = progress.on_upload_progress;
        self.download_progress = progress.on_download_progress;
        self.encrypt_progress = progress.on_encrypt_progress;
        self.decrypt_progress = progress.on_decrypt_progress;
        self.scan_progress = progress.on_scan_progress;
        self.fetch_state_progress = progress.on_fetch_state_progress;
    }

    /// Clear progress handlers
    pub fn clear_progress_handlers(&mut self) {
        self.upload_progress = None;
        self.download_progress = None;
        self.encrypt_progress = None;
        self.decrypt_progress = None;
        self.scan_progress = None;
        self.fetch_state_progress = None;
    }

    /// Clean up stale temp files from interrupted operations.
    /// Removes files in `.hippius/temp/` older than 1 hour.
    pub fn cleanup_stale_temp_files(&self) {
        let temp_dir = self.config_directory.join("temp");
        if !temp_dir.exists() {
            return;
        }

        let one_hour_ago = SystemTime::now()
            .checked_sub(std::time::Duration::from_secs(3600))
            .unwrap_or(UNIX_EPOCH);

        let entries = match std::fs::read_dir(&temp_dir) {
            Ok(e) => e,
            Err(_) => return,
        };

        for entry in entries.flatten() {
            let path = entry.path();
            if !path.is_file() {
                continue;
            }

            // Check if file is older than 1 hour
            let is_stale = path
                .metadata()
                .and_then(|m| m.modified())
                .map(|mtime| mtime < one_hour_ago)
                .unwrap_or(false);

            if is_stale && std::fs::remove_file(&path).is_ok() {
                info!(path = %path.display(), "Removed stale temp file");
            }
        }
    }

    /// Unlock the drive with password (caches signing and encryption keys)
    pub fn unlock(&mut self, password: &str) -> Result<(), Box<dyn std::error::Error>> {
        // Clean up any stale temp files from interrupted operations
        self.cleanup_stale_temp_files();
        let mnemonic = crate::auth::recover_mnemonic(&self.encrypted_mnemonic_path, password)?;
        // Use empty passphrase for deterministic key derivation - same mnemonic always produces same user_id
        // The password is only used for encrypting the mnemonic at rest, not for key derivation
        let seed = mnemonic.to_seed("");

        // Cache signing key
        let mut secret_bytes = [0u8; 32];
        secret_bytes.copy_from_slice(&seed[..32]);
        let signing_key = SigningKey::from_bytes(&secret_bytes);

        // Use SS58 account address from config if available, otherwise fall back to hex-encoded public key
        let ss58 = self.ss58_address.clone().unwrap_or_else(|| {
            let public_key = signing_key.verifying_key();
            hex::encode(public_key.as_bytes())
        });

        // Fall back to ss58_address as bearer token only when no explicit
        // token was configured (e.g., via CLI --bearer-token).
        if let Some(ref mut arc_client) = self.client
            && let Some(client) = Arc::get_mut(arc_client)
            && client.configured_bearer_token().is_empty()
        {
            client.set_bearer_token(ss58.clone());
        }

        self.ss58_address = Some(ss58);
        self.signing_key = Some(signing_key);

        // Cache encryption key
        let mut enc_key = [0u8; 32];
        enc_key.copy_from_slice(&seed[..32]);
        self.encryption_key = Some(enc_key);

        Ok(())
    }

    /// Get the composite user ID for backward compatibility.
    /// Returns `ss58_address` + `_` + `folder_hash` when both are set,
    /// or just `ss58_address` if no folder hash.
    pub fn user_id(&self) -> Option<String> {
        match (&self.ss58_address, &self.folder_hash) {
            (Some(ss58), Some(fhash)) if !fhash.is_empty() => Some(format!("{}_{}", ss58, fhash)),
            (Some(ss58), _) => Some(ss58.clone()),
            _ => None,
        }
    }

    /// Get the SS58 address (available after unlock or set_config)
    pub fn ss58_address(&self) -> Option<&str> {
        self.ss58_address.as_deref()
    }

    /// Check if the drive is unlocked
    pub fn is_unlocked(&self) -> bool {
        self.signing_key.is_some() && self.encryption_key.is_some()
    }

    /// Get the HCFS client (if configured)
    pub fn client(&self) -> Option<&HcfsClient> {
        self.client.as_deref()
    }

    /// Build a `FileOpCtx` from current Drive state for concurrent operations.
    fn file_op_ctx(&self) -> SyncResult<FileOpCtx> {
        let client = self.client.clone().ok_or(SyncError::ServerNotConfigured)?;
        let signing_key = self.signing_key.clone().ok_or_else(|| {
            SyncError::Encryption("Drive not unlocked. Call unlock() first.".to_string())
        })?;
        let encryption_key = self.encryption_key.ok_or_else(|| {
            SyncError::Encryption("Drive not unlocked. Call unlock() first.".to_string())
        })?;
        Ok(FileOpCtx {
            client,
            signing_key,
            encryption_key,
            root_path: self.root_path.clone(),
            config_directory: self.config_directory.clone(),
            encrypt_progress: self.encrypt_progress.clone(),
            upload_progress: self.upload_progress.clone(),
            download_progress: self.download_progress.clone(),
            decrypt_progress: self.decrypt_progress.clone(),
        })
    }

    /// Check if the drive has been initialized
    pub fn is_initialized(&self) -> bool {
        self.config_directory.exists() && self.encrypted_mnemonic_path.exists()
    }

    /// Get the config directory path
    pub fn config_directory(&self) -> &Path {
        &self.config_directory
    }

    // =========================================================================
    // Initialization
    // =========================================================================

    /// Initialize sync folder, generate or recover keys
    ///
    /// If `existing_mnemonic` is provided, validates and uses it for recovery.
    /// If `None`, generates a new 24-word mnemonic.
    ///
    /// Returns the mnemonic string (caller should display only if newly generated).
    pub fn init(
        &mut self,
        password: &str,
        existing_mnemonic: Option<&str>,
    ) -> Result<String, Box<dyn std::error::Error>> {
        // Check if the directory we are targeting exists and its a directory
        if !self.root_path.is_dir() {
            return Err(format!(
                "Directory: {} does not exist or not a directory",
                self.root_path.display()
            )
            .into());
        }

        // Create config directory if it does not exist (create_dir_all supports nested external paths)
        if !self.config_directory.exists() {
            std::fs::create_dir_all(&self.config_directory)?;
        }

        // Create or recover the mnemonic
        let mnemonic = match existing_mnemonic {
            Some(phrase) => {
                // Validate and parse the provided mnemonic
                use std::str::FromStr;
                Mnemonic::from_str(phrase).map_err(|e| {
                    format!(
                        "Invalid recovery phrase: {}. Please check your words and try again.",
                        e
                    )
                })?
            }
            None => {
                // Generate a new 24-word mnemonic
                Mnemonic::generate(24)?
            }
        };
        let mnemonic_str = mnemonic.to_string();

        // Encrypt and save the mnemonic using the shared helper
        crate::auth::save_encrypted_mnemonic(
            &self.encrypted_mnemonic_path,
            &mnemonic_str,
            password,
        )?;

        // Initialize empty sync state
        let sync_state = SyncState::default();
        self.save_sync_state(&sync_state)?;

        Ok(mnemonic_str)
    }

    // =========================================================================
    // Sync State Persistence
    // =========================================================================

    /// Load sync state from disk, or create default if not exists
    pub fn load_sync_state(&self) -> SyncResult<SyncState> {
        // Clean up any stale temp file from interrupted save
        let temp = self.config_directory.join("sync_state.json.tmp");
        if temp.exists() {
            let _ = std::fs::remove_file(&temp);
        }

        if !self.sync_state_path.exists() {
            return Ok(SyncState::default());
        }

        let contents = std::fs::read_to_string(&self.sync_state_path)
            .map_err(|e| SyncError::StateLoad(e.to_string()))?;

        serde_json::from_str(&contents).map_err(|e| SyncError::StateLoad(e.to_string()))
    }

    /// Save sync state to disk with atomic write (write to temp, rename)
    pub fn save_sync_state(&self, state: &SyncState) -> SyncResult<()> {
        let temp = self.config_directory.join("sync_state.json.tmp");

        // Serialize to compact JSON (no whitespace, ~30% smaller, faster to parse)
        let contents =
            serde_json::to_string(state).map_err(|e| SyncError::StateSave(e.to_string()))?;

        // Write to temp file
        std::fs::write(&temp, &contents).map_err(|e| SyncError::StateSave(e.to_string()))?;

        // Backup existing state (if exists)
        if self.sync_state_path.exists() {
            let _ = std::fs::copy(&self.sync_state_path, &self.sync_state_backup_path);
        }

        // Atomic rename
        std::fs::rename(&temp, &self.sync_state_path)
            .map_err(|e| SyncError::StateSave(e.to_string()))?;

        Ok(())
    }

    // =========================================================================
    // Local File Scanning
    // =========================================================================

    /// Scan local filesystem and update local tree
    pub fn scan_local_files(&self, state: &mut SyncState) -> SyncResult<()> {
        state.local.files.clear();
        state.path_index.clear();

        // Clone immutable references to avoid borrow conflicts during walk
        let synced = state.synced.clone();
        let fs_cache = state.fs_cache.clone();
        let ss58_address = state.ss58_address.clone();

        let mut files_scanned = 0u64;

        self.walk_directory(
            &self.root_path,
            &ss58_address,
            &synced,
            &fs_cache,
            &mut state.local,
            &mut state.path_index,
            &mut files_scanned,
        )?;
        state.local.version += 1;

        Ok(())
    }

    /// Recursively walk directory and populate local tree
    #[allow(clippy::too_many_arguments)]
    fn walk_directory(
        &self,
        dir: &Path,
        ss58_address: &str,
        synced: &FileTree,
        fs_cache: &HashMap<FileId, crate::sync::CachedFileInfo>,
        local: &mut FileTree,
        path_index: &mut HashMap<FileId, PathBuf>,
        files_scanned: &mut u64,
    ) -> SyncResult<()> {
        for entry in std::fs::read_dir(dir)? {
            let path = entry?.path();

            if should_skip_path(&path) {
                continue;
            }

            if path.is_dir() {
                self.walk_directory(
                    &path,
                    ss58_address,
                    synced,
                    fs_cache,
                    local,
                    path_index,
                    files_scanned,
                )?;
            } else if path.is_file() {
                let metadata = self.compute_file_metadata(&path, ss58_address, synced, fs_cache)?;
                let path_hash = metadata.path_hash;

                let relative_path = path
                    .strip_prefix(&self.root_path)
                    .unwrap_or(&path)
                    .to_path_buf();
                path_index.insert(path_hash, relative_path.clone());

                *files_scanned += 1;
                if let Some(ref progress) = self.scan_progress {
                    let path_str = relative_path.to_string_lossy();
                    progress(*files_scanned, Some(&path_str));
                }
                local.insert(metadata);
            }
        }

        Ok(())
    }

    /// Compute file metadata for a local file using streaming hash (memory-efficient).
    /// Uses mtime/size cache to skip hashing when the file hasn't changed on disk.
    /// If file exists in synced tree with different content, increment revision_seq.
    fn compute_file_metadata(
        &self,
        path: &Path,
        ss58_address: &str,
        synced: &FileTree,
        fs_cache: &HashMap<FileId, crate::sync::CachedFileInfo>,
    ) -> SyncResult<FileMetadata> {
        let relative_path = path
            .strip_prefix(&self.root_path)
            .unwrap_or(path)
            .to_string_lossy();

        // Compute path hash
        let path_hash: [u8; 32] = blake3::hash(relative_path.as_bytes()).into();

        // Check mtime/size cache to skip expensive hashing
        let fs_meta = std::fs::metadata(path)?;
        let current_mtime = fs_meta
            .modified()
            .ok()
            .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
            .map(|d| d.as_secs())
            .unwrap_or(0);
        let current_size = fs_meta.len();

        if let (Some(cached_info), Some(synced_meta)) =
            (fs_cache.get(&path_hash), synced.get(&path_hash))
            && cached_info.mtime_secs == current_mtime
            && cached_info.size_bytes == current_size
        {
            return Ok(synced_meta.clone());
        }

        // File changed or new — compute hash
        let file = std::fs::File::open(path)?;
        let mut reader = BufReader::new(file);
        let (salted_hash, size_bytes) = crypto::compute_salted_hash(&mut reader, ss58_address)?;

        // Determine revision_seq: increment if file changed from synced version
        let revision_seq = match synced.get(&path_hash) {
            Some(existing) if existing.salted_hash != salted_hash => existing.revision_seq + 1,
            Some(existing) => existing.revision_seq,
            None => 1,
        };

        Ok(FileMetadata {
            path_hash,
            salted_hash,
            size_bytes,
            revision_seq,
            revision_id: rand::random(),
            encryption_nonce: rand::random(),
        })
    }

    // =========================================================================
    // Main Sync Flow
    // =========================================================================

    /// Show local changes and sync state (stage command)
    pub fn stage(&self) -> SyncResult<SyncPlan> {
        if !self.is_initialized() {
            return Err(SyncError::NotInitialized);
        }

        let mut state = self.load_sync_state()?;

        // Scan local filesystem
        self.scan_local_files(&mut state)?;

        // For now, remote tree comes from synced (we'd need network to get actual remote)
        // In a real implementation, we'd fetch from server here

        // Compute sync plan
        let plan = SyncPlan::build(&state.local, &state.remote, &state.synced);

        Ok(plan)
    }

    /// Main sync entry point (synchronous wrapper for async sync)
    pub fn sync(&mut self, mode: SyncMode) -> SyncResult<SyncOutcome> {
        // Use tokio runtime for async operations
        let rt = tokio::runtime::Runtime::new()
            .map_err(|e| SyncError::Network(format!("Failed to create runtime: {}", e)))?;

        rt.block_on(self.sync_async(mode))
    }

    /// Main sync entry point (async version)
    pub async fn sync_async(&mut self, mode: SyncMode) -> SyncResult<SyncOutcome> {
        self.sync_with_resolver(mode, |_| SyncConflictResolution::Skip)
            .await
    }

    /// Sync with a custom conflict resolver callback
    /// The callback receives a SyncConflict (either Plan or Upload) and returns the user's resolution choice.
    ///
    /// Note: The `mode` parameter is kept for API compatibility but behavior is now driven
    /// entirely by the `conflict_resolver` callback. Pass `SyncMode::NonInteractive` with
    /// a resolver that returns `Skip` to auto-skip conflicts.
    pub async fn sync_with_resolver<F>(
        &mut self,
        _mode: SyncMode,
        conflict_resolver: F,
    ) -> SyncResult<SyncOutcome>
    where
        F: Fn(&SyncConflict) -> SyncConflictResolution,
    {
        if !self.is_initialized() {
            return Err(SyncError::NotInitialized);
        }

        // Ensure drive is unlocked and ss58_address is available
        let ss58 = self.ss58_address.clone().ok_or_else(|| {
            SyncError::Encryption("Drive not unlocked. Call unlock() first.".to_string())
        })?;
        let fhash = self.folder_hash.clone().unwrap_or_default();

        // Step 1: Load current sync state
        let mut state = self.load_sync_state()?;

        // Migrate old composite ss58_address if folder_hash is empty
        // (from serde alias "user_id" -> ss58_address in old sync_state.json)
        if state.folder_hash.is_empty() && !state.ss58_address.is_empty() {
            if let Some(pos) = state.ss58_address.rfind('_') {
                let suffix = &state.ss58_address[pos + 1..];
                if suffix.len() == 16 && suffix.chars().all(|c| c.is_ascii_hexdigit()) {
                    state.folder_hash = suffix.to_string();
                    state.ss58_address = state.ss58_address[..pos].to_string();
                }
            }
        }

        // Detect identity change: clear sync state for a full resync
        if (!state.ss58_address.is_empty() && state.ss58_address != ss58)
            || (!state.folder_hash.is_empty() && !fhash.is_empty() && state.folder_hash != fhash)
        {
            warn!(old_ss58 = %state.ss58_address, new_ss58 = %ss58,
                old_fhash = %state.folder_hash, new_fhash = %fhash,
                "Identity changed. Clearing sync state for full resync.");
            state.local = Default::default();
            state.remote = Default::default();
            state.synced = Default::default();
            state.fs_cache.clear();
            state.path_index.clear();
            state.last_sync_timestamp = 0;
        }
        state.ss58_address = ss58;
        state.folder_hash = fhash;

        // Step 2: Scan local filesystem to update local tree
        self.scan_local_files(&mut state)?;

        // Step 3: Fetch remote state from server (if configured)
        if self.client.is_some() {
            self.fetch_remote_state(&mut state).await?;
        }

        // Step 4: Compute sync plan using three-tree comparison
        let plan = SyncPlan::build(&state.local, &state.remote, &state.synced);

        info!(
            uploads = plan.uploads.len(),
            downloads = plan.downloads.len(),
            local_deletes = plan.local_deletes.len(),
            remote_deletes = plan.remote_deletes.len(),
            conflicts = plan.conflicts.len(),
            "Computed sync plan"
        );

        // Track plan conflict resolutions and actions separately
        let mut plan_conflicts_resolved = 0usize;
        let mut plan_conflicts_skipped = 0usize;
        let mut plan_files_uploaded = 0usize;
        let mut plan_files_downloaded = 0usize;
        let mut plan_files_deleted_locally = 0usize;
        let mut plan_files_deleted_remotely = 0usize;

        // Step 5: Handle plan conflicts using the resolver
        // Both Interactive and NonInteractive modes use the resolver callback.
        // The difference is in how the CLI invokes sync_with_resolver:
        // - Interactive: prompts user for each conflict
        // - NonInteractive: uses a resolver that auto-skips conflicts
        if plan.has_conflicts() {
            for conflict_info in &plan.conflicts {
                let plan_conflict = SyncPlanConflict {
                    file_id: conflict_info.path_hash,
                    local_path: state.path_index.get(&conflict_info.path_hash).cloned(),
                    conflict_type: conflict_info.conflict_type.clone(),
                    has_local: conflict_info.local_hash.is_some(),
                    has_remote: conflict_info.remote_hash.is_some(),
                };

                let sync_conflict = SyncConflict::Plan(plan_conflict.clone());
                let resolution = conflict_resolver(&sync_conflict);

                match self
                    .resolve_plan_conflict(&plan_conflict, resolution, &mut state)
                    .await
                {
                    Ok(result) => {
                        if result.resolved {
                            plan_conflicts_resolved += 1;
                            // Track the actual actions taken
                            plan_files_uploaded += result.files_uploaded;
                            plan_files_downloaded += result.files_downloaded;
                            plan_files_deleted_locally += result.files_deleted_locally;
                            plan_files_deleted_remotely += result.files_deleted_remotely;
                        } else {
                            plan_conflicts_skipped += 1;
                        }
                    }
                    Err(e) => {
                        warn!(
                            file_id = %hex::encode(&conflict_info.path_hash[..8]),
                            error = %e,
                            "Plan conflict resolution failed"
                        );
                        plan_conflicts_skipped += 1;
                    }
                }
            }
        }

        // Step 6: Execute sync operations
        let mut outcome = self
            .execute_sync_plan(&plan, &mut state, &conflict_resolver)
            .await?;

        // Add plan conflict counts and actions to outcome
        outcome.conflicts_resolved += plan_conflicts_resolved;
        outcome.conflicts_skipped += plan_conflicts_skipped;
        outcome.files_uploaded += plan_files_uploaded;
        outcome.files_downloaded += plan_files_downloaded;
        outcome.files_deleted_locally += plan_files_deleted_locally;
        outcome.files_deleted_remotely += plan_files_deleted_remotely;

        // Step 7: Update synced tree to reflect new baseline
        self.update_synced_tree(&mut state);

        // Step 8: Save state to disk
        state.last_sync_timestamp = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map(|d| d.as_secs())
            .unwrap_or(0);
        self.save_sync_state(&state)?;

        Ok(outcome)
    }

    /// Fetch remote state from server
    async fn fetch_remote_state(&self, state: &mut SyncState) -> SyncResult<()> {
        let client = self.client.as_ref().ok_or(SyncError::ServerNotConfigured)?;

        info!(
            ss58_address = %state.ss58_address,
            folder_hash = %state.folder_hash,
            "Fetching remote state"
        );

        // Create progress callback for fetching remote state
        let fetch_progress = self.fetch_state_progress.clone();
        let progress_fn = fetch_progress.map(|p| {
            move |fetched: u64, total: u64| {
                p(fetched, total);
            }
        });

        let remote_files = client
            .get_all_files(&state.ss58_address, &state.folder_hash, progress_fn)
            .await?;

        // Update remote tree, encrypted paths, and file names
        state.remote.files.clear();
        state.remote_encrypted_paths.clear();
        state.remote_file_names.clear();
        // Note: remote_arion_hashes is NOT cleared here — hashes captured from
        // upload responses must survive across sync cycles even if the server
        // doesn't yet return arion_hash in RemoteFileEntry.
        for entry in remote_files {
            let metadata = FileMetadata {
                path_hash: entry.path_hash,
                salted_hash: entry.salted_hash,
                size_bytes: entry.size_bytes,
                revision_seq: entry.revision_seq,
                revision_id: entry.revision_id,
                encryption_nonce: [0u8; 24], // Not stored on server
            };
            state.remote.insert(metadata);

            // Store encrypted path for later decryption during download
            if !entry.encrypted_path.is_empty() {
                info!(
                    path_hash = %hex::encode(&entry.path_hash[..8]),
                    encrypted_path_len = entry.encrypted_path.len(),
                    "Storing encrypted path from remote"
                );
                state
                    .remote_encrypted_paths
                    .insert(entry.path_hash, entry.encrypted_path);
            } else {
                info!(
                    path_hash = %hex::encode(&entry.path_hash[..8]),
                    "No encrypted path available from remote (file uploaded before path encryption feature)"
                );
            }

            // Store plaintext file name for progress display during download
            if let Some(name) = entry.file_name {
                state.remote_file_names.insert(entry.path_hash, name);
            }

            // Store Arion CID for display in UI
            if let Some(cid) = entry.arion_hash {
                state.remote_arion_hashes.insert(entry.path_hash, cid);
            }
        }
        state.remote.version += 1;

        info!(
            file_count = state.remote.files.len(),
            "Remote state updated"
        );

        Ok(())
    }

    /// Execute the sync plan with actual network operations.
    /// Uploads and downloads run concurrently (bounded by semaphore).
    /// Conflicts and deletes are processed serially.
    async fn execute_sync_plan<F>(
        &mut self,
        plan: &SyncPlan,
        state: &mut SyncState,
        conflict_resolver: &F,
    ) -> SyncResult<SyncOutcome>
    where
        F: Fn(&SyncConflict) -> SyncConflictResolution,
    {
        let mut outcome = SyncOutcome::default();

        // --- Concurrent uploads ---
        if !plan.uploads.is_empty() {
            let ctx = self.file_op_ctx()?;
            let semaphore = Arc::new(tokio::sync::Semaphore::new(MAX_CONCURRENT_UPLOADS));
            let mut upload_set = tokio::task::JoinSet::new();

            for file_id in &plan.uploads {
                let Some(local_meta) = state.local.get(file_id).cloned() else {
                    continue;
                };
                let Some(relative_path) = state.path_index.get(file_id).cloned() else {
                    continue;
                };
                let base_revision_id = state.synced.get(file_id).map(|m| m.revision_id);
                let ss58 = state.ss58_address.clone();
                let fhash = state.folder_hash.clone();
                let ctx = ctx.clone();
                let sem = semaphore.clone();
                let fid = *file_id;

                upload_set.spawn(async move {
                    let _permit = sem.acquire().await.expect("semaphore closed");
                    let result = upload_file_standalone(
                        &ctx,
                        fid,
                        local_meta,
                        relative_path,
                        ss58,
                        fhash,
                        base_revision_id,
                    )
                    .await;
                    (fid, result)
                });
            }

            // Collect upload results
            while let Some(join_result) = upload_set.join_next().await {
                match join_result {
                    Ok((fid, Ok(uploaded))) => {
                        state.remote_arion_hashes.insert(fid, uploaded.arion_hash);
                        state.synced.insert(uploaded.metadata.clone());
                        state.remote.insert(uploaded.metadata);
                        outcome.files_uploaded += 1;
                    }
                    Ok((
                        _fid,
                        Err(SyncError::Conflict {
                            file_id: conflict_file_id,
                            current_revision_id,
                            message,
                        }),
                    )) => {
                        let upload_conflict = SyncUploadConflict {
                            file_id: conflict_file_id,
                            local_path: state.path_index.get(&conflict_file_id).cloned(),
                            server_revision_id: current_revision_id,
                            base_revision_id: state
                                .synced
                                .get(&conflict_file_id)
                                .map(|m| m.revision_id),
                            message,
                        };
                        let sync_conflict = SyncConflict::Upload(upload_conflict.clone());
                        let resolution = conflict_resolver(&sync_conflict);
                        match self
                            .resolve_upload_conflict(&upload_conflict, resolution, state)
                            .await
                        {
                            Ok(result) => {
                                if result.resolved {
                                    outcome.conflicts_resolved += 1;
                                    outcome.files_uploaded += result.files_uploaded;
                                    outcome.files_downloaded += result.files_downloaded;
                                    outcome.files_deleted_locally += result.files_deleted_locally;
                                    outcome.files_deleted_remotely += result.files_deleted_remotely;
                                } else {
                                    outcome.conflicts_skipped += 1;
                                }
                            }
                            Err(e) => {
                                warn!(
                                    file_id = %hex::encode(&conflict_file_id[..8]),
                                    error = %e,
                                    "Conflict resolution failed"
                                );
                                outcome.conflicts_skipped += 1;
                            }
                        }
                    }
                    Ok((fid, Err(e))) => {
                        warn!(
                            file_id = %hex::encode(&fid[..8]),
                            error = %e,
                            "Upload failed"
                        );
                    }
                    Err(e) => {
                        warn!(error = %e, "Upload task panicked");
                    }
                }
            }
        }

        // --- Concurrent downloads ---
        if !plan.downloads.is_empty() {
            let ctx = self.file_op_ctx()?;
            let semaphore = Arc::new(tokio::sync::Semaphore::new(MAX_CONCURRENT_DOWNLOADS));
            let mut download_set = tokio::task::JoinSet::new();

            for file_id in &plan.downloads {
                let Some(remote_meta) = state.remote.get(file_id).cloned() else {
                    continue;
                };
                let known_path = state.path_index.get(file_id).cloned();
                let encrypted_remote_path = state.remote_encrypted_paths.get(file_id).cloned();
                let remote_file_name = state.remote_file_names.get(file_id).cloned();
                let ss58 = state.ss58_address.clone();
                let fhash = state.folder_hash.clone();
                let ctx = ctx.clone();
                let sem = semaphore.clone();
                let fid = *file_id;

                download_set.spawn(async move {
                    let _permit = sem.acquire().await.expect("semaphore closed");
                    let result = download_file_standalone(
                        &ctx,
                        fid,
                        remote_meta,
                        ss58,
                        fhash,
                        known_path,
                        encrypted_remote_path,
                        remote_file_name,
                    )
                    .await;
                    (fid, result)
                });
            }

            // Collect download results
            while let Some(join_result) = download_set.join_next().await {
                match join_result {
                    Ok((fid, Ok((local_meta, relative_path)))) => {
                        state.local.insert(local_meta.clone());
                        state.synced.insert(local_meta);
                        state.path_index.insert(fid, relative_path);
                        outcome.files_downloaded += 1;
                    }
                    Ok((fid, Err(e))) => {
                        warn!(
                            file_id = %hex::encode(&fid[..8]),
                            error = %e,
                            "Download failed"
                        );
                    }
                    Err(e) => {
                        warn!(error = %e, "Download task panicked");
                    }
                }
            }
        }

        // Local deletes (delete local files that were deleted on server)
        for file_id in &plan.local_deletes {
            if let Some(path) = state.path_index.get(file_id).cloned() {
                let full_path = self.root_path.join(&path);
                if full_path.exists() {
                    if let Err(e) = std::fs::remove_file(&full_path) {
                        warn!(path = %full_path.display(), error = %e, "Failed to delete local file");
                    } else {
                        // Clean up empty parent directories
                        self.cleanup_empty_directories(&full_path);
                        state.local.remove(file_id);
                        state.synced.remove(file_id);
                        state.path_index.remove(file_id);
                        outcome.files_deleted_locally += 1;
                    }
                }
            }
        }

        // Remote deletes (delete files on server that were deleted locally)
        for file_id in &plan.remote_deletes {
            match self.delete_remote_file(file_id, state).await {
                Ok(()) => {
                    state.remote.remove(file_id);
                    state.synced.remove(file_id);
                    outcome.files_deleted_remotely += 1;
                }
                Err(e) => {
                    warn!(file_id = %hex::encode(&file_id[..8]), error = %e, "Remote delete failed");
                }
            }
        }

        Ok(outcome)
    }

    // =========================================================================
    // File Operations
    // =========================================================================

    /// Upload a file to the server using in-memory streaming.
    ///
    /// Encrypts the file to memory (single file read), then streams the
    /// encrypted chunks to the server. No temp files, no extra disk I/O.
    async fn upload_file(
        &self,
        file_id: &[u8; 32],
        metadata: &FileMetadata,
        state: &SyncState,
    ) -> SyncResult<UploadedFile> {
        let client = self.client.as_ref().ok_or(SyncError::ServerNotConfigured)?;
        let signing_key = self.signing_key.as_ref().ok_or(SyncError::Encryption(
            "Drive not unlocked. Call unlock() first.".to_string(),
        ))?;
        let encryption_key = self.encryption_key.ok_or(SyncError::Encryption(
            "Drive not unlocked. Call unlock() first.".to_string(),
        ))?;

        let short_id = hex::encode(&file_id[..FILE_ID_SHORT_BYTES]);

        // Get file path
        let relative_path = state
            .path_index
            .get(file_id)
            .ok_or(SyncError::FileNotFound(short_id.clone()))?;
        let full_path = self.root_path.join(relative_path);

        // Get file size for progress
        let plaintext_size = std::fs::metadata(&full_path)?.len();

        // Clone values needed for the encryption task
        let encryption_key_clone = encryption_key;
        let full_path_clone = full_path.clone();
        let path_str = relative_path.to_string_lossy().to_string();
        let encrypt_progress = self.encrypt_progress.clone();

        // Encrypt file to memory (returns Vec<Bytes> + hash + nonce)
        let (encrypted_chunks, ciphertext_hash, ciphertext_size, nonce) =
            tokio::task::spawn_blocking(move || {
                Self::encrypt_to_memory(
                    &full_path_clone,
                    &encryption_key_clone,
                    plaintext_size,
                    encrypt_progress.map(|p| {
                        let path_str = path_str.clone();
                        move |bytes: u64, total: u64| {
                            p(bytes, total, Some(&path_str));
                        }
                    }),
                )
            })
            .await
            .map_err(|e| SyncError::Encryption(format!("Encryption task failed: {}", e)))??;

        // Create manifest with the ciphertext hash
        let manifest_text = Manifest::generate_text(&ciphertext_hash);
        let signature = signing_key.sign(manifest_text.as_bytes());

        let timestamp = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map(|d| d.as_secs() as i64)
            .unwrap_or(0);

        // Check if file exists on server (for base_revision_id)
        let base_revision_id = state.synced.get(file_id).map(|m| m.revision_id);

        // Encrypt the relative path for storage on server
        let path_str = relative_path.to_string_lossy();
        debug!(
            file_id = %hex::encode(&file_id[..8]),
            path = %path_str,
            "Encrypting path for streaming upload"
        );
        let encrypted_path = self.encrypt_small(path_str.as_bytes(), &encryption_key)?;

        let file_name = relative_path
            .file_name()
            .map(|f| f.to_string_lossy().to_string());

        let manifest = Manifest {
            ss58_address: state.ss58_address.clone(),
            folder_hash: state.folder_hash.clone(),
            ciphertext_hash,
            size_bytes: metadata.size_bytes,
            timestamp,
            signature: signature.to_bytes(),
            signing_key: signing_key.verifying_key().to_bytes(),
            path_hash: metadata.path_hash,
            salted_hash: metadata.salted_hash,
            revision_seq: metadata.revision_seq,
            base_revision_id,
            encrypted_path,
            file_name,
        };

        // Create channel and spawn task to feed chunks into it
        let (tx, rx) = mpsc::channel::<Bytes>(32);
        let chunk_feeder = tokio::spawn(async move {
            for chunk in encrypted_chunks {
                if tx.send(chunk).await.is_err() {
                    break; // Receiver dropped
                }
            }
        });

        // Upload from channel with progress callback
        let path_for_progress = relative_path.display().to_string();
        let progress_fn = self.upload_progress.clone().map(|p| {
            let path_str = path_for_progress.clone();
            move |sent: u64, total: u64| {
                p(sent, total, Some(&path_str));
            }
        });

        let response = client
            .upload_from_channel(manifest, rx, ciphertext_size, progress_fn)
            .await?;

        // Ensure chunk feeder completed without panic
        if let Err(e) = chunk_feeder.await {
            warn!(error = %e, "Chunk feeder task panicked");
        }

        info!(
            file = %path_for_progress,
            size_bytes = plaintext_size,
            upload_id = %response.upload_id,
            "File uploaded (streaming)"
        );

        Ok(UploadedFile {
            metadata: FileMetadata {
                path_hash: metadata.path_hash,
                salted_hash: metadata.salted_hash,
                size_bytes: metadata.size_bytes,
                revision_seq: metadata.revision_seq,
                revision_id: response.revision_id,
                encryption_nonce: nonce,
            },
            arion_hash: response.upload_id,
        })
    }

    /// Encrypt file to memory, returning (chunks, hash, size, nonce)
    ///
    /// Collects all encrypted chunks into a Vec for later streaming to upload.
    /// This avoids the deadlock of bounded channels by completing encryption first.
    fn encrypt_to_memory<F>(
        full_path: &Path,
        encryption_key: &[u8; 32],
        plaintext_size: u64,
        progress: Option<F>,
    ) -> SyncResult<(Vec<Bytes>, String, u64, [u8; 24])>
    where
        F: Fn(u64, u64),
    {
        let cipher = XChaCha20Poly1305::new(encryption_key.into());
        let base_nonce: [u8; 24] = XChaCha20Poly1305::generate_nonce(&mut OsRng).into();

        // Report initial progress
        if let Some(ref p) = progress {
            p(0, plaintext_size);
        }

        // Pre-compute chunk count from file size
        let chunk_count = if plaintext_size == 0 {
            1u32
        } else {
            let count = plaintext_size.div_ceil(ENCRYPTION_CHUNK_SIZE as u64);
            if count > u32::MAX as u64 {
                return Err(SyncError::Encryption(
                    "File too large: chunk count exceeds u32::MAX".to_string(),
                ));
            }
            count as u32
        };

        // Compute ciphertext size: header + chunks
        let ciphertext_size = if plaintext_size == 0 {
            24 + 4 + 4 + 16
        } else {
            let full_chunks = plaintext_size / ENCRYPTION_CHUNK_SIZE as u64;
            let last_chunk_plaintext = plaintext_size % ENCRYPTION_CHUNK_SIZE as u64;
            let last_chunk_size = if last_chunk_plaintext == 0 {
                0
            } else {
                4 + last_chunk_plaintext + 16
            };
            24 + 4 + (full_chunks * (4 + ENCRYPTION_CHUNK_SIZE as u64 + 16)) + last_chunk_size
        };

        let mut hasher = Blake3Hasher::new();
        let mut chunks = Vec::with_capacity(chunk_count as usize + 1);

        // Build header
        let mut header = Vec::with_capacity(28);
        header.extend_from_slice(&base_nonce);
        header.extend_from_slice(&chunk_count.to_le_bytes());
        hasher.update(&header);
        chunks.push(Bytes::from(header));

        // Handle empty file
        if plaintext_size == 0 {
            let chunk_nonce = crypto::derive_chunk_nonce(&base_nonce, 0);
            let nonce = chacha20poly1305::XNonce::from_slice(&chunk_nonce);
            let ciphertext = cipher.encrypt(nonce, &[][..]).map_err(|e| {
                SyncError::Encryption(format!("Empty chunk encryption failed: {}", e))
            })?;

            let mut chunk_data = Vec::with_capacity(4 + ciphertext.len());
            chunk_data.extend_from_slice(&(ciphertext.len() as u32).to_le_bytes());
            chunk_data.extend_from_slice(&ciphertext);
            hasher.update(&chunk_data);
            chunks.push(Bytes::from(chunk_data));

            if let Some(ref p) = progress {
                p(0, 0);
            }

            let hash = hasher.finalize().to_hex().to_string();
            return Ok((chunks, hash, ciphertext_size, base_nonce));
        }

        // Open file and encrypt chunks
        let file = std::fs::File::open(full_path)?;
        let mut reader = BufReader::new(file);
        let mut buffer = vec![0u8; ENCRYPTION_CHUNK_SIZE];
        let mut bytes_encrypted = 0u64;

        for chunk_index in 0..chunk_count as u64 {
            let bytes_read = crypto::read_exact_or_eof(&mut reader, &mut buffer)?;
            if bytes_read == 0 {
                return Err(SyncError::Encryption(format!(
                    "Unexpected EOF at chunk {}, expected {} chunks",
                    chunk_index, chunk_count
                )));
            }

            let chunk_nonce = crypto::derive_chunk_nonce(&base_nonce, chunk_index);
            let nonce = chacha20poly1305::XNonce::from_slice(&chunk_nonce);

            let ciphertext_len = bytes_read + 16;
            // Single allocation: length prefix + ciphertext + tag
            let mut chunk_data = Vec::with_capacity(4 + ciphertext_len);
            chunk_data.extend_from_slice(&(ciphertext_len as u32).to_le_bytes());
            chunk_data.extend_from_slice(&buffer[..bytes_read]);

            // Encrypt in-place within chunk_data (skip 4-byte length prefix)
            let tag = cipher
                .encrypt_in_place_detached(nonce, &[], &mut chunk_data[4..4 + bytes_read])
                .map_err(|e| {
                    SyncError::Encryption(format!("Chunk {} encryption failed: {}", chunk_index, e))
                })?;
            chunk_data.extend_from_slice(tag.as_slice());

            hasher.update(&chunk_data);
            chunks.push(Bytes::from(chunk_data));

            bytes_encrypted += bytes_read as u64;
            if let Some(ref p) = progress {
                p(bytes_encrypted, plaintext_size);
            }
        }

        let hash = hasher.finalize().to_hex().to_string();
        Ok((chunks, hash, ciphertext_size, base_nonce))
    }

    /// Resolve download path: local path_index > decrypted server path > fallback
    fn resolve_download_path(
        &self,
        file_id: &[u8; 32],
        file_id_hex: &str,
        state: &SyncState,
        encryption_key: &[u8; 32],
    ) -> PathBuf {
        // Priority 1: Known locally in path_index
        if let Some(path) = state.path_index.get(file_id) {
            info!(file_id = %file_id_hex, path = %path.display(), "Using path from local path_index");
            return path.clone();
        }

        // Priority 2: Decrypt encrypted_path from server
        if let Some(encrypted_path) = state.remote_encrypted_paths.get(file_id) {
            info!(file_id = %file_id_hex, encrypted_path_len = encrypted_path.len(), "Attempting to decrypt path from server");
            if let Ok(path_bytes) = self.decrypt_small(encrypted_path, encryption_key) {
                let path_str = String::from_utf8_lossy(&path_bytes);
                info!(file_id = %file_id_hex, decrypted_path = %path_str, "Successfully decrypted path");
                return PathBuf::from(path_str.as_ref());
            }
            warn!(file_id = %file_id_hex, encrypted_path_len = encrypted_path.len(), "Failed to decrypt path, using fallback");
        } else {
            info!(file_id = %file_id_hex, "No encrypted path in remote_encrypted_paths, using fallback");
        }

        // Fallback: use file ID as filename
        PathBuf::from(format!("downloaded_{}", file_id_hex))
    }

    /// Download a file from the server using streaming decryption
    /// Returns (FileMetadata, relative_path) so caller can update path_index
    async fn download_file(
        &self,
        file_id: &[u8; 32],
        remote_meta: &FileMetadata,
        state: &SyncState,
    ) -> SyncResult<(FileMetadata, PathBuf)> {
        let client = self.client.as_ref().ok_or(SyncError::ServerNotConfigured)?;
        let encryption_key = self.encryption_key.ok_or(SyncError::Decryption(
            "Drive not unlocked. Call unlock() first.".to_string(),
        ))?;

        let file_id_hex = hex::encode(file_id);
        let short_id = &file_id_hex[..FILE_ID_SHORT_BYTES * 2]; // 8 bytes = 16 hex chars

        // Create temp file for ciphertext download
        let temp_dir = self.config_directory.join("temp");
        std::fs::create_dir_all(&temp_dir)?;
        let random_suffix: u32 = rand::random();
        let temp_ciphertext_path =
            temp_dir.join(format!("download_{}_{:08x}.enc", short_id, random_suffix));

        // Determine path for progress reporting
        let path_for_progress = state
            .path_index
            .get(file_id)
            .map(|p| p.display().to_string())
            .or_else(|| state.remote_file_names.get(file_id).cloned())
            .unwrap_or_else(|| format!("file_{}", short_id));

        // Download ciphertext to temp file with optional progress
        let progress_fn = self.download_progress.clone().map(|p| {
            let path_str = path_for_progress.clone();
            move |received: u64, total: u64| {
                p(received, total, Some(&path_str));
            }
        });
        let result = client
            .download(
                &state.ss58_address,
                &state.folder_hash,
                &file_id_hex,
                &temp_ciphertext_path,
                progress_fn,
            )
            .await;

        // Clean up on download failure
        let result = match result {
            Ok(r) => r,
            Err(e) => {
                let _ = std::fs::remove_file(&temp_ciphertext_path);
                return Err(e);
            }
        };

        let relative_path =
            self.resolve_download_path(file_id, &file_id_hex, state, &encryption_key);

        let full_path = self.root_path.join(&relative_path);

        // Create parent directories if needed
        if let Some(parent) = full_path.parent() {
            std::fs::create_dir_all(parent)?;
        }

        // Stream decrypt from temp ciphertext to final destination (blocking I/O)
        let path_str = relative_path.to_string_lossy().to_string();
        let decrypt_progress = self.decrypt_progress.clone();
        let temp_path = temp_ciphertext_path.clone();
        let out_path = full_path.clone();

        let decrypted_size = tokio::task::spawn_blocking(move || -> SyncResult<u64> {
            let ciphertext_size = std::fs::metadata(&temp_path)?.len();
            let ciphertext_file = std::fs::File::open(&temp_path)?;
            let mut reader = BufReader::new(ciphertext_file);

            let plaintext_file = std::fs::File::create(&out_path)?;
            let mut writer = BufWriter::new(plaintext_file);

            let progress_fn = decrypt_progress.map(|p| {
                move |bytes: u64, total: u64| {
                    p(bytes, total, Some(&path_str));
                }
            });

            crypto::decrypt_stream(
                &mut reader,
                &mut writer,
                &encryption_key,
                Some(ciphertext_size),
                progress_fn,
            )?;

            writer.flush()?;
            drop(writer);

            Ok(std::fs::metadata(&out_path)?.len())
        })
        .await
        .map_err(|e| SyncError::Decryption(format!("Decryption task failed: {}", e)))??;

        // Clean up temp ciphertext file
        let _ = std::fs::remove_file(&temp_ciphertext_path);

        info!(
            file = %relative_path.display(),
            bytes = decrypted_size,
            "File downloaded and decrypted"
        );

        let metadata = FileMetadata {
            path_hash: remote_meta.path_hash,
            salted_hash: remote_meta.salted_hash,
            size_bytes: decrypted_size,
            revision_seq: result.revision_seq,
            revision_id: result.revision_id,
            encryption_nonce: remote_meta.encryption_nonce,
        };

        Ok((metadata, relative_path))
    }

    /// Delete a file from the server
    async fn delete_remote_file(&self, file_id: &[u8; 32], state: &SyncState) -> SyncResult<()> {
        let client = self.client.as_ref().ok_or(SyncError::ServerNotConfigured)?;

        let file_id_hex = hex::encode(file_id);
        client
            .delete(&state.ss58_address, &state.folder_hash, &file_id_hex)
            .await?;

        info!(file_id = %hex::encode(&file_id[..8]), "File deleted from server");

        Ok(())
    }

    // =========================================================================
    // Encryption/Decryption (Streaming)
    // =========================================================================

    /// Encrypt small data (like paths) in memory using buffered format.
    /// Delegates to [`crypto::encrypt_small`].
    fn encrypt_small(&self, plaintext: &[u8], key: &[u8; 32]) -> SyncResult<Vec<u8>> {
        crypto::encrypt_small(plaintext, key)
    }

    /// Decrypt small data (like paths) in memory using streaming format.
    /// Delegates to [`crypto::decrypt_small`].
    fn decrypt_small(&self, ciphertext: &[u8], key: &[u8; 32]) -> SyncResult<Vec<u8>> {
        crypto::decrypt_small(ciphertext, key)
    }

    /// Update synced tree after successful operations.
    /// Rebuilds synced tree as intersection of local and remote (matching content only).
    /// Also populates fs_cache with current mtime/size for each synced file.
    fn update_synced_tree(&self, state: &mut SyncState) {
        state.synced.files.clear();
        state.fs_cache.clear();

        for (file_id, local_meta) in &state.local.files {
            if let Some(remote_meta) = state.remote.get(file_id)
                && local_meta.content_matches(remote_meta)
            {
                state.synced.insert(local_meta.clone());

                // Populate fs_cache with current disk mtime/size
                if let Some(relative_path) = state.path_index.get(file_id) {
                    let full_path = self.root_path.join(relative_path);
                    if let Ok(fs_meta) = std::fs::metadata(&full_path) {
                        let mtime_secs = fs_meta
                            .modified()
                            .ok()
                            .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
                            .map(|d| d.as_secs())
                            .unwrap_or(0);
                        state.fs_cache.insert(
                            *file_id,
                            crate::sync::CachedFileInfo {
                                mtime_secs,
                                size_bytes: fs_meta.len(),
                            },
                        );
                    }
                }
            }
        }

        state.synced.version = std::cmp::max(state.local.version, state.remote.version);
    }

    // =========================================================================
    // Helper Functions
    // =========================================================================

    /// Remove empty parent directories up to root_path after deleting a file
    fn cleanup_empty_directories(&self, file_path: &std::path::Path) {
        let mut current = file_path.parent();

        while let Some(dir) = current {
            // Stop if we've reached or passed the root path
            if dir == self.root_path || !dir.starts_with(&self.root_path) {
                break;
            }

            // Try to remove the directory (will fail if not empty)
            match std::fs::remove_dir(dir) {
                Ok(()) => {
                    info!(dir = %dir.display(), "Removed empty directory");
                    current = dir.parent();
                }
                Err(_) => {
                    // Directory not empty or other error, stop climbing
                    break;
                }
            }
        }
    }

    // =========================================================================
    // Conflict Resolution
    // =========================================================================

    /// Resolve a plan conflict (from three-tree comparison) based on user's choice
    /// Returns resolution result with action counts
    async fn resolve_plan_conflict(
        &self,
        conflict: &SyncPlanConflict,
        resolution: SyncConflictResolution,
        state: &mut SyncState,
    ) -> SyncResult<SyncConflictResolutionResult> {
        let file_id = conflict.file_id;
        let path_display = conflict
            .local_path
            .as_ref()
            .map(|p| p.display().to_string())
            .unwrap_or_else(|| format!("<{}>", hex::encode(&file_id[..8])));

        info!(
            path = %path_display,
            conflict_type = ?conflict.conflict_type,
            resolution = ?resolution,
            "Resolving plan conflict"
        );

        match resolution {
            SyncConflictResolution::KeepLocal => {
                // Upload local version (will be added to uploads in next sync)
                // For now, just mark synced as needing update
                if let Some(local_meta) = state.local.get(&file_id).cloned() {
                    // Try to upload immediately
                    match self.upload_file(&file_id, &local_meta, state).await {
                        Ok(uploaded) => {
                            state.remote_arion_hashes.insert(file_id, uploaded.arion_hash);
                            state.synced.insert(uploaded.metadata.clone());
                            state.remote.insert(uploaded.metadata);
                            info!(path = %path_display, "Plan conflict resolved: local version uploaded");
                            Ok(SyncConflictResolutionResult::uploaded())
                        }
                        Err(e) => {
                            warn!(path = %path_display, error = %e, "Failed to upload local version");
                            Err(e)
                        }
                    }
                } else {
                    // Local was deleted - need to delete from remote
                    if conflict.has_remote {
                        let client = self.client.as_ref().ok_or(SyncError::ServerNotConfigured)?;
                        client
                            .delete(
                                &state.ss58_address,
                                &state.folder_hash,
                                &hex::encode(file_id),
                            )
                            .await?;
                        state.remote.remove(&file_id);
                        state.synced.remove(&file_id);
                        info!(path = %path_display, "Plan conflict resolved: deleted from remote (local was deleted)");
                        Ok(SyncConflictResolutionResult::deleted_remotely())
                    } else {
                        Ok(SyncConflictResolutionResult {
                            resolved: true,
                            ..Default::default()
                        })
                    }
                }
            }

            SyncConflictResolution::AcceptRemote => {
                // Download remote version
                if let Some(remote_meta) = state.remote.get(&file_id).cloned() {
                    match self.download_file(&file_id, &remote_meta, state).await {
                        Ok((new_meta, relative_path)) => {
                            state.local.insert(new_meta.clone());
                            state.synced.insert(new_meta);
                            state.path_index.insert(file_id, relative_path);
                            info!(path = %path_display, "Plan conflict resolved: remote version downloaded");
                            Ok(SyncConflictResolutionResult::downloaded())
                        }
                        Err(e) => {
                            warn!(path = %path_display, error = %e, "Failed to download remote version");
                            Err(e)
                        }
                    }
                } else {
                    // Remote was deleted - delete local
                    if let Some(local_path) = &conflict.local_path {
                        let full_path = self.root_path.join(local_path);
                        if full_path.exists() {
                            std::fs::remove_file(&full_path)?;
                            // Clean up empty parent directories
                            self.cleanup_empty_directories(&full_path);
                        }
                    }
                    state.local.remove(&file_id);
                    state.synced.remove(&file_id);
                    state.path_index.remove(&file_id);
                    info!(path = %path_display, "Plan conflict resolved: deleted locally (remote was deleted)");
                    Ok(SyncConflictResolutionResult::deleted_locally())
                }
            }

            SyncConflictResolution::KeepBoth => {
                // Save local as .conflict file, then download remote
                if let Some(local_path) = &conflict.local_path {
                    let conflict_path = self.generate_conflict_path(local_path);
                    let full_local_path = self.root_path.join(local_path);
                    let full_conflict_path = self.root_path.join(&conflict_path);

                    if full_local_path.exists() {
                        std::fs::rename(&full_local_path, &full_conflict_path)?;
                        info!(
                            original = %local_path.display(),
                            conflict_file = %conflict_path.display(),
                            "Local file saved as conflict copy"
                        );
                    }
                }

                // Download remote version if it exists
                if let Some(remote_meta) = state.remote.get(&file_id).cloned() {
                    match self.download_file(&file_id, &remote_meta, state).await {
                        Ok((new_meta, relative_path)) => {
                            state.local.insert(new_meta.clone());
                            state.synced.insert(new_meta);
                            state.path_index.insert(file_id, relative_path);
                            info!(path = %path_display, "Plan conflict resolved: both versions kept");
                            Ok(SyncConflictResolutionResult::kept_both())
                        }
                        Err(e) => {
                            warn!(path = %path_display, error = %e, "Failed to download remote version");
                            Err(e)
                        }
                    }
                } else {
                    // Remote was deleted, local saved as .conflict
                    state.synced.remove(&file_id);
                    info!(path = %path_display, "Plan conflict resolved: local saved as .conflict, remote was deleted");
                    Ok(SyncConflictResolutionResult {
                        resolved: true,
                        ..Default::default()
                    })
                }
            }

            SyncConflictResolution::Skip => {
                info!(path = %path_display, "Plan conflict skipped - will handle later");
                Ok(SyncConflictResolutionResult::skipped())
            }
        }
    }

    /// Resolve an upload conflict based on user's choice
    /// Returns resolution result with action counts
    async fn resolve_upload_conflict(
        &self,
        conflict: &SyncUploadConflict,
        resolution: SyncConflictResolution,
        state: &mut SyncState,
    ) -> SyncResult<SyncConflictResolutionResult> {
        let file_id = conflict.file_id;
        let path_display = conflict
            .local_path
            .as_ref()
            .map(|p| p.display().to_string())
            .unwrap_or_else(|| format!("<{}>", hex::encode(&file_id[..8])));

        match resolution {
            SyncConflictResolution::KeepLocal => {
                // Force upload by using server's current revision as base
                info!(path = %path_display, "Resolving conflict: keeping local version");

                if let Some(local_meta) = state.local.get(&file_id).cloned() {
                    // Update synced tree with server's revision so upload will use correct base
                    if let Some(synced_meta) = state.synced.files.get_mut(&file_id) {
                        synced_meta.revision_id = conflict.server_revision_id;
                    } else {
                        // File wasn't in synced - add it with server's revision
                        let mut meta = local_meta.clone();
                        meta.revision_id = conflict.server_revision_id;
                        state.synced.insert(meta);
                    }

                    // Now upload should succeed
                    match self.upload_file(&file_id, &local_meta, state).await {
                        Ok(uploaded) => {
                            state.remote_arion_hashes.insert(file_id, uploaded.arion_hash);
                            state.synced.insert(uploaded.metadata.clone());
                            state.remote.insert(uploaded.metadata);
                            info!(path = %path_display, "Conflict resolved: local version uploaded");
                            Ok(SyncConflictResolutionResult::uploaded())
                        }
                        Err(e) => {
                            warn!(path = %path_display, error = %e, "Failed to upload local version");
                            Err(e)
                        }
                    }
                } else {
                    warn!(path = %path_display, "Local file not found for conflict resolution");
                    Ok(SyncConflictResolutionResult::skipped())
                }
            }

            SyncConflictResolution::AcceptRemote => {
                // Download server's version, overwrite local
                info!(path = %path_display, "Resolving conflict: accepting remote version");

                if let Some(remote_meta) = state.remote.get(&file_id).cloned() {
                    match self.download_file(&file_id, &remote_meta, state).await {
                        Ok((new_meta, relative_path)) => {
                            state.local.insert(new_meta.clone());
                            state.synced.insert(new_meta);
                            state.path_index.insert(file_id, relative_path);
                            info!(path = %path_display, "Conflict resolved: remote version downloaded");
                            Ok(SyncConflictResolutionResult::downloaded())
                        }
                        Err(e) => {
                            warn!(path = %path_display, error = %e, "Failed to download remote version");
                            Err(e)
                        }
                    }
                } else {
                    warn!(path = %path_display, "Remote file not found for conflict resolution");
                    Ok(SyncConflictResolutionResult::skipped())
                }
            }

            SyncConflictResolution::KeepBoth => {
                // Save local as .conflict file, then download remote
                info!(path = %path_display, "Resolving conflict: keeping both versions");

                // First, rename local file to .conflict
                if let Some(local_path) = &conflict.local_path {
                    let conflict_path = self.generate_conflict_path(local_path);
                    let full_local_path = self.root_path.join(local_path);
                    let full_conflict_path = self.root_path.join(&conflict_path);

                    if full_local_path.exists() {
                        std::fs::rename(&full_local_path, &full_conflict_path)?;
                        info!(
                            original = %local_path.display(),
                            conflict_file = %conflict_path.display(),
                            "Local file saved as conflict copy"
                        );
                    }
                }

                // Now download remote version
                if let Some(remote_meta) = state.remote.get(&file_id).cloned() {
                    match self.download_file(&file_id, &remote_meta, state).await {
                        Ok((new_meta, relative_path)) => {
                            state.local.insert(new_meta.clone());
                            state.synced.insert(new_meta);
                            state.path_index.insert(file_id, relative_path);
                            info!(path = %path_display, "Conflict resolved: remote version downloaded, local saved as .conflict");
                            Ok(SyncConflictResolutionResult::kept_both())
                        }
                        Err(e) => {
                            warn!(path = %path_display, error = %e, "Failed to download remote version");
                            Err(e)
                        }
                    }
                } else {
                    warn!(path = %path_display, "Remote file not found for conflict resolution");
                    Ok(SyncConflictResolutionResult::skipped())
                }
            }

            SyncConflictResolution::Skip => {
                info!(path = %path_display, "Conflict skipped - will handle later");
                Ok(SyncConflictResolutionResult::skipped())
            }
        }
    }

    /// Display conflict information for user
    pub fn display_conflict(&self, conflict: &SyncConflictInfo, state: &SyncState) {
        let path_display = state
            .path_index
            .get(&conflict.path_hash)
            .map(|p| p.display().to_string())
            .unwrap_or_else(|| format!("<hash:{}>", hex::encode(&conflict.path_hash[..8])));

        println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
        println!("Conflict: {}", path_display);
        println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");

        match &conflict.conflict_type {
            SyncConflictType::ModifyModify => {
                println!("Type: Both local and server modified this file differently");
                if let Some(hash) = &conflict.local_hash {
                    println!("  Local hash:  {}", hex::encode(&hash[..8]));
                }
                if let Some(hash) = &conflict.remote_hash {
                    println!("  Server hash: {}", hex::encode(&hash[..8]));
                }
            }
            SyncConflictType::ModifyDelete => {
                println!("Type: You modified this file, but it was deleted on server");
            }
            SyncConflictType::DeleteModify => {
                println!("Type: You deleted this file, but it was modified on server");
            }
            SyncConflictType::CreateCreate => {
                println!("Type: Both you and server created this file with different content");
            }
        }

        println!();
        println!("Options:");
        println!("  [A] Accept server version (discard local changes)");
        println!("  [K] Keep local version (upload to replace server)");
        println!("  [B] Keep both (server stays, local saved as .conflict)");
        println!("  [S] Skip (handle later)");
    }

    /// Generate conflict file path: file.txt -> file.conflict.timestamp.txt
    pub fn generate_conflict_path(&self, original: &Path) -> PathBuf {
        let stem = original
            .file_stem()
            .map(|s| s.to_string_lossy().to_string())
            .unwrap_or_default();
        let ext = original
            .extension()
            .map(|e| format!(".{}", e.to_string_lossy()))
            .unwrap_or_default();

        let timestamp = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map(|d| d.as_secs())
            .unwrap_or(0);

        original.with_file_name(format!("{}.conflict.{}{}", stem, timestamp, ext))
    }
}

// =============================================================================
// Standalone file operation functions for concurrent execution
// =============================================================================

/// Upload a single file using in-memory streaming (standalone, for concurrent use).
async fn upload_file_standalone(
    ctx: &FileOpCtx,
    file_id: FileId,
    metadata: FileMetadata,
    relative_path: PathBuf,
    ss58_address: String,
    folder_hash: String,
    base_revision_id: Option<[u8; 32]>,
) -> SyncResult<UploadedFile> {
    let short_id = hex::encode(&file_id[..FILE_ID_SHORT_BYTES]);
    let full_path = ctx.root_path.join(&relative_path);
    let plaintext_size = std::fs::metadata(&full_path)?.len();

    let encryption_key = ctx.encryption_key;
    let full_path_clone = full_path.clone();
    let path_str = relative_path.to_string_lossy().to_string();
    let encrypt_progress = ctx.encrypt_progress.clone();

    let (encrypted_chunks, ciphertext_hash, ciphertext_size, nonce) =
        tokio::task::spawn_blocking(move || {
            Drive::encrypt_to_memory(
                &full_path_clone,
                &encryption_key,
                plaintext_size,
                encrypt_progress.map(|p| {
                    let ps = path_str.clone();
                    move |bytes: u64, total: u64| {
                        p(bytes, total, Some(&ps));
                    }
                }),
            )
        })
        .await
        .map_err(|e| SyncError::Encryption(format!("Encryption task failed: {}", e)))??;

    let manifest_text = Manifest::generate_text(&ciphertext_hash);
    let signature = ctx.signing_key.sign(manifest_text.as_bytes());

    let timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_secs() as i64)
        .unwrap_or(0);

    let path_str = relative_path.to_string_lossy();
    debug!(
        file_id = %short_id,
        path = %path_str,
        "Encrypting path for streaming upload (concurrent)"
    );
    let encrypted_path = crypto::encrypt_small(path_str.as_bytes(), &ctx.encryption_key)?;

    let file_name = relative_path
        .file_name()
        .map(|f| f.to_string_lossy().to_string());

    let manifest = Manifest {
        ss58_address,
        folder_hash,
        ciphertext_hash,
        size_bytes: metadata.size_bytes,
        timestamp,
        signature: signature.to_bytes(),
        signing_key: ctx.signing_key.verifying_key().to_bytes(),
        path_hash: metadata.path_hash,
        salted_hash: metadata.salted_hash,
        revision_seq: metadata.revision_seq,
        base_revision_id,
        encrypted_path,
        file_name,
    };

    let (tx, rx) = mpsc::channel::<Bytes>(32);
    let chunk_feeder = tokio::spawn(async move {
        for chunk in encrypted_chunks {
            if tx.send(chunk).await.is_err() {
                break;
            }
        }
    });

    let path_for_progress = relative_path.display().to_string();
    let progress_fn = ctx.upload_progress.clone().map(|p| {
        let ps = path_for_progress.clone();
        move |sent: u64, total: u64| {
            p(sent, total, Some(&ps));
        }
    });

    let response = ctx
        .client
        .upload_from_channel(manifest, rx, ciphertext_size, progress_fn)
        .await?;

    if let Err(e) = chunk_feeder.await {
        warn!(error = %e, "Chunk feeder task panicked");
    }

    info!(
        file = %path_for_progress,
        size_bytes = plaintext_size,
        upload_id = %response.upload_id,
        "File uploaded (streaming, concurrent)"
    );

    Ok(UploadedFile {
        metadata: FileMetadata {
            path_hash: metadata.path_hash,
            salted_hash: metadata.salted_hash,
            size_bytes: metadata.size_bytes,
            revision_seq: metadata.revision_seq,
            revision_id: response.revision_id,
            encryption_nonce: nonce,
        },
        arion_hash: response.upload_id,
    })
}

/// Download and decrypt a single file (standalone, for concurrent use).
/// Returns (FileMetadata, relative_path).
async fn download_file_standalone(
    ctx: &FileOpCtx,
    file_id: FileId,
    remote_meta: FileMetadata,
    ss58_address: String,
    folder_hash: String,
    known_path: Option<PathBuf>,
    encrypted_remote_path: Option<Vec<u8>>,
    remote_file_name: Option<String>,
) -> SyncResult<(FileMetadata, PathBuf)> {
    let file_id_hex = hex::encode(file_id);
    let short_id = &file_id_hex[..FILE_ID_SHORT_BYTES * 2];

    let temp_dir = ctx.config_directory.join("temp");
    std::fs::create_dir_all(&temp_dir)?;
    let random_suffix: u32 = rand::random();
    let temp_ciphertext_path =
        temp_dir.join(format!("download_{}_{:08x}.enc", short_id, random_suffix));

    let path_for_progress = known_path
        .as_ref()
        .map(|p| p.display().to_string())
        .or(remote_file_name)
        .unwrap_or_else(|| format!("file_{}", short_id));

    let progress_fn = ctx.download_progress.clone().map(|p| {
        let ps = path_for_progress.clone();
        move |received: u64, total: u64| {
            p(received, total, Some(&ps));
        }
    });
    let result = ctx
        .client
        .download(
            &ss58_address,
            &folder_hash,
            &file_id_hex,
            &temp_ciphertext_path,
            progress_fn,
        )
        .await;

    let result = match result {
        Ok(r) => r,
        Err(e) => {
            let _ = std::fs::remove_file(&temp_ciphertext_path);
            return Err(e);
        }
    };

    // Resolve download path
    let relative_path = if let Some(path) = known_path {
        path
    } else if let Some(ref encrypted_path) = encrypted_remote_path {
        crypto::decrypt_small(encrypted_path, &ctx.encryption_key)
            .ok()
            .map(|bytes| PathBuf::from(String::from_utf8_lossy(&bytes).as_ref()))
            .unwrap_or_else(|| PathBuf::from(format!("downloaded_{}", file_id_hex)))
    } else {
        PathBuf::from(format!("downloaded_{}", file_id_hex))
    };

    let full_path = ctx.root_path.join(&relative_path);
    if let Some(parent) = full_path.parent() {
        std::fs::create_dir_all(parent)?;
    }

    // Decrypt on a blocking thread to avoid blocking the tokio runtime
    let path_str = relative_path.to_string_lossy().to_string();
    let decrypt_progress = ctx.decrypt_progress.clone();
    let encryption_key = ctx.encryption_key;
    let temp_path = temp_ciphertext_path.clone();
    let out_path = full_path.clone();

    let decrypted_size = tokio::task::spawn_blocking(move || -> SyncResult<u64> {
        let ciphertext_size = std::fs::metadata(&temp_path)?.len();
        let ciphertext_file = std::fs::File::open(&temp_path)?;
        let mut reader = BufReader::new(ciphertext_file);

        let plaintext_file = std::fs::File::create(&out_path)?;
        let mut writer = BufWriter::new(plaintext_file);

        let progress_fn = decrypt_progress.map(|p| {
            move |bytes: u64, total: u64| {
                p(bytes, total, Some(&path_str));
            }
        });

        crypto::decrypt_stream(
            &mut reader,
            &mut writer,
            &encryption_key,
            Some(ciphertext_size),
            progress_fn,
        )?;

        writer.flush()?;
        drop(writer);

        Ok(std::fs::metadata(&out_path)?.len())
    })
    .await
    .map_err(|e| SyncError::Decryption(format!("Decryption task failed: {}", e)))??;

    let _ = std::fs::remove_file(&temp_ciphertext_path);

    info!(
        file = %relative_path.display(),
        bytes = decrypted_size,
        "File downloaded and decrypted (concurrent)"
    );

    let metadata = FileMetadata {
        path_hash: remote_meta.path_hash,
        salted_hash: remote_meta.salted_hash,
        size_bytes: decrypted_size,
        revision_seq: result.revision_seq,
        revision_id: result.revision_id,
        encryption_nonce: remote_meta.encryption_nonce,
    };

    Ok((metadata, relative_path))
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use std::io::Cursor;
    use std::sync::atomic::{AtomicUsize, Ordering};

    // Unique counter for test directories
    static TEST_COUNTER: AtomicUsize = AtomicUsize::new(0);

    fn create_test_dir() -> PathBuf {
        let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
        let dir = PathBuf::from(format!("/tmp/hcfs_test_{}", id));
        let _ = fs::remove_dir_all(&dir);
        fs::create_dir_all(&dir).unwrap();
        dir
    }

    fn cleanup_test_dir(dir: &Path) {
        let _ = fs::remove_dir_all(dir);
    }

    #[test]
    fn test_mnemonic_create_and_recover() {
        let test_dir = create_test_dir();

        let mut drive = Drive::new(&test_dir);
        let mnemonic_str = drive.init("password", None).unwrap();
        assert!(!mnemonic_str.is_empty());

        let mnemonic =
            crate::auth::recover_mnemonic(&drive.encrypted_mnemonic_path, "password").unwrap();
        let seed = mnemonic.to_seed("");
        let _ = crate::auth::recover_signing_key(seed).unwrap();

        cleanup_test_dir(&test_dir);
    }

    #[test]
    #[should_panic]
    fn test_mnemonic_create_and_recover_with_bad_password() {
        let test_dir = create_test_dir();

        let mut drive = Drive::new(&test_dir);
        let _ = drive.init("password", None).unwrap();
        let _ = crate::auth::recover_mnemonic(&drive.encrypted_mnemonic_path, "wrong_password")
            .unwrap();

        cleanup_test_dir(&test_dir);
    }

    #[test]
    fn test_sync_state_persistence() {
        let test_dir = create_test_dir();

        let mut drive = Drive::new(&test_dir);
        let _ = drive.init("password", None).unwrap();

        // Create initial state
        let state = SyncState {
            ss58_address: "test_user".to_string(),
            last_sync_timestamp: 12345,
            ..Default::default()
        };

        // Save
        drive.save_sync_state(&state).unwrap();

        // Load and verify
        let loaded = drive.load_sync_state().unwrap();
        assert_eq!(loaded.ss58_address, "test_user");
        assert_eq!(loaded.last_sync_timestamp, 12345);

        cleanup_test_dir(&test_dir);
    }

    #[test]
    fn test_local_file_scan() {
        let test_dir = create_test_dir();

        // Create a test file in the directory
        fs::write(test_dir.join("test.txt"), "hello world").unwrap();

        let mut drive = Drive::new(&test_dir);
        let _ = drive.init("password", None).unwrap();

        let mut state = SyncState {
            ss58_address: "test_user".to_string(),
            ..Default::default()
        };

        // Scan should find the test file
        drive.scan_local_files(&mut state).unwrap();

        // Should have found the test file
        assert!(!state.local.files.is_empty());
        assert!(!state.path_index.is_empty());

        cleanup_test_dir(&test_dir);
    }

    #[test]
    fn test_sync_first_run_local_only() {
        let test_dir = create_test_dir();

        // Create a test file
        fs::write(test_dir.join("document.txt"), "test content").unwrap();

        let mut drive = Drive::new(&test_dir);
        let _ = drive.init("password", None).unwrap();

        // Without server, sync computes the plan but doesn't upload
        // Check that stage() correctly identifies files to upload
        let plan = drive.stage().unwrap();

        // Should have files to upload (local-only, nothing on server)
        assert!(!plan.uploads.is_empty());
        assert!(plan.downloads.is_empty());
        assert!(plan.conflicts.is_empty());

        cleanup_test_dir(&test_dir);
    }

    #[tokio::test]
    async fn test_sync_with_mock_server() {
        use wiremock::matchers::{method, path, path_regex};
        use wiremock::{Mock, MockServer, ResponseTemplate};

        let test_dir = create_test_dir();
        let mock_server = MockServer::start().await;

        // Create a test file
        fs::write(test_dir.join("document.txt"), "test content").unwrap();

        // Mock get_state endpoint (empty state for new user)
        Mock::given(method("GET"))
            .and(path_regex(r"/get_state/.*"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "Success": {
                    "ss58_address": "test_user",
                    "folder_hash": "abcdef0123456789",
                    "files": [],
                    "total_count": 0,
                    "has_more": false,
                    "offset": 0,
                    "limit": 1000
                }
            })))
            .mount(&mock_server)
            .await;

        // Mock upload endpoint
        let revision_id: Vec<u8> = vec![42u8; 32];
        Mock::given(method("POST"))
            .and(path("/upload"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "Success": {
                    "upload_id": "upload-123",
                    "bytes_accepted": 100,
                    "timestamp": 1234567890,
                    "revision_id": revision_id
                }
            })))
            .mount(&mock_server)
            .await;

        let mut drive = Drive::new(&test_dir);
        let _ = drive.init("password", None).unwrap();

        // Configure server BEFORE unlock so ss58_address is available
        let config = HcfsClientConfig {
            base_url: mock_server.uri(),
            api_key: "test-key".to_string(),
            bearer_token: "test-token".to_string(),
            accept_invalid_certs: true,
            billing_bypass_token: None,
            ss58_address: "test_user".to_string(),
            folder_hash: "abcdef0123456789".to_string(),
        };
        drive.set_config(config).unwrap();
        drive.unlock("password").unwrap();

        // Sync with server
        let outcome = drive.sync_async(SyncMode::NonInteractive).await.unwrap();

        // Should have uploaded files
        assert!(outcome.files_uploaded > 0);
        assert_eq!(outcome.conflicts_skipped, 0);

        cleanup_test_dir(&test_dir);
    }

    #[test]
    fn test_conflict_path_generation() {
        let test_dir = create_test_dir();
        let drive = Drive::new(&test_dir);

        let original = PathBuf::from("documents/report.pdf");
        let conflict = drive.generate_conflict_path(&original);

        let conflict_str = conflict.to_string_lossy();
        assert!(conflict_str.contains("report.conflict."));
        assert!(conflict_str.ends_with(".pdf"));

        cleanup_test_dir(&test_dir);
    }

    // =========================================================================
    // Streaming Encryption/Decryption Tests
    // =========================================================================

    #[test]
    fn test_encrypt_decrypt_roundtrip_empty() {
        let key = [42u8; 32];
        let plaintext = vec![];

        // Encrypt
        let mut ciphertext = Vec::new();
        let _nonce =
            crypto::encrypt_buffered(&mut Cursor::new(&plaintext), &mut ciphertext, &key).unwrap();

        // Decrypt
        let mut decrypted = Vec::new();
        crypto::decrypt_stream(
            &mut Cursor::new(&ciphertext),
            &mut decrypted,
            &key,
            None,
            None::<fn(u64, u64)>,
        )
        .unwrap();

        assert_eq!(plaintext, decrypted);
    }

    #[test]
    fn test_encrypt_decrypt_roundtrip_small() {
        let key = [42u8; 32];
        let plaintext = b"Hello, World!".to_vec();

        // Encrypt
        let mut ciphertext = Vec::new();
        let _nonce =
            crypto::encrypt_buffered(&mut Cursor::new(&plaintext), &mut ciphertext, &key).unwrap();

        // Decrypt
        let mut decrypted = Vec::new();
        crypto::decrypt_stream(
            &mut Cursor::new(&ciphertext),
            &mut decrypted,
            &key,
            None,
            None::<fn(u64, u64)>,
        )
        .unwrap();

        assert_eq!(plaintext, decrypted);
    }

    #[test]
    fn test_encrypt_decrypt_roundtrip_exact_chunk_size() {
        let key = [42u8; 32];
        // Exactly one chunk
        let plaintext = vec![0xAB; ENCRYPTION_CHUNK_SIZE];

        // Encrypt
        let mut ciphertext = Vec::new();
        let _nonce =
            crypto::encrypt_buffered(&mut Cursor::new(&plaintext), &mut ciphertext, &key).unwrap();

        // Decrypt
        let mut decrypted = Vec::new();
        crypto::decrypt_stream(
            &mut Cursor::new(&ciphertext),
            &mut decrypted,
            &key,
            None,
            None::<fn(u64, u64)>,
        )
        .unwrap();

        assert_eq!(plaintext, decrypted);
    }

    #[test]
    fn test_encrypt_decrypt_roundtrip_multi_chunk() {
        let key = [42u8; 32];
        // 2.5 chunks
        let plaintext = vec![0xCD; ENCRYPTION_CHUNK_SIZE * 2 + ENCRYPTION_CHUNK_SIZE / 2];

        // Encrypt
        let mut ciphertext = Vec::new();
        let _nonce =
            crypto::encrypt_buffered(&mut Cursor::new(&plaintext), &mut ciphertext, &key).unwrap();

        // Verify chunk count in header
        let chunk_count = u32::from_le_bytes([
            ciphertext[24],
            ciphertext[25],
            ciphertext[26],
            ciphertext[27],
        ]);
        assert_eq!(chunk_count, 3); // 2.5 chunks rounds up to 3

        // Decrypt
        let mut decrypted = Vec::new();
        crypto::decrypt_stream(
            &mut Cursor::new(&ciphertext),
            &mut decrypted,
            &key,
            None,
            None::<fn(u64, u64)>,
        )
        .unwrap();

        assert_eq!(plaintext, decrypted);
    }

    #[test]
    fn test_decrypt_wrong_key_fails() {
        let key1 = [42u8; 32];
        let key2 = [99u8; 32];
        let plaintext = b"Secret message".to_vec();

        // Encrypt with key1
        let mut ciphertext = Vec::new();
        let _nonce =
            crypto::encrypt_buffered(&mut Cursor::new(&plaintext), &mut ciphertext, &key1).unwrap();

        // Try to decrypt with key2
        let mut decrypted = Vec::new();
        let result = crypto::decrypt_stream(
            &mut Cursor::new(&ciphertext),
            &mut decrypted,
            &key2,
            None,
            None::<fn(u64, u64)>,
        );

        assert!(result.is_err());
    }

    #[test]
    fn test_decrypt_truncated_header_fails() {
        let key = [42u8; 32];
        // Only 20 bytes - not enough for 24-byte nonce
        let bad_ciphertext = vec![0u8; 20];

        let mut decrypted = Vec::new();
        let result = crypto::decrypt_stream(
            &mut Cursor::new(&bad_ciphertext),
            &mut decrypted,
            &key,
            None,
            None::<fn(u64, u64)>,
        );

        assert!(result.is_err());
    }

    #[test]
    fn test_decrypt_truncated_chunk_fails() {
        let key = [42u8; 32];
        let plaintext = b"Hello".to_vec();

        // Encrypt
        let mut ciphertext = Vec::new();
        let _nonce =
            crypto::encrypt_buffered(&mut Cursor::new(&plaintext), &mut ciphertext, &key).unwrap();

        // Truncate the ciphertext (remove last 5 bytes)
        ciphertext.truncate(ciphertext.len() - 5);

        let mut decrypted = Vec::new();
        let result = crypto::decrypt_stream(
            &mut Cursor::new(&ciphertext),
            &mut decrypted,
            &key,
            None,
            None::<fn(u64, u64)>,
        );

        assert!(result.is_err());
    }

    #[test]
    fn test_hash_file_streaming_correctness() {
        use crate::crypto;
        let user_id = "test_user";
        let content = b"Test file content for hashing";

        // Compute hash with streaming
        let (streaming_hash, size) =
            crypto::compute_salted_hash(&mut Cursor::new(content), user_id).unwrap();

        assert_eq!(size, content.len() as u64);

        // Compute hash manually
        let mut hasher = Blake3Hasher::new();
        hasher.update(user_id.as_bytes());
        hasher.update(content);
        let expected_hash: [u8; 32] = hasher.finalize().into();

        assert_eq!(streaming_hash, expected_hash);
    }

    #[test]
    fn test_encrypt_small_decrypt_small_roundtrip() {
        let test_dir = create_test_dir();
        let drive = Drive::new(&test_dir);

        let key = [42u8; 32];
        let plaintext = b"Small path data".to_vec();

        // Encrypt
        let ciphertext = drive.encrypt_small(&plaintext, &key).unwrap();

        // Decrypt
        let decrypted = drive.decrypt_small(&ciphertext, &key).unwrap();

        assert_eq!(plaintext, decrypted);

        cleanup_test_dir(&test_dir);
    }

    #[test]
    fn test_derive_chunk_nonce_uniqueness() {
        let base_nonce = [1u8; 24];

        let nonce0 = crypto::derive_chunk_nonce(&base_nonce, 0);
        let nonce1 = crypto::derive_chunk_nonce(&base_nonce, 1);
        let nonce2 = crypto::derive_chunk_nonce(&base_nonce, 2);
        let nonce_max = crypto::derive_chunk_nonce(&base_nonce, u64::MAX);

        // All nonces should be different
        assert_ne!(nonce0, nonce1);
        assert_ne!(nonce1, nonce2);
        assert_ne!(nonce0, nonce_max);

        // Chunk 0 with base nonce should be unchanged (XOR with 0)
        assert_eq!(nonce0, base_nonce);
    }
}
