//! FUSE write operation with copy-on-write overlay integration.
//!
//! This module implements the `write` FUSE operation, which redirects all
//! modifications to an overlay file while preserving the immutability of the
//! base snapshot. The write path uses **block-level copy-on-write (COW)**:
//! before modifying a block for the first time, the original snapshot data
//! is copied into the overlay to ensure partial-block writes preserve
//! unmodified bytes.
//!
//! # Write Semantics
//!
//! - **Target**: Only inode 2 (disk) is writable when overlay is configured
//! - **Atomicity**: Writes are not atomic across blocks; a crash mid-write may
//!   leave some blocks modified and others untouched
//! - **Alignment**: Writes do not need to be block-aligned; partial-block
//!   writes are supported via the COW mechanism
//! - **Persistence**: Modified block metadata is flushed to the `.meta` file
//!   synchronously on first write to each block
//!
//! # Copy-on-Write Flow
//!
//! 1. **Block Seeding**: For each 4 KiB block `B` touched by the write:
//!    - If `overlay.is_block_modified(B.idx)` is false:
//!      - Read original 4 KiB from snapshot at block offset
//!      - Write original data into overlay at block offset
//!      - Call `overlay.mark_block_modified(B.idx)` to record modification
//!      - Append block index to `.meta` file for recovery
//!
//! 2. **Data Write**: Write the user's data into the overlay at the requested
//!    offset. This may overwrite only part of a block, with the rest preserved
//!    from the seeding step.
//!
//! This ensures that partial-block writes (e.g., writing 100 bytes at offset
//! 50) do not corrupt the surrounding bytes, as the full block is seeded first.
//!
//! # Overlay Integration
//!
//! The overlay file stores modified blocks at their **logical file offsets**,
//! not in a separate log. This means:
//! - Block 0 at file offset 0 in the overlay corresponds to block 0 in the disk
//! - Sparse regions (unmodified blocks) may appear as holes in the overlay file
//! - The `.meta` sidecar records which blocks have been written
//!
//! Reads consult the `.meta` set to decide whether to read from overlay or snapshot.
//!
//! # Persistence and Recovery
//!
//! On first write to each block, `mark_block_modified` appends an 8-byte
//! block index to the `.meta` file and flushes it. This ensures that after
//! a crash, the next mount can reconstruct the set of modified blocks by
//! scanning `.meta`.
//!
//! Data writes to the overlay file itself are not explicitly flushed; the
//! OS page cache controls durability. For strict durability, external tools
//! should call `fsync` on the overlay file.
//!
//! # Performance Characteristics
//!
//! - **First write to a block**: Incurs an extra snapshot read (4 KiB) plus
//!   overlay write (4 KiB) plus `.meta` append (8 bytes + flush). Typical
//!   latency: 50-200 µs depending on storage speed.
//! - **Subsequent writes to same block**: Only overlay write, no seeding.
//!   Typical latency: 10-50 µs.
//! - **Fragmented workloads**: Writing 1 byte each to 1000 different blocks
//!   incurs 1000 block-seeding operations. This is I/O intensive but necessary
//!   to preserve correctness.
//!
//! # Examples
//!
//! ## First Write to a Clean Block
//!
//! ```text
//! write(ino=2, offset=100, data=[0xAA; 50]) to block 0:
//! 1. Block 0 not modified -> seed with snapshot.read_at(Disk, 0, 4096)
//! 2. overlay.write_file(0, snapshot_block_0)
//! 3. overlay.mark_block_modified(0) -> append 0u64.to_le_bytes() to .meta
//! 4. overlay.write_file(100, [0xAA; 50]) -> overwrite bytes 100..150
//! Result: Overlay block 0 = snapshot[0..100] ++ [0xAA; 50] ++ snapshot[150..4096]
//! ```
//!
//! ## Write to Already-Modified Block
//!
//! ```text
//! write(ino=2, offset=200, data=[0xBB; 100]) to block 0 (already modified):
//! 1. Block 0 modified -> skip seeding
//! 2. overlay.write_file(200, [0xBB; 100])
//! Result: Overlay block 0 bytes 200..300 updated
//! ```

use super::Hexz;
use crate::vfs::BLOCK_SIZE;
use fuser::{ReplyWrite, Request};
use hexz_core::SnapshotStream;
use libc::{EIO, EROFS};

