use crate::modes::server::api::common::ResponseBuilder;
use crate::modes::server::common::{
    ApiResponse, AppState, CreateItemQuery, ItemContentQuery, ItemInfo, ItemInfoListResponse,
    ItemInfoResponse, ItemQuery, ListItemsQuery, MetadataResponse, TagsQuery,
};
use crate::services::error::CoreError;
use crate::services::item_service::ItemService;
use crate::services::utils::parse_comma_tags;
use axum::{
    body::Body,
    extract::{Path, Query, State},
    http::{StatusCode, header},
    response::{Json, Response},
};
use http_body_util::BodyExt;
use log::{debug, warn};
use std::collections::HashMap;
use std::io::Read;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::sync::mpsc;
use tokio::task;

/// Bridges an async mpsc receiver to a synchronous `Read` trait.
///
/// Used in `spawn_blocking` contexts to consume data from an async body
/// stream as a regular reader. Blocks on each `read()` call until data
/// is available or the channel is closed. No internal buffering — each
/// channel message is consumed fully before the next is fetched.
struct MpscReader {
    rx: mpsc::Receiver<Result<Vec<u8>, std::io::Error>>,
    current: Vec<u8>,
    pos: usize,
}

impl MpscReader {
    fn new(rx: mpsc::Receiver<Result<Vec<u8>, std::io::Error>>) -> Self {
        Self {
            rx,
            current: Vec::new(),
            pos: 0,
        }
    }
}

impl Read for MpscReader {
    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
        if self.pos < self.current.len() {
            let remaining = &self.current[self.pos..];
            let n = std::cmp::min(buf.len(), remaining.len());
            buf[..n].copy_from_slice(&remaining[..n]);
            self.pos += n;
            return Ok(n);
        }

        match self.rx.blocking_recv() {
            Some(Ok(data)) => {
                let n = std::cmp::min(buf.len(), data.len());
                buf[..n].copy_from_slice(&data[..n]);
                if n < data.len() {
                    self.current = data;
                    self.pos = n;
                }
                Ok(n)
            }
            Some(Err(e)) => Err(e),
            None => Ok(0),
        }
    }
}

/// Helper function to get mime type from metadata
fn get_mime_type(metadata: &HashMap<String, String>) -> String {
    metadata
        .get("mime_type")
        .map(|s| s.to_string())
        .unwrap_or_else(|| "application/octet-stream".to_string())
}

/// Helper function to handle item not found errors
fn handle_item_error(error: CoreError) -> StatusCode {
    match error {
        CoreError::ItemNotFound(_) | CoreError::ItemNotFoundGeneric => StatusCode::NOT_FOUND,
        _ => {
            warn!("Failed to get item: {error}");
            StatusCode::INTERNAL_SERVER_ERROR
        }
    }
}

#[utoipa::path(
    get,
    path = "/api/item/",
    operation_id = "keep_list_items",
    summary = "List stored items",
    description = "Get paginated items with metadata and tags. Filter by tags, sort by creation time.",
    responses(
        (status = 200, description = "Items retrieved", body = ItemInfoListResponse),
        (status = 401, description = "Unauthorized"),
        (status = 500, description = "Internal server error")
    ),
    params(
        ("tags" = Option<String>, Query, description = "Comma-separated tags to filter"),
        ("order" = Option<String>, Query, description = "Sort order: 'newest' or 'oldest'"),
        ("start" = Option<u64>, Query, description = "Pagination start index"),
        ("count" = Option<u64>, Query, description = "Number of items to return")
    ),
    security(
        ("bearerAuth" = [])
    ),
    tag = "item"
)]
pub async fn handle_list_items(
    State(state): State<AppState>,
    Query(params): Query<ListItemsQuery>,
) -> Result<Response, StatusCode> {
    let tags: Vec<String> = params
        .tags
        .as_ref()
        .map(|s| parse_comma_tags(s))
        .unwrap_or_default();

    // Parse metadata filter from query parameter
    let meta_filter: HashMap<String, Option<String>> = if let Some(ref meta_str) = params.meta {
        serde_json::from_str(meta_str).map_err(|e| {
            warn!("Failed to parse meta filter JSON string: {e}");
            StatusCode::BAD_REQUEST
        })?
    } else {
        HashMap::new()
    };

    let data_dir = state.data_dir.clone();
    let db = state.db.clone();

    let mut items_with_meta = task::spawn_blocking(move || {
        let conn = db.blocking_lock();
        let item_service = ItemService::new(data_dir);
        item_service.list_items(&conn, &tags, &meta_filter)
    })
    .await
    .map_err(|e| {
        warn!("Blocking task failed: {e}");
        StatusCode::INTERNAL_SERVER_ERROR
    })?
    .map_err(|e| {
        warn!("Failed to get items: {e}");
        StatusCode::INTERNAL_SERVER_ERROR
    })?;

    // Apply ordering (default is newest first)
    match params.order.as_deref().unwrap_or("newest") {
        "newest" => items_with_meta.sort_by(|a, b| b.item.ts.cmp(&a.item.ts)),
        "oldest" => items_with_meta.sort_by(|a, b| a.item.ts.cmp(&b.item.ts)),
        _ => items_with_meta.sort_by(|a, b| b.item.ts.cmp(&a.item.ts)), // default to newest
    }

    // Apply pagination
    let start = params.start.unwrap_or(0) as usize;
    let count = params.count.unwrap_or(100).min(10000) as usize;
    let items_with_meta: Vec<_> = items_with_meta
        .into_iter()
        .skip(start)
        .take(count)
        .collect();

    let item_infos: Vec<ItemInfo> = items_with_meta
        .into_iter()
        .map(|item_with_meta| {
            let item_id = item_with_meta.item.id.unwrap_or(0);
            let item_tags: Vec<String> =
                item_with_meta.tags.iter().map(|t| t.name.clone()).collect();
            let item_meta = item_with_meta.meta_as_map();

            ItemInfo {
                id: item_id,
                ts: item_with_meta.item.ts.to_rfc3339(),
                size: item_with_meta.item.size,
                compression: item_with_meta.item.compression,
                tags: item_tags,
                metadata: item_meta,
            }
        })
        .collect();

    ResponseBuilder::json(ApiResponse {
        success: true,
        data: Some(item_infos),
        error: None,
    })
}

