use crate::actions::helpers;
use crate::errors::{DynoxideError, Result};
use crate::storage_backend::StorageBackend;
use crate::types::{self, AttributeValue};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Internal result from the transactional update work closure.
struct UpdateWorkResult {
    old_item: HashMap<String, AttributeValue>,
    item: HashMap<String, AttributeValue>,
    item_json: String,
    size: usize,
}

/// Internal deserialization struct for detecting missing fields.
#[derive(Debug, Default, Deserialize)]
struct UpdateItemRequestRaw {
    #[serde(rename = "TableName", default)]
    table_name: Option<String>,
    #[serde(rename = "Key", default)]
    key: Option<HashMap<String, AttributeValue>>,
    #[serde(rename = "UpdateExpression", default)]
    update_expression: Option<String>,
    #[serde(rename = "ConditionExpression", default)]
    condition_expression: Option<String>,
    #[serde(rename = "ExpressionAttributeNames", default)]
    expression_attribute_names: Option<HashMap<String, String>>,
    #[serde(rename = "ExpressionAttributeValues", default)]
    expression_attribute_values: Option<HashMap<String, AttributeValue>>,
    #[serde(rename = "ReturnValues", default)]
    return_values: Option<String>,
    #[serde(rename = "ReturnConsumedCapacity", default)]
    return_consumed_capacity: Option<String>,
    #[serde(rename = "ReturnValuesOnConditionCheckFailure", default)]
    return_values_on_condition_check_failure: Option<String>,
    #[serde(rename = "ReturnItemCollectionMetrics", default)]
    return_item_collection_metrics: Option<String>,
    #[serde(rename = "AttributeUpdates", default)]
    attribute_updates: Option<HashMap<String, AttributeValueUpdate>>,
    #[serde(rename = "Expected", default)]
    expected: Option<serde_json::Value>,
    #[serde(rename = "ConditionalOperator", default)]
    conditional_operator: Option<String>,
}

#[derive(Debug, Default)]
pub struct UpdateItemRequest {
    pub table_name: String,
    pub key: HashMap<String, AttributeValue>,
    pub update_expression: Option<String>,
    pub condition_expression: Option<String>,
    pub expression_attribute_names: Option<HashMap<String, String>>,
    pub expression_attribute_values: Option<HashMap<String, AttributeValue>>,
    pub return_values: Option<String>,
    pub return_consumed_capacity: Option<String>,
    pub return_values_on_condition_check_failure: Option<String>,
    pub return_item_collection_metrics: Option<String>,
    pub attribute_updates: Option<HashMap<String, AttributeValueUpdate>>,
    pub expected: Option<serde_json::Value>,
    pub conditional_operator: Option<String>,
}

/// First invalid `Return*` enum for UpdateItem, ReturnValues first, or `None` if
/// all valid. UpdateItem stops at the first; PutItem aggregates.
fn first_invalid_return_enum(
    return_values: Option<&str>,
    return_consumed_capacity: Option<&str>,
    return_item_collection_metrics: Option<&str>,
) -> Option<String> {
    if let Some(rv) = return_values {
        if !["ALL_NEW", "UPDATED_OLD", "ALL_OLD", "NONE", "UPDATED_NEW"].contains(&rv) {
            return Some(format!(
                "Value '{}' at 'returnValues' failed to satisfy constraint: \
                 Member must satisfy enum value set: \
                 [ALL_NEW, UPDATED_OLD, ALL_OLD, NONE, UPDATED_NEW]",
                rv
            ));
        }
    }
    if let Some(rcc) = return_consumed_capacity {
        if !["INDEXES", "TOTAL", "NONE"].contains(&rcc) {
            return Some(format!(
                "Value '{}' at 'returnConsumedCapacity' failed to satisfy constraint: \
                 Member must satisfy enum value set: [INDEXES, TOTAL, NONE]",
                rcc
            ));
        }
    }
    if let Some(ricm) = return_item_collection_metrics {
        if !["SIZE", "NONE"].contains(&ricm) {
            return Some(format!(
                "Value '{}' at 'returnItemCollectionMetrics' failed to satisfy constraint: \
                 Member must satisfy enum value set: [SIZE, NONE]",
                ricm
            ));
        }
    }
    None
}