/// Writes a byte range into the overlay-backed disk inode with COW semantics.
///
/// This operation redirects all disk modifications to the overlay file while
/// ensuring that the base snapshot remains immutable. The write path
/// implements **block-level copy-on-write** to preserve unmodified bytes
/// within partially written blocks.
///
/// # Write Path Algorithm
///
/// 1. **Validate inode**: Only inode 2 (disk) with overlay enabled is writable
/// 2. **Compute affected blocks**: Calculate `start_block..=end_block` from
///    the write range `[offset, offset + data.len())`
/// 3. **Seed unmodified blocks**: For each block `B` in the range:
///    - If not already modified, read original 4 KiB from snapshot
///    - Write original data to overlay at block offset
///    - Mark block as modified and persist to `.meta` file
/// 4. **Write user data**: Write the caller's data to the overlay at the
///    requested offset, potentially overwriting part of the seeded data
/// 5. **Report written count**: Return the number of bytes written (usually
///    equals `data.len()` unless overlay write fails)
///
/// # Copy-on-Write Guarantees
///
/// The seeding step ensures that partial-block writes do not corrupt adjacent
/// bytes. For example, writing 10 bytes at offset 4090 (spans blocks 0 and 1):
/// - Block 0 (offset 0..4096): Seed with snapshot[0..4096], then overwrite [4090..4096]
/// - Block 1 (offset 4096..8192): Seed with snapshot[4096..8192], then overwrite [4096..4100]
///
/// Result: Bytes [0..4090] and [4100..8192] retain original snapshot content.
///
/// # Parameters
///
/// - `fs`: Mutable reference to the FUSE filesystem state
/// - `_req`: Request context (unused)
/// - `ino`: Inode number to write to (must be 2 for disk)
/// - `_fh`: File handle from prior `open` (unused)
/// - `offset`: Starting byte offset in the file
/// - `data`: Byte slice to write
/// - `_write_flags`: FUSE write flags (e.g., O_APPEND, unused)
/// - `_flags`: Open flags from `open()` call (unused)
/// - `_lock`: Optional POSIX file lock owner (unused)
/// - `reply`: Callback to send written byte count or error
///
/// # Errors
///
/// - `EROFS`: Write attempted to wrong inode or no overlay configured
/// - `EIO`: Overlay write failed (e.g., no disk space, I/O error)
/// - Propagates OS error code from `write_file` on failure
///
/// # Examples
///
/// ## Simple Block-Aligned Write
///
/// ```text
/// write(ino=2, offset=4096, data=[0xFF; 4096]) to clean block 1:
/// 1. Block 1 not modified -> seed with snapshot.read_at(Disk, 4096, 4096)
/// 2. overlay.write_file(4096, snapshot_block_1)
/// 3. overlay.mark_block_modified(1) -> append to .meta
/// 4. overlay.write_file(4096, [0xFF; 4096]) -> full block overwrite
/// Result: Block 1 in overlay = [0xFF; 4096]
/// ```
///
/// ## Partial Cross-Block Write
///
/// ```text
/// write(ino=2, offset=4090, data=[0xAA; 12]) spans blocks 0 and 1:
/// Block 0: Seed if needed, then overwrite [4090..4096]
/// Block 1: Seed if needed, then overwrite [4096..4102]
/// Result: 6 bytes modified in block 0, 6 bytes modified in block 1
/// ```
///
/// # Performance
///
/// - **Time complexity**: O(n) where n = number of blocks touched
/// - **First write to block**: 50-200 µs (snapshot read + overlay write + flush)
/// - **Subsequent writes**: 10-50 µs (overlay write only)
/// - **Fragmented writes**: O(n) block seeding operations can be expensive for
///   random write patterns (e.g., database workloads)
/// - **Space usage**: Each modified block adds 4 KiB to overlay + 8 bytes to `.meta`
#[allow(clippy::too_many_arguments)]
pub fn handle_write(
    fs: &mut Hexz,
    _req: &Request,
    ino: u64,
    _fh: u64,
    offset: i64,
    data: &[u8],
    _write_flags: u32,
    _flags: i32,
    _lock: Option<u64>,
    reply: ReplyWrite,
) {
    if let Some(overlay) = &mut fs.overlay {
        if ino == 2 {
            if data.is_empty() {
                reply.written(0);
                return;
            }

            let start = offset as u64;
            let len = data.len() as u64;
            let end = start + len;

            let start_block = start / BLOCK_SIZE;
            let end_block = (end - 1) / BLOCK_SIZE;

            // Seed unmodified blocks with original data
            for blk in start_block..=end_block {
                if !overlay.is_block_modified(blk) {
                    let blk_start = blk * BLOCK_SIZE;
                    if blk_start < fs.snap.size(SnapshotStream::Disk) {
                        let mut seed_buf = vec![0u8; BLOCK_SIZE as usize];
                        let original = fs
                            .snap
                            .read_at(SnapshotStream::Disk, blk_start, BLOCK_SIZE as usize)
                            .unwrap_or_default();
                        // Copy what we got, rest stays zero-filled
                        seed_buf[..original.len()].copy_from_slice(&original);
                        if overlay.write_file(blk_start, &seed_buf).is_err() {
                            reply.error(EIO);
                            return;
                        }
                    }
                    if overlay.mark_block_modified(blk).is_err() {
                        reply.error(EIO);
                        return;
                    }
                }
            }

            // Write the actual data
            match overlay.write_file(start, data) {
                Ok(written) => reply.written(written as u32),
                Err(e) => reply.error(e.raw_os_error().unwrap_or(EIO)),
            }
            return;
        }
    }
    reply.error(EROFS);
}