/// Handle as_meta=true response by returning JSON with metadata and content
async fn handle_as_meta_response(
    db: &Arc<tokio::sync::Mutex<rusqlite::Connection>>,
    data_dir: &std::path::Path,
    item_id: i64,
    offset: u64,
    length: u64,
) -> Result<Response, StatusCode> {
    let db1 = db.clone();
    let data_dir1 = data_dir.to_path_buf();
    let item_with_meta = task::spawn_blocking(move || {
        let conn = db1.blocking_lock();
        let item_service = ItemService::new(data_dir1);
        item_service.get_item(&conn, item_id)
    })
    .await
    .map_err(|e| {
        warn!("Blocking task failed for item {item_id}: {e}");
        StatusCode::INTERNAL_SERVER_ERROR
    })?
    .map_err(|e| {
        warn!("Failed to get item {item_id} for as_meta content: {e}");
        StatusCode::INTERNAL_SERVER_ERROR
    })?;

    let metadata = item_with_meta.meta_as_map();
    handle_as_meta_response_with_metadata(db, data_dir, item_id, &metadata, offset, length).await
}

/// Handle as_meta=true response with pre-fetched metadata using streaming.
///
/// Reads a sample to check binary status, then streams remaining content with
/// offset/length applied at the stream level — never loads the full item into memory.
async fn handle_as_meta_response_with_metadata(
    db: &Arc<tokio::sync::Mutex<rusqlite::Connection>>,
    data_dir: &std::path::Path,
    item_id: i64,
    metadata: &HashMap<String, String>,
    offset: u64,
    length: u64,
) -> Result<Response, StatusCode> {
    // Binary detection: read a sample in a blocking task, check, and return early
    let db1 = db.clone();
    let data_dir1 = data_dir.to_path_buf();
    let is_binary = task::spawn_blocking(move || {
        let conn = db1.blocking_lock();
        let item_service = ItemService::new(data_dir1);
        let (mut reader, _) = item_service.get_item_content_streaming(&conn, item_id)?;
        let mut sample = vec![0u8; crate::common::PIPESIZE];
        let n = reader.read(&mut sample)?;
        sample.truncate(n);
        Ok::<bool, CoreError>(crate::common::is_binary::is_binary(&sample))
    })
    .await
    .map_err(|e| {
        warn!("Blocking task failed for item {item_id}: {e}");
        StatusCode::INTERNAL_SERVER_ERROR
    })?
    .map_err(|e| {
        warn!("Failed to check binary status for item {item_id}: {e}");
        StatusCode::INTERNAL_SERVER_ERROR
    })?;

    if is_binary {
        let response_body = serde_json::json!({
            "metadata": metadata,
            "content": serde_json::Value::Null,
            "error": "Content is binary"
        });
        return Response::builder()
            .header(header::CONTENT_TYPE, "application/json")
            .status(StatusCode::UNPROCESSABLE_ENTITY)
            .body(axum::body::Body::from(response_body.to_string()))
            .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR);
    }

    // Stream content with offset/length applied at the stream level
    let db2 = db.clone();
    let data_dir2 = data_dir.to_path_buf();
    let content_str = task::spawn_blocking(move || -> Result<String, CoreError> {
        let conn = db2.blocking_lock();
        let item_service = ItemService::new(data_dir2);
        let (mut reader, item_with_meta) =
            item_service.get_item_content_streaming(&conn, item_id)?;
        let content_len = item_with_meta.item.size.unwrap_or(0) as u64;

        let result = crate::common::read_with_bounds(&mut reader, offset, length, content_len)
            .map_err(CoreError::Io)?;

        String::from_utf8(result)
            .map_err(|_| CoreError::InvalidInput("Content is not valid UTF-8".to_string()))
    })
    .await
    .map_err(|e| {
        warn!("Blocking task failed for item {item_id}: {e}");
        StatusCode::INTERNAL_SERVER_ERROR
    })?;

    let content_str = match content_str {
        Ok(s) => s,
        Err(CoreError::InvalidInput(msg)) if msg == "Content is not valid UTF-8" => {
            let response_body = serde_json::json!({
                "metadata": metadata,
                "content": serde_json::Value::Null,
                "error": "Content is not valid UTF-8"
            });
            return Response::builder()
                .header(header::CONTENT_TYPE, "application/json")
                .status(StatusCode::UNPROCESSABLE_ENTITY)
                .body(axum::body::Body::from(response_body.to_string()))
                .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR);
        }
        Err(e) => {
            warn!("Failed to read content for item {item_id}: {e}");
            return Err(StatusCode::INTERNAL_SERVER_ERROR);
        }
    };

    // Return JSON with metadata and content
    let response_body = serde_json::json!({
        "metadata": metadata,
        "content": content_str,
        "error": serde_json::Value::Null
    });

    Response::builder()
        .header(header::CONTENT_TYPE, "application/json")
        .body(axum::body::Body::from(response_body.to_string()))
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
}

