//! Smithy-aligned input validation for Bedrock control-plane actions.
//!
//! Generated negative conformance probes hit every JSON action with three
//! synthetic shapes: an omitted required field, an out-of-range string
//! length / integer range, and an invalid enum value. The historical
//! handlers in `customization.rs`, `evaluation.rs`, `guardrails.rs`, etc.
//! validated only a handful of identifiers and let everything else pass
//! through with default values, so negative variants returned HTTP 201
//! / HTTP 200 instead of the declared `ValidationException`.
//!
//! [`prevalidate`] is invoked at the top of the service dispatcher with
//! the resolved action name, the parsed JSON body, and the request's
//! query parameters. The constraint table is hand-derived from
//! `aws-models/bedrock.json` and only covers the constraints the
//! autogenerated probe actually exercises; per-handler logic downstream
//! still owns the business-rule checks.
//!
//! All errors emerge as the standard `ValidationException` shape declared
//! on every Bedrock op that needs one.

use http::StatusCode;
use serde_json::Value;

use fakecloud_core::service::{AwsRequest, AwsServiceError};

/// Constraint table entry for one Bedrock control-plane action.
struct OpConstraints {
    /// JSON body fields that must be present and non-empty.
    required: &'static [&'static str],
    /// `(field, min, max)` length constraints applied to string fields in
    /// the request body. Skipped when the field is absent.
    strings: &'static [(&'static str, u64, u64)],
    /// `(field, allowed_values)` checks for enum-typed body fields.
    enums: &'static [(&'static str, &'static [&'static str])],
    /// `(field, min, max)` constraints applied to integer body fields.
    integers: &'static [(&'static str, i64, i64)],
    /// `(query_param, min, max)` length constraints on httpQuery members.
    query_strings: &'static [(&'static str, u64, u64)],
    /// `(query_param, min, max)` integer-range checks on httpQuery
    /// members (typically `maxResults`).
    query_integers: &'static [(&'static str, i64, i64)],
    /// `(query_param, allowed_values)` for enum-typed httpQuery members.
    query_enums: &'static [(&'static str, &'static [&'static str])],
    /// `(label_name, min, max)` length constraints on the primary
    /// `@httpLabel` field (positional `resource_id` in the dispatcher).
    /// A `None` resource_id (route doesn't carry one) skips the check.
    path_label: Option<(&'static str, u64, u64)>,
    /// `(label_name, min, max)` length constraints on the secondary
    /// `@httpLabel` field (positional `extra_id` in the dispatcher).
    path_label_extra: Option<(&'static str, u64, u64)>,
}

const EMPTY: OpConstraints = OpConstraints {
    required: &[],
    strings: &[],
    enums: &[],
    integers: &[],
    query_strings: &[],
    query_integers: &[],
    query_enums: &[],
    path_label: None,
    path_label_extra: None,
};

/// Run Smithy-aligned prevalidation on `req` for the resolved Bedrock
/// `action`. Returns `Ok(())` for actions with no entry in the table or
/// when every constraint is satisfied; otherwise emits a
/// `ValidationException` matching the wire shape declared on the op.
pub(crate) fn prevalidate(
    action: &str,
    req: &AwsRequest,
    resource_id: Option<&str>,
    extra_id: Option<&str>,
) -> Result<(), AwsServiceError> {
    let Some(spec) = lookup(action) else {
        return Ok(());
    };

    let body = req.json_body();

    // Path-label length checks. The probe drives `@httpLabel` members by
    // putting the value in the body; when it generates an empty string for
    // `too_short` (min = 1), the harness still URL-encodes it into the path,
    // so we get an empty `resource_id`. Likewise `too_long` produces a path
    // segment longer than `max`. Verify before per-handler logic runs.
    if let Some((name, min, max)) = spec.path_label {
        check_path_label(name, resource_id.unwrap_or(""), min, max)?;
    }
    if let Some((name, min, max)) = spec.path_label_extra {
        check_path_label(name, extra_id.unwrap_or(""), min, max)?;
    }

    // Required body fields: a missing key OR a JSON `null` is treated as
    // absent. Empty strings, empty arrays, and empty objects are rejected
    // because the probe builds them as the "required omitted" stand-in
    // when the field's target shape carries a `@length` min > 0 / a
    // structure with required members.
    for field in spec.required {
        match body.get(*field) {
            None | Some(Value::Null) => {
                return Err(validation_error(&format!("{field} is required",)))
            }
            Some(Value::String(s)) if s.is_empty() => {
                return Err(validation_error(&format!("{field} must not be empty")));
            }
            Some(Value::Array(a)) if a.is_empty() => {
                return Err(validation_error(&format!("{field} must not be empty")));
            }
            Some(Value::Object(o)) if o.is_empty() => {
                return Err(validation_error(&format!("{field} must not be empty")));
            }
            _ => {}
        }
    }

    for (field, min, max) in spec.strings {
        if let Some(Value::String(s)) = body.get(*field) {
            // Empty strings count as zero-length and fail when `min > 0`.
            // Optional fields that allow empty values are declared with
            // `min == 0` in the table.
            let len = s.chars().count() as u64;
            if len < *min || len > *max {
                return Err(validation_error(&format!(
                    "{field} length {len} is outside the valid range {min}-{max}",
                )));
            }
        }
    }

    for (field, allowed) in spec.enums {
        if let Some(Value::String(s)) = body.get(*field) {
            if !s.is_empty() && !allowed.iter().any(|a| *a == s) {
                return Err(validation_error(&format!(
                    "{field} value '{s}' is not one of the supported enum values",
                )));
            }
        }
    }

    for (field, min, max) in spec.integers {
        if let Some(v) = body.get(*field) {
            if let Some(n) = v.as_i64() {
                if n < *min || n > *max {
                    return Err(validation_error(&format!(
                        "{field} value {n} is outside the valid range {min}-{max}",
                    )));
                }
            } else if let Some(n) = v.as_f64() {
                if !n.is_finite() {
                    return Err(validation_error(&format!(
                        "{field} must be a finite number"
                    )));
                }
                let i = n as i64;
                if i < *min || i > *max {
                    return Err(validation_error(&format!(
                        "{field} value {n} is outside the valid range {min}-{max}",
                    )));
                }
            }
        }
    }

    for (name, min, max) in spec.query_strings {
        if let Some(value) = req.query_params.get(*name) {
            let len = value.chars().count() as u64;
            if len < *min || len > *max {
                return Err(validation_error(&format!(
                    "{name} length {len} is outside the valid range {min}-{max}",
                )));
            }
        }
    }

    for (name, min, max) in spec.query_integers {
        if let Some(value) = req.query_params.get(*name) {
            if let Ok(parsed) = value.parse::<i64>() {
                if parsed < *min || parsed > *max {
                    return Err(validation_error(&format!(
                        "{name} value {parsed} is outside the valid range {min}-{max}",
                    )));
                }
            }
        }
    }

    for (name, allowed) in spec.query_enums {
        if let Some(value) = req.query_params.get(*name) {
            if !value.is_empty() && !allowed.iter().any(|a| *a == value) {
                return Err(validation_error(&format!(
                    "{name} value '{value}' is not one of the supported enum values",
                )));
            }
        }
    }

    Ok(())
}

fn validation_error(message: &str) -> AwsServiceError {
    AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "ValidationException", message)
}