impl<'de> serde::Deserialize<'de> for UpdateItemRequest {
    fn deserialize<D: serde::Deserializer<'de>>(
        deserializer: D,
    ) -> std::result::Result<Self, D::Error> {
        let raw = UpdateItemRequestRaw::deserialize(deserializer)?;
        use crate::validation::{
            TableNameContext, format_validation_errors, table_name_constraint_errors,
        };

        // AWS reports an invalid table name on its own, before the key and
        // Return* enum checks (eu-west-2).
        let table_name_errors =
            table_name_constraint_errors(raw.table_name.as_deref(), TableNameContext::ReadWrite);
        if let Some(msg) = format_validation_errors(&table_name_errors) {
            return Err(serde::de::Error::custom(format!("VALIDATION:{}", msg)));
        }
        let table_name = raw.table_name.unwrap_or_default();

        let mut errors = Vec::new();

        // Key constraint
        if raw.key.is_none() {
            errors.push(
                "Value null at 'key' failed to satisfy constraint: \
                 Member must not be null"
                    .to_string(),
            );
        }

        // UpdateItem stops at the first invalid enum (ReturnValues first) and
        // reports one error, where PutItem aggregates all of them. Keep separate.
        if let Some(enum_err) = first_invalid_return_enum(
            raw.return_values.as_deref(),
            raw.return_consumed_capacity.as_deref(),
            raw.return_item_collection_metrics.as_deref(),
        ) {
            errors.push(enum_err);
        }

        if let Some(msg) = format_validation_errors(&errors) {
            return Err(serde::de::Error::custom(format!("VALIDATION:{}", msg)));
        }

        Ok(UpdateItemRequest {
            table_name,
            key: raw.key.unwrap_or_default(),
            update_expression: raw.update_expression,
            condition_expression: raw.condition_expression,
            expression_attribute_names: raw.expression_attribute_names,
            expression_attribute_values: raw.expression_attribute_values,
            return_values: raw.return_values,
            return_consumed_capacity: raw.return_consumed_capacity,
            return_values_on_condition_check_failure: raw.return_values_on_condition_check_failure,
            return_item_collection_metrics: raw.return_item_collection_metrics,
            attribute_updates: raw.attribute_updates,
            expected: raw.expected,
            conditional_operator: raw.conditional_operator,
        })
    }
}

/// Legacy `AttributeUpdates` entry — one per attribute being modified.
#[derive(Debug, Clone, Default, Deserialize)]
pub struct AttributeValueUpdate {
    #[serde(rename = "Action", default = "default_put_action")]
    pub action: String,
    #[serde(rename = "Value", default)]
    pub value: Option<AttributeValue>,
}

fn default_put_action() -> String {
    "PUT".to_string()
}

#[derive(Debug, Default, Serialize)]
pub struct UpdateItemResponse {
    #[serde(rename = "Attributes", skip_serializing_if = "Option::is_none")]
    pub attributes: Option<HashMap<String, AttributeValue>>,
    #[serde(rename = "ConsumedCapacity", skip_serializing_if = "Option::is_none")]
    pub consumed_capacity: Option<types::ConsumedCapacity>,
    #[serde(
        rename = "ItemCollectionMetrics",
        skip_serializing_if = "Option::is_none"
    )]
    pub item_collection_metrics: Option<crate::types::ItemCollectionMetrics>,
}

/// Apply the `Invalid UpdateExpression:` prefix to a sub-error message at the
/// UpdateItem dispatch boundary. AWS DynamoDB tags the missing-EAV error
/// (and similar UpdateExpression-scoped errors) with this prefix; the prefix
/// must not leak into ConditionExpression contexts that share the same
/// underlying validators in `crate::expressions::mod`. Idempotent so that
/// errors which already carry the prefix (e.g. parser-level syntax errors)
/// are not double-wrapped.
fn wrap_invalid_update_expression(err: String) -> String {
    if err.starts_with("Invalid UpdateExpression:") {
        err
    } else {
        format!("Invalid UpdateExpression: {err}")
    }
}