#[utoipa::path(
    post,
    path = "/api/item/",
    operation_id = "keep_post_item",
    summary = "Store new item",
    description = "Upload content to store as a new item. Content is compressed, analyzed for metadata, and stored. \
                   Query parameters: tags (comma-separated), metadata (JSON string). Body: raw binary content.",
    responses(
        (status = 201, description = "Item created", body = ItemInfoResponse),
        (status = 400, description = "Bad request"),
        (status = 401, description = "Unauthorized"),
        (status = 500, description = "Internal server error")
    ),
    request_body(
        content = Vec<u8>,
        description = "Raw binary content to store",
        content_type = "application/octet-stream"
    ),
    params(
        ("tags" = Option<String>, Query, description = "Comma-separated tags to associate with the item"),
        ("metadata" = Option<String>, Query, description = "Metadata as JSON string")
    ),
    security(
        ("bearerAuth" = [])
    ),
    tag = "item"
)]
pub async fn handle_post_item(
    State(state): State<AppState>,
    Query(params): Query<CreateItemQuery>,
    body: Body,
) -> Result<Json<ApiResponse<ItemInfo>>, StatusCode> {
    let db = state.db.clone();
    let data_dir = state.data_dir.clone();
    let settings = state.settings.clone();

    // Parse tags from query parameter
    let tags: Vec<String> = params
        .tags
        .as_deref()
        .map(parse_comma_tags)
        .unwrap_or_default();

    // Validate tag lengths
    for tag in &tags {
        if tag.len() > 256 {
            warn!("Tag too long: {} chars (max 256)", tag.len());
            return Err(StatusCode::BAD_REQUEST);
        }
    }

    // Parse metadata from query parameter
    let metadata: HashMap<String, String> = if let Some(ref meta_str) = params.metadata {
        serde_json::from_str(meta_str).map_err(|e| {
            warn!("Failed to parse metadata JSON string: {e}");
            StatusCode::BAD_REQUEST
        })?
    } else {
        HashMap::new()
    };

    // Validate metadata key/value lengths
    for (key, value) in &metadata {
        if key.len() > 128 {
            warn!("Metadata key too long: {} chars (max 128)", key.len());
            return Err(StatusCode::BAD_REQUEST);
        }
        if value.len() > 4096 {
            warn!("Metadata value too long: {} chars (max 4096)", value.len());
            return Err(StatusCode::BAD_REQUEST);
        }
    }

    let compress = params.compress;
    let run_meta = params.meta;

    // Parse client-specified compression type (only used when compress=false)
    let client_compression_type = params.compression_type.as_deref().map(|ct| {
        ct.parse::<crate::compression_engine::CompressionType>()
            .unwrap_or_else(|_| {
                warn!("Unknown compression type from client: {ct}, defaulting to none");
                crate::compression_engine::CompressionType::None
            })
    });

    // Parse optional timestamp for import
    let import_ts = params.ts.as_deref().and_then(|ts_str| {
        match chrono::DateTime::parse_from_rfc3339(ts_str) {
            Ok(dt) => Some(dt.with_timezone(&chrono::Utc)),
            Err(e) => {
                warn!("Invalid timestamp from client: {ts_str}: {e}");
                None
            }
        }
    });

    // Stream body through an mpsc channel with fixed-size frames.
    // Size tracking ensures we never buffer the whole body in memory.
    // Treat Some(0) as unlimited (None).
    let max_body_size: Option<u64> = settings
        .server
        .as_ref()
        .and_then(|s| s.max_body_size)
        .filter(|&v| v > 0);

    let (tx, rx) = mpsc::channel::<Result<Vec<u8>, std::io::Error>>(16);
    let body_truncated = Arc::new(AtomicBool::new(false));
    let truncated_flag = body_truncated.clone();

    // Async task: read body frames, track size, stop when limit exceeded
    tokio::spawn(async move {
        let mut body = body;
        let mut total_bytes: u64 = 0;
        loop {
            match body.frame().await {
                None => break,
                Some(Err(e)) => {
                    let _ = tx
                        .send(Err(std::io::Error::other(format!("Body error: {e}"))))
                        .await;
                    break;
                }
                Some(Ok(frame)) => {
                    if let Ok(data) = frame.into_data() {
                        total_bytes += data.len() as u64;
                        if let Some(limit) = max_body_size
                            && total_bytes > limit
                        {
                            truncated_flag.store(true, Ordering::Relaxed);
                            break; // Drop sender → reader sees EOF
                        }
                        if tx.send(Ok(data.to_vec())).await.is_err() {
                            break;
                        }
                    }
                }
            }
        }
    });

    // Blocking task: consume streaming reader, save via save_item_raw_streaming
    let item_with_meta = task::spawn_blocking(move || {
        let mut conn = db.blocking_lock();
        let item_service = crate::services::ItemService::new(data_dir);
        let mut reader = MpscReader::new(rx);
        item_service.save_item_raw_streaming(
            &mut conn,
            &mut reader,
            tags,
            metadata,
            compress,
            run_meta,
            client_compression_type,
            import_ts,
            &settings,
        )
    })
    .await
    .map_err(|e| {
        warn!("Failed to save item (task error): {e}");
        StatusCode::INTERNAL_SERVER_ERROR
    })?
    .map_err(|e| {
        warn!("Failed to save item: {e}");
        StatusCode::INTERNAL_SERVER_ERROR
    })?;

    // If body was truncated due to size limit, return 413 but keep the partial item.
    //
    // Design rationale for preserving truncated data:
    //
    // 1. Keep is a temporary file manager — partial content is still useful data.
    //    The client sent it, the server received it, and it was written through the
    //    normal save pipeline (compression, meta plugins). Discarding it would waste
    //    work already done and lose data the user may want to inspect or retry from.
    //
    // 2. The streaming pipeline is nonfatal by design. The sender stops pushing
    //    frames, the channel closes cleanly (sender drop → reader sees EOF), and
    //    save_item_raw_streaming finishes with whatever it received. No corruption,
    //    no partial writes to the storage file — the compression engine flushes and
    //    closes normally.
    //
    // 3. No destructive rollback. Deleting the item would require either a second
    //    DB transaction (fragile under concurrency) or leaving orphaned files on
    //    disk. Keeping the item is simpler and safer.
    //
    // 4. The 413 response tells the client exactly what happened. The client can
    //    use GET /api/item/ to list items and find the partial one by timestamp
    //    if it needs to resume or clean up.
    //
    if body_truncated.load(Ordering::Relaxed) {
        return Err(StatusCode::PAYLOAD_TOO_LARGE);
    }

    let compression = item_with_meta.item.compression.clone();
    let tags = item_with_meta.tags.iter().map(|t| t.name.clone()).collect();
    let metadata = item_with_meta.meta_as_map();

    let item_info = ItemInfo {
        id: item_with_meta.item.id.ok_or_else(|| {
            warn!("Item missing ID");
            StatusCode::INTERNAL_SERVER_ERROR
        })?,
        ts: item_with_meta.item.ts.to_rfc3339(),
        size: item_with_meta.item.size,
        compression,
        tags,
        metadata,
    };

    let response = ApiResponse {
        success: true,
        data: Some(item_info),
        error: None,
    };

    Ok(Json(response))
}

#[utoipa::path(
    get,
    path = "/api/item/latest/content",
    operation_id = "keep_get_item_latest_content",
    summary = "Download latest item content",
    description = "Get raw content of the most recent item. Filter by tags. Binary content can be restricted. \
                   AI agents should use as_meta=true to get content and metadata in a structured JSON format.",
    responses(
        (status = 200, description = "Content retrieved"),
        (status = 400, description = "Binary content not allowed"),
        (status = 401, description = "Unauthorized"),
        (status = 404, description = "Item not found"),
        (status = 500, description = "Internal server error")
    ),
    params(
        ("tags" = Option<String>, Query, description = "Tags to filter latest item"),
        ("allow_binary" = Option<bool>, Query, description = "Allow binary content"),
        ("offset" = Option<u64>, Query, description = "Byte offset to start reading"),
        ("length" = Option<u64>, Query, description = "Number of bytes to read"),
        ("stream" = Option<bool>, Query, description = "Stream response (true) or build in memory (false)"),
        ("as_meta" = Option<bool>, Query, description = "Return content and metadata in JSON format (recommended for AI agents)")
    ),
    security(
        ("bearerAuth" = [])
    ),
    tag = "item"
)]
pub async fn handle_get_item_latest_content(
    State(state): State<AppState>,
    Query(params): Query<ItemContentQuery>,
) -> Result<Response, StatusCode> {
    let tags: Vec<String> = params
        .tags
        .as_ref()
        .map(|s| parse_comma_tags(s))
        .unwrap_or_default();

    let db = state.db.clone();
    let data_dir = state.data_dir.clone();

    // First find the item to get its ID and metadata
    let find_tags = tags;
    let item_with_meta = task::spawn_blocking(move || {
        let conn = db.blocking_lock();
        let item_service = ItemService::new(data_dir);
        item_service.find_item(&conn, &[], &find_tags, &HashMap::new())
    })
    .await
    .map_err(|e| {
        warn!("Blocking task failed: {e}");
        StatusCode::INTERNAL_SERVER_ERROR
    })?;

    match item_with_meta {
        Ok(item) => {
            let item_id = item.item.id.ok_or_else(|| {
                warn!("Item missing ID");
                StatusCode::INTERNAL_SERVER_ERROR
            })?;
            let metadata = item.meta_as_map();
            // Handle as_meta parameter
            if params.as_meta {
                handle_as_meta_response_with_metadata(
                    &state.db,
                    &state.data_dir,
                    item_id,
                    &metadata,
                    params.offset,
                    params.length,
                )
                .await
            } else {
                stream_item_content_response_with_metadata(
                    &state.db,
                    &state.data_dir,
                    item_id,
                    &metadata,
                    params.allow_binary,
                    params.offset,
                    params.length,
                    params.stream,
                    None,
                    params.decompress,
                )
                .await
            }
        }
        Err(CoreError::ItemNotFoundGeneric) => Err(StatusCode::NOT_FOUND),
        Err(e) => {
            warn!("Failed to find latest item for content: {e}");
            Err(StatusCode::INTERNAL_SERVER_ERROR)
        }
    }
}