/// Validate a path label value against a Smithy `@length` constraint. The
/// probe leaves the literal `{Name}` substring in the URI when it omits the
/// label, so treat any value that still contains the unsubstituted template
/// as "missing" rather than measuring its character count.
fn check_path_label(name: &str, value: &str, min: u64, max: u64) -> Result<(), AwsServiceError> {
    if value.starts_with('{') && value.ends_with('}') {
        return Err(validation_error(&format!("{name} is required")));
    }
    let len = value.chars().count() as u64;
    if len < min || len > max {
        return Err(validation_error(&format!(
            "{name} length {len} is outside the valid range {min}-{max}",
        )));
    }
    Ok(())
}

fn lookup(action: &str) -> Option<OpConstraints> {
    Some(match action {
        // ── Custom models ────────────────────────────────────────────
        "CreateCustomModel" => OpConstraints {
            required: &["modelName", "modelSourceConfig"],
            strings: &[
                ("clientRequestToken", 1, 256),
                ("modelKmsKeyArn", 1, 2048),
                ("modelName", 1, 63),
                ("roleArn", 0, 2048),
            ],
            ..EMPTY
        },
        "ListCustomModels" => OpConstraints {
            query_strings: &[
                // ModelArn @length(20, 1011) — the @length min was previously
                // mis-set to 1 so `negative_too_short` (19-char string)
                // slipped past validation.
                ("baseModelArnEquals", 20, 1011),
                ("foundationModelArnEquals", 20, 1011),
                ("nameContains", 1, 63),
                ("nextToken", 1, 2048),
            ],
            query_integers: &[("maxResults", 1, 1000)],
            query_enums: &[
                ("isOwned", &["true", "false"]),
                ("modelStatus", &["Active", "Creating", "Failed"]),
                ("sortBy", &["CreationTime"]),
                ("sortOrder", &["Ascending", "Descending"]),
            ],
            ..EMPTY
        },
        "ListImportedModels" => OpConstraints {
            query_strings: &[("nameContains", 1, 63), ("nextToken", 1, 2048)],
            query_integers: &[("maxResults", 1, 1000)],
            query_enums: &[
                ("sortBy", &["CreationTime"]),
                ("sortOrder", &["Ascending", "Descending"]),
            ],
            ..EMPTY
        },
        "ListCustomModelDeployments" => OpConstraints {
            query_strings: &[
                ("modelArnEquals", 20, 1011),
                ("nameContains", 1, 63),
                ("nextToken", 1, 2048),
            ],
            query_integers: &[("maxResults", 1, 1000)],
            query_enums: &[
                ("statusEquals", &["Creating", "Active", "Failed"]),
                ("sortBy", &["CreationTime"]),
                ("sortOrder", &["Ascending", "Descending"]),
            ],
            ..EMPTY
        },

        // ── Custom model deployments ─────────────────────────────────
        "CreateCustomModelDeployment" => OpConstraints {
            required: &["modelArn", "modelDeploymentName"],
            strings: &[
                ("clientRequestToken", 1, 256),
                ("description", 1, 200),
                ("modelArn", 20, 1011),
                ("modelDeploymentName", 1, 63),
            ],
            ..EMPTY
        },
        "UpdateCustomModelDeployment" => OpConstraints {
            required: &["modelArn"],
            strings: &[
                ("clientRequestToken", 1, 256),
                ("description", 1, 200),
                ("modelArn", 20, 1011),
            ],
            ..EMPTY
        },

        // ── Customization jobs ───────────────────────────────────────
        "CreateModelCustomizationJob" => OpConstraints {
            required: &[
                "baseModelIdentifier",
                "customModelName",
                "jobName",
                "outputDataConfig",
                "roleArn",
                "trainingDataConfig",
            ],
            strings: &[
                ("baseModelIdentifier", 1, 2048),
                ("clientRequestToken", 1, 256),
                ("customModelKmsKeyId", 1, 2048),
                ("customModelName", 1, 63),
                ("jobName", 1, 63),
                ("roleArn", 0, 2048),
            ],
            enums: &[(
                "customizationType",
                &[
                    "FINE_TUNING",
                    "CONTINUED_PRE_TRAINING",
                    "DISTILLATION",
                    "IMPORTED",
                ],
            )],
            ..EMPTY
        },
        "ListModelCustomizationJobs" => OpConstraints {
            query_strings: &[("nameContains", 1, 63), ("nextToken", 1, 2048)],
            query_integers: &[("maxResults", 1, 1000)],
            query_enums: &[
                (
                    "statusEquals",
                    &["InProgress", "Completed", "Failed", "Stopping", "Stopped"],
                ),
                ("sortBy", &["CreationTime"]),
                ("sortOrder", &["Ascending", "Descending"]),
            ],
            ..EMPTY
        },

        // ── Evaluation jobs ─────────────────────────────────────────
        "CreateEvaluationJob" => OpConstraints {
            required: &[
                "evaluationConfig",
                "inferenceConfig",
                "jobName",
                "outputDataConfig",
                "roleArn",
            ],
            strings: &[
                ("clientRequestToken", 1, 256),
                ("customerEncryptionKeyId", 1, 2048),
                ("jobDescription", 1, 200),
                ("jobName", 1, 63),
                ("roleArn", 0, 2048),
            ],
            enums: &[("applicationType", &["ModelEvaluation", "RagEvaluation"])],
            ..EMPTY
        },
        "ListEvaluationJobs" => OpConstraints {
            query_strings: &[("nameContains", 1, 63), ("nextToken", 1, 2048)],
            query_integers: &[("maxResults", 1, 1000)],
            query_enums: &[
                (
                    "applicationTypeEquals",
                    &["ModelEvaluation", "RagEvaluation"],
                ),
                (
                    "statusEquals",
                    &[
                        "InProgress",
                        "Completed",
                        "Failed",
                        "Stopping",
                        "Stopped",
                        "Deleting",
                    ],
                ),
                ("sortBy", &["CreationTime"]),
                ("sortOrder", &["Ascending", "Descending"]),
            ],
            ..EMPTY
        },

        // ── Guardrails ───────────────────────────────────────────────
        "CreateGuardrail" => OpConstraints {
            required: &["blockedInputMessaging", "blockedOutputsMessaging", "name"],
            strings: &[
                ("blockedInputMessaging", 1, 500),
                ("blockedOutputsMessaging", 1, 500),
                ("clientRequestToken", 1, 256),
                ("description", 1, 200),
                ("kmsKeyId", 1, 2048),
                ("name", 1, 50),
            ],
            ..EMPTY
        },
        "ListGuardrails" => OpConstraints {
            query_strings: &[("guardrailIdentifier", 0, 2048), ("nextToken", 1, 2048)],
            query_integers: &[("maxResults", 1, 1000)],
            ..EMPTY
        },

        // ── Inference profiles ───────────────────────────────────────
        "CreateInferenceProfile" => OpConstraints {
            required: &["inferenceProfileName", "modelSource"],
            strings: &[
                ("clientRequestToken", 1, 256),
                ("description", 1, 200),
                ("inferenceProfileName", 1, 64),
            ],
            ..EMPTY
        },
        "ListInferenceProfiles" => OpConstraints {
            query_strings: &[("nextToken", 1, 2048)],
            query_integers: &[("maxResults", 1, 1000)],
            // The wire name from `@httpQuery("type")` is `type`, but the
            // smithy member is `typeEquals`. Conformance probes use the
            // member name for their variant labels; the actual query param
            // on the wire is `type`.
            query_enums: &[("type", &["SYSTEM_DEFINED", "APPLICATION"])],
            ..EMPTY
        },

        // ── Marketplace endpoints ────────────────────────────────────
        "CreateMarketplaceModelEndpoint" => OpConstraints {
            required: &["endpointConfig", "modelSourceIdentifier"],
            strings: &[
                ("clientRequestToken", 1, 256),
                // EndpointName @length(1, 30).
                ("endpointName", 1, 30),
                ("modelSourceIdentifier", 1, 2048),
            ],
            ..EMPTY
        },
        "UpdateMarketplaceModelEndpoint" => OpConstraints {
            required: &["endpointConfig"],
            strings: &[("clientRequestToken", 1, 256)],
            ..EMPTY
        },
        "RegisterMarketplaceModelEndpoint" => OpConstraints {
            required: &["modelSourceIdentifier"],
            strings: &[("modelSourceIdentifier", 1, 2048)],
            ..EMPTY
        },
        "ListMarketplaceModelEndpoints" => OpConstraints {
            // Wire name from `@httpQuery("modelSourceIdentifier")` — the
            // smithy member is `modelSourceEquals` (probe variant label),
            // but the actual query string key is `modelSourceIdentifier`.
            // ModelSourceIdentifier has @length(max:2048), no min.
            query_strings: &[("modelSourceIdentifier", 0, 2048), ("nextToken", 1, 2048)],
            query_integers: &[("maxResults", 1, 1000)],
            ..EMPTY
        },

        // ── Model copy / import / invocation jobs ────────────────────
        "CreateModelCopyJob" => OpConstraints {
            required: &["sourceModelArn", "targetModelName"],
            strings: &[
                ("clientRequestToken", 1, 256),
                ("modelKmsKeyId", 1, 2048),
                ("sourceModelArn", 20, 1011),
                ("targetModelName", 1, 63),
            ],
            ..EMPTY
        },
        "ListModelCopyJobs" => OpConstraints {
            // Wire names from `@httpQuery` traits — smithy member names
            // differ from query string keys for several filter members.
            query_strings: &[
                ("sourceAccountEquals", 0, 50),
                ("sourceModelArnEquals", 20, 1011),
                // @httpQuery("outputModelNameContains") on `targetModelNameContains`.
                ("outputModelNameContains", 1, 63),
                ("nextToken", 1, 2048),
            ],
            query_integers: &[("maxResults", 1, 1000)],
            query_enums: &[
                ("statusEquals", &["InProgress", "Completed", "Failed"]),
                ("sortBy", &["CreationTime"]),
                ("sortOrder", &["Ascending", "Descending"]),
            ],
            ..EMPTY
        },
        "CreateModelImportJob" => OpConstraints {
            required: &["importedModelName", "jobName", "modelDataSource", "roleArn"],
            strings: &[
                ("clientRequestToken", 1, 256),
                ("importedModelKmsKeyId", 1, 2048),
                ("importedModelName", 1, 63),
                ("jobName", 1, 63),
                ("roleArn", 0, 2048),
            ],
            ..EMPTY
        },
        "ListModelImportJobs" => OpConstraints {
            query_strings: &[("nameContains", 1, 63), ("nextToken", 1, 2048)],
            query_integers: &[("maxResults", 1, 1000)],
            query_enums: &[
                ("statusEquals", &["InProgress", "Completed", "Failed"]),
                ("sortBy", &["CreationTime"]),
                ("sortOrder", &["Ascending", "Descending"]),
            ],
            ..EMPTY
        },
        "CreateModelInvocationJob" => OpConstraints {
            required: &[
                "inputDataConfig",
                "jobName",
                "modelId",
                "outputDataConfig",
                "roleArn",
            ],
            strings: &[
                ("clientRequestToken", 1, 256),
                ("jobName", 1, 63),
                ("modelId", 1, 2048),
                ("roleArn", 0, 2048),
            ],
            enums: &[("modelInvocationType", &["BATCH"])],
            integers: &[("timeoutDurationInHours", 24, 168)],
            ..EMPTY
        },
        "ListModelInvocationJobs" => OpConstraints {
            query_strings: &[("nameContains", 1, 63), ("nextToken", 1, 2048)],
            query_integers: &[("maxResults", 1, 1000)],
            query_enums: &[
                (
                    "statusEquals",
                    &[
                        "Submitted",
                        "InProgress",
                        "Completed",
                        "Failed",
                        "Stopping",
                        "Stopped",
                        "PartiallyCompleted",
                        "Expired",
                        "Validating",
                        "Scheduled",
                    ],
                ),
                ("sortBy", &["CreationTime"]),
                ("sortOrder", &["Ascending", "Descending"]),
            ],
            ..EMPTY
        },

        // ── Prompt routers ───────────────────────────────────────────
        "CreatePromptRouter" => OpConstraints {
            required: &[
                "fallbackModel",
                "models",
                "promptRouterName",
                "routingCriteria",
            ],
            strings: &[
                ("clientRequestToken", 1, 256),
                ("description", 1, 200),
                ("promptRouterName", 1, 63),
            ],
            ..EMPTY
        },
        "ListPromptRouters" => OpConstraints {
            query_strings: &[("nextToken", 1, 2048)],
            query_integers: &[("maxResults", 1, 1000)],
            query_enums: &[("type", &["custom", "default"])],
            ..EMPTY
        },

        // ── Provisioned throughput ───────────────────────────────────
        "CreateProvisionedModelThroughput" => OpConstraints {
            required: &["modelId", "modelUnits", "provisionedModelName"],
            strings: &[
                ("clientRequestToken", 1, 256),
                ("modelId", 1, 2048),
                ("provisionedModelName", 1, 63),
            ],
            ..EMPTY
        },
        "ListProvisionedModelThroughputs" => OpConstraints {
            query_strings: &[
                ("modelArnEquals", 20, 1011),
                ("nameContains", 1, 63),
                ("nextToken", 1, 2048),
            ],
            query_integers: &[("maxResults", 1, 1000)],
            query_enums: &[
                (
                    "statusEquals",
                    &["Creating", "InService", "Updating", "Failed"],
                ),
                ("sortBy", &["CreationTime"]),
                ("sortOrder", &["Ascending", "Descending"]),
            ],
            ..EMPTY
        },

        // ── Foundation model agreements / availability ───────────────
        "CreateFoundationModelAgreement" => OpConstraints {
            required: &["modelId", "offerToken"],
            strings: &[("modelId", 1, 140)],
            ..EMPTY
        },
        // modelId is bound as @httpLabel (path), not body/query. BedrockModelId
        // has `@length max:140` (no min) but the field is `@required`, so the
        // `path_label` check_path_label() invocation also catches "missing".
        "ListFoundationModelAgreementOffers" => OpConstraints {
            path_label: Some(("modelId", 1, 140)),
            query_enums: &[("offerType", &["ALL", "PUBLIC"])],
            ..EMPTY
        },
        "GetFoundationModelAvailability" => OpConstraints {
            path_label: Some(("modelId", 1, 140)),
            ..EMPTY
        },
        // Filters are bound as @httpQuery. Catch invalid enum values.
        "ListFoundationModels" => OpConstraints {
            query_enums: &[
                (
                    "byCustomizationType",
                    &["FINE_TUNING", "CONTINUED_PRE_TRAINING", "DISTILLATION"],
                ),
                ("byInferenceType", &["ON_DEMAND", "PROVISIONED"]),
                ("byOutputModality", &["EMBEDDING", "IMAGE", "TEXT"]),
            ],
            ..EMPTY
        },

        // ── Tags / resource policies ─────────────────────────────────
        "ListTagsForResource" => OpConstraints {
            required: &["resourceARN"],
            strings: &[("resourceARN", 20, 1011)],
            ..EMPTY
        },
        "UntagResource" => OpConstraints {
            required: &["resourceARN", "tagKeys"],
            strings: &[("resourceARN", 20, 1011)],
            ..EMPTY
        },
        "PutResourcePolicy" => OpConstraints {
            required: &["resourceArn", "resourcePolicy"],
            strings: &[
                ("resourceArn", 1, 255),
                // ResourcePolicyDocument @length(1, 20480).
                ("resourcePolicy", 1, 20480),
            ],
            ..EMPTY
        },

        // ── Enforced guardrails ──────────────────────────────────────
        "PutEnforcedGuardrailConfiguration" => OpConstraints {
            required: &["guardrailInferenceConfig"],
            ..EMPTY
        },
        "ListEnforcedGuardrailsConfiguration" => OpConstraints {
            query_strings: &[("nextToken", 1, 2048)],
            ..EMPTY
        },

        // ── Automated reasoning ─────────────────────────────────────
        "ListAutomatedReasoningPolicies" => OpConstraints {
            // policyArn targets AutomatedReasoningPolicyArn (@length 1,2048).
            // nextToken targets PaginationToken (@length 1,2048).
            query_strings: &[("policyArn", 1, 2048), ("nextToken", 1, 2048)],
            query_integers: &[("maxResults", 1, 100)],
            ..EMPTY
        },
        "ListAutomatedReasoningPolicyTestResults" => OpConstraints {
            path_label: Some(("policyArn", 1, 2048)),
            query_strings: &[("nextToken", 1, 2048)],
            query_integers: &[("maxResults", 1, 100)],
            ..EMPTY
        },

        // ── Automated reasoning policy path-label ops ────────────────
        "CancelAutomatedReasoningPolicyBuildWorkflow"
        | "CreateAutomatedReasoningPolicyTestCase"
        | "CreateAutomatedReasoningPolicyVersion"
        | "DeleteAutomatedReasoningPolicy"
        | "DeleteAutomatedReasoningPolicyBuildWorkflow"
        | "DeleteAutomatedReasoningPolicyTestCase"
        | "GetAutomatedReasoningPolicy"
        | "GetAutomatedReasoningPolicyAnnotations"
        | "GetAutomatedReasoningPolicyBuildWorkflow"
        | "GetAutomatedReasoningPolicyBuildWorkflowResultAssets"
        | "GetAutomatedReasoningPolicyNextScenario"
        | "GetAutomatedReasoningPolicyTestCase"
        | "GetAutomatedReasoningPolicyTestResult"
        | "StartAutomatedReasoningPolicyBuildWorkflow"
        | "StartAutomatedReasoningPolicyTestWorkflow"
        | "UpdateAutomatedReasoningPolicy"
        | "UpdateAutomatedReasoningPolicyAnnotations"
        | "UpdateAutomatedReasoningPolicyTestCase" => OpConstraints {
            path_label: Some(("policyArn", 1, 2048)),
            ..EMPTY
        },

        // ── Custom model / imported model / foundation model path ops ─
        "DeleteCustomModel" | "GetCustomModel" => OpConstraints {
            path_label: Some(("modelIdentifier", 1, 2048)),
            ..EMPTY
        },
        "DeleteImportedModel" | "GetImportedModel" => OpConstraints {
            path_label: Some(("modelIdentifier", 1, 2048)),
            ..EMPTY
        },
        "GetFoundationModel" => OpConstraints {
            path_label: Some(("modelIdentifier", 1, 2048)),
            ..EMPTY
        },

        // ── Custom model deployment path ops ─────────────────────────
        "DeleteCustomModelDeployment" | "GetCustomModelDeployment" => OpConstraints {
            path_label: Some(("customModelDeploymentIdentifier", 1, 93)),
            ..EMPTY
        },

        // ── Inference profile path ops ───────────────────────────────
        "DeleteInferenceProfile" | "GetInferenceProfile" => OpConstraints {
            path_label: Some(("inferenceProfileIdentifier", 1, 2048)),
            ..EMPTY
        },

        // ── Prompt router path ops ───────────────────────────────────
        "DeletePromptRouter" | "GetPromptRouter" => OpConstraints {
            path_label: Some(("promptRouterArn", 1, 2048)),
            ..EMPTY
        },

        // ── Resource policy path ops ─────────────────────────────────
        "DeleteResourcePolicy" | "GetResourcePolicy" => OpConstraints {
            path_label: Some(("resourceArn", 1, 255)),
            ..EMPTY
        },

        // ── Tag op ───────────────────────────────────────────────────
        "TagResource" => OpConstraints {
            required: &["resourceARN", "tags"],
            strings: &[("resourceARN", 20, 1011)],
            ..EMPTY
        },

        _ => return None,
    })
}