pub async fn execute<S: StorageBackend>(
    storage: &S,
    mut request: UpdateItemRequest,
) -> Result<UpdateItemResponse> {
    // Validate table name format before checking existence (DynamoDB validates input first)
    crate::validation::validate_table_name(&request.table_name)?;

    // Validate expression/non-expression parameter conflicts BEFORE Expected conversion
    {
        let mut non_expr = Vec::new();
        let mut expr_params = Vec::new();
        if request.attribute_updates.is_some() {
            non_expr.push("AttributeUpdates");
        }
        if request.expected.is_some() {
            non_expr.push("Expected");
        }
        if request.update_expression.is_some() {
            expr_params.push("UpdateExpression");
        }
        if request.condition_expression.is_some() {
            expr_params.push("ConditionExpression");
        }
        let no_raw_eav: Option<serde_json::Value> = None;
        let ctx = helpers::ExpressionParamContext {
            non_expression_params: non_expr,
            expression_params: expr_params,
            all_expression_param_names: vec!["UpdateExpression", "ConditionExpression"],
            expression_attribute_names: &request.expression_attribute_names,
            expression_attribute_values: &request.expression_attribute_values,
            expression_attribute_values_raw: &no_raw_eav,
        };
        helpers::validate_expression_params(&ctx)?;
    }

    // Validate key attribute values (unsupported datatypes, invalid numbers)
    crate::validation::validate_key_attribute_values(&request.key)?;

    // Validate legacy AttributeUpdates parameters
    if request.update_expression.is_none() {
        if let Some(ref updates) = request.attribute_updates {
            for (attr_name, update) in updates {
                let action = update.action.to_uppercase();
                if update.value.is_none() && action != "DELETE" {
                    return Err(DynoxideError::ValidationException(
                        "One or more parameter values were invalid: \
                         Only DELETE action is allowed when no attribute value is specified"
                            .to_string(),
                    ));
                }
                if action == "DELETE" {
                    if let Some(ref val) = update.value {
                        let type_name = match val {
                            AttributeValue::SS(_)
                            | AttributeValue::NS(_)
                            | AttributeValue::BS(_) => None,
                            _ => Some(val.type_name()),
                        };
                        if let Some(tn) = type_name {
                            return Err(DynoxideError::ValidationException(format!(
                                "One or more parameter values were invalid: \
                                 DELETE action with value is not supported for the type {tn}"
                            )));
                        }
                    }
                }
                if action == "ADD" {
                    if let Some(ref val) = update.value {
                        let allowed = matches!(
                            val,
                            AttributeValue::N(_)
                                | AttributeValue::SS(_)
                                | AttributeValue::NS(_)
                                | AttributeValue::BS(_)
                                | AttributeValue::L(_)
                        );
                        if !allowed {
                            let tn = val.type_name();
                            return Err(DynoxideError::ValidationException(format!(
                                "One or more parameter values were invalid: \
                                 ADD action is not supported for the type {tn}"
                            )));
                        }
                    }
                }
                let _ = attr_name; // suppress unused warning
            }
        }
    }

    // Validate legacy Expected parameter
    if request.condition_expression.is_none() && request.update_expression.is_none() {
        if let Some(ref expected_val) = request.expected {
            if let Ok(expected) = serde_json::from_value::<
                HashMap<String, helpers::ExpectedCondition>,
            >(expected_val.clone())
            {
                helpers::validate_expected_conditions(&expected)?;
            }
        }
    }

    // Validate empty UpdateExpression
    if let Some(ref ue) = request.update_expression {
        if ue.is_empty() {
            return Err(DynoxideError::ValidationException(
                "Invalid UpdateExpression: The expression can not be empty;".to_string(),
            ));
        }
    }

    // Validate empty ConditionExpression
    if let Some(ref ce) = request.condition_expression {
        if ce.is_empty() {
            return Err(DynoxideError::ValidationException(
                "Invalid ConditionExpression: The expression can not be empty;".to_string(),
            ));
        }
    }

    // Pre-validate UpdateExpression syntax BEFORE table lookup.
    // DynamoDB validates expression syntax, reserved keywords, undefined attribute
    // names/values, overlapping paths, etc. before checking table existence.
    if let Some(ref ue) = request.update_expression {
        let parsed =
            crate::expressions::update::parse(ue).map_err(DynoxideError::ValidationException)?;

        // Track all attribute name/value references statically (without evaluating)
        let tracker = crate::expressions::TrackedExpressionAttributes::new(
            &request.expression_attribute_names,
            &request.expression_attribute_values,
        );
        crate::expressions::update::track_references(&parsed, &tracker)
            .map_err(|e| DynoxideError::ValidationException(wrap_invalid_update_expression(e)))?;

        // Also walk the ConditionExpression to track its attribute usage
        if let Some(ref ce) = request.condition_expression {
            if let Ok(cond_parsed) = crate::expressions::condition::parse(ce) {
                crate::expressions::condition::track_references(&cond_parsed, &tracker)
                    .map_err(DynoxideError::ValidationException)?;
            }
        }

        // Check for unused expression attribute names/values
        tracker.check_unused()?;
    }

    // Statically validate ConditionExpression (syntax + BETWEEN bounds, etc.) before table lookup
    if let Some(ref ce) = request.condition_expression {
        let parsed = crate::expressions::condition::parse(ce).map_err(|e| {
            DynoxideError::ValidationException(format!("Invalid ConditionExpression: {e}"))
        })?;
        crate::expressions::condition::validate_static(
            &parsed,
            &request.expression_attribute_values,
        )
        .map_err(DynoxideError::ValidationException)?;
        crate::expressions::condition::validate_operand_semantics(
            &parsed,
            &request.expression_attribute_names,
            &request.expression_attribute_values,
        )
        .map_err(|e| {
            DynoxideError::ValidationException(format!("Invalid ConditionExpression: {e}"))
        })?;
    }

    // Convert legacy Expected parameter to ConditionExpression if no expression is set
    if request.condition_expression.is_none() {
        if let Some(ref expected_val) = request.expected {
            if let Ok(expected) = serde_json::from_value::<
                HashMap<String, helpers::ExpectedCondition>,
            >(expected_val.clone())
            {
                if !expected.is_empty() {
                    let (cond_expr, values) = helpers::convert_expected_to_condition(
                        &expected,
                        request.conditional_operator.as_deref(),
                    )?;
                    if !cond_expr.is_empty() {
                        let names = helpers::expected_attr_names(&expected);
                        request.condition_expression = Some(cond_expr);
                        let expr_values = request
                            .expression_attribute_values
                            .get_or_insert_with(HashMap::new);
                        expr_values.extend(values);
                        let expr_names = request
                            .expression_attribute_names
                            .get_or_insert_with(HashMap::new);
                        expr_names.extend(names);
                    }
                }
            }
        }
    }

    let meta = helpers::require_table_for_item_op(storage, &request.table_name).await?;
    let key_schema = helpers::parse_key_schema(&meta)?;

    // Validate ReturnValues parameter
    if let Some(ref rv) = request.return_values {
        let rv_upper = rv.to_uppercase();
        if !["NONE", "ALL_OLD", "ALL_NEW", "UPDATED_OLD", "UPDATED_NEW"]
            .contains(&rv_upper.as_str())
        {
            return Err(DynoxideError::ValidationException(format!(
                "1 validation error detected: Value '{rv}' at 'returnValues' failed to satisfy constraint: \
                 Member must satisfy enum value set: [ALL_NEW, ALL_OLD, NONE, UPDATED_NEW, UPDATED_OLD]"
            )));
        }
    }

    // Validate key
    helpers::validate_key_only(&request.key, &key_schema)?;

    // Extract key values
    // TODO: validation must precede this call -- if reaching this line, caller has already validated keys.
    let (pk, sk) = helpers::extract_key_strings(&request.key, &key_schema)?;

    // Collect the set of attribute names affected by the legacy AttributeUpdates
    // parameter, used later for UPDATED_OLD / UPDATED_NEW extraction.
    let legacy_attr_names: Option<Vec<String>> = request
        .attribute_updates
        .as_ref()
        .map(|updates| updates.keys().cloned().collect());

    // Execution tracker — tracking disabled because unused-reference validation was
    // already done statically by Tracker 1 (pre-validation block above). This tracker
    // only needs name/value resolution, not usage tracking.
    let tracker = crate::expressions::TrackedExpressionAttributes::without_tracking(
        &request.expression_attribute_names,
        &request.expression_attribute_values,
    );

    // Wrap the condition check, base write and the GSI/LSI fan-out in a single
    // transaction so a mid-fan-out failure rolls the whole update back, leaving
    // no torn index. Unconditional because the atomicity guarantee applies to
    // every single-item write. The block captures everything from get_item
    // through the GSI/LSI fan-out and stream record.
    let (
        UpdateWorkResult {
            old_item,
            item,
            item_json,
            size,
        },
        gsi_units,
    ) = helpers::with_write_transaction(storage, async {
        // Fetch existing item (or create empty one for upsert)
        let existing_json = storage.get_item(&request.table_name, &pk, &sk).await?;
        let existing_item: HashMap<String, AttributeValue> = existing_json
            .as_ref()
            .and_then(|j| serde_json::from_str(j).ok())
            .unwrap_or_default();

        // Evaluate ConditionExpression against the original existing item BEFORE
        // populating key attributes for upsert. Otherwise attribute_exists(PK)
        // would always pass because the key was pre-populated.
        if let Some(ref cond_expr) = request.condition_expression {
            let parsed = crate::expressions::condition::parse(cond_expr)
                .map_err(DynoxideError::ValidationException)?;
            let result = crate::expressions::condition::evaluate(&parsed, &existing_item, &tracker)
                .map_err(DynoxideError::ValidationException)?;
            if !result {
                let return_item = if request.return_values_on_condition_check_failure.as_deref()
                    == Some("ALL_OLD")
                    && existing_json.is_some()
                {
                    Some(existing_item.clone())
                } else {
                    None
                };
                return Err(DynoxideError::ConditionalCheckFailedException(
                    "The conditional request failed".to_string(),
                    return_item,
                ));
            }
        }

        // Build mutable item for the update expression.
        // If item doesn't exist, populate key attributes for upsert.
        let mut item = existing_item;
        if existing_json.is_none() {
            for (k, v) in &request.key {
                item.insert(k.clone(), v.clone());
            }
        }

        // Save old item for ReturnValues
        let old_item = item.clone();

        // Apply UpdateExpression
        if let Some(ref update_expr) = request.update_expression {
            let parsed = crate::expressions::update::parse(update_expr)
                .map_err(DynoxideError::ValidationException)?;

            // Validate: cannot modify key attributes with SET
            // (key validation uses the free function, not tracked)
            for action in &parsed.set_actions {
                validate_not_key_attr(
                    action.path.first(),
                    &key_schema,
                    &request.expression_attribute_names,
                )?;
            }

            // Validate: cannot REMOVE key attributes
            for path in &parsed.remove_actions {
                validate_not_key_attr(
                    path.first(),
                    &key_schema,
                    &request.expression_attribute_names,
                )?;
            }

            // Validate: cannot ADD to key attributes
            for action in &parsed.add_actions {
                validate_not_key_attr(
                    action.path.first(),
                    &key_schema,
                    &request.expression_attribute_names,
                )?;
            }

            // Validate: cannot DELETE from key attributes
            for action in &parsed.delete_actions {
                validate_not_key_attr(
                    action.path.first(),
                    &key_schema,
                    &request.expression_attribute_names,
                )?;
            }

            crate::expressions::update::apply(&mut item, &parsed, &tracker)
                .map_err(DynoxideError::ValidationException)?;
        }

        // Apply legacy AttributeUpdates (if no UpdateExpression was provided)
        if request.update_expression.is_none() {
            if let Some(ref updates) = request.attribute_updates {
                apply_attribute_updates(&mut item, updates, &key_schema)?;
            }
        }

        // Note: unused expression attribute validation already done in pre-validation
        // block (Tracker 1). Not repeated here — runtime evaluation may skip branches
        // (e.g., if_not_exists short-circuits) which would cause false positives.

        // Validate attribute values after update expression applied
        crate::validation::validate_item_attribute_values(&item)?;
        crate::validation::normalize_item_sets(&mut item);

        // Reject an index key set to an invalid value, before the fan-out so the
        // error surfaces. Checks only what this update changed (see the helper).
        helpers::validate_updated_index_keys(&old_item, &item, &meta)?;

        // Validate updated item size
        let size = types::item_size(&item);
        if size > types::MAX_ITEM_SIZE {
            return Err(DynoxideError::ValidationException(
                "Item size to update has exceeded the maximum allowed size".to_string(),
            ));
        }

        // Serialize and store
        let item_json = serde_json::to_string(&item)
            .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
        let hash_prefix = request
            .key
            .get(&key_schema.partition_key)
            .map(crate::storage::compute_hash_prefix)
            .unwrap_or_default();
        storage
            .put_item_with_hash(
                &request.table_name,
                &pk,
                &sk,
                &item_json,
                size,
                &hash_prefix,
            )
            .await?;

        // Maintain GSI tables (inside the transaction)
        let gsi_units = super::gsi::maintain_gsis_after_write(
            storage,
            &request.table_name,
            &meta,
            &pk,
            &sk,
            &item,
            &key_schema.partition_key,
            key_schema.sort_key.as_deref(),
        )
        .await?;

        // Maintain LSI tables (inside the transaction)
        super::lsi::maintain_lsis_after_write(
            storage,
            &request.table_name,
            &meta,
            &pk,
            &sk,
            &item,
            &key_schema.partition_key,
            key_schema.sort_key.as_deref(),
        )
        .await?;

        // Record stream event (inside the transaction)
        let old_for_stream = if existing_json.is_some() {
            Some(&old_item)
        } else {
            None
        };
        crate::streams::record_stream_event(storage, &meta, old_for_stream, Some(&item)).await?;

        Ok((
            UpdateWorkResult {
                old_item,
                item,
                item_json,
                size,
            },
            gsi_units,
        ))
    })
    .await?;

    // Handle ReturnValues
    let return_values = request.return_values.as_deref().unwrap_or("NONE");
    let attributes = match return_values.to_uppercase().as_str() {
        "ALL_OLD" => Some(old_item),
        "ALL_NEW" => Some(item),
        "UPDATED_OLD" => {
            if let Some(ref update_expr) = request.update_expression {
                // Expression-based: extract only the attributes targeted by the expression.
                let parsed = crate::expressions::update::parse(update_expr)
                    .map_err(DynoxideError::ValidationException)?;
                omit_if_empty(extract_updated_attrs(
                    &old_item,
                    &parsed,
                    &request.expression_attribute_names,
                ))
            } else {
                // Legacy AttributeUpdates: extract the named attributes from the old item.
                legacy_attr_names
                    .as_ref()
                    .map(|names| extract_named_attrs(&old_item, names))
                    .and_then(omit_if_empty)
            }
        }
        "UPDATED_NEW" => {
            if let Some(ref update_expr) = request.update_expression {
                // Expression-based: extract only the attributes targeted by the expression.
                let parsed = crate::expressions::update::parse(update_expr)
                    .map_err(DynoxideError::ValidationException)?;
                let new_item: HashMap<String, AttributeValue> = serde_json::from_str(&item_json)
                    .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
                omit_if_empty(extract_updated_attrs(
                    &new_item,
                    &parsed,
                    &request.expression_attribute_names,
                ))
            } else {
                // Legacy AttributeUpdates: extract the named attributes from the new item.
                legacy_attr_names
                    .as_ref()
                    .map(|names| {
                        let new_item: HashMap<String, AttributeValue> =
                            serde_json::from_str(&item_json).unwrap_or_default();
                        extract_named_attrs(&new_item, names)
                    })
                    .and_then(omit_if_empty)
            }
        }
        _ => None, // "NONE" or default
    };

    // Build item collection metrics (only for tables with LSIs)
    let pk_value = request.key.get(&key_schema.partition_key).cloned();
    let item_collection_metrics = helpers::build_item_collection_metrics(
        storage,
        &meta,
        &request.table_name,
        &pk,
        &key_schema.partition_key,
        pk_value
            .as_ref()
            .unwrap_or(&AttributeValue::S(String::new())),
        &request.return_item_collection_metrics,
    )
    .await?;

    let consumed_capacity = types::consumed_capacity_with_indexes(
        &request.table_name,
        types::write_capacity_units(size),
        &gsi_units,
        &request.return_consumed_capacity,
    );

    Ok(UpdateItemResponse {
        attributes,
        consumed_capacity,
        item_collection_metrics,
    })
}