#[utoipa::path(
    get,
    path = "/api/item/{item_id}/content",
    operation_id = "keep_get_item_content",
    summary = "Download item content",
    description = "Get raw content of a specific item by ID. Binary content can be restricted. \
                   AI agents should use as_meta=true to get content and metadata in a structured JSON format.",
    responses(
        (status = 200, description = "Content retrieved"),
        (status = 400, description = "Invalid ID or binary not allowed"),
        (status = 401, description = "Unauthorized"),
        (status = 404, description = "Item not found"),
        (status = 500, description = "Internal server error")
    ),
    params(
        ("item_id" = i64, Path, description = "Item ID"),
        ("allow_binary" = Option<bool>, Query, description = "Allow binary content"),
        ("offset" = Option<u64>, Query, description = "Byte offset to start reading"),
        ("length" = Option<u64>, Query, description = "Number of bytes to read"),
        ("stream" = Option<bool>, Query, description = "Stream response (true) or build in memory (false)"),
        ("as_meta" = Option<bool>, Query, description = "Return content and metadata in JSON format (recommended for AI agents)")
    ),
    security(
        ("bearerAuth" = [])
    ),
    tag = "item"
)]
pub async fn handle_get_item_content(
    State(state): State<AppState>,
    Path(item_id): Path<i64>,
    Query(params): Query<ItemQuery>,
) -> Result<Response, StatusCode> {
    // Validate that item ID is positive to prevent path traversal issues
    if item_id <= 0 {
        return Err(StatusCode::BAD_REQUEST);
    }

    debug!(
        "ITEM_API: Getting content for item {item_id} with stream={}, allow_binary={}, offset={}, length={}",
        params.stream, params.allow_binary, params.offset, params.length
    );

    if params.as_meta {
        // Force stream=false and allow_binary=false for as_meta=true
        let result = handle_as_meta_response(
            &state.db,
            &state.data_dir,
            item_id,
            params.offset,
            params.length,
        )
        .await;
        if let Ok(response) = &result {
            debug!(
                "ITEM_API: Response content-length: {:?}",
                response.headers().get("content-length")
            );
        }
        result
    } else {
        let result = stream_item_content_response(
            &state.db,
            &state.data_dir,
            item_id,
            params.allow_binary,
            params.offset,
            params.length,
            params.stream,
            None,
            params.decompress,
        )
        .await;
        if let Ok(response) = &result {
            debug!(
                "ITEM_API: Response content-length: {:?}",
                response.headers().get("content-length")
            );
        }
        result
    }
}

#[allow(clippy::too_many_arguments)]
async fn stream_item_content_response(
    db: &Arc<tokio::sync::Mutex<rusqlite::Connection>>,
    data_dir: &std::path::Path,
    item_id: i64,
    allow_binary: bool,
    offset: u64,
    length: u64,
    stream: bool,
    _filter: Option<String>,
    decompress: bool,
) -> Result<Response, StatusCode> {
    debug!("STREAM_ITEM_CONTENT_RESPONSE: stream={stream}, decompress={decompress}");
    // Get the item with metadata once
    let db_clone = db.clone();
    let data_dir_clone = data_dir.to_path_buf();
    let item_with_meta = task::spawn_blocking(move || {
        let conn = db_clone.blocking_lock();
        let item_service = ItemService::new(data_dir_clone);
        item_service.get_item(&conn, item_id)
    })
    .await
    .map_err(|e| {
        warn!("Blocking task failed for item {item_id}: {e}");
        StatusCode::INTERNAL_SERVER_ERROR
    })?
    .map_err(|e| {
        warn!("Failed to get item {item_id} for content: {e}");
        StatusCode::INTERNAL_SERVER_ERROR
    })?;

    let metadata = item_with_meta.meta_as_map();
    stream_item_content_response_with_metadata(
        db,
        data_dir,
        item_id,
        &metadata,
        allow_binary,
        offset,
        length,
        stream,
        None,
        decompress,
    )
    .await
}

/// Stream raw (unprocessed) content directly from the item file.
///
/// Returns the stored file bytes without decompression or filtering using streaming.
async fn stream_raw_content_response(
    db: &Arc<tokio::sync::Mutex<rusqlite::Connection>>,
    data_dir: &std::path::Path,
    item_id: i64,
    offset: u64,
    length: u64,
) -> Result<Response, StatusCode> {
    // Get item info to find the compression type
    let db_clone = db.clone();
    let data_dir_clone = data_dir.to_path_buf();
    let item_with_meta = task::spawn_blocking(move || {
        let conn = db_clone.blocking_lock();
        let item_service = ItemService::new(data_dir_clone);
        item_service.get_item(&conn, item_id)
    })
    .await
    .map_err(|e| {
        warn!("Blocking task failed for item {item_id}: {e}");
        StatusCode::INTERNAL_SERVER_ERROR
    })?
    .map_err(|e| {
        warn!("Failed to get item {item_id} for raw content: {e}");
        StatusCode::INTERNAL_SERVER_ERROR
    })?;

    let compression = item_with_meta.item.compression.clone();

    // Open file directly for raw content streaming
    let item_path = data_dir.join(item_id.to_string());
    let reader: Box<dyn Read + Send> = task::spawn_blocking(move || {
        let file = std::fs::File::open(&item_path).map_err(|e| {
            CoreError::Io(std::io::Error::new(
                std::io::ErrorKind::NotFound,
                format!("Item file not found: {item_path:?}: {e}"),
            ))
        })?;
        Ok::<Box<dyn Read + Send>, CoreError>(Box::new(file))
    })
    .await
    .map_err(|e| {
        warn!("Blocking task failed for item {item_id}: {e}");
        StatusCode::INTERNAL_SERVER_ERROR
    })?
    .map_err(|e| {
        warn!("Failed to get raw content reader for item {item_id}: {e}");
        StatusCode::INTERNAL_SERVER_ERROR
    })?;

    // Get the actual file size on disk (raw bytes, not uncompressed size)
    let file_size = {
        let item_path = data_dir.join(item_id.to_string());
        std::fs::metadata(&item_path).map(|m| m.len()).unwrap_or(0)
    };

    // Calculate the actual response length
    let content_len = file_size;
    let start = std::cmp::min(offset, content_len);
    let end = if length > 0 {
        std::cmp::min(start + length, content_len)
    } else {
        content_len
    };
    let response_len = end - start;

    // Create a channel to stream data between blocking thread and async runtime
    let (tx, rx) = tokio::sync::mpsc::channel::<Result<Vec<u8>, std::io::Error>>(16);

    // Spawn blocking task to read with offset and length
    tokio::task::spawn_blocking(move || {
        let mut reader = reader;
        let mut buf = [0u8; crate::common::PIPESIZE];

        // Apply offset by reading and discarding bytes
        if offset > 0 {
            let mut remaining = offset;
            while remaining > 0 {
                let to_read = std::cmp::min(remaining, buf.len() as u64) as usize;
                match reader.read(&mut buf[..to_read]) {
                    Ok(0) => break,
                    Ok(n) => remaining -= n as u64,
                    Err(e) => {
                        let _ = tx.blocking_send(Err(e));
                        return;
                    }
                }
            }
        }

        // Read and send data up to the specified length
        let mut remaining_length = length;

        loop {
            let to_read = if length > 0 {
                std::cmp::min(remaining_length, buf.len() as u64) as usize
            } else {
                buf.len()
            };

            if to_read == 0 {
                break;
            }

            match reader.read(&mut buf[..to_read]) {
                Ok(0) => break,
                Ok(n) => {
                    let chunk = buf[..n].to_vec();
                    if tx.blocking_send(Ok(chunk)).is_err() {
                        break;
                    }
                    if length > 0 {
                        remaining_length -= n as u64;
                        if remaining_length == 0 {
                            break;
                        }
                    }
                }
                Err(e) => {
                    let _ = tx.blocking_send(Err(e));
                    break;
                }
            }
        }
    });

    // Convert the receiver into a stream
    let stream = tokio_stream::wrappers::ReceiverStream::new(rx);
    let body = axum::body::Body::from_stream(stream);

    let response = Response::builder()
        .header(header::CONTENT_TYPE, "application/octet-stream")
        .header("X-Keep-Compression", &compression)
        .header(header::CONTENT_LENGTH, response_len)
        .body(body)
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    Ok(response)
}

#[allow(clippy::too_many_arguments)]
async fn stream_item_content_response_with_metadata(
    db: &Arc<tokio::sync::Mutex<rusqlite::Connection>>,
    data_dir: &std::path::Path,
    item_id: i64,
    metadata: &HashMap<String, String>,
    allow_binary: bool,
    offset: u64,
    length: u64,
    stream: bool,
    _filter: Option<String>,
    decompress: bool,
) -> Result<Response, StatusCode> {
    debug!("STREAM_ITEM_CONTENT_RESPONSE_WITH_METADATA: stream={stream}, decompress={decompress}");

    // When decompress=false, return raw stored bytes
    if !decompress {
        return stream_raw_content_response(db, data_dir, item_id, offset, length).await;
    }

    let mime_type = get_mime_type(metadata);

    // Check if content is binary when allow_binary is false.
    // Uses a sample of actual content bytes (not metadata-only) for reliable detection.
    if !allow_binary {
        let db_check = db.clone();
        let data_dir_check = data_dir.to_path_buf();
        let is_binary = task::spawn_blocking(move || {
            let conn = db_check.blocking_lock();
            let item_service = ItemService::new(data_dir_check);
            let (mut reader, _) = item_service.get_item_content_streaming(&conn, item_id)?;
            let mut sample = vec![0u8; crate::common::PIPESIZE];
            let n = reader.read(&mut sample)?;
            sample.truncate(n);
            Ok::<bool, CoreError>(crate::common::is_binary::is_binary(&sample))
        })
        .await
        .map_err(|e| {
            warn!("Blocking task failed for binary check on item {item_id}: {e}");
            StatusCode::INTERNAL_SERVER_ERROR
        })?
        .map_err(|e| {
            warn!("Failed to check binary status for item {item_id}: {e}");
            StatusCode::INTERNAL_SERVER_ERROR
        })?;

        if is_binary {
            return Err(StatusCode::BAD_REQUEST);
        }
    }

    if stream {
        debug!("STREAMING: Using streaming approach");
        let db = db.clone();
        let data_dir = data_dir.to_path_buf();
        let (tx, rx) = mpsc::channel::<Result<Vec<u8>, std::io::Error>>(16);

        tokio::task::spawn_blocking(move || {
            let conn = db.blocking_lock();
            let item_service = ItemService::new(data_dir);
            let (mut reader, _, _) =
                match item_service.get_item_content_info_streaming(&conn, item_id, None) {
                    Ok(r) => r,
                    Err(e) => {
                        let _ = tx.blocking_send(Err(std::io::Error::other(format!("{e}"))));
                        return;
                    }
                };

            // Apply offset
            if offset > 0 {
                let mut buf = [0u8; crate::common::PIPESIZE];
                let mut remaining = offset;
                while remaining > 0 {
                    let to_read = std::cmp::min(remaining, buf.len() as u64) as usize;
                    match reader.read(&mut buf[..to_read]) {
                        Ok(0) => break,
                        Ok(n) => remaining -= n as u64,
                        Err(e) => {
                            let _ = tx.blocking_send(Err(e));
                            return;
                        }
                    }
                }
            }

            // Read and send data
            let mut buf = [0u8; crate::common::PIPESIZE];
            let mut remaining_length = length;
            loop {
                let to_read = if length > 0 {
                    std::cmp::min(remaining_length, buf.len() as u64) as usize
                } else {
                    buf.len()
                };
                if to_read == 0 {
                    break;
                }
                match reader.read(&mut buf[..to_read]) {
                    Ok(0) => break,
                    Ok(n) => {
                        if tx.blocking_send(Ok(buf[..n].to_vec())).is_err() {
                            break;
                        }
                        if length > 0 {
                            remaining_length -= n as u64;
                            if remaining_length == 0 {
                                break;
                            }
                        }
                    }
                    Err(e) => {
                        let _ = tx.blocking_send(Err(e));
                        break;
                    }
                }
            }
        });

        let stream = tokio_stream::wrappers::ReceiverStream::new(rx);
        let body = axum::body::Body::from_stream(stream);
        let response = Response::builder()
            .header(header::CONTENT_TYPE, mime_type)
            .body(body)
            .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
        Ok(response)
    } else {
        debug!("NON-STREAMING: Building full response in memory using streaming reader");
        let db = db.clone();
        let data_dir = data_dir.to_path_buf();

        // Get streaming reader from item service
        let (reader, content_len_result) = tokio::task::spawn_blocking(move || {
            let conn = db.blocking_lock();
            let item_service = ItemService::new(data_dir);
            let (reader, item_with_meta) =
                item_service.get_item_content_streaming(&conn, item_id)?;
            let content_len = item_with_meta.item.size.unwrap_or(0);
            Ok::<_, CoreError>((reader, content_len))
        })
        .await
        .map_err(|e| {
            warn!("Blocking task failed for item {item_id}: {e}");
            StatusCode::INTERNAL_SERVER_ERROR
        })?
        .map_err(|e| {
            warn!("Failed to get content reader for item {item_id}: {e}");
            StatusCode::INTERNAL_SERVER_ERROR
        })?;

        let content_len = content_len_result as u64;

        // Read content with offset and length using fixed-size buffer
        let content = tokio::task::spawn_blocking(move || {
            let mut r = reader;
            crate::common::read_with_bounds(&mut r, offset, length, content_len)
                .map_err(CoreError::Io)
        })
        .await
        .map_err(|e| {
            warn!("Blocking task failed for item {item_id}: {e}");
            StatusCode::INTERNAL_SERVER_ERROR
        })?
        .map_err(|e| {
            warn!("Failed to read content for item {item_id}: {e}");
            StatusCode::INTERNAL_SERVER_ERROR
        })?;

        debug!("NON-STREAMING: Response length: {}", content.len());

        ResponseBuilder::binary(&content, &mime_type)
    }
}