/// Apply legacy `AttributeUpdates` to the item, mutating it in place.
///
/// Each entry maps an attribute name to an action:
/// - `PUT` (default): set the attribute to the given value
/// - `ADD`: add a number or union a set
/// - `DELETE`: remove the attribute, or remove elements from a set
fn apply_attribute_updates(
    item: &mut HashMap<String, AttributeValue>,
    updates: &HashMap<String, AttributeValueUpdate>,
    key_schema: &helpers::KeySchema,
) -> Result<()> {
    for (attr_name, update) in updates {
        // Cannot modify key attributes
        if attr_name == &key_schema.partition_key
            || key_schema
                .sort_key
                .as_ref()
                .is_some_and(|sk| sk == attr_name)
        {
            return Err(DynoxideError::ValidationException(format!(
                "One or more parameter values were invalid: \
                 Cannot update attribute {attr_name}. This attribute is part of the key"
            )));
        }

        let action = update.action.to_uppercase();
        match action.as_str() {
            "PUT" => {
                if let Some(ref value) = update.value {
                    item.insert(attr_name.clone(), value.clone());
                }
            }
            "ADD" => {
                if let Some(ref add_val) = update.value {
                    let path = vec![crate::expressions::PathElement::Attribute(
                        attr_name.clone(),
                    )];
                    crate::expressions::update::apply_add_public(item, &path, add_val)
                        .map_err(DynoxideError::ValidationException)?;
                }
            }
            "DELETE" => {
                if let Some(ref del_val) = update.value {
                    // DELETE with a value: remove elements from a set
                    let path = vec![crate::expressions::PathElement::Attribute(
                        attr_name.clone(),
                    )];
                    crate::expressions::update::apply_delete_public(item, &path, del_val)
                        .map_err(DynoxideError::ValidationException)?;
                } else {
                    // DELETE without a value: remove the attribute entirely
                    item.remove(attr_name);
                }
            }
            _ => {
                return Err(DynoxideError::ValidationException(format!(
                    "1 validation error detected: Value '{action}' at 'attributeUpdates.{attr_name}.member.action' \
                     failed to satisfy constraint: Member must satisfy enum value set: [ADD, PUT, DELETE]"
                )));
            }
        }
    }
    Ok(())
}