#[utoipa::path(
    get,
    path = "/api/item/latest/meta",
    operation_id = "keep_get_item_latest_meta",
    summary = "Get latest item metadata",
    description = "Retrieve metadata for the most recent item. Filter by tags.",
    responses(
        (status = 200, description = "Metadata retrieved", body = MetadataResponse),
        (status = 401, description = "Unauthorized"),
        (status = 404, description = "Item not found"),
        (status = 500, description = "Internal server error")
    ),
    params(
        ("tags" = Option<String>, Query, description = "Tags to filter latest item")
    ),
    security(
        ("bearerAuth" = [])
    ),
    tag = "item"
)]
pub async fn handle_get_item_latest_meta(
    State(state): State<AppState>,
    Query(params): Query<TagsQuery>,
) -> Result<Json<ApiResponse<HashMap<String, String>>>, StatusCode> {
    let tags: Vec<String> = params
        .tags
        .as_ref()
        .map(|s| parse_comma_tags(s))
        .unwrap_or_default();

    let db = state.db.clone();
    let data_dir = state.data_dir.clone();

    let find_tags = tags;
    let result = task::spawn_blocking(move || {
        let conn = db.blocking_lock();
        let item_service = ItemService::new(data_dir);
        item_service.find_item(&conn, &[], &find_tags, &HashMap::new())
    })
    .await
    .map_err(|e| {
        warn!("Blocking task failed: {e}");
        StatusCode::INTERNAL_SERVER_ERROR
    })?;

    match result {
        Ok(item_with_meta) => {
            let item_meta = item_with_meta.meta_as_map();

            let response = ApiResponse {
                success: true,
                data: Some(item_meta),
                error: None,
            };

            Ok(Json(response))
        }
        Err(e) => Err(handle_item_error(e)),
    }
}

#[utoipa::path(
    get,
    path = "/api/item/{item_id}/meta",
    operation_id = "keep_get_item_meta",
    summary = "Get item metadata",
    description = "Retrieve metadata for a specific item by ID.",
    responses(
        (status = 200, description = "Metadata retrieved", body = MetadataResponse),
        (status = 400, description = "Invalid ID"),
        (status = 401, description = "Unauthorized"),
        (status = 404, description = "Item not found"),
        (status = 500, description = "Internal server error")
    ),
    params(
        ("item_id" = i64, Path, description = "Item ID")
    ),
    security(
        ("bearerAuth" = [])
    ),
    tag = "item"
)]
pub async fn handle_get_item_meta(
    State(state): State<AppState>,
    Path(item_id): Path<i64>,
) -> Result<Json<ApiResponse<HashMap<String, String>>>, StatusCode> {
    if item_id <= 0 {
        return Err(StatusCode::BAD_REQUEST);
    }

    let db = state.db.clone();
    let data_dir = state.data_dir.clone();

    let result = task::spawn_blocking(move || {
        let conn = db.blocking_lock();
        let item_service = ItemService::new(data_dir);
        item_service.get_item(&conn, item_id)
    })
    .await
    .map_err(|e| {
        warn!("Blocking task failed for item {item_id}: {e}");
        StatusCode::INTERNAL_SERVER_ERROR
    })?;

    match result {
        Ok(item_with_meta) => {
            let item_meta = item_with_meta.meta_as_map();

            let response = ApiResponse {
                success: true,
                data: Some(item_meta),
                error: None,
            };

            Ok(Json(response))
        }
        Err(e) => Err(handle_item_error(e)),
    }
}

pub async fn handle_post_item_meta(
    State(state): State<AppState>,
    Path(item_id): Path<i64>,
    Json(metadata): Json<HashMap<String, String>>,
) -> Result<Json<ApiResponse<()>>, StatusCode> {
    if item_id <= 0 {
        return Err(StatusCode::BAD_REQUEST);
    }

    let db = state.db.clone();
    let data_dir = state.data_dir.clone();
    let meta = metadata.clone();

    // Verify item exists and add metadata in a single blocking task
    task::spawn_blocking(move || {
        let conn = db.blocking_lock();
        let item_service = ItemService::new(data_dir);
        item_service
            .get_item(&conn, item_id)
            .map_err(handle_item_error)?;

        for (key, value) in &meta {
            crate::db::add_meta(&conn, item_id, key, value).map_err(|e| {
                warn!("Failed to add metadata {key} for item {item_id}: {e}");
                StatusCode::INTERNAL_SERVER_ERROR
            })?;
        }
        Ok::<(), StatusCode>(())
    })
    .await
    .map_err(|e| {
        warn!("Blocking task failed for item {item_id}: {e}");
        StatusCode::INTERNAL_SERVER_ERROR
    })??;

    let response = ApiResponse {
        success: true,
        data: Some(()),
        error: None,
    };

    Ok(Json(response))
}

#[utoipa::path(
    delete,
    path = "/api/item/{item_id}",
    tag = "items",
    params(
        ("item_id" = i64, Path, description = "ID of the item to delete")
    ),
    responses(
        (status = 200, description = "Item deleted successfully", body = ApiResponse<ItemInfo>),
        (status = 401, description = "Unauthorized"),
        (status = 404, description = "Item not found"),
        (status = 500, description = "Internal server error")
    ),
    security(
        ("bearerAuth" = [])
    )
)]
pub async fn handle_delete_item(
    State(state): State<AppState>,
    Path(item_id): Path<i64>,
) -> Result<Json<ApiResponse<ItemInfo>>, StatusCode> {
    if item_id <= 0 {
        return Err(StatusCode::BAD_REQUEST);
    }

    let db = state.db.clone();
    let data_dir = state.data_dir.clone();

    let deleted_item = task::spawn_blocking(move || {
        let mut conn = db.blocking_lock();
        let item_service = crate::services::ItemService::new(data_dir);
        item_service.delete_item(&mut conn, item_id)
    })
    .await
    .map_err(|e| {
        warn!("Blocking task failed for delete item {item_id}: {e}");
        StatusCode::INTERNAL_SERVER_ERROR
    })?
    .map_err(handle_item_error)?;

    let item_info = ItemInfo {
        id: deleted_item.id.ok_or_else(|| {
            warn!("Item missing ID");
            StatusCode::INTERNAL_SERVER_ERROR
        })?,
        ts: deleted_item.ts.to_rfc3339(),
        size: deleted_item.size,
        compression: deleted_item.compression,
        tags: vec![],
        metadata: HashMap::new(),
    };

    let response = ApiResponse {
        success: true,
        data: Some(item_info),
        error: None,
    };

    Ok(Json(response))
}

#[utoipa::path(
    get,
    path = "/api/item/{item_id}/info",
    tag = "items",
    params(
        ("item_id" = i64, Path, description = "ID of the item to get info for")
    ),
    responses(
        (status = 200, description = "Item info retrieved successfully", body = ApiResponse<ItemInfo>),
        (status = 401, description = "Unauthorized"),
        (status = 404, description = "Item not found"),
        (status = 500, description = "Internal server error")
    ),
    security(
        ("bearerAuth" = [])
    )
)]
pub async fn handle_get_item_info(
    State(state): State<AppState>,
    Path(item_id): Path<i64>,
) -> Result<Json<ApiResponse<ItemInfo>>, StatusCode> {
    if item_id <= 0 {
        return Err(StatusCode::BAD_REQUEST);
    }

    let db = state.db.clone();
    let data_dir = state.data_dir.clone();

    let item_with_meta = task::spawn_blocking(move || {
        let conn = db.blocking_lock();
        let item_service = crate::services::ItemService::new(data_dir);
        item_service.get_item(&conn, item_id)
    })
    .await
    .map_err(|e| {
        warn!("Blocking task failed for get item info {item_id}: {e}");
        StatusCode::INTERNAL_SERVER_ERROR
    })?
    .map_err(handle_item_error)?;

    let tags: Vec<String> = item_with_meta.tags.iter().map(|t| t.name.clone()).collect();
    let metadata = item_with_meta.meta_as_map();

    let item_info = ItemInfo {
        id: item_with_meta.item.id.ok_or_else(|| {
            warn!("Item missing ID");
            StatusCode::INTERNAL_SERVER_ERROR
        })?,
        ts: item_with_meta.item.ts.to_rfc3339(),
        size: item_with_meta.item.size,
        compression: item_with_meta.item.compression.clone(),
        tags,
        metadata,
    };

    let response = ApiResponse {
        success: true,
        data: Some(item_info),
        error: None,
    };

    Ok(Json(response))
}

#[derive(serde::Deserialize)]
pub struct DiffQuery {
    id_a: Option<i64>,
    id_b: Option<i64>,
    tag_a: Option<String>,
    tag_b: Option<String>,
}

#[utoipa::path(
    get,
    path = "/api/diff",
    tag = "items",
    params(
        ("id_a" = Option<i64>, Query, description = "First item ID"),
        ("id_b" = Option<i64>, Query, description = "Second item ID"),
        ("tag_a" = Option<String>, Query, description = "Tag to find first item"),
        ("tag_b" = Option<String>, Query, description = "Tag to find second item"),
    ),
    responses(
        (status = 200, description = "Diff between two items", body = ApiResponse<Vec<String>>),
        (status = 400, description = "Invalid request - need two items to compare"),
        (status = 401, description = "Unauthorized"),
        (status = 404, description = "Item not found"),
        (status = 500, description = "Internal server error")
    ),
    security(
        ("bearerAuth" = [])
    )
)]
pub async fn handle_diff_items(
    State(state): State<AppState>,
    Query(query): Query<DiffQuery>,
) -> Result<Json<ApiResponse<Vec<String>>>, StatusCode> {
    let db = state.db.clone();
    let data_dir = state.data_dir.clone();
    let id_a_param = query.id_a;
    let id_b_param = query.id_b;
    let tag_a_param = query.tag_a;
    let tag_b_param = query.tag_b;

    let diff_lines = task::spawn_blocking(move || {
        let conn = db.blocking_lock();
        let item_service = crate::services::ItemService::new(data_dir);

        let item_a = if let Some(id_a) = id_a_param {
            item_service
                .get_item(&conn, id_a)
                .map_err(handle_item_error)?
        } else if let Some(tag) = &tag_a_param {
            #[allow(clippy::cloned_ref_to_slice_refs)]
            let tags = &[tag.clone()];
            item_service
                .find_item(&conn, &[], tags, &HashMap::new())
                .map_err(handle_item_error)?
        } else {
            return Err(StatusCode::BAD_REQUEST);
        };

        let item_b = if let Some(id_b) = id_b_param {
            item_service
                .get_item(&conn, id_b)
                .map_err(handle_item_error)?
        } else if let Some(tag) = &tag_b_param {
            #[allow(clippy::cloned_ref_to_slice_refs)]
            let tags = &[tag.clone()];
            item_service
                .find_item(&conn, &[], tags, &HashMap::new())
                .map_err(handle_item_error)?
        } else {
            return Err(StatusCode::BAD_REQUEST);
        };

        let id_a = item_a.item.id.ok_or(StatusCode::BAD_REQUEST)?;
        let id_b = item_b.item.id.ok_or(StatusCode::BAD_REQUEST)?;

        // Size limit for diff operation (10MB per item)
        const MAX_DIFF_SIZE: i64 = 10 * 1024 * 1024;

        if let Some(size_a) = item_a.item.size
            && size_a > MAX_DIFF_SIZE
        {
            warn!(
                "Item A ({}) exceeds diff size limit: {} > {}",
                id_a, size_a, MAX_DIFF_SIZE
            );
            return Err(StatusCode::PAYLOAD_TOO_LARGE);
        }
        if let Some(size_b) = item_b.item.size
            && size_b > MAX_DIFF_SIZE
        {
            warn!(
                "Item B ({}) exceeds diff size limit: {} > {}",
                id_b, size_b, MAX_DIFF_SIZE
            );
            return Err(StatusCode::PAYLOAD_TOO_LARGE);
        }

        let (mut reader_a, _) = item_service
            .get_item_content_streaming(&conn, id_a)
            .map_err(handle_item_error)?;
        let (mut reader_b, _) = item_service
            .get_item_content_streaming(&conn, id_b)
            .map_err(handle_item_error)?;

        let mut content_a = Vec::new();
        reader_a.read_to_end(&mut content_a).map_err(|e| {
            log::error!("Failed to read content A: {e}");
            StatusCode::INTERNAL_SERVER_ERROR
        })?;

        let mut content_b = Vec::new();
        reader_b.read_to_end(&mut content_b).map_err(|e| {
            log::error!("Failed to read content B: {e}");
            StatusCode::INTERNAL_SERVER_ERROR
        })?;

        Ok::<Vec<String>, StatusCode>(compute_diff(&content_a, &content_b))
    })
    .await
    .map_err(|e| {
        warn!("Blocking task failed for diff items: {e}");
        StatusCode::INTERNAL_SERVER_ERROR
    })??;

    let response = ApiResponse {
        success: true,
        data: Some(diff_lines),
        error: None,
    };

    Ok(Json(response))
}