/// Extract only the attributes that were affected by the update expression,
/// at full path granularity.
///
/// For a nested target like `SET parent.child = :v` this returns only the
/// changed fragment (`{parent: {M: {child}}}`), not the whole `parent` map,
/// matching how AWS scopes `UPDATED_NEW` / `UPDATED_OLD`. A path that no longer
/// resolves in `item` (a removed attribute under `UPDATED_NEW`) contributes
/// nothing, so a REMOVE-only update yields an empty map.
fn extract_updated_attrs(
    item: &HashMap<String, AttributeValue>,
    expr: &crate::expressions::update::UpdateExpr,
    attr_names: &Option<HashMap<String, String>>,
) -> HashMap<String, AttributeValue> {
    use crate::expressions::{PathElement, resolve_path, resolve_path_elements};

    let no_values: Option<HashMap<String, AttributeValue>> = None;
    let tracker =
        crate::expressions::TrackedExpressionAttributes::without_tracking(attr_names, &no_values);

    // Collect every target path across all clauses, in clause order.
    let mut paths: Vec<&[PathElement]> = Vec::new();
    paths.extend(expr.set_actions.iter().map(|a| a.path.as_slice()));
    paths.extend(expr.remove_actions.iter().map(|p| p.as_slice()));
    paths.extend(expr.add_actions.iter().map(|a| a.path.as_slice()));
    paths.extend(expr.delete_actions.iter().map(|a| a.path.as_slice()));

    let mut result = HashMap::new();
    for path in paths {
        let Ok(resolved) = resolve_path_elements(path, &tracker) else {
            continue;
        };

        // `insert_at_path` rebuilds a list from index 0, so it can't represent a
        // target that dives through a list index (`list[2]`) as a pruned
        // fragment without mislocating the element and dropping its siblings.
        // For those, fall back to returning the whole top-level attribute, the
        // coarse-but-correct shape. Pure attribute paths get the granular
        // fragment AWS scopes `UPDATED_NEW` / `UPDATED_OLD` to.
        if resolved.iter().any(|e| matches!(e, PathElement::Index(_))) {
            if let Some(PathElement::Attribute(top)) = resolved.first() {
                if let Some(val) = item.get(top) {
                    result.insert(top.clone(), val.clone());
                }
            }
            continue;
        }

        if let Some(val) = resolve_path(item, &resolved) {
            crate::expressions::projection::insert_at_path(&mut result, &resolved, val);
        }
    }

    result
}