fn compute_diff(a: &[u8], b: &[u8]) -> Vec<String> {
    let text_a = String::from_utf8_lossy(a);
    let text_b = String::from_utf8_lossy(b);

    let old_lines: Vec<&str> = text_a.lines().collect();
    let new_lines: Vec<&str> = text_b.lines().collect();

    let text_diff = similar::TextDiff::from_lines(text_a.as_ref(), text_b.as_ref());
    let ops = text_diff.ops();

    let mut diff_lines = Vec::new();

    for op in ops {
        for change in op.iter_changes(&old_lines, &new_lines) {
            match change.tag() {
                similar::ChangeTag::Equal => {
                    diff_lines.push(format!("  {}", change.value()));
                }
                similar::ChangeTag::Delete => {
                    diff_lines.push(format!("- {}", change.value()));
                }
                similar::ChangeTag::Insert => {
                    diff_lines.push(format!("+ {}", change.value()));
                }
            }
        }
    }

    diff_lines
}

/// Updates metadata for an existing item by re-running meta plugins on stored content.
///
/// Reads the item's stored file, decompresses it, runs the specified meta plugins,
/// and stores the resulting metadata. Also applies any direct metadata overrides.
///
/// # Arguments
///
/// * `state` - Application state with DB connection and data directory.
/// * `item_id` - The item ID to update.
/// * `params` - Query parameters specifying plugins and metadata.
///
/// # Returns
///
/// JSON response with updated item info, or error status code.
pub async fn handle_update_item(
    State(state): State<AppState>,
    Path(item_id): Path<i64>,
    Query(params): Query<crate::modes::server::common::UpdateItemQuery>,
) -> Result<Json<ApiResponse<ItemInfo>>, StatusCode> {
    // Parse plugin names
    let plugin_names: Vec<String> = params
        .plugins
        .as_deref()
        .map(|s| {
            s.split(',')
                .map(|p| p.trim().to_string())
                .filter(|p| !p.is_empty())
                .collect()
        })
        .unwrap_or_default();

    // Parse metadata overrides
    let metadata: HashMap<String, String> = if let Some(ref meta_str) = params.metadata {
        serde_json::from_str(meta_str).map_err(|e| {
            warn!("Failed to parse metadata JSON string: {e}");
            StatusCode::BAD_REQUEST
        })?
    } else {
        HashMap::new()
    };

    // Parse tags
    let tags: Vec<String> = params
        .tags
        .as_deref()
        .map(crate::services::utils::parse_comma_tags)
        .unwrap_or_default();

    let db = state.db.clone();
    let data_dir = state.data_dir.clone();
    let settings = state.settings.clone();
    let size_param = params.size;

    let item_info = task::spawn_blocking(move || {
        let mut conn = db.blocking_lock();
        let item_service = crate::services::ItemService::new(data_dir);

        // If only size is being set (no plugins/metadata/tags), do a lightweight update
        #[allow(clippy::collapsible_if)]
        if let Some(size) = size_param {
            if plugin_names.is_empty() && metadata.is_empty() && tags.is_empty() {
                return match crate::db::get_item(&conn, item_id) {
                    Ok(Some(mut item)) => {
                        item.size = Some(size);
                        if let Err(e) = crate::db::update_item(&conn, item) {
                            warn!("Failed to update item size: {e}");
                            return Err(StatusCode::INTERNAL_SERVER_ERROR);
                        }
                        match item_service.get_item(&conn, item_id) {
                            Ok(iwm) => {
                                let tags: Vec<String> =
                                    iwm.tags.iter().map(|t| t.name.clone()).collect();
                                let metadata = iwm.meta_as_map();
                                Ok(ItemInfo {
                                    id: item_id,
                                    ts: iwm.item.ts.to_rfc3339(),
                                    size: iwm.item.size,
                                    compression: iwm.item.compression.clone(),
                                    tags,
                                    metadata,
                                })
                            }
                            Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
                        }
                    }
                    Ok(None) => Err(StatusCode::NOT_FOUND),
                    Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
                };
            }
        }

        let result = item_service.update_item_plugins(
            &mut conn,
            item_id,
            &plugin_names,
            metadata,
            &tags,
            &settings,
        );

        match result {
            Ok(item_with_meta) => {
                let tags: Vec<String> =
                    item_with_meta.tags.iter().map(|t| t.name.clone()).collect();
                let metadata = item_with_meta.meta_as_map();

                Ok(ItemInfo {
                    id: item_with_meta.item.id.ok_or_else(|| {
                        warn!("Item missing ID");
                        StatusCode::INTERNAL_SERVER_ERROR
                    })?,
                    ts: item_with_meta.item.ts.to_rfc3339(),
                    size: item_with_meta.item.size,
                    compression: item_with_meta.item.compression.clone(),
                    tags,
                    metadata,
                })
            }
            Err(CoreError::ItemNotFound(_)) => Err(StatusCode::NOT_FOUND),
            Err(e) => {
                warn!("Failed to update item {item_id}: {e}");
                Err(StatusCode::INTERNAL_SERVER_ERROR)
            }
        }
    })
    .await
    .map_err(|e| {
        warn!("Blocking task failed for update item {item_id}: {e}");
        StatusCode::INTERNAL_SERVER_ERROR
    })??;

    Ok(Json(ApiResponse {
        success: true,
        data: Some(item_info),
        error: None,
    }))
}