/// Collapse an empty projection to `None` so `Attributes` is omitted entirely.
///
/// AWS omits `Attributes` from a `UPDATED_NEW` / `UPDATED_OLD` response when
/// nothing was projected — for example a REMOVE-only update under `UPDATED_NEW`,
/// where no attribute was set to a new value. Returning `Some({})` instead would
/// serialise an empty `Attributes` map, which AWS never does.
fn omit_if_empty(map: HashMap<String, AttributeValue>) -> Option<HashMap<String, AttributeValue>> {
    if map.is_empty() { None } else { Some(map) }
}

/// Extract named attributes from an item (used for legacy AttributeUpdates ReturnValues).
fn extract_named_attrs(
    item: &HashMap<String, AttributeValue>,
    attr_names: &[String],
) -> HashMap<String, AttributeValue> {
    let mut result = HashMap::new();
    for name in attr_names {
        if let Some(val) = item.get(name) {
            result.insert(name.clone(), val.clone());
        }
    }
    result
}

/// Validate that a path element does not target a key attribute.
fn validate_not_key_attr(
    first_element: Option<&crate::expressions::PathElement>,
    key_schema: &helpers::KeySchema,
    expression_attribute_names: &Option<HashMap<String, String>>,
) -> crate::errors::Result<()> {
    if let Some(crate::expressions::PathElement::Attribute(name)) = first_element {
        let resolved_name = if name.starts_with('#') {
            crate::expressions::resolve_name(name, expression_attribute_names)
                .map_err(DynoxideError::ValidationException)?
        } else {
            name.clone()
        };
        if resolved_name == key_schema.partition_key
            || key_schema
                .sort_key
                .as_ref()
                .is_some_and(|sk| sk == &resolved_name)
        {
            return Err(DynoxideError::ValidationException(format!(
                "One or more parameter values were invalid: Cannot update attribute {resolved_name}. This attribute is part of the key"
            )));
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use crate::actions::{create_table, put_item, update_item};
    use crate::storage::Storage;
    use crate::storage_backend::StorageBackend;

    #[test]
    fn update_item_stops_at_first_invalid_enum() {
        // eu-west-2: UpdateItem reports one error for the first invalid enum
        // (ReturnValues first), unlike PutItem which aggregates them.
        let err = serde_json::from_value::<super::UpdateItemRequest>(serde_json::json!({
            "TableName": "_conformance_valid_table_name",
            "Key": {"pk": {"S": "test"}},
            "ReturnValues": "INVALID",
            "ReturnConsumedCapacity": "INVALID"
        }))
        .unwrap_err()
        .to_string();
        assert!(err.contains("1 validation error detected"), "got: {err}");
        assert!(err.contains("enum value set"), "got: {err}");
        assert!(err.contains("returnValues"), "got: {err}");
        assert!(!err.contains("returnConsumedCapacity"), "got: {err}");
    }

    #[test]
    fn update_item_empty_table_name_reports_only_table_name() {
        let err = serde_json::from_value::<super::UpdateItemRequest>(serde_json::json!({
            "TableName": "",
            "Key": {}
        }))
        .unwrap_err()
        .to_string();
        assert!(err.contains("1 validation error detected"), "got: {err}");
        assert!(err.to_lowercase().contains("tablename"), "got: {err}");
    }

    /// An update and its GSI fan-out succeed or fail as one unit: a mid-fan-out
    /// failure leaves the item at its pre-update value.
    #[test]
    fn update_item_rolls_back_base_write_when_gsi_fan_out_fails() {
        let storage = Storage::memory().unwrap();

        let create = serde_json::from_value(serde_json::json!({
            "TableName": "Orders",
            "KeySchema": [{"AttributeName": "UserId", "KeyType": "HASH"}],
            "AttributeDefinitions": [
                {"AttributeName": "UserId", "AttributeType": "S"},
                {"AttributeName": "Status", "AttributeType": "S"},
                {"AttributeName": "Priority", "AttributeType": "S"}
            ],
            "GlobalSecondaryIndexes": [
                {"IndexName": "StatusIndex", "KeySchema": [{"AttributeName": "Status", "KeyType": "HASH"}], "Projection": {"ProjectionType": "ALL"}},
                {"IndexName": "PriorityIndex", "KeySchema": [{"AttributeName": "Priority", "KeyType": "HASH"}], "Projection": {"ProjectionType": "ALL"}}
            ]
        }))
        .unwrap();
        pollster::block_on(create_table::execute(&storage, create)).unwrap();

        let put = serde_json::from_value(serde_json::json!({
            "TableName": "Orders",
            "Item": {"UserId": {"S": "u1"}, "Status": {"S": "SHIPPED"}, "Priority": {"S": "HIGH"}, "Note": {"S": "before"}}
        }))
        .unwrap();
        pollster::block_on(put_item::execute(&storage, put)).unwrap();

        // Break the second GSI's fan-out by dropping its physical table.
        storage.drop_gsi_table("Orders", "PriorityIndex").unwrap();

        let update = serde_json::from_value(serde_json::json!({
            "TableName": "Orders",
            "Key": {"UserId": {"S": "u1"}},
            "UpdateExpression": "SET Note = :n",
            "ExpressionAttributeValues": {":n": {"S": "after"}}
        }))
        .unwrap();
        let res = pollster::block_on(update_item::execute(&storage, update));
        assert!(
            res.is_err(),
            "a mid-fan-out failure must surface as an error"
        );

        // The base write must roll back: the item is still present at its
        // pre-update value.
        let rows = pollster::block_on(<Storage as StorageBackend>::scan_items(
            &storage,
            "Orders",
            &Default::default(),
        ))
        .unwrap();
        assert_eq!(
            rows.len(),
            1,
            "the item must still be present after rollback"
        );
        let raw = &rows[0].2;
        assert!(
            raw.contains("\"before\"") && !raw.contains("\"after\""),
            "update must roll back when fan-out fails, leaving the original value: {raw}"
        );
    }
}
