use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use aws_local_core::db::DbPool;
use chrono::Utc;
use serde_json::Value;
use std::collections::HashMap;
use uuid::Uuid;
use crate::formats;
use crate::store;

/// Dispatch a query-param action (EC2, ELB, Route53, ACM, AutoScaling, CloudWatch protocol).
/// These use XML responses.
pub fn dispatch_xml_action(
    action: &str,
    params: &HashMap<String, String>,
    body: &[u8],
    db: &DbPool,
    region: &str,
) -> Response {
    // Cross-service tag operations — return empty/OK responses before
    // classify_xml_action which cannot route these by action name alone.
    match action {
        "ListTagsForResource" | "ListTags" => {
            let ns = guess_namespace(params);
            let request_id = uuid::Uuid::new_v4();
            let xml = format!(
                "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
                 <{action}Response xmlns=\"{ns}\">\
                 <{action}Result><TagList/></{action}Result>\
                 <ResponseMetadata><RequestId>{request_id}</RequestId></ResponseMetadata>\
                 </{action}Response>"
            );
            return (StatusCode::OK, [("content-type", "text/xml")], xml).into_response();
        }
        "AddTagsToResource" | "RemoveTagsFromResource" | "TagResource" | "UntagResource" => {
            let ns = guess_namespace(params);
            let request_id = uuid::Uuid::new_v4();
            let xml = format!(
                "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
                 <{action}Response xmlns=\"{ns}\">\
                 <{action}Result/>\
                 <ResponseMetadata><RequestId>{request_id}</RequestId></ResponseMetadata>\
                 </{action}Response>"
            );
            return (StatusCode::OK, [("content-type", "text/xml")], xml).into_response();
        }
        _ => {}
    }

    let (service, resource_type, operation) = match classify_xml_action(action) {
        Some(t) => t,
        None => {
            tracing::warn!("Unknown XML action: {action}");
            return formats::xml::error_response(
                StatusCode::BAD_REQUEST,
                "InvalidAction",
                &format!("Unknown action: {action}"),
            );
        }
    };

    let body_json: Value = if body.is_empty() {
        params_to_json(params)
    } else {
        serde_json::from_slice(body).unwrap_or_else(|_| params_to_json(params))
    };

    // EC2 needs custom XML wrapping — each resource type has a specific
    // wrapper element name (vpc, subnet, securityGroupRuleSet, etc.)
    if service == "ec2" {
        return dispatch_ec2(action, operation, resource_type, &body_json, db, region);
    }

    // ElastiCache — CreateCacheSubnetGroup / DescribeCacheSubnetGroups
    // need proper response envelope wrapping.
    if service == "elasticache" && resource_type == "subnet-group" {
        return dispatch_elasticache_subnet_group(action, &body_json, params, db, region);
    }

    // CloudWatch Monitoring actions (PutDashboard, DescribeAlarms, etc.)
    // arrive via ?Action= form-encoded but the SDK expects JSON responses
    // with application/x-amz-json-1.0 content type (smithy JSON protocol).
    if service == "cloudwatch" {
        return dispatch_cloudwatch(action, operation, resource_type, &body_json, db, region);
    }

    // ElastiCache CreateReplicationGroup needs a specific response shape
    // with a nested <ReplicationGroup> element containing ReplicationGroupId and ARN.
    if service == "elasticache" && action == "CreateReplicationGroup" {
        let rg_id = params.get("ReplicationGroupId").cloned()
            .or_else(|| body_json["ReplicationGroupId"].as_str().map(String::from))
            .unwrap_or_else(|| "unknown".to_string());
        let desc = params.get("ReplicationGroupDescription").cloned()
            .or_else(|| body_json["ReplicationGroupDescription"].as_str().map(String::from))
            .unwrap_or_default();
        let node_type = params.get("CacheNodeType").cloned()
            .or_else(|| body_json["CacheNodeType"].as_str().map(String::from))
            .unwrap_or_else(|| "cache.t3.micro".to_string());
        let arn = format!("arn:aws:elasticache:{region}:000000000000:replicationgroup:{rg_id}");
        let _ = store::create(db, service, resource_type, Some(&rg_id), body_json, region);
        let xml = format!(
            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
             <CreateReplicationGroupResponse xmlns=\"http://elasticache.amazonaws.com/doc/2015-02-02/\">\
             <CreateReplicationGroupResult><ReplicationGroup>\
             <ReplicationGroupId>{rg_id}</ReplicationGroupId>\
             <ARN>{arn}</ARN>\
             <Status>available</Status>\
             <Description>{desc}</Description>\
             <Engine>redis</Engine>\
             <CacheNodeType>{node_type}</CacheNodeType>\
             <AutomaticFailover>disabled</AutomaticFailover>\
             <MultiAZ>disabled</MultiAZ>\
             <SnapshotRetentionLimit>0</SnapshotRetentionLimit>\
             <SnapshottingClusterId>{rg_id}-001</SnapshottingClusterId>\
             <ClusterEnabled>false</ClusterEnabled>\
             <TransitEncryptionEnabled>true</TransitEncryptionEnabled>\
             <AtRestEncryptionEnabled>true</AtRestEncryptionEnabled>\
             <MemberClusters><ClusterId>{rg_id}-001</ClusterId></MemberClusters>\
             <NodeGroups><NodeGroup>\
             <NodeGroupId>0001</NodeGroupId>\
             <Status>available</Status>\
             <PrimaryEndpoint><Address>{rg_id}.000000.0001.use1.cache.amazonaws.com</Address><Port>6379</Port></PrimaryEndpoint>\
             <ReaderEndpoint><Address>{rg_id}-ro.000000.0001.use1.cache.amazonaws.com</Address><Port>6379</Port></ReaderEndpoint>\
             <NodeGroupMembers><NodeGroupMember>\
             <CacheClusterId>{rg_id}-001</CacheClusterId>\
             <CacheNodeId>0001</CacheNodeId>\
             <CurrentRole>primary</CurrentRole>\
             </NodeGroupMember></NodeGroupMembers>\
             </NodeGroup></NodeGroups>\
             </ReplicationGroup></CreateReplicationGroupResult>\
             <ResponseMetadata><RequestId>00000000-0000-0000-0000-000000000000</RequestId></ResponseMetadata>\
             </CreateReplicationGroupResponse>"
        );
        return (axum::http::StatusCode::OK, [("content-type", "text/xml")], xml).into_response();
    }

    // ElastiCache DescribeReplicationGroups
    if service == "elasticache" && action == "DescribeReplicationGroups" {
        let rg_id = params.get("ReplicationGroupId").cloned()
            .or_else(|| body_json["ReplicationGroupId"].as_str().map(String::from))
            .unwrap_or_default();
        let filters: std::collections::HashMap<String, String> = if rg_id.is_empty() {
            std::collections::HashMap::new()
        } else {
            [("ResourceId".to_string(), rg_id.clone())].into_iter().collect()
        };
        let items = store::list(db, service, resource_type, &filters).unwrap_or_default();
        let rg_xml: String = items.iter().map(|item| {
            let id = prop_str(item, "ResourceId");
            let desc = item.get("ReplicationGroupDescription")
                .and_then(|v| v.as_str()).unwrap_or("");
            let node_type = item.get("CacheNodeType")
                .and_then(|v| v.as_str()).unwrap_or("cache.t3.micro");
            let arn = format!("arn:aws:elasticache:{region}:000000000000:replicationgroup:{id}");
            format!(
                "<ReplicationGroup>\
                 <ReplicationGroupId>{id}</ReplicationGroupId>\
                 <ARN>{arn}</ARN>\
                 <Status>available</Status>\
                 <Description>{desc}</Description>\
                 <Engine>redis</Engine>\
                 <CacheNodeType>{node_type}</CacheNodeType>\
                 <AutomaticFailover>disabled</AutomaticFailover>\
                 <MultiAZ>disabled</MultiAZ>\
                 <SnapshotRetentionLimit>0</SnapshotRetentionLimit>\
                 <SnapshottingClusterId>{id}-001</SnapshottingClusterId>\
                 <ClusterEnabled>false</ClusterEnabled>\
                 <TransitEncryptionEnabled>true</TransitEncryptionEnabled>\
                 <AtRestEncryptionEnabled>true</AtRestEncryptionEnabled>\
                 <MemberClusters><ClusterId>{id}-001</ClusterId></MemberClusters>\
                 <NodeGroups><NodeGroup>\
                 <NodeGroupId>0001</NodeGroupId>\
                 <Status>available</Status>\
                 <PrimaryEndpoint><Address>{id}.000000.0001.use1.cache.amazonaws.com</Address><Port>6379</Port></PrimaryEndpoint>\
                 <ReaderEndpoint><Address>{id}-ro.000000.0001.use1.cache.amazonaws.com</Address><Port>6379</Port></ReaderEndpoint>\
                 <NodeGroupMembers><NodeGroupMember>\
                 <CacheClusterId>{id}-001</CacheClusterId>\
                 <CacheNodeId>0001</CacheNodeId>\
                 <CurrentRole>primary</CurrentRole>\
                 </NodeGroupMember></NodeGroupMembers>\
                 </NodeGroup></NodeGroups>\
                 </ReplicationGroup>"
            )
        }).collect();
        let xml = format!(
            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
             <DescribeReplicationGroupsResponse xmlns=\"http://elasticache.amazonaws.com/doc/2015-02-02/\">\
             <DescribeReplicationGroupsResult>\
             <ReplicationGroups>{rg_xml}</ReplicationGroups>\
             </DescribeReplicationGroupsResult>\
             <ResponseMetadata><RequestId>00000000-0000-0000-0000-000000000000</RequestId></ResponseMetadata>\
             </DescribeReplicationGroupsResponse>"
        );
        return (axum::http::StatusCode::OK, [("content-type", "text/xml")], xml).into_response();
    }

    // ElastiCache DescribeCacheClusters — after creating a ReplicationGroup the
    // provider calls DescribeCacheClusters for the member cluster ("{rg_id}-001").
    // Look up the parent replication group and synthesize a CacheCluster response.
    if service == "elasticache" && (action == "DescribeCacheClusters" || resource_type == "cache-cluster") {
        let cluster_id = params.get("CacheClusterId").cloned()
            .or_else(|| body_json["CacheClusterId"].as_str().map(String::from))
            .unwrap_or_default();
        // Member cluster IDs are "{rg_id}-001"; strip the suffix to find the
        // parent replication group.
        let rg_id = cluster_id.strip_suffix("-001").unwrap_or(&cluster_id);
        let rg_filter: std::collections::HashMap<String, String> =
            [("ResourceId".to_string(), rg_id.to_string())].into_iter().collect();
        let items = store::list(db, "elasticache", "replication-group", &rg_filter)
            .unwrap_or_default();
        let cluster_xml: String = if items.is_empty() && !cluster_id.is_empty() {
            // Synthesize a minimal cache cluster even if the RG wasn't found
            format!(
                "<CacheCluster>\
                 <CacheClusterId>{cluster_id}</CacheClusterId>\
                 <CacheClusterStatus>available</CacheClusterStatus>\
                 <Engine>redis</Engine>\
                 <EngineVersion>7.1</EngineVersion>\
                 <CacheNodeType>cache.t3.micro</CacheNodeType>\
                 <NumCacheNodes>1</NumCacheNodes>\
                 <PreferredAvailabilityZone>us-east-1a</PreferredAvailabilityZone>\
                 <CacheClusterCreateTime>2024-01-01T00:00:00Z</CacheClusterCreateTime>\
                 <ReplicationGroupId>{rg_id}</ReplicationGroupId>\
                 <ARN>arn:aws:elasticache:{region}:000000000000:cluster:{cluster_id}</ARN>\
                 <TransitEncryptionEnabled>true</TransitEncryptionEnabled>\
                 <AtRestEncryptionEnabled>true</AtRestEncryptionEnabled>\
                 </CacheCluster>"
            )
        } else {
            items.iter().map(|item| {
                let id = prop_str(item, "ResourceId");
                let cid = format!("{id}-001");
                let node_type = item.get("CacheNodeType")
                    .and_then(|v| v.as_str()).unwrap_or("cache.t3.micro");
                let engine_ver = item.get("EngineVersion")
                    .and_then(|v| v.as_str()).unwrap_or("7.1");
                format!(
                    "<CacheCluster>\
                     <CacheClusterId>{cid}</CacheClusterId>\
                     <CacheClusterStatus>available</CacheClusterStatus>\
                     <Engine>redis</Engine>\
                     <EngineVersion>{engine_ver}</EngineVersion>\
                     <CacheNodeType>{node_type}</CacheNodeType>\
                     <NumCacheNodes>1</NumCacheNodes>\
                     <PreferredAvailabilityZone>us-east-1a</PreferredAvailabilityZone>\
                     <CacheClusterCreateTime>2024-01-01T00:00:00Z</CacheClusterCreateTime>\
                     <ReplicationGroupId>{id}</ReplicationGroupId>\
                     <ARN>arn:aws:elasticache:{region}:000000000000:cluster:{cid}</ARN>\
                     <TransitEncryptionEnabled>true</TransitEncryptionEnabled>\
                     <AtRestEncryptionEnabled>true</AtRestEncryptionEnabled>\
                     </CacheCluster>"
                )
            }).collect()
        };
        let xml = format!(
            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
             <DescribeCacheClustersResponse xmlns=\"http://elasticache.amazonaws.com/doc/2015-02-02/\">\
             <DescribeCacheClustersResult>\
             <CacheClusters>{cluster_xml}</CacheClusters>\
             </DescribeCacheClustersResult>\
             <ResponseMetadata><RequestId>00000000-0000-0000-0000-000000000000</RequestId></ResponseMetadata>\
             </DescribeCacheClustersResponse>"
        );
        return (axum::http::StatusCode::OK, [("content-type", "text/xml")], xml).into_response();
    }

    match operation {
        "Create" | "Allocate" => {
            match store::create(
                db,
                service,
                resource_type,
                extract_identifier(params, &body_json, service, resource_type).as_deref(),
                body_json,
                region,
            ) {
                Ok((_, props)) => formats::xml::action_response(action, &props),
                Err(e) => store_error_to_xml(e),
            }
        }
        "Describe" | "List" | "Get" => {
            let filters = extract_filters(params);
            match store::list(db, service, resource_type, &filters) {
                Ok(items) => formats::xml::list_response(action, service, resource_type, &items),
                Err(e) => store_error_to_xml(e),
            }
        }
        "Delete" | "Remove" | "Release" => {
            let id = extract_id_for_delete(params, &body_json, service, resource_type);
            match id {
                Some(identifier) => {
                    match store::delete_by_id(db, service, resource_type, &identifier) {
                        Ok(()) => formats::xml::action_response(
                            action,
                            &serde_json::json!({"return": "true"}),
                        ),
                        Err(e) => store_error_to_xml(e),
                    }
                }
                None => formats::xml::error_response(
                    StatusCode::BAD_REQUEST,
                    "MissingParameter",
                    "Resource identifier required",
                ),
            }
        }
        "Modify" | "Update" | "Set" | "Put" | "Change" | "Attach" | "Detach" | "Associate"
        | "Disassociate" | "Register" | "Deregister" | "Add" | "Request" => {
            // For modifications, try to find the resource and update it, or create it
            let id = extract_identifier(params, &body_json, service, resource_type);
            if let Some(ref identifier) = id {
                match store::lookup(db, service, resource_type, identifier) {
                    Ok(_existing) => {
                        match store::update_by_lookup(
                            db,
                            service,
                            resource_type,
                            identifier,
                            body_json.clone(),
                        ) {
                            Ok(props) => formats::xml::action_response(action, &props),
                            Err(e) => store_error_to_xml(e),
                        }
                    }
                    Err(_) => {
                        match store::create(db, service, resource_type, id.as_deref(), body_json, region) {
                            Ok((_, props)) => formats::xml::action_response(action, &props),
                            Err(e) => store_error_to_xml(e),
                        }
                    }
                }
            } else {
                match store::create(db, service, resource_type, None, body_json, region) {
                    Ok((_, props)) => formats::xml::action_response(action, &props),
                    Err(e) => store_error_to_xml(e),
                }
            }
        }
        _ => formats::xml::error_response(
            StatusCode::BAD_REQUEST,
            "InvalidAction",
            &format!("Unsupported operation: {operation}"),
        ),
    }
}

/// Dispatch a JSON-protocol action (Lambda, SSM, SecretsManager, KMS, CodeBuild, etc.).
pub fn dispatch_json_action(
    target: &str,
    body: Value,
    db: &DbPool,
    region: &str,
) -> Response {
    // KMS (TrentService) needs a custom response envelope — the Go AWS
    // SDK dereferences KeyMetadata and panics with a nil pointer if the
    // response is a flat JSON object.
    if target.contains("TrentService") {
        return dispatch_kms(target, body, db, region);
    }

    // CloudWatch Logs needs custom handling — CreateLogGroup returns empty
    // 200, DescribeLogGroups wraps results in {"logGroups": [...]}.
    if target.contains("Logs") && !target.contains("Granite") {
        return dispatch_logs(target, body, db, region);
    }

    // CloudWatch Monitoring — dispatch metric alarms and dashboards.
    if target.contains("Granite") || target.contains("monitoring") {
        let op = target.rsplit('.').next().unwrap_or(target);
        return dispatch_cloudwatch_monitoring(op, body, db, region);
    }

    // Secrets Manager needs a custom dispatcher
    if target.contains("secretsmanager") || target.contains("SecretsManager") {
        return dispatch_secretsmanager(target, body, db, region);
    }

    // SSM Parameter Store needs custom handling — GetParameter wraps in {"Parameter": {...}}
    if target.contains("SSM") || target.contains("SimpleSystemsManager") {
        return dispatch_ssm(target, body, db, region);
    }

    // CodeBuild needs custom handling for ImportSourceCredentials —
    // the provider reads back via ListSourceCredentials and expects
    // the ARN returned by the import call.
    if target.contains("CodeBuild") {
        return dispatch_codebuild(target, body, db, region);
    }

    // ACM (CertificateManager) needs custom handling — DescribeCertificate
    // must return Status "ISSUED" with DomainValidationOptions so the
    // Terraform/Pulumi provider doesn't wait forever for validation.
    if target.contains("CertificateManager") || target.contains("ACM") {
        return dispatch_acm(target, body, db, region);
    }

    // EventBridge — PutTargets stores targets per rule; ListTargetsByRule
    // must return them in a {"Targets": [...]} envelope.
    if target.contains("AWSEvents") || target.contains("Events") {
        return dispatch_eventbridge(target, body, db, region);
    }

    // WAFv2 — CreateIPSet / CreateWebACL etc. must return a { Summary: { Id, ARN, ... } }
    // envelope or the provider panics with nil pointer dereference.
    if target.contains("AWSWAF") {
        return dispatch_wafv2(target, body, db, region);
    }

    // Application Auto Scaling — RegisterScalableTarget uses the
    // "Register" prefix which doesn't map to the generic CRUD arms.
    // Custom dispatcher ensures correct response envelopes.
    if target.contains("AnyScaleFrontend") || target.contains("ApplicationAutoScaling") {
        return dispatch_autoscaling(target, body, db, region);
    }

    let operation_name = target.rsplit('.').next().unwrap_or(target);
    let (service, resource_type, operation) = match classify_json_target(target) {
        Some(t) => t,
        None => {
            return formats::json::error_response(
                StatusCode::BAD_REQUEST,
                "UnknownOperation",
                &format!("Unknown target: {target}"),
            );
        }
    };

    match operation {
        "Create" | "Put" | "Request" | "Tag" | "Import" => {
            let id = extract_json_identifier(&body, service, resource_type);
            match store::create(db, service, resource_type, id.as_deref(), body.clone(), region) {
                Ok((_, props)) => formats::json::success_response(&props),
                Err(store::StoreError::AlreadyExists(_)) => {
                    // For Put operations (like PutParameter), update instead
                    if let Some(identifier) =
                        extract_json_identifier(&body, service, resource_type)
                    {
                        match store::update_by_lookup(
                            db,
                            service,
                            resource_type,
                            &identifier,
                            body,
                        ) {
                            Ok(props) => formats::json::success_response(&props),
                            Err(e) => store_error_to_json(e),
                        }
                    } else {
                        store_error_to_json(store::StoreError::AlreadyExists(
                            "already exists".into(),
                        ))
                    }
                }
                Err(e) => store_error_to_json(e),
            }
        }
        "Describe" | "List" | "Get" => {
            let id = extract_json_identifier(&body, service, resource_type);
            if let Some(ref identifier) = id {
                match store::lookup(db, service, resource_type, identifier) {
                    Ok(props) => formats::json::success_response(&props),
                    Err(e) => store_error_to_json(e),
                }
            } else {
                match store::list(db, service, resource_type, &HashMap::new()) {
                    Ok(items) => formats::json::list_response(operation_name, &items),
                    Err(e) => store_error_to_json(e),
                }
            }
        }
        "Delete" | "Deregister" | "Remove" => {
            let id = extract_json_identifier(&body, service, resource_type);
            match id {
                Some(identifier) => {
                    match store::delete_by_id(db, service, resource_type, &identifier) {
                        Ok(()) => formats::json::success_response(&serde_json::json!({})),
                        Err(e) => store_error_to_json(e),
                    }
                }
                None => formats::json::error_response(
                    StatusCode::BAD_REQUEST,
                    "MissingParameter",
                    "identifier required",
                ),
            }
        }
        "Update" | "Modify" => {
            let id = extract_json_identifier(&body, service, resource_type);
            if let Some(ref identifier) = id {
                match store::update_by_lookup(db, service, resource_type, identifier, body) {
                    Ok(props) => formats::json::success_response(&props),
                    Err(e) => store_error_to_json(e),
                }
            } else {
                formats::json::error_response(
                    StatusCode::BAD_REQUEST,
                    "MissingParameter",
                    "identifier required",
                )
            }
        }
        _ => formats::json::error_response(
            StatusCode::BAD_REQUEST,
            "UnknownOperation",
            operation,
        ),
    }
}

// --- EC2 ---

const EC2_XMLNS: &str = "http://ec2.amazonaws.com/doc/2016-11-15/";

/// Build a complete EC2 XML response with the xmlns and requestId.
fn ec2_response(action: &str, inner: &str) -> Response {
    let request_id = Uuid::new_v4();
    let body = format!(
        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
         <{action}Response xmlns=\"{EC2_XMLNS}\">\
         <requestId>{request_id}</requestId>\
         {inner}\
         </{action}Response>"
    );
    ([("content-type", "application/xml")], body).into_response()
}

/// Render VPC fields (without the outer `<vpc>` tag).
fn vpc_fields(props: &Value) -> String {
    let vpc_id = prop_str(props, "ResourceId");
    let cidr = prop_str_or(props, "CidrBlock", "10.0.0.0/16");
    let assoc_id = format!(
        "vpc-cidr-assoc-{}",
        &Uuid::new_v4().simple().to_string()[..8]
    );
    let dhcp_id = format!("dopt-{}", &Uuid::new_v4().simple().to_string()[..8]);
    let owner = "000000000000";
    let instance_tenancy = prop_str_or(props, "InstanceTenancy", "default");
    let enable_dns_support = prop_str_or(props, "EnableDnsSupport", "true");
    let enable_dns_hostnames = prop_str_or(props, "EnableDnsHostnames", "false");
    format!(
        "<vpcId>{vpc_id}</vpcId>\
         <state>available</state>\
         <cidrBlock>{cidr}</cidrBlock>\
         <cidrBlockAssociationSet>\
           <item>\
             <cidrBlock>{cidr}</cidrBlock>\
             <associationId>{assoc_id}</associationId>\
             <cidrBlockState><state>associated</state></cidrBlockState>\
           </item>\
         </cidrBlockAssociationSet>\
         <dhcpOptionsId>{dhcp_id}</dhcpOptionsId>\
         <instanceTenancy>{instance_tenancy}</instanceTenancy>\
         <isDefault>false</isDefault>\
         <ownerId>{owner}</ownerId>\
         <enableDnsSupport>{enable_dns_support}</enableDnsSupport>\
         <enableDnsHostnames>{enable_dns_hostnames}</enableDnsHostnames>\
         <tagSet/>"
    )
}

/// Render a `<vpc>` element from stored properties.
fn vpc_xml(props: &Value) -> String {
    format!("<vpc>{}</vpc>", vpc_fields(props))
}

/// Render subnet fields (without the outer `<subnet>` tag).
fn subnet_fields(props: &Value, region: &str) -> String {
    let subnet_id = prop_str(props, "ResourceId");
    let vpc_id = prop_str(props, "VpcId");
    let cidr = prop_str_or(props, "CidrBlock", "10.0.0.0/24");
    let az = prop_str_or(props, "AvailabilityZone", &format!("{region}a"));
    let map_public = prop_str_or(props, "MapPublicIpOnLaunch", "false");
    let owner = "000000000000";
    format!(
        "<subnetId>{subnet_id}</subnetId>\
         <state>available</state>\
         <vpcId>{vpc_id}</vpcId>\
         <cidrBlock>{cidr}</cidrBlock>\
         <availableIpAddressCount>251</availableIpAddressCount>\
         <availabilityZone>{az}</availabilityZone>\
         <availabilityZoneId>{az}-id</availabilityZoneId>\
         <defaultForAz>false</defaultForAz>\
         <mapPublicIpOnLaunch>{map_public}</mapPublicIpOnLaunch>\
         <assignIpv6AddressOnCreation>false</assignIpv6AddressOnCreation>\
         <ownerId>{owner}</ownerId>\
         <tagSet/>"
    )
}

/// Render a `<subnet>` element from stored properties.
fn subnet_xml(props: &Value, region: &str) -> String {
    format!("<subnet>{}</subnet>", subnet_fields(props, region))
}

/// Render security group fields (already flat — no wrapper element
/// needed because DescribeSecurityGroups uses `<item>` directly).
fn sg_fields(props: &Value) -> String {
    sg_fields_with_perms(props, "<ipPermissions/>", "<ipPermissionsEgress/>")
}

fn sg_fields_with_perms(props: &Value, ip_perms: &str, ip_perms_egress: &str) -> String {
    let group_id = prop_str(props, "ResourceId");
    let group_name = prop_str_or(props, "GroupName", "default");
    let vpc_id = prop_str(props, "VpcId");
    let description = prop_str_or(props, "GroupDescription", "");
    let owner = "000000000000";
    format!(
        "<groupId>{group_id}</groupId>\
         <groupName>{group_name}</groupName>\
         <groupDescription>{description}</groupDescription>\
         <vpcId>{vpc_id}</vpcId>\
         <ownerId>{owner}</ownerId>\
         {ip_perms}\
         {ip_perms_egress}\
         <tagSet/>"
    )
}

/// Convert stored security group rules into `<ipPermissions>` XML for
/// DescribeSecurityGroups.  The provider expects IpPermissions format,
/// not SecurityGroupRules format.
fn sg_rules_to_ip_permissions(rules: &[Value], egress: bool) -> String {
    let filtered: Vec<&Value> = rules.iter().filter(|r| {
        let is_egress = prop_str_or(r, "IsEgress", "false") == "true";
        is_egress == egress
    }).collect();
    if filtered.is_empty() {
        return if egress { "<ipPermissionsEgress/>".to_string() } else { "<ipPermissions/>".to_string() };
    }
    let items: String = filtered.iter().map(|r| {
        let protocol = prop_str_or(r, "IpProtocol", "-1");
        let from_port = prop_str_or(r, "FromPort", "-1");
        let to_port = prop_str_or(r, "ToPort", "-1");
        let cidr = prop_str(r, "CidrIpv4");
        let src_sg = prop_str(r, "ReferencedGroupId");
        let ranges = if !cidr.is_empty() {
            format!("<ipRanges><item><cidrIp>{cidr}</cidrIp></item></ipRanges>")
        } else {
            "<ipRanges/>".to_string()
        };
        let groups = if !src_sg.is_empty() {
            format!(
                "<groups><item>\
                 <groupId>{src_sg}</groupId>\
                 <userId>000000000000</userId>\
                 </item></groups>"
            )
        } else {
            "<groups/>".to_string()
        };
        format!(
            "<item>\
             <ipProtocol>{protocol}</ipProtocol>\
             <fromPort>{from_port}</fromPort>\
             <toPort>{to_port}</toPort>\
             {ranges}\
             <ipv6Ranges/>\
             <prefixListIds/>\
             {groups}\
             </item>"
        )
    }).collect();
    if egress {
        format!("<ipPermissionsEgress>{items}</ipPermissionsEgress>")
    } else {
        format!("<ipPermissions>{items}</ipPermissions>")
    }
}

/// Render internet gateway fields (without `<internetGateway>` wrapper).
fn igw_fields(props: &Value) -> String {
    let igw_id = prop_str(props, "ResourceId");
    let vpc_id = prop_str(props, "VpcId");
    let attachment_xml = if vpc_id.is_empty() {
        "<attachmentSet/>".to_string()
    } else {
        format!(
            "<attachmentSet><item>\
             <vpcId>{vpc_id}</vpcId>\
             <state>available</state>\
             </item></attachmentSet>"
        )
    };
    format!(
        "<internetGatewayId>{igw_id}</internetGatewayId>\
         {attachment_xml}\
         <tagSet/>"
    )
}

/// Render an `<internetGateway>` element.
fn igw_xml(props: &Value) -> String {
    format!("<internetGateway>{}</internetGateway>", igw_fields(props))
}

/// Render NAT gateway fields (without `<natGateway>` wrapper).
fn nat_fields(props: &Value) -> String {
    let nat_id = prop_str(props, "ResourceId");
    let subnet_id = prop_str(props, "SubnetId");
    let vpc_id = prop_str(props, "VpcId");
    let alloc_id = prop_str(props, "AllocationId");
    format!(
        "<natGatewayId>{nat_id}</natGatewayId>\
         <state>available</state>\
         <subnetId>{subnet_id}</subnetId>\
         <vpcId>{vpc_id}</vpcId>\
         <natGatewayAddressSet>\
           <item>\
             <allocationId>{alloc_id}</allocationId>\
           </item>\
         </natGatewayAddressSet>\
         <tagSet/>"
    )
}

/// Render a `<natGateway>` element.
fn nat_xml(props: &Value) -> String {
    format!("<natGateway>{}</natGateway>", nat_fields(props))
}

/// Render route table fields (without `<routeTable>` wrapper).
fn rtb_fields(props: &Value) -> String {
    let rtb_id = prop_str(props, "ResourceId");
    let vpc_id = prop_str(props, "VpcId");
    format!(
        "<routeTableId>{rtb_id}</routeTableId>\
         <vpcId>{vpc_id}</vpcId>\
         <routeSet/>\
         <associationSet/>\
         <tagSet/>"
    )
}

/// Render route table fields with pre-built routeSet and associationSet XML.
fn rtb_fields_with_routes(props: &Value, route_set: &str, assoc_set: &str) -> String {
    let rtb_id = prop_str(props, "ResourceId");
    let vpc_id = prop_str(props, "VpcId");
    format!(
        "<routeTableId>{rtb_id}</routeTableId>\
         <vpcId>{vpc_id}</vpcId>\
         {route_set}\
         {assoc_set}\
         <tagSet/>"
    )
}

/// Render a `<routeTable>` element.
fn rtb_xml(props: &Value) -> String {
    format!("<routeTable>{}</routeTable>", rtb_fields(props))
}

/// Render network ACL fields (without `<networkAcl>` wrapper).
fn nacl_fields(props: &Value) -> String {
    let nacl_id = prop_str(props, "ResourceId");
    let vpc_id = prop_str(props, "VpcId");
    format!(
        "<networkAclId>{nacl_id}</networkAclId>\
         <vpcId>{vpc_id}</vpcId>\
         <default>false</default>\
         <entrySet/>\
         <associationSet/>\
         <tagSet/>"
    )
}

/// Render a `<networkAcl>` element.
fn nacl_xml(props: &Value) -> String {
    format!("<networkAcl>{}</networkAcl>", nacl_fields(props))
}

/// Helper: read a string property, falling back to empty string.
fn prop_str(v: &Value, key: &str) -> String {
    v.get(key)
        .and_then(|v| v.as_str())
        .unwrap_or("")
        .to_string()
}

/// Helper: read a string property with a default.
fn prop_str_or<'a>(v: &'a Value, key: &str, default: &'a str) -> String {
    v.get(key)
        .and_then(|v| v.as_str())
        .unwrap_or(default)
        .to_string()
}

/// Build the XML body for a single EC2 instance.  This must contain all
/// fields that the Go AWS SDK v2 dereferences when "collecting instance
/// settings" — omitting any causes a nil-pointer panic or "couldn't find
/// resource" error.
fn instance_xml_item(
    instance_id: &str, ami_id: &str, instance_type: &str,
    subnet_id: &str, vpc_id: &str, private_ip: &str,
    key_name: &str, iam_profile_arn: &str, eni_id: &str,
    sg_xml: &str, region: &str,
) -> String {
    let az = format!("{region}a");
    let iam_section = if !iam_profile_arn.is_empty() {
        format!(
            "<iamInstanceProfile>\
               <arn>{iam_profile_arn}</arn>\
               <id>{}</id>\
             </iamInstanceProfile>",
            &Uuid::new_v4().simple().to_string()[..17]
        )
    } else {
        String::new()
    };
    let key_section = if !key_name.is_empty() {
        format!("<keyName>{key_name}</keyName>")
    } else {
        String::new()
    };
    format!(
        "<instanceId>{instance_id}</instanceId>\
         <imageId>{ami_id}</imageId>\
         <instanceState><code>16</code><name>running</name></instanceState>\
         <privateDnsName>{instance_id}.ec2.internal</privateDnsName>\
         <dnsName/>\
         <reason/>\
         {key_section}\
         <amiLaunchIndex>0</amiLaunchIndex>\
         <productCodes/>\
         <instanceType>{instance_type}</instanceType>\
         <launchTime>2024-01-01T00:00:00.000Z</launchTime>\
         <placement>\
           <availabilityZone>{az}</availabilityZone>\
           <groupName/>\
           <tenancy>default</tenancy>\
         </placement>\
         <monitoring><state>disabled</state></monitoring>\
         <subnetId>{subnet_id}</subnetId>\
         <vpcId>{vpc_id}</vpcId>\
         <privateIpAddress>{private_ip}</privateIpAddress>\
         <sourceDestCheck>true</sourceDestCheck>\
         <groupSet>{sg_xml}</groupSet>\
         <architecture>x86_64</architecture>\
         <rootDeviceType>ebs</rootDeviceType>\
         <rootDeviceName>/dev/xvda</rootDeviceName>\
         <blockDeviceMapping>\
           <item>\
             <deviceName>/dev/xvda</deviceName>\
             <ebs>\
               <volumeId>vol-{}</volumeId>\
               <status>attached</status>\
               <attachTime>2024-01-01T00:00:00.000Z</attachTime>\
               <deleteOnTermination>true</deleteOnTermination>\
             </ebs>\
           </item>\
         </blockDeviceMapping>\
         <virtualizationType>hvm</virtualizationType>\
         <hypervisor>xen</hypervisor>\
         <clientToken/>\
         <tagSet/>\
         {iam_section}\
         <ebsOptimized>false</ebsOptimized>\
         <enaSupport>true</enaSupport>\
         <networkInterfaceSet>\
           <item>\
             <networkInterfaceId>{eni_id}</networkInterfaceId>\
             <subnetId>{subnet_id}</subnetId>\
             <vpcId>{vpc_id}</vpcId>\
             <ownerId>000000000000</ownerId>\
             <status>in-use</status>\
             <privateIpAddress>{private_ip}</privateIpAddress>\
             <sourceDestCheck>true</sourceDestCheck>\
             <groupSet>{sg_xml}</groupSet>\
             <attachment>\
               <attachmentId>eni-attach-{}</attachmentId>\
               <deviceIndex>0</deviceIndex>\
               <status>attached</status>\
               <attachTime>2024-01-01T00:00:00.000Z</attachTime>\
               <deleteOnTermination>true</deleteOnTermination>\
             </attachment>\
             <privateIpAddressesSet>\
               <item>\
                 <privateIpAddress>{private_ip}</privateIpAddress>\
                 <primary>true</primary>\
               </item>\
             </privateIpAddressesSet>\
           </item>\
         </networkInterfaceSet>\
         <cpuOptions><coreCount>1</coreCount><threadsPerCore>2</threadsPerCore></cpuOptions>\
         <capacityReservationSpecification>\
           <capacityReservationPreference>open</capacityReservationPreference>\
         </capacityReservationSpecification>\
         <maintenanceOptions><autoRecovery>default</autoRecovery></maintenanceOptions>\
         <metadataOptions>\
           <state>applied</state>\
           <httpTokens>required</httpTokens>\
           <httpPutResponseHopLimit>2</httpPutResponseHopLimit>\
           <httpEndpoint>enabled</httpEndpoint>\
           <httpProtocolIpv6>disabled</httpProtocolIpv6>\
           <instanceMetadataTags>disabled</instanceMetadataTags>\
         </metadataOptions>\
         <enclaveOptions><enabled>false</enabled></enclaveOptions>\
         <privateDnsNameOptions>\
           <hostnameType>ip-name</hostnameType>\
           <enableResourceNameDnsARecord>false</enableResourceNameDnsARecord>\
           <enableResourceNameDnsAAAARecord>false</enableResourceNameDnsAAAARecord>\
         </privateDnsNameOptions>",
        &Uuid::new_v4().simple().to_string()[..17],
        &Uuid::new_v4().simple().to_string()[..17],
    )
}

/// Handle EC2 operations with proper XML response wrapping.
///
/// The Go AWS SDK dereferences nested struct fields (e.g.
/// `resp.Vpc.VpcId`) and panics with nil pointer if the response
/// doesn't wrap the resource in the expected element name with
/// camelCase field names.
fn dispatch_ec2(
    action: &str,
    operation: &str,
    resource_type: &str,
    body: &Value,
    db: &DbPool,
    region: &str,
) -> Response {
    match action {
        // ---- VPC ----
        "CreateVpc" => {
            match store::create(db, "ec2", "vpc", None, body.clone(), region) {
                Ok((_, props)) => ec2_response(action, &vpc_xml(&props)),
                Err(e) => store_error_to_xml(e),
            }
        }
        "DescribeVpcs" => {
            let filters = extract_describe_filters(body);
            match store::list(db, "ec2", "vpc", &filters) {
                Ok(items) => {
                    let inner: String = items
                        .iter()
                        .map(|p| format!("<item>{}</item>", vpc_fields(p)))
                        .collect();
                    ec2_response(action, &format!("<vpcSet>{inner}</vpcSet>"))
                }
                Err(e) => store_error_to_xml(e),
            }
        }
        "ModifyVpcAttribute" => {
            let vpc_id = body.get("VpcId").and_then(|v| v.as_str()).unwrap_or("");
            if !vpc_id.is_empty() {
                let _ = store::update_by_lookup(db, "ec2", "vpc", vpc_id, body.clone());
            }
            ec2_response(action, "<return>true</return>")
        }
        "DescribeVpcAttribute" => {
            // The Go AWS SDK polls this after ModifyVpcAttribute to verify
            // the change took effect.  It sends ?Action=DescribeVpcAttribute
            // &Attribute=enableDnsHostnames&VpcId=vpc-xxx.
            let vpc_id = body.get("VpcId").and_then(|v| v.as_str()).unwrap_or("");
            let attribute = body.get("Attribute").and_then(|v| v.as_str()).unwrap_or("");

            // Look up the VPC to read stored attribute values
            let (dns_support, dns_hostnames) = if !vpc_id.is_empty() {
                match store::lookup(db, "ec2", "vpc", vpc_id) {
                    Ok(props) => {
                        let support = props.get("EnableDnsSupport")
                            .and_then(|v| v.as_str())
                            .or_else(|| props.get("EnableDnsSupport.Value").and_then(|v| v.as_str()))
                            .unwrap_or("true");
                        let hostnames = props.get("EnableDnsHostnames")
                            .and_then(|v| v.as_str())
                            .or_else(|| props.get("EnableDnsHostnames.Value").and_then(|v| v.as_str()))
                            .unwrap_or("false");
                        (support.to_string(), hostnames.to_string())
                    }
                    Err(_) => ("true".to_string(), "true".to_string()),
                }
            } else {
                ("true".to_string(), "true".to_string())
            };

            let attr_xml = match attribute {
                "enableDnsSupport" => format!(
                    "<enableDnsSupport><value>{dns_support}</value></enableDnsSupport>"
                ),
                "enableDnsHostnames" => format!(
                    "<enableDnsHostnames><value>{dns_hostnames}</value></enableDnsHostnames>"
                ),
                other => format!(
                    "<{other}><value>true</value></{other}>"
                ),
            };
            ec2_response(
                action,
                &format!("<vpcId>{vpc_id}</vpcId>{attr_xml}"),
            )
        }
        "DeleteVpc" => {
            let vpc_id = body.get("VpcId").and_then(|v| v.as_str()).unwrap_or("");
            if !vpc_id.is_empty() {
                let _ = store::delete_by_id(db, "ec2", "vpc", vpc_id);
            }
            ec2_response(action, "<return>true</return>")
        }

        // ---- Subnet ----
        "CreateSubnet" => {
            match store::create(db, "ec2", "subnet", None, body.clone(), region) {
                Ok((_, props)) => ec2_response(action, &subnet_xml(&props, region)),
                Err(e) => store_error_to_xml(e),
            }
        }
        "DescribeSubnets" => {
            let filters = extract_describe_filters(body);
            match store::list(db, "ec2", "subnet", &filters) {
                Ok(items) => {
                    let inner: String = items
                        .iter()
                        .map(|p| format!("<item>{}</item>", subnet_fields(p, region)))
                        .collect();
                    ec2_response(action, &format!("<subnetSet>{inner}</subnetSet>"))
                }
                Err(e) => store_error_to_xml(e),
            }
        }

        "ModifySubnetAttribute" => {
            let subnet_id = body.get("SubnetId").and_then(|v| v.as_str()).unwrap_or("");
            if !subnet_id.is_empty() {
                // EC2 form params use dotted keys: MapPublicIpOnLaunch.Value=true
                let mut updates = serde_json::Map::new();
                if let Some(v) = body.get("MapPublicIpOnLaunch.Value").and_then(|v| v.as_str()) {
                    updates.insert("MapPublicIpOnLaunch".to_string(), Value::String(v.to_string()));
                }
                // Also handle nested JSON form (just in case)
                if let Some(v) = body.get("MapPublicIpOnLaunch") {
                    let val = v.get("Value").and_then(|x| x.as_str())
                        .or_else(|| v.as_str())
                        .unwrap_or("false");
                    updates.insert("MapPublicIpOnLaunch".to_string(), Value::String(val.to_string()));
                }
                if !updates.is_empty() {
                    let _ = store::update_by_lookup(
                        db, "ec2", "subnet", subnet_id, Value::Object(updates),
                    );
                }
            }
            ec2_response(action, "<return>true</return>")
        }

        // ---- Security Group ----
        "CreateSecurityGroup" => {
            // Generate an sg-xxx ID; store GroupName as a property but use
            // the generated ID as the identifier so lookups by GroupId work.
            match store::create(
                db,
                "ec2",
                "security-group",
                None,
                body.clone(),
                region,
            ) {
                Ok((_, props)) => {
                    let group_id = prop_str(&props, "ResourceId");
                    ec2_response(
                        action,
                        &format!("<groupId>{group_id}</groupId><return>true</return>"),
                    )
                }
                Err(e) => store_error_to_xml(e),
            }
        }
        "DescribeSecurityGroups" => {
            let filters = extract_describe_filters(body);
            match store::list(db, "ec2", "security-group", &filters) {
                Ok(items) => {
                    let inner: String = items
                        .iter()
                        .map(|p| {
                            let group_id = prop_str(p, "ResourceId");
                            // Fetch stored rules for this security group
                            let rule_filter: HashMap<String, String> =
                                [("GroupId".to_string(), group_id.clone())].into_iter().collect();
                            let rules = store::list(db, "ec2", "security-group-rule", &rule_filter)
                                .unwrap_or_default();
                            let ip_perms = sg_rules_to_ip_permissions(&rules, false);
                            let ip_perms_egress = sg_rules_to_ip_permissions(&rules, true);
                            format!("<item>{}</item>", sg_fields_with_perms(p, &ip_perms, &ip_perms_egress))
                        })
                        .collect();
                    ec2_response(
                        action,
                        &format!("<securityGroupInfo>{inner}</securityGroupInfo>"),
                    )
                }
                Err(e) => store_error_to_xml(e),
            }
        }
        "AuthorizeSecurityGroupIngress" | "AuthorizeSecurityGroupEgress"
        | "RevokeSecurityGroupIngress" | "RevokeSecurityGroupEgress" => {
            // Store or delete the rule so DescribeSecurityGroupRules can find it.
            tracing::info!("EC2 {action} body={body}");
            let group_id = body.get("GroupId").and_then(|v| v.as_str()).unwrap_or("");
            let is_egress = action.contains("Egress");
            let is_authorize = action.starts_with("Authorize");
            if is_authorize && !group_id.is_empty() {
                let protocol = body.get("IpPermissions.1.IpProtocol")
                    .and_then(|v| v.as_str()).unwrap_or("-1");
                let from_port = body.get("IpPermissions.1.FromPort")
                    .and_then(|v| v.as_str()).unwrap_or("-1");
                let to_port = body.get("IpPermissions.1.ToPort")
                    .and_then(|v| v.as_str()).unwrap_or("-1");
                let cidr = body.get("IpPermissions.1.IpRanges.1.CidrIp")
                    .and_then(|v| v.as_str()).unwrap_or("");
                let src_sg = body.get("IpPermissions.1.UserIdGroupPairs.1.GroupId")
                    .or_else(|| body.get("IpPermissions.1.Groups.1.GroupId"))
                    .and_then(|v| v.as_str()).unwrap_or("");
                let rule_props = serde_json::json!({
                    "GroupId": group_id,
                    "IsEgress": is_egress.to_string(),
                    "IpProtocol": protocol,
                    "FromPort": from_port,
                    "ToPort": to_port,
                    "CidrIpv4": cidr,
                    "ReferencedGroupId": src_sg,
                });
                let result = store::create(db, "ec2", "security-group-rule", None, rule_props, region);
                // Return securityGroupRuleSet so the provider gets the rule ID
                if let Ok((_arn, props)) = result {
                    let rule_id = props.get("ResourceId").and_then(|v| v.as_str()).unwrap_or("");
                    let cidr_xml = if !cidr.is_empty() {
                        format!("<cidrIpv4>{cidr}</cidrIpv4>")
                    } else { String::new() };
                    let ref_xml = if !src_sg.is_empty() {
                        format!("<referencedGroupInfo><groupId>{src_sg}</groupId><userId>000000000000</userId></referencedGroupInfo>")
                    } else { String::new() };
                    let rule_xml = format!(
                        "<securityGroupRuleSet><item>\
                         <securityGroupRuleId>{rule_id}</securityGroupRuleId>\
                         <groupId>{group_id}</groupId>\
                         <groupOwnerId>000000000000</groupOwnerId>\
                         <isEgress>{is_egress}</isEgress>\
                         <ipProtocol>{protocol}</ipProtocol>\
                         <fromPort>{from_port}</fromPort>\
                         <toPort>{to_port}</toPort>\
                         {cidr_xml}{ref_xml}\
                         <tags/>\
                         </item></securityGroupRuleSet>"
                    );
                    return ec2_response(action, &format!("<return>true</return>{rule_xml}"));
                }
            }
            ec2_response(action, "<return>true</return>")
        }
        "DescribeSecurityGroupRules" => {
            tracing::info!("DescribeSecurityGroupRules body={body}");
            let filters = extract_describe_filters(body);
            let mut sg_filter: HashMap<String, String> = HashMap::new();
            if let Some(gid) = filters.get("ResourceId") {
                sg_filter.insert("GroupId".to_string(), gid.clone());
            }
            // Check Filter.N.Name=group-id format
            for i in 1..=5 {
                let name_key = format!("Filter.{i}.Name");
                let val_key = format!("Filter.{i}.Value.1");
                if let (Some(name), Some(val)) = (
                    body.get(&name_key).and_then(|v| v.as_str()),
                    body.get(&val_key).and_then(|v| v.as_str()),
                ) {
                    if name == "group-id" {
                        sg_filter.insert("GroupId".to_string(), val.to_string());
                    }
                }
            }
            // Also support SecurityGroupRuleId.N format (direct rule ID lookup)
            let mut rule_id_filter: Vec<String> = Vec::new();
            for i in 1..=5 {
                let key = format!("SecurityGroupRuleId.{i}");
                if let Some(val) = body.get(&key).and_then(|v| v.as_str()) {
                    rule_id_filter.push(val.to_string());
                }
            }
            let all_rules = store::list(db, "ec2", "security-group-rule", &sg_filter)
                .unwrap_or_default();
            // Apply rule ID filter if specified
            let rules: Vec<_> = if rule_id_filter.is_empty() {
                all_rules
            } else {
                all_rules.into_iter().filter(|r| {
                    let rid = prop_str(r, "ResourceId");
                    rule_id_filter.contains(&rid)
                }).collect()
            };
            let inner: String = rules.iter().map(|r| {
                let rule_id = prop_str(r, "ResourceId");
                let group_id = prop_str(r, "GroupId");
                let is_egress = prop_str_or(r, "IsEgress", "false");
                let protocol = prop_str_or(r, "IpProtocol", "-1");
                let from_port = prop_str_or(r, "FromPort", "-1");
                let to_port = prop_str_or(r, "ToPort", "-1");
                let cidr = prop_str(r, "CidrIpv4");
                let src_sg = prop_str(r, "ReferencedGroupId");
                let cidr_xml = if !cidr.is_empty() {
                    format!("<cidrIpv4>{cidr}</cidrIpv4>")
                } else { String::new() };
                let ref_xml = if !src_sg.is_empty() {
                    format!("<referencedGroupInfo><groupId>{src_sg}</groupId></referencedGroupInfo>")
                } else { String::new() };
                format!(
                    "<item>\
                     <securityGroupRuleId>{rule_id}</securityGroupRuleId>\
                     <groupId>{group_id}</groupId>\
                     <isEgress>{is_egress}</isEgress>\
                     <ipProtocol>{protocol}</ipProtocol>\
                     <fromPort>{from_port}</fromPort>\
                     <toPort>{to_port}</toPort>\
                     {cidr_xml}{ref_xml}\
                     <tags/>\
                     </item>"
                )
            }).collect();
            ec2_response(
                action,
                &format!("<securityGroupRuleSet>{inner}</securityGroupRuleSet>"),
            )
        }

        // ---- Internet Gateway ----
        "CreateInternetGateway" => {
            match store::create(db, "ec2", "internet-gateway", None, body.clone(), region) {
                Ok((_, props)) => ec2_response(action, &igw_xml(&props)),
                Err(e) => store_error_to_xml(e),
            }
        }
        "DescribeInternetGateways" => {
            let filters = extract_describe_filters(body);
            match store::list(db, "ec2", "internet-gateway", &filters) {
                Ok(items) => {
                    let inner: String = items
                        .iter()
                        .map(|p| format!("<item>{}</item>", igw_fields(p)))
                        .collect();
                    ec2_response(
                        action,
                        &format!("<internetGatewaySet>{inner}</internetGatewaySet>"),
                    )
                }
                Err(e) => store_error_to_xml(e),
            }
        }
        "AttachInternetGateway" => {
            let igw_id = body
                .get("InternetGatewayId")
                .and_then(|v| v.as_str())
                .unwrap_or("");
            let vpc_id = body.get("VpcId").and_then(|v| v.as_str()).unwrap_or("");
            if !igw_id.is_empty() {
                let _ = store::update_by_lookup(
                    db,
                    "ec2",
                    "internet-gateway",
                    igw_id,
                    serde_json::json!({"VpcId": vpc_id}),
                );
            }
            ec2_response(action, "<return>true</return>")
        }
        "DetachInternetGateway" => {
            ec2_response(action, "<return>true</return>")
        }

        // ---- Elastic IP (AllocateAddress / ReleaseAddress) ----
        "AllocateAddress" => {
            match store::create(db, "ec2", "elastic-ip", None, body.clone(), region) {
                Ok((_, props)) => {
                    let alloc_id = prop_str(&props, "ResourceId");
                    // Generate a fake public IP in the 127.x range for local use
                    let uuid_bytes = *Uuid::new_v4().as_bytes();
                    let public_ip = format!(
                        "127.{}.{}.{}",
                        uuid_bytes[0], uuid_bytes[1], uuid_bytes[2] % 254 + 1
                    );
                    // Store the public IP back
                    let _ = store::update_by_lookup(
                        db,
                        "ec2",
                        "elastic-ip",
                        &alloc_id,
                        serde_json::json!({"PublicIp": public_ip}),
                    );
                    ec2_response(
                        action,
                        &format!(
                            "<publicIp>{public_ip}</publicIp>\
                             <allocationId>{alloc_id}</allocationId>\
                             <domain>vpc</domain>"
                        ),
                    )
                }
                Err(e) => store_error_to_xml(e),
            }
        }
        "DescribeAddresses" => {
            let filters = extract_describe_filters(body);
            match store::list(db, "ec2", "elastic-ip", &filters) {
                Ok(items) => {
                    let inner: String = items
                        .iter()
                        .map(|p| {
                            let alloc_id = prop_str(p, "ResourceId");
                            let public_ip = prop_str_or(p, "PublicIp", "127.0.0.1");
                            let instance_id = prop_str_or(p, "InstanceId", "");
                            let assoc_id = prop_str_or(p, "AssociationId", "");
                            let mut extra = String::new();
                            if !instance_id.is_empty() {
                                extra.push_str(&format!("<instanceId>{instance_id}</instanceId>"));
                            }
                            if !assoc_id.is_empty() {
                                extra.push_str(&format!("<associationId>{assoc_id}</associationId>"));
                            }
                            format!(
                                "<item>\
                                   <publicIp>{public_ip}</publicIp>\
                                   <allocationId>{alloc_id}</allocationId>\
                                   <domain>vpc</domain>\
                                   {extra}\
                                   <tagSet/>\
                                 </item>"
                            )
                        })
                        .collect();
                    ec2_response(
                        action,
                        &format!("<addressesSet>{inner}</addressesSet>"),
                    )
                }
                Err(e) => store_error_to_xml(e),
            }
        }
        "AssociateAddress" => {
            let alloc_id = body.get("AllocationId").and_then(|v| v.as_str()).unwrap_or("");
            let instance_id = body.get("InstanceId").and_then(|v| v.as_str()).unwrap_or("");
            let assoc_id = format!("eipassoc-{}", &Uuid::new_v4().simple().to_string()[..17]);
            if !alloc_id.is_empty() {
                let _ = store::update_by_lookup(
                    db, "ec2", "elastic-ip", alloc_id,
                    serde_json::json!({
                        "AssociationId": assoc_id,
                        "InstanceId": instance_id,
                    }),
                );
            }
            ec2_response(action, &format!("<associationId>{assoc_id}</associationId>"))
        }
        "DisassociateAddress" => {
            // Find EIP by association ID and clear association fields
            let assoc_id = body.get("AssociationId").and_then(|v| v.as_str()).unwrap_or("");
            if !assoc_id.is_empty() {
                if let Ok(items) = store::list(db, "ec2", "elastic-ip", &HashMap::new()) {
                    for item in &items {
                        if item.get("AssociationId").and_then(|v| v.as_str()) == Some(assoc_id) {
                            let alloc_id = prop_str(item, "ResourceId");
                            let _ = store::update_by_lookup(
                                db, "ec2", "elastic-ip", &alloc_id,
                                serde_json::json!({
                                    "AssociationId": null,
                                    "InstanceId": null,
                                }),
                            );
                            break;
                        }
                    }
                }
            }
            ec2_response(action, "<return>true</return>")
        }
        "ReleaseAddress" => {
            let alloc_id = body
                .get("AllocationId")
                .and_then(|v| v.as_str())
                .unwrap_or("");
            if !alloc_id.is_empty() {
                let _ = store::delete_by_id(db, "ec2", "elastic-ip", alloc_id);
            }
            ec2_response(action, "<return>true</return>")
        }

        // ---- NAT Gateway ----
        "CreateNatGateway" => {
            match store::create(db, "ec2", "nat-gateway", None, body.clone(), region) {
                Ok((_, props)) => ec2_response(action, &nat_xml(&props)),
                Err(e) => store_error_to_xml(e),
            }
        }
        "DescribeNatGateways" => {
            let filters = extract_describe_filters(body);
            match store::list(db, "ec2", "nat-gateway", &filters) {
                Ok(items) => {
                    let inner: String = items
                        .iter()
                        .map(|p| format!("<item>{}</item>", nat_fields(p)))
                        .collect();
                    ec2_response(
                        action,
                        &format!("<natGatewaySet>{inner}</natGatewaySet>"),
                    )
                }
                Err(e) => store_error_to_xml(e),
            }
        }
        "DeleteNatGateway" => {
            let nat_id = body
                .get("NatGatewayId")
                .and_then(|v| v.as_str())
                .unwrap_or("");
            if !nat_id.is_empty() {
                let _ = store::delete_by_id(db, "ec2", "nat-gateway", nat_id);
            }
            ec2_response(action, "<return>true</return>")
        }

        // ---- Route Table ----
        "CreateRouteTable" => {
            match store::create(db, "ec2", "route-table", None, body.clone(), region) {
                Ok((_, props)) => ec2_response(action, &rtb_xml(&props)),
                Err(e) => store_error_to_xml(e),
            }
        }
        "DescribeRouteTables" => {
            let mut filters = extract_describe_filters(body);
            let assoc_id_filter = filters.remove("_assoc_id").unwrap_or_default();
            tracing::info!("DescribeRouteTables filters={:?} assoc_id_filter={assoc_id_filter}", filters);
            // If filtering by association ID, find which route table owns it
            // and add that route table's ID to the filters.
            if !assoc_id_filter.is_empty() {
                let assoc_lookup: HashMap<String, String> =
                    [("ResourceId".to_string(), assoc_id_filter.clone())].into_iter().collect();
                if let Ok(assocs) = store::list(db, "ec2", "route-table-association", &assoc_lookup) {
                    if let Some(a) = assocs.first() {
                        let rtb_id = prop_str(a, "RouteTableId");
                        if !rtb_id.is_empty() {
                            filters.insert("ResourceId".to_string(), rtb_id);
                        }
                    }
                }
            }
            match store::list(db, "ec2", "route-table", &filters) {
                Ok(items) => {
                    tracing::info!("DescribeRouteTables found {} route tables", items.len());
                    let inner: String = items
                        .iter()
                        .map(|p| {
                            let rtb_id = prop_str(p, "ResourceId");
                            // Fetch routes for this route table
                            let route_filter: HashMap<String, String> =
                                [("RouteTableId".to_string(), rtb_id.clone())].into_iter().collect();
                            let routes = store::list(db, "ec2", "route", &route_filter)
                                .unwrap_or_default();
                            tracing::info!("DescribeRouteTables rtb={rtb_id} routes={}", routes.len());
                            let route_xml: String = routes.iter().map(|r| {
                                let dest = prop_str_or(r, "DestinationCidrBlock", "0.0.0.0/0");
                                let gw = prop_str(r, "GatewayId");
                                let nat = prop_str(r, "NatGatewayId");
                                let target = if !gw.is_empty() {
                                    format!("<gatewayId>{gw}</gatewayId>")
                                } else if !nat.is_empty() {
                                    format!("<natGatewayId>{nat}</natGatewayId>")
                                } else {
                                    "<gatewayId>local</gatewayId>".to_string()
                                };
                                format!(
                                    "<item>\
                                     <destinationCidrBlock>{dest}</destinationCidrBlock>\
                                     {target}\
                                     <state>active</state>\
                                     <origin>CreateRoute</origin>\
                                     </item>"
                                )
                            }).collect();
                            let route_set = if route_xml.is_empty() {
                                "<routeSet/>".to_string()
                            } else {
                                format!("<routeSet>{route_xml}</routeSet>")
                            };
                            // Fetch associations for this route table
                            let assoc_filter: HashMap<String, String> =
                                [("RouteTableId".to_string(), rtb_id.clone())].into_iter().collect();
                            let assocs = store::list(db, "ec2", "route-table-association", &assoc_filter)
                                .unwrap_or_default();
                            let assoc_set = if assocs.is_empty() {
                                "<associationSet/>".to_string()
                            } else {
                                let assoc_xml: String = assocs.iter().map(|a| {
                                    let aid = prop_str(a, "ResourceId");
                                    let sid = prop_str(a, "SubnetId");
                                    let gid = prop_str(a, "GatewayId");
                                    let target_xml = if !sid.is_empty() {
                                        format!("<subnetId>{sid}</subnetId>")
                                    } else if !gid.is_empty() {
                                        format!("<gatewayId>{gid}</gatewayId>")
                                    } else {
                                        String::new()
                                    };
                                    format!(
                                        "<item>\
                                         <routeTableAssociationId>{aid}</routeTableAssociationId>\
                                         <routeTableId>{rtb_id}</routeTableId>\
                                         {target_xml}\
                                         <associationState><state>associated</state></associationState>\
                                         <main>false</main>\
                                         </item>"
                                    )
                                }).collect();
                                format!("<associationSet>{assoc_xml}</associationSet>")
                            };
                            format!("<item>{}</item>", rtb_fields_with_routes(p, &route_set, &assoc_set))
                        })
                        .collect();
                    ec2_response(
                        action,
                        &format!("<routeTableSet>{inner}</routeTableSet>"),
                    )
                }
                Err(e) => store_error_to_xml(e),
            }
        }
        "DeleteRouteTable" => {
            let rtb_id = body
                .get("RouteTableId")
                .and_then(|v| v.as_str())
                .unwrap_or("");
            if !rtb_id.is_empty() {
                let _ = store::delete_by_id(db, "ec2", "route-table", rtb_id);
            }
            ec2_response(action, "<return>true</return>")
        }

        // ---- Routes ----
        "CreateRoute" => {
            let rtb_id = body.get("RouteTableId").and_then(|v| v.as_str()).unwrap_or("?");
            let dest = body.get("DestinationCidrBlock").and_then(|v| v.as_str()).unwrap_or("?");
            tracing::info!("CreateRoute rtb={rtb_id} dest={dest}");
            let _ = store::create(db, "ec2", "route", None, body.clone(), region);
            ec2_response(action, "<return>true</return>")
        }
        "DeleteRoute" => {
            ec2_response(action, "<return>true</return>")
        }
        "ReplaceRoute" => {
            ec2_response(action, "<return>true</return>")
        }

        // ---- Route Table Associations ----
        "AssociateRouteTable" => {
            let assoc_id = format!(
                "rtbassoc-{}",
                &Uuid::new_v4().simple().to_string()[..8]
            );
            let rtb_id = body.get("RouteTableId").and_then(|v| v.as_str()).unwrap_or("").to_string();
            let subnet_id = body.get("SubnetId").and_then(|v| v.as_str()).unwrap_or("").to_string();
            let gateway_id = body.get("GatewayId").and_then(|v| v.as_str()).unwrap_or("").to_string();
            let assoc_body = serde_json::json!({
                "RouteTableId": rtb_id,
                "SubnetId": subnet_id,
                "GatewayId": gateway_id,
            });
            let _ = store::create(db, "ec2", "route-table-association", Some(&assoc_id), assoc_body, region);
            ec2_response(
                action,
                &format!("<associationId>{assoc_id}</associationId>"),
            )
        }
        "DisassociateRouteTable" => {
            let assoc_id = body.get("AssociationId").and_then(|v| v.as_str()).unwrap_or("");
            if !assoc_id.is_empty() {
                let _ = store::delete_by_id(db, "ec2", "route-table-association", assoc_id);
            }
            ec2_response(action, "<return>true</return>")
        }
        "ReplaceRouteTableAssociation" => {
            let assoc_id = format!(
                "rtbassoc-{}",
                &Uuid::new_v4().simple().to_string()[..8]
            );
            ec2_response(
                action,
                &format!("<newAssociationId>{assoc_id}</newAssociationId>"),
            )
        }

        // ---- Network ACL ----
        "CreateNetworkAcl" => {
            match store::create(db, "ec2", "network-acl", None, body.clone(), region) {
                Ok((_, props)) => ec2_response(action, &nacl_xml(&props)),
                Err(e) => store_error_to_xml(e),
            }
        }
        "DescribeNetworkAcls" => {
            let filters = extract_describe_filters(body);
            match store::list(db, "ec2", "network-acl", &filters) {
                Ok(items) => {
                    let inner: String = items
                        .iter()
                        .map(|p| format!("<item>{}</item>", nacl_fields(p)))
                        .collect();
                    ec2_response(
                        action,
                        &format!("<networkAclSet>{inner}</networkAclSet>"),
                    )
                }
                Err(e) => store_error_to_xml(e),
            }
        }
        "DeleteNetworkAcl" => {
            let nacl_id = body
                .get("NetworkAclId")
                .and_then(|v| v.as_str())
                .unwrap_or("");
            if !nacl_id.is_empty() {
                let _ = store::delete_by_id(db, "ec2", "network-acl", nacl_id);
            }
            ec2_response(action, "<return>true</return>")
        }
        "CreateNetworkAclEntry" | "DeleteNetworkAclEntry" | "ReplaceNetworkAclEntry" => {
            ec2_response(action, "<return>true</return>")
        }

        // ---- Flow Logs ----
        "CreateFlowLogs" => {
            match store::create(db, "ec2", "flow-log", None, body.clone(), region) {
                Ok((_, props)) => {
                    let flow_log_id = prop_str(&props, "ResourceId");
                    ec2_response(
                        action,
                        &format!(
                            "<FlowLogIdSet><item>{flow_log_id}</item></FlowLogIdSet>\
                             <unsuccessful/>"
                        ),
                    )
                }
                Err(e) => store_error_to_xml(e),
            }
        }
        "DescribeFlowLogs" => {
            let filters = extract_describe_filters(body);
            match store::list(db, "ec2", "flow-log", &filters) {
                Ok(items) => {
                    let inner: String = items
                        .iter()
                        .map(|p| {
                            let fl_id = prop_str(p, "ResourceId");
                            format!(
                                "<item><flowLogId>{fl_id}</flowLogId>\
                                 <flowLogStatus>ACTIVE</flowLogStatus></item>"
                            )
                        })
                        .collect();
                    ec2_response(
                        action,
                        &format!("<flowLogSet>{inner}</flowLogSet>"),
                    )
                }
                Err(e) => store_error_to_xml(e),
            }
        }
        "DeleteFlowLogs" => {
            ec2_response(action, "<unsuccessful/>")
        }

        // ---- Tags ----
        "CreateTags" | "DeleteTags" => {
            ec2_response(action, "<return>true</return>")
        }
        "DescribeTags" => {
            ec2_response(action, "<tagSet/>")
        }

        // ---- Generic Delete / Modify fallback for EC2 ----
        _ if operation == "Delete" || operation == "Release" => {
            let id = extract_identifier_from_body(body, resource_type);
            if let Some(identifier) = id {
                let _ = store::delete_by_id(db, "ec2", resource_type, &identifier);
            }
            ec2_response(action, "<return>true</return>")
        }
        _ if operation == "Modify" || operation == "Update" || operation == "Attach"
            || operation == "Detach" || operation == "Associate"
            || operation == "Disassociate" =>
        {
            ec2_response(action, "<return>true</return>")
        }
        // ---- AMIs ----
        "DescribeImages" => {
            // The Pulumi/Terraform provider calls DescribeImages to look up
            // AMIs by filter.  Return a single dummy AMI so `getAmi` resolves
            // instead of erroring out and blocking dependent resources.
            // Use the requested AMI ID if provided, otherwise generate one.
            let ami_id = body.get("ImageId.1")
                .and_then(|v| v.as_str())
                .map(|s| s.to_string())
                .unwrap_or_else(|| format!("ami-{}", &Uuid::new_v4().simple().to_string()[..12]));
            let xml = format!(
                "<imagesSet>\
                   <item>\
                     <imageId>{ami_id}</imageId>\
                     <imageState>available</imageState>\
                     <imageOwnerId>000000000000</imageOwnerId>\
                     <isPublic>false</isPublic>\
                     <architecture>x86_64</architecture>\
                     <imageType>machine</imageType>\
                     <name>aws-local-dummy-ami</name>\
                     <description>Dummy AMI for local testing</description>\
                     <rootDeviceType>ebs</rootDeviceType>\
                     <rootDeviceName>/dev/xvda</rootDeviceName>\
                     <virtualizationType>hvm</virtualizationType>\
                     <hypervisor>xen</hypervisor>\
                     <enaSupport>true</enaSupport>\
                     <blockDeviceMapping>\
                       <item>\
                         <deviceName>/dev/xvda</deviceName>\
                         <ebs>\
                           <snapshotId>snap-00000000000000000</snapshotId>\
                           <volumeSize>8</volumeSize>\
                           <deleteOnTermination>true</deleteOnTermination>\
                           <volumeType>gp3</volumeType>\
                           <encrypted>false</encrypted>\
                         </ebs>\
                       </item>\
                     </blockDeviceMapping>\
                     <creationDate>2024-01-01T00:00:00.000Z</creationDate>\
                   </item>\
                 </imagesSet>"
            );
            ec2_response(action, &xml)
        }

        // ---- EC2 Instances ----
        "RunInstances" => {
            let instance_id = format!("i-{}", &Uuid::new_v4().simple().to_string()[..17]);
            let ami_id = body.get("ImageId").and_then(|v| v.as_str()).unwrap_or("ami-00000000000000000");
            let instance_type = body.get("InstanceType").and_then(|v| v.as_str()).unwrap_or("t3.micro");
            let subnet_id = body.get("SubnetId").and_then(|v| v.as_str()).unwrap_or("");
            let key_name = body.get("KeyName").and_then(|v| v.as_str()).unwrap_or("");
            let iam_profile = body.get("IamInstanceProfile")
                .and_then(|v| v.get("Arn").or(v.get("Name")))
                .and_then(|v| v.as_str())
                .unwrap_or("");

            // Extract security group IDs
            let sg_ids: Vec<&str> = body.get("SecurityGroupId")
                .and_then(|v| v.as_array())
                .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
                .or_else(|| {
                    // Try numbered keys: SecurityGroupId.1, SecurityGroupId.2, ...
                    let mut ids = Vec::new();
                    for i in 1..=10 {
                        if let Some(sg) = body.get(&format!("SecurityGroupId.{i}")).and_then(|v| v.as_str()) {
                            ids.push(sg);
                        }
                    }
                    if ids.is_empty() { None } else { Some(ids) }
                })
                .unwrap_or_default();

            // Look up the VPC from the subnet if we have one
            let vpc_id = if !subnet_id.is_empty() {
                store::lookup(db, "ec2", "subnet", subnet_id)
                    .ok()
                    .and_then(|p| p.get("VpcId").and_then(|v| v.as_str()).map(|s| s.to_string()))
                    .unwrap_or_default()
            } else { String::new() };

            // Build security group XML
            let sg_xml: String = sg_ids.iter().map(|sg| {
                format!("<item><groupId>{sg}</groupId><groupName>{sg}</groupName></item>")
            }).collect();

            // Build network interface XML
            let eni_id = format!("eni-{}", &Uuid::new_v4().simple().to_string()[..17]);
            let private_ip = "10.0.0.100";

            let mut props = body.clone();
            if let Some(obj) = props.as_object_mut() {
                obj.insert("InstanceId".to_string(), Value::String(instance_id.clone()));
                obj.insert("InstanceState".to_string(), Value::String("running".into()));
                obj.insert("PrivateIpAddress".to_string(), Value::String(private_ip.into()));
                obj.insert("VpcId".to_string(), Value::String(vpc_id.clone()));
            }
            let _ = store::create(db, "ec2", "instance", Some(&instance_id), props, region);

            let instance_xml = instance_xml_item(
                &instance_id, ami_id, instance_type, subnet_id, &vpc_id,
                private_ip, key_name, iam_profile, &eni_id, &sg_xml, region,
            );

            let reservation_id = format!("r-{}", &Uuid::new_v4().simple().to_string()[..17]);
            ec2_response(action, &format!(
                "<reservationId>{reservation_id}</reservationId>\
                 <ownerId>000000000000</ownerId>\
                 <groupSet/>\
                 <instancesSet><item>{instance_xml}</item></instancesSet>"
            ))
        }
        "DescribeInstances" => {
            // Filters: InstanceId.N or Filter.N
            let filters = extract_describe_filters(body);
            let instance_ids: Vec<&str> = (1..=50)
                .filter_map(|i| body.get(&format!("InstanceId.{i}")).and_then(|v| v.as_str()))
                .collect();

            tracing::info!(
                instance_ids = ?instance_ids,
                filter_count = filters.len(),
                body_keys = ?body.as_object().map(|o| o.keys().collect::<Vec<_>>()),
                "DescribeInstances"
            );

            let items = if !instance_ids.is_empty() {
                let mut result = Vec::new();
                for iid in &instance_ids {
                    match store::lookup(db, "ec2", "instance", iid) {
                        Ok(p) => result.push(p),
                        Err(e) => tracing::warn!(iid, error = ?e, "DescribeInstances lookup failed"),
                    }
                }
                result
            } else {
                store::list(db, "ec2", "instance", &filters).unwrap_or_default()
            };

            let reservations_xml: String = items.iter().map(|p| {
                let iid = prop_str(p, "InstanceId");
                let ami = prop_str(p, "ImageId");
                let itype = p.get("InstanceType").and_then(|v| v.as_str()).unwrap_or("t3.micro");
                let subnet = p.get("SubnetId").and_then(|v| v.as_str()).unwrap_or("");
                let vpc = p.get("VpcId").and_then(|v| v.as_str()).unwrap_or("");
                let pip = p.get("PrivateIpAddress").and_then(|v| v.as_str()).unwrap_or("10.0.0.100");
                let key = p.get("KeyName").and_then(|v| v.as_str()).unwrap_or("");
                let iam = p.get("IamInstanceProfile")
                    .and_then(|v| v.get("Arn").or(v.get("Name")))
                    .and_then(|v| v.as_str())
                    .unwrap_or("");
                let eni = format!("eni-{}", &Uuid::new_v4().simple().to_string()[..17]);
                let rid = format!("r-{}", &Uuid::new_v4().simple().to_string()[..17]);
                let inst = instance_xml_item(&iid, &ami, itype, subnet, vpc, pip, key, iam, &eni, "", region);
                format!(
                    "<item>\
                       <reservationId>{rid}</reservationId>\
                       <ownerId>000000000000</ownerId>\
                       <groupSet/>\
                       <instancesSet><item>{inst}</item></instancesSet>\
                     </item>"
                )
            }).collect();

            ec2_response(action, &format!("<reservationSet>{reservations_xml}</reservationSet>"))
        }
        "DescribeInstanceTypes" => {
            // SDK sometimes calls this to validate instance type. Return the
            // requested type as valid.
            let itype = body.get("InstanceType.1").and_then(|v| v.as_str()).unwrap_or("t3.micro");
            ec2_response(action, &format!(
                "<instanceTypeSet>\
                   <item>\
                     <instanceType>{itype}</instanceType>\
                     <currentGeneration>true</currentGeneration>\
                     <freeTierEligible>false</freeTierEligible>\
                     <supportedUsageClasses><item>on-demand</item></supportedUsageClasses>\
                     <supportedRootDeviceTypes><item>ebs</item></supportedRootDeviceTypes>\
                     <supportedVirtualizationTypes><item>hvm</item></supportedVirtualizationTypes>\
                     <bareMetal>false</bareMetal>\
                     <hypervisor>nitro</hypervisor>\
                     <processorInfo><supportedArchitectures><item>x86_64</item></supportedArchitectures></processorInfo>\
                     <vCpuInfo><defaultVCpus>2</defaultVCpus></vCpuInfo>\
                     <memoryInfo><sizeInMiB>4096</sizeInMiB></memoryInfo>\
                     <networkInfo>\
                       <networkPerformance>Up to 5 Gigabit</networkPerformance>\
                       <maximumNetworkInterfaces>3</maximumNetworkInterfaces>\
                       <ipv4AddressesPerInterface>6</ipv4AddressesPerInterface>\
                     </networkInfo>\
                   </item>\
                 </instanceTypeSet>"
            ))
        }
        "TerminateInstances" => {
            let mut terminated = Vec::new();
            for i in 1..=50 {
                if let Some(iid) = body.get(&format!("InstanceId.{i}")).and_then(|v| v.as_str()) {
                    let _ = store::delete_by_id(db, "ec2", "instance", iid);
                    terminated.push(format!(
                        "<item>\
                           <instanceId>{iid}</instanceId>\
                           <currentState><code>48</code><name>terminated</name></currentState>\
                           <previousState><code>16</code><name>running</name></previousState>\
                         </item>"
                    ));
                }
            }
            ec2_response(action, &format!("<instancesSet>{}</instancesSet>", terminated.join("")))
        }
        "DescribeInstanceAttribute" => {
            let iid = body.get("InstanceId").and_then(|v| v.as_str()).unwrap_or("");
            let attr = body.get("Attribute").and_then(|v| v.as_str()).unwrap_or("");
            let value_xml = match attr {
                "disableApiTermination" => "<disableApiTermination><value>false</value></disableApiTermination>",
                "disableApiStop" => "<disableApiStop><value>false</value></disableApiStop>",
                "instanceInitiatedShutdownBehavior" => "<instanceInitiatedShutdownBehavior><value>stop</value></instanceInitiatedShutdownBehavior>",
                "userData" => "<userData><value/></userData>",
                _ => "",
            };
            ec2_response(action, &format!("<instanceId>{iid}</instanceId>{value_xml}"))
        }
        "GetInstanceUefiData" => {
            ec2_response(action, "<instanceId/><uefiData/>")
        }
        "MonitorInstances" | "UnmonitorInstances" => {
            // Return monitoring state for each requested instance
            let mut items = String::new();
            for i in 1..=50 {
                if let Some(iid) = body.get(&format!("InstanceId.{i}")).and_then(|v| v.as_str()) {
                    let state = if action == "MonitorInstances" { "enabled" } else { "disabled" };
                    items.push_str(&format!(
                        "<item><instanceId>{iid}</instanceId><monitoring><state>{state}</state></monitoring></item>"
                    ));
                } else { break; }
            }
            ec2_response(action, &format!("<instancesSet>{items}</instancesSet>"))
        }
        "ModifyInstanceAttribute" => {
            ec2_response(action, "<return>true</return>")
        }
        "DescribeInstanceCreditSpecifications" => {
            ec2_response(action, "<instanceCreditSpecificationSet/>")
        }

        _ if operation == "Describe" || operation == "Get" => {
            // Generic describe: return items in a generic set wrapper
            let filters = extract_describe_filters(body);
            match store::list(db, "ec2", resource_type, &filters) {
                Ok(items) => {
                    let inner: String = items
                        .iter()
                        .map(|p| {
                            let id = prop_str(p, "ResourceId");
                            format!("<item><resourceId>{id}</resourceId></item>")
                        })
                        .collect();
                    ec2_response(action, &format!("<resourceSet>{inner}</resourceSet>"))
                }
                Err(e) => store_error_to_xml(e),
            }
        }
        _ if operation == "Create" || operation == "Allocate" => {
            // Generic create fallback
            match store::create(db, "ec2", resource_type, None, body.clone(), region) {
                Ok((_, props)) => {
                    let id = prop_str(&props, "ResourceId");
                    ec2_response(action, &format!("<resourceId>{id}</resourceId>"))
                }
                Err(e) => store_error_to_xml(e),
            }
        }
        _ => {
            tracing::warn!("EC2 unhandled action: {action}");
            ec2_response(action, "<return>true</return>")
        }
    }
}

/// Extract a resource identifier from the body for EC2 delete operations.
fn extract_identifier_from_body(body: &Value, resource_type: &str) -> Option<String> {
    // Try the standard ID key pattern for EC2 resources
    let keys: &[&str] = match resource_type {
        "vpc" => &["VpcId"],
        "subnet" => &["SubnetId"],
        "security-group" => &["GroupId"],
        "internet-gateway" => &["InternetGatewayId"],
        "nat-gateway" => &["NatGatewayId"],
        "route-table" => &["RouteTableId"],
        "elastic-ip" => &["AllocationId"],
        "network-acl" => &["NetworkAclId"],
        "flow-log" => &["FlowLogId"],
        "instance" => &["InstanceId"],
        _ => &["Id", "ResourceId"],
    };
    for key in keys {
        if let Some(v) = body.get(*key).and_then(|v| v.as_str()) {
            return Some(v.to_string());
        }
    }
    None
}

// --- CloudWatch Monitoring ---

/// Handle CloudWatch Monitoring operations.  These arrive via ?Action= form
/// encoding but the AWS SDK for Go v2 expects JSON responses with the
/// smithy-protocol content type.
fn dispatch_cloudwatch(
    action: &str,
    operation: &str,
    resource_type: &str,
    body: &Value,
    db: &DbPool,
    region: &str,
) -> Response {
    let json_response = |val: &Value| -> Response {
        (
            StatusCode::OK,
            [("content-type", "application/x-amz-json-1.0")],
            serde_json::to_string(val).unwrap_or_else(|_| "{}".to_string()),
        )
            .into_response()
    };

    match action {
        "PutDashboard" => {
            let name = body
                .get("DashboardName")
                .and_then(|v| v.as_str())
                .unwrap_or("unknown");
            let _ = store::create(
                db,
                "cloudwatch",
                "dashboard",
                Some(name),
                body.clone(),
                region,
            );
            json_response(&serde_json::json!({
                "DashboardValidationMessages": []
            }))
        }
        "GetDashboard" | "DescribeDashboard" => {
            let name = body
                .get("DashboardName")
                .and_then(|v| v.as_str())
                .unwrap_or("");
            match store::lookup(db, "cloudwatch", "dashboard", name) {
                Ok(props) => json_response(&props),
                Err(_) => (
                    StatusCode::NOT_FOUND,
                    [("content-type", "application/x-amz-json-1.0")],
                    "{\"__type\":\"ResourceNotFound\"}".to_string(),
                )
                    .into_response(),
            }
        }
        "DeleteDashboards" | "DeleteDashboard" => {
            let name = body
                .get("DashboardName")
                .and_then(|v| v.as_str())
                .unwrap_or("");
            if !name.is_empty() {
                let _ = store::delete_by_id(db, "cloudwatch", "dashboard", name);
            }
            json_response(&serde_json::json!({}))
        }
        "PutMetricAlarm" => {
            let name = body
                .get("AlarmName")
                .and_then(|v| v.as_str())
                .unwrap_or("unknown");
            // Store the alarm with AlarmArn and other required fields
            let arn = format!("arn:aws:cloudwatch:{region}:000000000000:alarm:{name}");
            let mut alarm_data = body.clone();
            if let Some(obj) = alarm_data.as_object_mut() {
                obj.insert("AlarmArn".to_string(), serde_json::json!(arn));
                obj.insert("AlarmName".to_string(), serde_json::json!(name));
                obj.insert("StateValue".to_string(), serde_json::json!("OK"));
                obj.insert("StateReason".to_string(), serde_json::json!("Unchecked: Initial alarm creation"));
                obj.insert("ActionsEnabled".to_string(), serde_json::json!(true));
                obj.insert(
                    "AlarmConfigurationUpdatedTimestamp".to_string(),
                    serde_json::json!(chrono::Utc::now().to_rfc3339()),
                );
                obj.insert(
                    "StateUpdatedTimestamp".to_string(),
                    serde_json::json!(chrono::Utc::now().to_rfc3339()),
                );
            }
            let _ = store::create(
                db,
                "cloudwatch",
                "alarm",
                Some(name),
                alarm_data,
                region,
            );
            json_response(&serde_json::json!({}))
        }
        "DescribeAlarms" => {
            // Filter by AlarmNames if provided
            let filter_names: Vec<&str> = body
                .get("AlarmNames")
                .and_then(|v| v.as_array())
                .map(|a| a.iter().filter_map(|v| v.as_str()).collect())
                .unwrap_or_default();
            let alarms =
                store::list(db, "cloudwatch", "alarm", &HashMap::new()).unwrap_or_default();
            let filtered: Vec<&Value> = if filter_names.is_empty() {
                alarms.iter().collect()
            } else {
                alarms
                    .iter()
                    .filter(|a| {
                        a.get("AlarmName")
                            .and_then(|n| n.as_str())
                            .map(|n| filter_names.contains(&n))
                            .unwrap_or(false)
                    })
                    .collect()
            };
            json_response(&serde_json::json!({
                "MetricAlarms": filtered,
                "CompositeAlarms": []
            }))
        }
        _ => {
            // Generic: store and return empty success
            if operation == "Create" || operation == "Put" {
                let _ = store::create(db, "cloudwatch", resource_type, None, body.clone(), region);
            }
            json_response(&serde_json::json!({}))
        }
    }
}

/// Bridge from dispatch_json_action to the CloudWatch monitoring dispatch.
/// The JSON-protocol CloudWatch target uses "monitoring.PutMetricAlarm" etc.
fn dispatch_cloudwatch_monitoring(
    operation: &str,
    body: Value,
    db: &DbPool,
    region: &str,
) -> Response {
    // The dispatch_cloudwatch function expects the operation name,
    // an "action" (Describe/Put/Create), and a resource type.
    // For the JSON-protocol path, just pass the operation directly.
    dispatch_cloudwatch(operation, operation, "monitoring", &body, db, region)
}

/// Extract filters from EC2 Describe call body.
///
/// The AWS EC2 query protocol sends resource IDs as numbered params
/// (e.g. `VpcId.1=vpc-xxx`) and filters as `Filter.N.Name` /
/// `Filter.N.Value.M`.  We parse the numbered ID params and map them
/// to the `ResourceId` property key used by our store.
fn extract_describe_filters(body: &Value) -> HashMap<String, String> {
    let mut filters = HashMap::new();
    let obj = match body.as_object() {
        Some(o) => o,
        None => return filters,
    };

    // Collect numbered ID params like VpcId.1, SubnetId.1, GroupId.1, etc.
    // These are direct resource ID filters — map to our ResourceId property.
    let id_prefixes = [
        "VpcId", "SubnetId", "GroupId", "InternetGatewayId",
        "NatGatewayId", "RouteTableId", "AllocationId",
        "NetworkAclId", "InstanceId",
    ];
    for prefix in &id_prefixes {
        for (key, val) in obj {
            if key == *prefix || key.starts_with(&format!("{prefix}.")) {
                if let Some(v) = val.as_str() {
                    filters.insert("ResourceId".to_string(), v.to_string());
                    return filters; // single-ID lookup
                }
            }
        }
    }

    // Handle Filter.N.Name / Filter.N.Value.1 patterns.
    // Maps well-known filter names to stored property names.
    for i in 1..=10 {
        let name_key = format!("Filter.{i}.Name");
        let val_key = format!("Filter.{i}.Value.1");
        if let (Some(name), Some(val)) = (
            obj.get(&name_key).and_then(|v| v.as_str()),
            obj.get(&val_key).and_then(|v| v.as_str()),
        ) {
            match name {
                "vpc-id" => { filters.insert("VpcId".to_string(), val.to_string()); }
                "subnet-id" => { filters.insert("SubnetId".to_string(), val.to_string()); }
                "route-table-id" => { filters.insert("ResourceId".to_string(), val.to_string()); }
                "internet-gateway-id" => { filters.insert("ResourceId".to_string(), val.to_string()); }
                "nat-gateway-id" | "natgateway-id" => { filters.insert("ResourceId".to_string(), val.to_string()); }
                "network-acl-id" => { filters.insert("ResourceId".to_string(), val.to_string()); }
                "group-id" => { filters.insert("GroupId".to_string(), val.to_string()); }
                "allocation-id" => { filters.insert("ResourceId".to_string(), val.to_string()); }
                "instance-id" => { filters.insert("InstanceId".to_string(), val.to_string()); }
                "association-id" => { filters.insert("AssociationId".to_string(), val.to_string()); }
                // association filters — store as metadata for caller to interpret
                "association.route-table-association-id" => {
                    filters.insert("_assoc_id".to_string(), val.to_string());
                }
                _ => {}
            }
        }
    }

    filters
}

// --- KMS (TrentService) ---

/// Handle KMS operations with the KeyMetadata envelope the AWS SDKs
/// expect.  Without this wrapper the Go SDK nil-dereferences because
/// it tries to read `resp.KeyMetadata.KeyId`.
fn dispatch_kms(target: &str, body: Value, db: &DbPool, region: &str) -> Response {
    let op = target.rsplit('.').next().unwrap_or(target);
    match op {
        "CreateKey" => {
            let description = body
                .get("Description")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string();
            let key_usage = body
                .get("KeyUsage")
                .and_then(|v| v.as_str())
                .unwrap_or("ENCRYPT_DECRYPT")
                .to_string();

            match store::create(db, "kms", "key", None, body, region) {
                Ok((_, props)) => {
                    let meta = build_key_metadata(&props, region, &description, &key_usage);
                    formats::json::success_response(&serde_json::json!({ "KeyMetadata": meta }))
                }
                Err(e) => store_error_to_json(e),
            }
        }
        "DescribeKey" => {
            let key_id = body.get("KeyId").and_then(|v| v.as_str()).unwrap_or("");
            match store::lookup(db, "kms", "key", key_id) {
                Ok(props) => {
                    let description = props
                        .get("Description")
                        .and_then(|v| v.as_str())
                        .unwrap_or("")
                        .to_string();
                    let key_usage = props
                        .get("KeyUsage")
                        .and_then(|v| v.as_str())
                        .unwrap_or("ENCRYPT_DECRYPT")
                        .to_string();
                    let meta = build_key_metadata(&props, region, &description, &key_usage);
                    formats::json::success_response(&serde_json::json!({ "KeyMetadata": meta }))
                }
                Err(e) => store_error_to_json(e),
            }
        }
        "ListKeys" => match store::list(db, "kms", "key", &HashMap::new()) {
            Ok(items) => {
                let keys: Vec<Value> = items
                    .iter()
                    .map(|v| {
                        let kid = v
                            .get("ResourceId")
                            .and_then(|v| v.as_str())
                            .unwrap_or("");
                        let arn = v.get("Arn").and_then(|v| v.as_str()).unwrap_or("");
                        serde_json::json!({ "KeyId": kid, "KeyArn": arn })
                    })
                    .collect();
                formats::json::success_response(
                    &serde_json::json!({ "Keys": keys, "Truncated": false }),
                )
            }
            Err(e) => store_error_to_json(e),
        },
        "TagResource" => {
            // Store tags on the key resource
            let key_id = body.get("KeyId").and_then(|v| v.as_str()).unwrap_or("");
            let tags = body.get("Tags").cloned().unwrap_or(Value::Array(vec![]));
            if !key_id.is_empty() {
                let _ = store::update_by_lookup(db, "kms", "key", key_id, serde_json::json!({"Tags": tags}));
            }
            formats::json::success_response(&serde_json::json!({}))
        }
        "CreateAlias" => {
            let alias_name = body.get("AliasName").and_then(|v| v.as_str()).unwrap_or("");
            let target_key_id = body.get("TargetKeyId").and_then(|v| v.as_str()).unwrap_or("");
            // Resolve the target key to get its ARN
            let target_key_arn = store::lookup(db, "kms", "key", target_key_id)
                .ok()
                .and_then(|p| p.get("Arn").and_then(|v| v.as_str()).map(|s| s.to_string()))
                .unwrap_or_else(|| format!("arn:aws:kms:{region}:000000000000:key/{target_key_id}"));
            let alias_arn = format!("arn:aws:kms:{region}:000000000000:{alias_name}");
            let props = serde_json::json!({
                "AliasName": alias_name,
                "AliasArn": alias_arn,
                "TargetKeyId": target_key_id,
                "TargetKeyArn": target_key_arn,
            });
            let _ = store::create(db, "kms", "alias", Some(alias_name), props, region);
            formats::json::success_response(&serde_json::json!({}))
        }
        "DeleteAlias" => {
            let alias_name = body.get("AliasName").and_then(|v| v.as_str()).unwrap_or("");
            let _ = store::delete_by_id(db, "kms", "alias", alias_name);
            formats::json::success_response(&serde_json::json!({}))
        }
        "UpdateAlias" => {
            let alias_name = body.get("AliasName").and_then(|v| v.as_str()).unwrap_or("");
            let target_key_id = body.get("TargetKeyId").and_then(|v| v.as_str()).unwrap_or("");
            let _ = store::update_by_lookup(db, "kms", "alias", alias_name,
                serde_json::json!({"TargetKeyId": target_key_id}));
            formats::json::success_response(&serde_json::json!({}))
        }
        "ListAliases" => {
            let key_id_filter = body.get("KeyId").and_then(|v| v.as_str()).unwrap_or("");
            match store::list(db, "kms", "alias", &HashMap::new()) {
                Ok(items) => {
                    let aliases: Vec<Value> = items
                        .iter()
                        .filter(|v| {
                            if key_id_filter.is_empty() {
                                return true;
                            }
                            // Match by TargetKeyId or TargetKeyArn
                            let tid = v.get("TargetKeyId").and_then(|v| v.as_str()).unwrap_or("");
                            let tarn = v.get("TargetKeyArn").and_then(|v| v.as_str()).unwrap_or("");
                            tid == key_id_filter || tarn == key_id_filter
                        })
                        .map(|v| {
                            serde_json::json!({
                                "AliasName": v.get("AliasName").and_then(|v| v.as_str()).unwrap_or(""),
                                "AliasArn": v.get("AliasArn").and_then(|v| v.as_str()).unwrap_or(""),
                                "TargetKeyId": v.get("TargetKeyId").and_then(|v| v.as_str()).unwrap_or(""),
                            })
                        })
                        .collect();
                    formats::json::success_response(
                        &serde_json::json!({ "Aliases": aliases, "Truncated": false }),
                    )
                }
                Err(e) => store_error_to_json(e),
            }
        }
        "EnableKey" | "DisableKey" | "ScheduleKeyDeletion"
        | "CancelKeyDeletion" | "UntagResource" | "UpdateKeyDescription"
        | "EnableKeyRotation" | "DisableKeyRotation" => {
            formats::json::success_response(&serde_json::json!({}))
        }
        "GetKeyRotationStatus" => {
            formats::json::success_response(
                &serde_json::json!({ "KeyRotationEnabled": true }),
            )
        }
        "GetKeyPolicy" => {
            // Return stored policy if available, otherwise default
            let key_id = body.get("KeyId").and_then(|v| v.as_str()).unwrap_or("");
            let default_policy = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"arn:aws:iam::000000000000:root\"},\"Action\":\"kms:*\",\"Resource\":\"*\"}]}";
            let policy = if !key_id.is_empty() {
                store::lookup(db, "kms", "key", key_id)
                    .ok()
                    .and_then(|p| p.get("Policy").and_then(|v| v.as_str()).map(|s| s.to_string()))
                    .unwrap_or_else(|| default_policy.to_string())
            } else {
                default_policy.to_string()
            };
            formats::json::success_response(&serde_json::json!({ "Policy": policy }))
        }
        "PutKeyPolicy" => {
            // Store the policy on the key
            let key_id = body.get("KeyId").and_then(|v| v.as_str()).unwrap_or("");
            let policy = body.get("Policy").and_then(|v| v.as_str()).unwrap_or("{}");
            if !key_id.is_empty() {
                let _ = store::update_by_lookup(db, "kms", "key", key_id,
                    serde_json::json!({"Policy": policy}));
            }
            formats::json::success_response(&serde_json::json!({}))
        }
        "ListResourceTags" => {
            // Try to find the key and return its tags
            let key_id = body.get("KeyId").and_then(|v| v.as_str()).unwrap_or("");
            let tags = if !key_id.is_empty() {
                store::lookup(db, "kms", "key", key_id)
                    .ok()
                    .and_then(|props| props.get("Tags").cloned())
                    .unwrap_or(Value::Array(vec![]))
            } else {
                Value::Array(vec![])
            };
            formats::json::success_response(
                &serde_json::json!({ "Tags": tags, "Truncated": false }),
            )
        }
        _ => formats::json::error_response(
            StatusCode::BAD_REQUEST,
            "UnsupportedOperation",
            &format!("KMS operation not supported: {op}"),
        ),
    }
}

/// Build a KeyMetadata object with all fields the AWS SDKs expect.
fn build_key_metadata(
    props: &Value,
    region: &str,
    description: &str,
    key_usage: &str,
) -> Value {
    let key_id = props
        .get("ResourceId")
        .and_then(|v| v.as_str())
        .unwrap_or_else(|| {
            props
                .get("KeyId")
                .and_then(|v| v.as_str())
                .unwrap_or("unknown")
        });
    let arn = props.get("Arn").and_then(|v| v.as_str()).unwrap_or("");
    // Real AWS uses arn:aws:kms:REGION:ACCOUNT:key/KEY_ID — fall back to
    // a synthetic one if the store-generated ARN doesn't look right.
    let arn = if arn.contains(":key/") {
        arn.to_string()
    } else {
        format!("arn:aws:kms:{region}:000000000000:key/{key_id}")
    };
    let creation_epoch = Utc::now().timestamp() as f64;

    serde_json::json!({
        "AWSAccountId": "000000000000",
        "KeyId": key_id,
        "Arn": arn,
        "CreationDate": creation_epoch,
        "Enabled": true,
        "Description": description,
        "KeyUsage": key_usage,
        "KeyState": "Enabled",
        "Origin": "AWS_KMS",
        "KeyManager": "CUSTOMER",
        "CustomerMasterKeySpec": "SYMMETRIC_DEFAULT",
        "KeySpec": "SYMMETRIC_DEFAULT",
        "EncryptionAlgorithms": ["SYMMETRIC_DEFAULT"],
        "MultiRegion": false
    })
}

fn dispatch_logs(target: &str, body: Value, db: &DbPool, region: &str) -> Response {
    let op = target.rsplit('.').next().unwrap_or(target);
    match op {
        "CreateLogGroup" => {
            let name = body.get("logGroupName").and_then(|v| v.as_str()).unwrap_or("unknown").to_string();
            let _ = store::create(db, "logs", "log-group", Some(&name), body, region);
            // CloudWatch Logs CreateLogGroup returns empty 200
            (StatusCode::OK, "").into_response()
        }
        "DeleteLogGroup" => {
            let name = body.get("logGroupName").and_then(|v| v.as_str()).unwrap_or("");
            let _ = store::delete_by_id(db, "logs", "log-group", name);
            (StatusCode::OK, "").into_response()
        }
        "DescribeLogGroups" => {
            let prefix = body.get("logGroupNamePrefix").and_then(|v| v.as_str());
            let groups = store::list(db, "logs", "log-group", &HashMap::new()).unwrap_or_default();
            let filtered: Vec<Value> = groups.into_iter().filter(|g| {
                match prefix {
                    Some(p) => g.get("logGroupName").and_then(|v| v.as_str()).is_some_and(|n| n.starts_with(p)),
                    None => true,
                }
            }).map(|mut g| {
                // Ensure required fields exist
                if let Some(obj) = g.as_object_mut() {
                    if !obj.contains_key("arn") {
                        let name = obj.get("logGroupName").and_then(|v| v.as_str()).unwrap_or("");
                        obj.insert("arn".to_string(), Value::String(
                            format!("arn:aws:logs:{region}:000000000000:log-group:{name}:*")
                        ));
                    }
                    if !obj.contains_key("creationTime") {
                        obj.insert("creationTime".to_string(), Value::Number(
                            serde_json::Number::from(chrono::Utc::now().timestamp())
                        ));
                    }
                    if !obj.contains_key("retentionInDays") {
                        obj.insert("retentionInDays".to_string(), Value::Null);
                    }
                    if !obj.contains_key("storedBytes") {
                        obj.insert("storedBytes".to_string(), Value::Number(serde_json::Number::from(0)));
                    }
                }
                g
            }).collect();
            formats::json::success_response(&serde_json::json!({ "logGroups": filtered }))
        }
        "ListTagsForResource" | "ListTagsLogGroup" => {
            formats::json::success_response(&serde_json::json!({ "tags": {} }))
        }
        "PutMetricFilter" => {
            let log_group = body.get("logGroupName").and_then(|v| v.as_str()).unwrap_or("").to_string();
            let filter_name = body.get("filterName").and_then(|v| v.as_str()).unwrap_or("").to_string();
            let id = format!("{log_group}:{filter_name}");
            let _ = store::create(db, "logs", "metric-filter", Some(&id), body, region);
            (StatusCode::OK, "").into_response()
        }
        "DescribeMetricFilters" => {
            let log_group = body.get("logGroupName").and_then(|v| v.as_str()).unwrap_or("");
            let filter_name = body.get("filterNamePrefix").and_then(|v| v.as_str());
            let items = store::list(db, "logs", "metric-filter", &HashMap::new()).unwrap_or_default();
            let filtered: Vec<Value> = items.into_iter().filter(|f| {
                let lg = f.get("logGroupName").and_then(|v| v.as_str()).unwrap_or("");
                let matches_lg = log_group.is_empty() || lg == log_group;
                let matches_fn = filter_name.is_none_or(|prefix| {
                    f.get("filterName").and_then(|v| v.as_str()).is_some_and(|n| n.starts_with(prefix))
                });
                matches_lg && matches_fn
            }).collect();
            formats::json::success_response(&serde_json::json!({ "metricFilters": filtered }))
        }
        "DeleteMetricFilter" => {
            let log_group = body.get("logGroupName").and_then(|v| v.as_str()).unwrap_or("");
            let filter_name = body.get("filterName").and_then(|v| v.as_str()).unwrap_or("");
            let id = format!("{log_group}:{filter_name}");
            let _ = store::delete_by_id(db, "logs", "metric-filter", &id);
            (StatusCode::OK, "").into_response()
        }
        "PutRetentionPolicy" | "DeleteRetentionPolicy" | "TagLogGroup" | "UntagLogGroup"
        | "CreateLogStream" | "PutLogEvents" | "TagResource" | "UntagResource" => {
            (StatusCode::OK, "").into_response()
        }
        _ => {
            formats::json::error_response(StatusCode::BAD_REQUEST, "UnknownOperation", &format!("Logs operation not implemented: {op}"))
        }
    }
}

// --- SSM Parameter Store ---

fn dispatch_ssm(target: &str, body: Value, db: &DbPool, region: &str) -> Response {
    let op = target.rsplit('.').next().unwrap_or(target);
    match op {
        "PutParameter" => {
            let name = body.get("Name").and_then(|v| v.as_str()).unwrap_or("").to_string();
            let value = body.get("Value").and_then(|v| v.as_str()).unwrap_or("").to_string();
            let param_type = body.get("Type").and_then(|v| v.as_str()).unwrap_or("String").to_string();
            let now = Utc::now().timestamp() as f64;

            // Upsert: try update first, then create
            let props = serde_json::json!({
                "Name": name,
                "Value": value,
                "Type": param_type,
                "Version": 1,
                "LastModifiedDate": now,
                "ARN": format!("arn:aws:ssm:{region}:000000000000:parameter{name}"),
            });

            match store::lookup(db, "ssm", "parameter", &name) {
                Ok(_) => { let _ = store::update_by_lookup(db, "ssm", "parameter", &name, props); }
                Err(_) => { let _ = store::create(db, "ssm", "parameter", Some(&name), props, region); }
            }

            formats::json::success_response(&serde_json::json!({
                "Version": 1,
                "Tier": "Standard",
            }))
        }
        "GetParameter" => {
            let name = body.get("Name").and_then(|v| v.as_str()).unwrap_or("");
            match store::lookup(db, "ssm", "parameter", name) {
                Ok(props) => {
                    formats::json::success_response(&serde_json::json!({
                        "Parameter": {
                            "Name": props.get("Name").and_then(|v| v.as_str()).unwrap_or(name),
                            "Type": props.get("Type").and_then(|v| v.as_str()).unwrap_or("String"),
                            "Value": props.get("Value").and_then(|v| v.as_str()).unwrap_or(""),
                            "Version": props.get("Version").and_then(|v| v.as_i64()).unwrap_or(1),
                            "ARN": props.get("ARN").and_then(|v| v.as_str()).unwrap_or(""),
                            "LastModifiedDate": props.get("LastModifiedDate").and_then(|v| v.as_f64()).unwrap_or(0.0),
                        }
                    }))
                }
                Err(_) => formats::json::error_response(
                    StatusCode::BAD_REQUEST,
                    "ParameterNotFound",
                    &format!("Parameter {name} not found"),
                ),
            }
        }
        "GetParameters" => {
            let names: Vec<&str> = body.get("Names")
                .and_then(|v| v.as_array())
                .map(|a| a.iter().filter_map(|v| v.as_str()).collect())
                .unwrap_or_default();
            let mut params = vec![];
            let mut invalid = vec![];
            for name in &names {
                match store::lookup(db, "ssm", "parameter", name) {
                    Ok(props) => params.push(serde_json::json!({
                        "Name": name,
                        "Type": props.get("Type").and_then(|v| v.as_str()).unwrap_or("String"),
                        "Value": props.get("Value").and_then(|v| v.as_str()).unwrap_or(""),
                        "Version": 1,
                    })),
                    Err(_) => invalid.push(name.to_string()),
                }
            }
            formats::json::success_response(&serde_json::json!({
                "Parameters": params,
                "InvalidParameters": invalid,
            }))
        }
        "DeleteParameter" => {
            let name = body.get("Name").and_then(|v| v.as_str()).unwrap_or("");
            let _ = store::delete_by_id(db, "ssm", "parameter", name);
            formats::json::success_response(&serde_json::json!({}))
        }
        "DescribeParameters" => {
            // The provider sends ParameterFilters to narrow results.
            // Filter by Name if provided in ParameterFilters or Filters.
            let name_filter = body.get("ParameterFilters")
                .and_then(|v| v.as_array())
                .and_then(|arr| arr.iter().find(|f| {
                    f.get("Key").and_then(|k| k.as_str()) == Some("Name")
                }))
                .and_then(|f| f.get("Values").and_then(|v| v.as_array()))
                .and_then(|vals| vals.first())
                .and_then(|v| v.as_str());

            let mut filters = HashMap::new();
            if let Some(name) = name_filter {
                filters.insert("Name".to_string(), name.to_string());
            }

            let params = store::list(db, "ssm", "parameter", &filters).unwrap_or_default();
            let param_list: Vec<Value> = params.iter().map(|p| {
                serde_json::json!({
                    "Name": p.get("Name").and_then(|v| v.as_str()).unwrap_or(""),
                    "Type": p.get("Type").and_then(|v| v.as_str()).unwrap_or("String"),
                    "Version": 1,
                    "LastModifiedDate": p.get("LastModifiedDate").and_then(|v| v.as_f64()).unwrap_or(0.0),
                    "ARN": p.get("ARN").and_then(|v| v.as_str()).unwrap_or(""),
                    "DataType": "text",
                    "Tier": "Standard",
                })
            }).collect();
            formats::json::success_response(&serde_json::json!({ "Parameters": param_list }))
        }
        "GetParametersByPath" => {
            let params = store::list(db, "ssm", "parameter", &HashMap::new()).unwrap_or_default();
            let param_list: Vec<Value> = params.iter().map(|p| {
                serde_json::json!({
                    "Name": p.get("Name").and_then(|v| v.as_str()).unwrap_or(""),
                    "Type": p.get("Type").and_then(|v| v.as_str()).unwrap_or("String"),
                    "Value": p.get("Value").and_then(|v| v.as_str()).unwrap_or(""),
                    "Version": 1,
                })
            }).collect();
            formats::json::success_response(&serde_json::json!({ "Parameters": param_list }))
        }
        "AddTagsToResource" | "RemoveTagsFromResource" | "ListTagsForResource" => {
            formats::json::success_response(&serde_json::json!({ "TagList": [] }))
        }
        "LabelParameterVersion" | "GetParameterHistory" => {
            formats::json::success_response(&serde_json::json!({}))
        }

        // --- Maintenance Window operations ---
        "CreateMaintenanceWindow" => {
            let window_id = format!("mw-{}", &Uuid::new_v4().simple().to_string()[..17]);
            let mut props = body.clone();
            if let Some(obj) = props.as_object_mut() {
                obj.insert("WindowId".to_string(), Value::String(window_id.clone()));
            }
            let _ = store::create(db, "ssm", "maintenance-window", Some(&window_id), props, region);
            formats::json::success_response(&serde_json::json!({
                "WindowId": window_id,
            }))
        }
        "GetMaintenanceWindow" | "DescribeMaintenanceWindows" => {
            let window_id = body.get("WindowId").and_then(|v| v.as_str()).unwrap_or("");
            if !window_id.is_empty() {
                match store::lookup(db, "ssm", "maintenance-window", window_id) {
                    Ok(props) => formats::json::success_response(&props),
                    Err(_) => formats::json::error_response(
                        StatusCode::BAD_REQUEST,
                        "DoesNotExistException",
                        &format!("Maintenance window {window_id} does not exist"),
                    ),
                }
            } else {
                // List all
                let windows = store::list(db, "ssm", "maintenance-window", &HashMap::new()).unwrap_or_default();
                formats::json::success_response(&serde_json::json!({
                    "WindowIdentities": windows,
                }))
            }
        }
        "UpdateMaintenanceWindow" => {
            let window_id = body.get("WindowId").and_then(|v| v.as_str()).unwrap_or("").to_string();
            match store::update_by_lookup(db, "ssm", "maintenance-window", &window_id, body) {
                Ok(props) => formats::json::success_response(&props),
                Err(e) => {
                    tracing::warn!("UpdateMaintenanceWindow error: {e:?}");
                    formats::json::error_response(
                        StatusCode::BAD_REQUEST,
                        "DoesNotExistException",
                        &format!("Maintenance window {window_id} does not exist"),
                    )
                }
            }
        }
        "DeleteMaintenanceWindow" => {
            let window_id = body.get("WindowId").and_then(|v| v.as_str()).unwrap_or("");
            let _ = store::delete_by_id(db, "ssm", "maintenance-window", window_id);
            formats::json::success_response(&serde_json::json!({
                "WindowId": window_id,
            }))
        }

        // --- Maintenance Window Target operations ---
        "RegisterTargetWithMaintenanceWindow" => {
            let window_id = body.get("WindowId").and_then(|v| v.as_str()).unwrap_or("");
            let target_id = Uuid::new_v4().simple().to_string()[..12].to_string();
            let mut props = body.clone();
            if let Some(obj) = props.as_object_mut() {
                obj.insert("WindowTargetId".to_string(), Value::String(target_id.clone()));
            }
            let _ = store::create(db, "ssm", "maintenance-window-target", Some(&target_id), props, region);
            formats::json::success_response(&serde_json::json!({
                "WindowTargetId": target_id,
            }))
        }
        "DescribeMaintenanceWindowTargets" => {
            let window_id = body.get("WindowId").and_then(|v| v.as_str()).unwrap_or("");
            let mut filters = HashMap::new();
            if !window_id.is_empty() {
                filters.insert("WindowId".to_string(), window_id.to_string());
            }
            let targets = store::list(db, "ssm", "maintenance-window-target", &filters).unwrap_or_default();
            formats::json::success_response(&serde_json::json!({
                "Targets": targets,
            }))
        }
        "DeregisterTargetFromMaintenanceWindow" => {
            let window_target_id = body.get("WindowTargetId").and_then(|v| v.as_str()).unwrap_or("");
            let _ = store::delete_by_id(db, "ssm", "maintenance-window-target", window_target_id);
            formats::json::success_response(&serde_json::json!({
                "WindowTargetId": window_target_id,
            }))
        }

        // --- Maintenance Window Task operations ---
        "RegisterTaskWithMaintenanceWindow" => {
            let window_id = body.get("WindowId").and_then(|v| v.as_str()).unwrap_or("");
            let task_id = Uuid::new_v4().simple().to_string()[..12].to_string();
            let mut props = body.clone();
            if let Some(obj) = props.as_object_mut() {
                obj.insert("WindowTaskId".to_string(), Value::String(task_id.clone()));
            }
            let _ = store::create(db, "ssm", "maintenance-window-task", Some(&task_id), props, region);
            formats::json::success_response(&serde_json::json!({
                "WindowTaskId": task_id,
            }))
        }
        "DescribeMaintenanceWindowTasks" => {
            let window_id = body.get("WindowId").and_then(|v| v.as_str()).unwrap_or("");
            let mut filters = HashMap::new();
            if !window_id.is_empty() {
                filters.insert("WindowId".to_string(), window_id.to_string());
            }
            let tasks = store::list(db, "ssm", "maintenance-window-task", &filters).unwrap_or_default();
            formats::json::success_response(&serde_json::json!({
                "Tasks": tasks,
            }))
        }
        "GetMaintenanceWindowTask" => {
            let window_id = body.get("WindowId").and_then(|v| v.as_str()).unwrap_or("");
            let task_id = body.get("WindowTaskId").and_then(|v| v.as_str()).unwrap_or("");
            let composite = format!("{window_id}/{task_id}");
            match store::lookup(db, "ssm", "maintenance-window-task", &composite) {
                Ok(props) => formats::json::success_response(&props),
                Err(_) => {
                    // Try without composite
                    match store::lookup(db, "ssm", "maintenance-window-task", task_id) {
                        Ok(props) => formats::json::success_response(&props),
                        Err(_) => formats::json::error_response(
                            StatusCode::BAD_REQUEST,
                            "DoesNotExistException",
                            "Maintenance window task does not exist",
                        ),
                    }
                }
            }
        }
        "DeregisterTaskFromMaintenanceWindow" => {
            let window_id = body.get("WindowId").and_then(|v| v.as_str()).unwrap_or("");
            let task_id = body.get("WindowTaskId").and_then(|v| v.as_str()).unwrap_or("");
            let composite = format!("{window_id}/{task_id}");
            let _ = store::delete_by_id(db, "ssm", "maintenance-window-task", &composite);
            formats::json::success_response(&serde_json::json!({
                "WindowTaskId": task_id,
            }))
        }

        // --- SSM Document operations ---
        "CreateDocument" => {
            let name = body.get("Name").and_then(|v| v.as_str()).unwrap_or("unknown").to_string();
            let doc_type = body.get("DocumentType").and_then(|v| v.as_str()).unwrap_or("Command");
            let doc_format = body.get("DocumentFormat").and_then(|v| v.as_str()).unwrap_or("YAML");
            let now = Utc::now().timestamp() as f64;
            let doc_version = "1";

            let description = serde_json::json!({
                "Name": name,
                "DocumentType": doc_type,
                "DocumentFormat": doc_format,
                "DocumentVersion": doc_version,
                "LatestVersion": doc_version,
                "DefaultVersion": doc_version,
                "Status": "Active",
                "StatusInformation": "",
                "Owner": "000000000000",
                "CreatedDate": now,
                "SchemaVersion": body.get("SchemaVersion").and_then(|v| v.as_str()).unwrap_or("2.2"),
                "Hash": Uuid::new_v4().simple().to_string(),
                "HashType": "Sha256",
                "PlatformTypes": ["Linux"],
                "Tags": body.get("Tags").cloned().unwrap_or(Value::Array(vec![])),
            });

            let mut props = body.clone();
            if let Some(obj) = props.as_object_mut() {
                obj.insert("DocumentDescription".to_string(), description.clone());
                obj.insert("Status".to_string(), Value::String("Active".to_string()));
                obj.insert("DocumentVersion".to_string(), Value::String(doc_version.to_string()));
            }
            let _ = store::create(db, "ssm", "document", Some(&name), props, region);

            formats::json::success_response(&serde_json::json!({
                "DocumentDescription": description,
            }))
        }
        "DescribeDocument" | "GetDocument" => {
            let name = body.get("Name").and_then(|v| v.as_str()).unwrap_or("");
            match store::lookup(db, "ssm", "document", name) {
                Ok(props) => {
                    if op == "DescribeDocument" {
                        let desc = props.get("DocumentDescription").cloned().unwrap_or_else(|| {
                            serde_json::json!({
                                "Name": name,
                                "Status": "Active",
                                "DocumentType": props.get("DocumentType").and_then(|v| v.as_str()).unwrap_or("Command"),
                                "DocumentFormat": props.get("DocumentFormat").and_then(|v| v.as_str()).unwrap_or("YAML"),
                                "DocumentVersion": "1",
                                "LatestVersion": "1",
                                "DefaultVersion": "1",
                                "Owner": "000000000000",
                                "SchemaVersion": "2.2",
                                "PlatformTypes": ["Linux"],
                                "Tags": props.get("Tags").cloned().unwrap_or(Value::Array(vec![])),
                            })
                        });
                        formats::json::success_response(&serde_json::json!({
                            "Document": desc,
                        }))
                    } else {
                        // GetDocument
                        formats::json::success_response(&serde_json::json!({
                            "Name": name,
                            "Content": props.get("Content").and_then(|v| v.as_str()).unwrap_or(""),
                            "DocumentType": props.get("DocumentType").and_then(|v| v.as_str()).unwrap_or("Command"),
                            "DocumentFormat": props.get("DocumentFormat").and_then(|v| v.as_str()).unwrap_or("YAML"),
                            "DocumentVersion": "1",
                            "Status": "Active",
                        }))
                    }
                }
                Err(_) => formats::json::error_response(
                    StatusCode::BAD_REQUEST,
                    "InvalidDocument",
                    &format!("Document {name} does not exist"),
                ),
            }
        }
        "UpdateDocument" => {
            let name = body.get("Name").and_then(|v| v.as_str()).unwrap_or("").to_string();
            match store::update_by_lookup(db, "ssm", "document", &name, body) {
                Ok(props) => {
                    let desc = props.get("DocumentDescription").cloned().unwrap_or_else(|| {
                        serde_json::json!({ "Name": name, "Status": "Active" })
                    });
                    formats::json::success_response(&serde_json::json!({
                        "DocumentDescription": desc,
                    }))
                }
                Err(_) => formats::json::error_response(
                    StatusCode::BAD_REQUEST,
                    "InvalidDocument",
                    &format!("Document {name} does not exist"),
                ),
            }
        }
        "DeleteDocument" => {
            let name = body.get("Name").and_then(|v| v.as_str()).unwrap_or("");
            let _ = store::delete_by_id(db, "ssm", "document", name);
            formats::json::success_response(&serde_json::json!({}))
        }
        "DescribeDocumentPermission" => {
            formats::json::success_response(&serde_json::json!({
                "AccountIds": [],
                "AccountSharingInfoList": [],
            }))
        }
        "UpdateDocumentDefaultVersion" => {
            formats::json::success_response(&serde_json::json!({
                "Description": {
                    "Name": body.get("Name").and_then(|v| v.as_str()).unwrap_or(""),
                    "DefaultVersion": body.get("DocumentVersion").and_then(|v| v.as_str()).unwrap_or("1"),
                }
            }))
        }

        _ => {
            tracing::warn!("SSM unhandled operation: {op}");
            formats::json::success_response(&serde_json::json!({}))
        }
    }
}

// --- Secrets Manager ---

/// Handle Secrets Manager operations with the exact response shape the
/// AWS SDK expects.  DescribeSecret must include VersionIdsToStages and
/// epoch-float timestamps or the Pulumi provider times out waiting for
/// the secret to appear.
fn dispatch_secretsmanager(target: &str, body: Value, db: &DbPool, region: &str) -> Response {
    let op = target.rsplit('.').next().unwrap_or(target);
    match op {
        "CreateSecret" => {
            let name = body.get("Name").and_then(|v| v.as_str()).unwrap_or("unknown").to_string();
            let secret_string = body.get("SecretString").cloned();

            let version_id = Uuid::new_v4().to_string();
            let now_epoch = Utc::now().timestamp() as f64;
            let arn = format!(
                "arn:aws:secretsmanager:{region}:000000000000:secret:{name}-{}",
                &Uuid::new_v4().simple().to_string()[..6]
            );

            let props = serde_json::json!({
                "Name": name,
                "ARN": arn,
                "VersionId": version_id,
                "SecretString": secret_string,
                "VersionIdsToStages": { &version_id: ["AWSCURRENT"] },
                "CreatedDate": now_epoch,
                "LastChangedDate": now_epoch,
            });

            let _ = store::create(db, "secretsmanager", "secret", Some(&name), props.clone(), region);

            formats::json::success_response(&serde_json::json!({
                "Name": name,
                "ARN": arn,
                "VersionId": version_id,
                "VersionIdsToStages": { &version_id: ["AWSCURRENT"] },
            }))
        }
        "DescribeSecret" => {
            let secret_id = body.get("SecretId").and_then(|v| v.as_str()).unwrap_or("");
            // Try lookup by name first, then by ARN scan
            let result = store::lookup(db, "secretsmanager", "secret", secret_id)
                .or_else(|_| {
                    // SecretId might be an ARN — scan all secrets
                    let all = store::list(db, "secretsmanager", "secret", &HashMap::new())?;
                    all.into_iter()
                        .find(|s| s.get("ARN").and_then(|v| v.as_str()) == Some(secret_id))
                        .ok_or_else(|| store::StoreError::NotFound(secret_id.to_string()))
                });

            match result {
                Ok(props) => {
                    let name = props.get("Name").and_then(|v| v.as_str()).unwrap_or("");
                    let arn = props.get("ARN").and_then(|v| v.as_str()).unwrap_or("");
                    let version_id = props.get("VersionId").and_then(|v| v.as_str())
                        .unwrap_or("00000000-0000-0000-0000-000000000000");
                    let created = props.get("CreatedDate").and_then(|v| v.as_f64())
                        .unwrap_or_else(|| Utc::now().timestamp() as f64);
                    let changed = props.get("LastChangedDate").and_then(|v| v.as_f64())
                        .unwrap_or(created);
                    let versions = props.get("VersionIdsToStages").cloned()
                        .unwrap_or_else(|| serde_json::json!({ version_id: ["AWSCURRENT"] }));

                    formats::json::success_response(&serde_json::json!({
                        "Name": name,
                        "ARN": arn,
                        "VersionIdsToStages": versions,
                        "CreatedDate": created,
                        "LastChangedDate": changed,
                    }))
                }
                Err(e) => store_error_to_json(e),
            }
        }
        "GetSecretValue" => {
            let secret_id = body.get("SecretId").and_then(|v| v.as_str()).unwrap_or("");
            let result = store::lookup(db, "secretsmanager", "secret", secret_id)
                .or_else(|_| {
                    let all = store::list(db, "secretsmanager", "secret", &HashMap::new())?;
                    all.into_iter()
                        .find(|s| s.get("ARN").and_then(|v| v.as_str()) == Some(secret_id))
                        .ok_or_else(|| store::StoreError::NotFound(secret_id.to_string()))
                });

            match result {
                Ok(props) => {
                    let name = props.get("Name").and_then(|v| v.as_str()).unwrap_or("");
                    let arn = props.get("ARN").and_then(|v| v.as_str()).unwrap_or("");
                    let version_id = props.get("VersionId").and_then(|v| v.as_str())
                        .unwrap_or("00000000-0000-0000-0000-000000000000");
                    let secret_string = props.get("SecretString").cloned()
                        .unwrap_or(Value::Null);
                    let created = props.get("CreatedDate").and_then(|v| v.as_f64())
                        .unwrap_or_else(|| Utc::now().timestamp() as f64);

                    formats::json::success_response(&serde_json::json!({
                        "Name": name,
                        "ARN": arn,
                        "VersionId": version_id,
                        "SecretString": secret_string,
                        "VersionStages": ["AWSCURRENT"],
                        "CreatedDate": created,
                    }))
                }
                Err(e) => store_error_to_json(e),
            }
        }
        "PutSecretValue" => {
            let secret_id = body.get("SecretId").and_then(|v| v.as_str()).unwrap_or("");
            let secret_string = body.get("SecretString").cloned();
            let version_id = Uuid::new_v4().to_string();
            let now_epoch = Utc::now().timestamp() as f64;

            let updates = serde_json::json!({
                "SecretString": secret_string,
                "VersionId": version_id,
                "VersionIdsToStages": { &version_id: ["AWSCURRENT"] },
                "LastChangedDate": now_epoch,
            });

            let _ = store::update_by_lookup(db, "secretsmanager", "secret", secret_id, updates);

            // Also try ARN-based lookup
            if let Ok(all) = store::list(db, "secretsmanager", "secret", &HashMap::new()) {
                if let Some(s) = all.iter().find(|s| s.get("ARN").and_then(|v| v.as_str()) == Some(secret_id)) {
                    if let Some(arn) = s.get("Arn").and_then(|v| v.as_str()) {
                        let _ = store::update(db, arn, serde_json::json!({
                            "SecretString": secret_string,
                            "VersionId": version_id,
                            "VersionIdsToStages": { &version_id: ["AWSCURRENT"] },
                            "LastChangedDate": now_epoch,
                        }));
                    }
                }
            }

            let name = store::lookup(db, "secretsmanager", "secret", secret_id)
                .ok()
                .and_then(|p| p.get("Name").and_then(|v| v.as_str()).map(|s| s.to_string()))
                .unwrap_or_else(|| secret_id.to_string());
            let arn = store::lookup(db, "secretsmanager", "secret", secret_id)
                .ok()
                .and_then(|p| p.get("ARN").and_then(|v| v.as_str()).map(|s| s.to_string()))
                .unwrap_or_default();

            formats::json::success_response(&serde_json::json!({
                "Name": name,
                "ARN": arn,
                "VersionId": version_id,
                "VersionStages": ["AWSCURRENT"],
            }))
        }
        "UpdateSecret" => {
            let secret_id = body.get("SecretId").and_then(|v| v.as_str()).unwrap_or("");
            let now_epoch = Utc::now().timestamp() as f64;
            let mut updates = body.clone();
            if let Some(obj) = updates.as_object_mut() {
                obj.insert("LastChangedDate".to_string(), serde_json::json!(now_epoch));
            }
            let _ = store::update_by_lookup(db, "secretsmanager", "secret", secret_id, updates);
            formats::json::success_response(&serde_json::json!({
                "Name": secret_id,
            }))
        }
        "DeleteSecret" => {
            let secret_id = body.get("SecretId").and_then(|v| v.as_str()).unwrap_or("");
            let _ = store::delete_by_id(db, "secretsmanager", "secret", secret_id);
            formats::json::success_response(&serde_json::json!({
                "Name": secret_id,
                "DeletionDate": Utc::now().timestamp() as f64,
            }))
        }
        "TagResource" | "UntagResource" | "ListSecretVersionIds" => {
            formats::json::success_response(&serde_json::json!({}))
        }
        "GetResourcePolicy" => {
            formats::json::success_response(&serde_json::json!({
                "Name": body.get("SecretId").and_then(|v| v.as_str()).unwrap_or(""),
                "ResourcePolicy": Value::Null,
            }))
        }
        "ValidateResourcePolicy" | "PutResourcePolicy" | "DeleteResourcePolicy"
        | "RotateSecret" | "CancelRotateSecret" | "RestoreSecret"
        | "ReplicateSecretToRegions" | "RemoveRegionsFromReplication" => {
            formats::json::success_response(&serde_json::json!({}))
        }
        _ => {
            // Return empty success for unknown operations to avoid cascade failures
            tracing::warn!("SecretsManager unhandled operation: {op}");
            formats::json::success_response(&serde_json::json!({}))
        }
    }
}

// --- CodeBuild ---

/// Handle CodeBuild operations.  ImportSourceCredentials must return an
/// ARN that the provider can read back via ListSourceCredentials.
/// WAFv2 — the provider expects Create* responses wrapped in a `Summary` envelope
/// Application Auto Scaling — RegisterScalableTarget / PutScalingPolicy
/// use "Register" and "Put" prefixes that need custom handling.  The SDK
/// expects specific envelope shapes for Describe responses.
fn dispatch_autoscaling(target: &str, body: Value, db: &DbPool, region: &str) -> Response {
    let op = target.rsplit('.').next().unwrap_or(target);
    match op {
        "RegisterScalableTarget" => {
            let ns = body.get("ServiceNamespace").and_then(|v| v.as_str()).unwrap_or("custom");
            let resource_id = body.get("ResourceId").and_then(|v| v.as_str()).unwrap_or("unknown");
            let dimension = body.get("ScalableDimension").and_then(|v| v.as_str()).unwrap_or("custom");
            let id = format!("{ns}/{resource_id}/{dimension}");
            let arn = format!(
                "arn:aws:application-autoscaling:{region}:000000000000:scalable-target/{id}"
            );
            // Save the original ResourceId under a separate key so
            // store::create's ResourceId injection (which uses the
            // composite internal ID) doesn't lose the real value.
            let mut props = body.clone();
            if let Some(obj) = props.as_object_mut() {
                obj.insert("ScalableTargetARN".to_string(), Value::String(arn.clone()));
                obj.insert("OriginalResourceId".to_string(), Value::String(resource_id.to_string()));
            }
            let _ = store::create(db, "autoscaling", "target", Some(&id), props, region);
            formats::json::success_response(&serde_json::json!({
                "ScalableTargetARN": arn
            }))
        }
        "DeregisterScalableTarget" => {
            let ns = body.get("ServiceNamespace").and_then(|v| v.as_str()).unwrap_or("");
            let resource_id = body.get("ResourceId").and_then(|v| v.as_str()).unwrap_or("");
            let dimension = body.get("ScalableDimension").and_then(|v| v.as_str()).unwrap_or("");
            let id = format!("{ns}/{resource_id}/{dimension}");
            let _ = store::delete_by_id(db, "autoscaling", "target", &id);
            formats::json::success_response(&serde_json::json!({}))
        }
        "DescribeScalableTargets" => {
            // Filter by ServiceNamespace/ResourceId/ScalableDimension if
            // provided — the SDK calls this after RegisterScalableTarget
            // to verify the target was created.
            let ns = body.get("ServiceNamespace").and_then(|v| v.as_str());
            let resource_ids = body.get("ResourceIds").and_then(|v| v.as_array());
            let dimension = body.get("ScalableDimension").and_then(|v| v.as_str());

            // store::create injects the composite internal ID as
            // "ResourceId", clobbering the original value.  We stash
            // the real one under "OriginalResourceId" during Register
            // and restore it here so the SDK sees the value it expects.
            fn fix_target_props(mut props: Value) -> Value {
                if let Some(obj) = props.as_object_mut() {
                    if let Some(orig) = obj.remove("OriginalResourceId") {
                        obj.insert("ResourceId".to_string(), orig);
                    }
                    // Remove internal store fields the SDK doesn't expect
                    obj.remove("Arn");
                }
                props
            }

            if let (Some(ns), Some(ids)) = (ns, resource_ids) {
                let mut targets = Vec::new();
                for rid in ids.iter().filter_map(|v| v.as_str()) {
                    let dim = dimension.unwrap_or("custom");
                    let id = format!("{ns}/{rid}/{dim}");
                    if let Ok(props) = store::lookup(db, "autoscaling", "target", &id) {
                        targets.push(fix_target_props(props));
                    }
                }
                formats::json::success_response(&serde_json::json!({
                    "ScalableTargets": targets
                }))
            } else {
                match store::list(db, "autoscaling", "target", &HashMap::new()) {
                    Ok(items) => {
                        let fixed: Vec<Value> = items.into_iter().map(fix_target_props).collect();
                        formats::json::success_response(&serde_json::json!({
                            "ScalableTargets": fixed
                        }))
                    }
                    Err(e) => store_error_to_json(e),
                }
            }
        }
        "PutScalingPolicy" => {
            let policy_name = body.get("PolicyName").and_then(|v| v.as_str()).unwrap_or("policy");
            let ns = body.get("ServiceNamespace").and_then(|v| v.as_str()).unwrap_or("custom");
            let resource_id = body.get("ResourceId").and_then(|v| v.as_str()).unwrap_or("unknown");
            let id = format!("{ns}/{resource_id}/{policy_name}");
            let policy_arn = format!(
                "arn:aws:autoscaling:{region}:000000000000:scalingPolicy:{}:resource/{ns}/{resource_id}:policyName/{policy_name}",
                uuid::Uuid::new_v4()
            );
            let mut props = body.clone();
            if let Some(obj) = props.as_object_mut() {
                obj.insert("PolicyARN".to_string(), Value::String(policy_arn.clone()));
            }
            // Upsert: try create, fall back to update
            match store::create(db, "autoscaling", "policy", Some(&id), props.clone(), region) {
                Ok(_) => {}
                Err(store::StoreError::AlreadyExists(_)) => {
                    let _ = store::update_by_lookup(db, "autoscaling", "policy", &id, props);
                }
                Err(_) => {}
            }
            formats::json::success_response(&serde_json::json!({
                "PolicyARN": policy_arn,
                "Alarms": []
            }))
        }
        "DescribeScalingPolicies" => {
            match store::list(db, "autoscaling", "policy", &HashMap::new()) {
                Ok(items) => formats::json::success_response(&serde_json::json!({
                    "ScalingPolicies": items
                })),
                Err(e) => store_error_to_json(e),
            }
        }
        "DeleteScalingPolicy" => {
            let policy_name = body.get("PolicyName").and_then(|v| v.as_str()).unwrap_or("");
            let ns = body.get("ServiceNamespace").and_then(|v| v.as_str()).unwrap_or("");
            let resource_id = body.get("ResourceId").and_then(|v| v.as_str()).unwrap_or("");
            let id = format!("{ns}/{resource_id}/{policy_name}");
            let _ = store::delete_by_id(db, "autoscaling", "policy", &id);
            formats::json::success_response(&serde_json::json!({}))
        }
        _ => {
            // Catch-all: store generically
            let id = body.get("ResourceId").and_then(|v| v.as_str()).map(|s| s.to_string());
            let _ = store::create(db, "autoscaling", "resource", id.as_deref(), body, region);
            formats::json::success_response(&serde_json::json!({}))
        }
    }
}

/// with `Id`, `ARN`, `Name`, `LockToken`.  Get*/List* need the full resource in
/// a type-specific envelope (e.g. `{"IPSet": {...}}`).  Without this, the
/// provider dereferences nil pointers and crashes the entire process.
fn dispatch_wafv2(target: &str, body: Value, db: &DbPool, region: &str) -> Response {
    let op = target.rsplit('.').next().unwrap_or(target);

    // Determine the resource type from the operation name
    let resource_type = if op.contains("IPSet") {
        "ip-set"
    } else if op.contains("WebACL") || op.contains("WebAcl") {
        "web-acl"
    } else if op.contains("RuleGroup") {
        "rule-group"
    } else if op.contains("RegexPattern") {
        "regex-pattern-set"
    } else {
        "resource"
    };

    // Envelope key for Get responses
    let envelope_key = match resource_type {
        "ip-set" => "IPSet",
        "web-acl" => "WebACL",
        "rule-group" => "RuleGroup",
        "regex-pattern-set" => "RegexPatternSet",
        _ => "Resource",
    };

    if op == "AssociateWebACL" {
        let web_acl_arn = body.get("WebACLArn").and_then(|v| v.as_str()).unwrap_or("");
        let resource_arn = body.get("ResourceArn").and_then(|v| v.as_str()).unwrap_or("");
        let id = format!("{web_acl_arn},{resource_arn}");
        let props = serde_json::json!({
            "WebACLArn": web_acl_arn,
            "ResourceArn": resource_arn,
        });
        let _ = store::create(db, "wafv2", "web-acl-association", Some(&id), props, region);
        return wafv2_json_response(&serde_json::json!({}));
    } else if op == "DisassociateWebACL" {
        let resource_arn = body.get("ResourceArn").and_then(|v| v.as_str()).unwrap_or("");
        // Find and delete by resource ARN
        let items = store::list(db, "wafv2", "web-acl-association", &HashMap::new()).unwrap_or_default();
        for item in &items {
            if item.get("ResourceArn").and_then(|v| v.as_str()) == Some(resource_arn) {
                if let Some(wacl) = item.get("WebACLArn").and_then(|v| v.as_str()) {
                    let id = format!("{wacl},{resource_arn}");
                    let _ = store::delete_by_id(db, "wafv2", "web-acl-association", &id);
                }
            }
        }
        return wafv2_json_response(&serde_json::json!({}));
    } else if op == "GetWebACLForResource" {
        let resource_arn = body.get("ResourceArn").and_then(|v| v.as_str()).unwrap_or("");
        let items = store::list(db, "wafv2", "web-acl-association", &HashMap::new()).unwrap_or_default();
        if let Some(assoc) = items.iter().find(|item| {
            item.get("ResourceArn").and_then(|v| v.as_str()) == Some(resource_arn)
        }) {
            let web_acl_arn = assoc.get("WebACLArn").and_then(|v| v.as_str()).unwrap_or("");
            // Look up the actual WebACL by ARN
            let acls = store::list(db, "wafv2", "web-acl", &HashMap::new()).unwrap_or_default();
            if let Some(acl) = acls.iter().find(|a| {
                a.get("ARN").and_then(|v| v.as_str()) == Some(web_acl_arn)
            }) {
                return wafv2_json_response(&serde_json::json!({
                    "WebACL": acl,
                }));
            }
        }
        // Return empty (no associated WebACL) — not an error
        return wafv2_json_response(&serde_json::json!({}));
    } else if op.starts_with("Create") {
        let name = body.get("Name").and_then(|v| v.as_str()).unwrap_or("unnamed");
        let id = Uuid::new_v4().simple().to_string();
        let scope = body.get("Scope").and_then(|v| v.as_str()).unwrap_or("REGIONAL");
        let arn = format!(
            "arn:aws:wafv2:{region}:000000000000:{}/{}/{}",
            scope.to_lowercase(), resource_type, id
        );
        let lock_token = Uuid::new_v4().simple().to_string();

        let mut props = body.clone();
        if let Some(obj) = props.as_object_mut() {
            obj.insert("Id".to_string(), serde_json::json!(id));
            obj.insert("ARN".to_string(), serde_json::json!(arn));
            obj.insert("LockToken".to_string(), serde_json::json!(lock_token));
        }

        let _ = store::create(db, "wafv2", resource_type, Some(&id), props, region);

        wafv2_json_response(&serde_json::json!({
            "Summary": {
                "Id": id,
                "Name": name,
                "ARN": arn,
                "LockToken": lock_token,
                "Description": body.get("Description").and_then(|v| v.as_str()).unwrap_or(""),
            }
        }))
    } else if op.starts_with("Get") || op.starts_with("Describe") {
        let id = body.get("Id").and_then(|v| v.as_str()).unwrap_or("");
        match store::lookup(db, "wafv2", resource_type, id) {
            Ok(props) => {
                let lock_token = props.get("LockToken")
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();
                wafv2_json_response(&serde_json::json!({
                    envelope_key: props,
                    "LockToken": lock_token,
                }))
            }
            Err(_) => (
                StatusCode::NOT_FOUND,
                [("content-type", "application/x-amz-json-1.1")],
                serde_json::to_string(&serde_json::json!({
                    "__type": "WAFNonexistentItemException",
                    "message": format!("{resource_type} {id} not found"),
                })).unwrap_or_default(),
            ).into_response(),
        }
    } else if op.starts_with("Update") {
        let id = body.get("Id").and_then(|v| v.as_str()).unwrap_or("");
        let lock_token = Uuid::new_v4().simple().to_string();
        if let Ok(_) = store::lookup(db, "wafv2", resource_type, id) {
            let mut updated = body.clone();
            if let Some(obj) = updated.as_object_mut() {
                obj.insert("LockToken".to_string(), serde_json::json!(lock_token));
            }
            let _ = store::update_by_lookup(db, "wafv2", resource_type, id, updated);
        }
        wafv2_json_response(&serde_json::json!({
            "NextLockToken": lock_token,
        }))
    } else if op.starts_with("Delete") {
        let id = body.get("Id").and_then(|v| v.as_str()).unwrap_or("");
        let _ = store::delete_by_id(db, "wafv2", resource_type, id);
        wafv2_json_response(&serde_json::json!({}))
    } else if op.starts_with("List") && op.contains("Tags") {
        // ListTagsForResource — return empty tag list
        wafv2_json_response(&serde_json::json!({
            "TagInfoForResource": {
                "TagList": [],
            }
        }))
    } else if op.starts_with("List") {
        // ListIPSets, ListWebACLs, etc.
        match store::list(db, "wafv2", resource_type, &HashMap::new()) {
            Ok(items) => {
                let summaries: Vec<Value> = items.iter().map(|item| {
                    serde_json::json!({
                        "Id": item.get("Id").unwrap_or(&Value::Null),
                        "Name": item.get("Name").unwrap_or(&Value::Null),
                        "ARN": item.get("ARN").unwrap_or(&Value::Null),
                        "LockToken": item.get("LockToken").unwrap_or(&Value::Null),
                        "Description": item.get("Description").unwrap_or(&Value::Null),
                    })
                }).collect();
                // The list key is plural: IPSets, WebACLs, etc.
                let list_key = format!("{envelope_key}s");
                wafv2_json_response(&serde_json::json!({
                    list_key: summaries,
                }))
            }
            Err(_) => wafv2_json_response(&serde_json::json!({
                format!("{envelope_key}s"): [],
            })),
        }
    } else if op.starts_with("Tag") || op.starts_with("Untag") {
        wafv2_json_response(&serde_json::json!({}))
    } else {
        tracing::warn!("WAFv2: unhandled operation {op}");
        wafv2_json_response(&serde_json::json!({}))
    }
}

/// WAFv2 uses application/x-amz-json-1.1 — the AWS SDK Go v2 deserializer
/// may silently produce nil output with the wrong content type.
fn wafv2_json_response(body: &Value) -> Response {
    (
        StatusCode::OK,
        [("content-type", "application/x-amz-json-1.1")],
        serde_json::to_string(body).unwrap_or_else(|_| "{}".to_string()),
    ).into_response()
}

/// ACM certificate manager — handles RequestCertificate, DescribeCertificate,
/// AddTagsToCertificate, ListTagsForCertificate, DeleteCertificate.
///
/// The Pulumi/Terraform provider creates an ACM certificate, then reads it
/// back via DescribeCertificate to get `DomainValidationOptions` for DNS
/// validation.  It then creates a `CertificateValidation` resource that
/// polls DescribeCertificate until `Status == "ISSUED"`.  We return ISSUED
/// immediately so the provider never blocks.
/// Guess the XML namespace for cross-service tag operations based on
/// the resource ARN present in the request params.
fn guess_namespace(params: &HashMap<String, String>) -> &'static str {
    let arn = params
        .get("ResourceName")
        .or_else(|| params.get("ResourceArn"))
        .map(|s| s.as_str())
        .unwrap_or("");
    if arn.contains(":elasticache:") {
        "http://elasticache.amazonaws.com/doc/2015-02-02/"
    } else if arn.contains(":rds:") {
        "http://rds.amazonaws.com/doc/2014-10-31/"
    } else if arn.contains(":ec2:") || arn.contains(":vpc:") {
        "http://ec2.amazonaws.com/doc/2016-11-15/"
    } else {
        "https://amazonaws.com/doc/2010-03-31/"
    }
}

fn elasticache_xml_response(action: &str, inner: &str) -> Response {
    let request_id = Uuid::new_v4();
    let body = format!(
        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
         <{action}Response xmlns=\"http://elasticache.amazonaws.com/doc/2015-02-02/\">\
         <{action}Result>{inner}</{action}Result>\
         <ResponseMetadata><RequestId>{request_id}</RequestId></ResponseMetadata>\
         </{action}Response>"
    );
    ([("content-type", "application/xml")], body).into_response()
}

fn dispatch_elasticache_subnet_group(
    action: &str,
    body: &Value,
    params: &HashMap<String, String>,
    db: &DbPool,
    region: &str,
) -> Response {
    match action {
        "CreateCacheSubnetGroup" => {
            let group_name = params.get("CacheSubnetGroupName")
                .or_else(|| body.get("CacheSubnetGroupName").and_then(|v| v.as_str()).map(|_| body.get("CacheSubnetGroupName").unwrap()).and_then(|_| None))
                .cloned()
                .unwrap_or_else(|| body.get("CacheSubnetGroupName").and_then(|v| v.as_str()).unwrap_or("default").to_string());
            let description = params.get("CacheSubnetGroupDescription")
                .cloned()
                .unwrap_or_else(|| body.get("CacheSubnetGroupDescription").and_then(|v| v.as_str()).unwrap_or("").to_string());
            let _ = store::create(db, "elasticache", "subnet-group", Some(&group_name), body.clone(), region);
            let arn = format!("arn:aws:elasticache:{region}:000000000000:subnetgroup:{group_name}");
            let xml = format!(
                "<CacheSubnetGroup>\
                   <CacheSubnetGroupName>{group_name}</CacheSubnetGroupName>\
                   <CacheSubnetGroupDescription>{description}</CacheSubnetGroupDescription>\
                   <VpcId>vpc-00000000</VpcId>\
                   <Subnets/>\
                   <ARN>{arn}</ARN>\
                   <SupportedNetworkTypes><member>ipv4</member></SupportedNetworkTypes>\
                 </CacheSubnetGroup>"
            );
            elasticache_xml_response(action, &xml)
        }
        "DescribeCacheSubnetGroups" => {
            let group_name = params.get("CacheSubnetGroupName")
                .cloned()
                .unwrap_or_else(|| body.get("CacheSubnetGroupName").and_then(|v| v.as_str()).unwrap_or("").to_string());
            if !group_name.is_empty() {
                let arn = format!("arn:aws:elasticache:{region}:000000000000:subnetgroup:{group_name}");
                let xml = format!(
                    "<CacheSubnetGroups>\
                       <CacheSubnetGroup>\
                         <CacheSubnetGroupName>{group_name}</CacheSubnetGroupName>\
                         <CacheSubnetGroupDescription>Subnet group</CacheSubnetGroupDescription>\
                         <VpcId>vpc-00000000</VpcId>\
                         <Subnets/>\
                         <ARN>{arn}</ARN>\
                         <SupportedNetworkTypes><member>ipv4</member></SupportedNetworkTypes>\
                       </CacheSubnetGroup>\
                     </CacheSubnetGroups>"
                );
                elasticache_xml_response(action, &xml)
            } else {
                elasticache_xml_response(action, "<CacheSubnetGroups/>")
            }
        }
        "DeleteCacheSubnetGroup" => {
            let group_name = params.get("CacheSubnetGroupName")
                .cloned()
                .unwrap_or_else(|| body.get("CacheSubnetGroupName").and_then(|v| v.as_str()).unwrap_or("").to_string());
            let _ = store::delete_by_id(db, "elasticache", "subnet-group", &group_name);
            elasticache_xml_response(action, "")
        }
        _ => {
            // Fallback for other elasticache subnet-group operations
            elasticache_xml_response(action, "")
        }
    }
}

fn dispatch_eventbridge(target: &str, body: Value, db: &DbPool, region: &str) -> Response {
    let op = target.rsplit('.').next().unwrap_or(target);
    match op {
        "PutRule" => {
            let rule_name = body.get("Name").and_then(|v| v.as_str()).unwrap_or("rule");
            let arn = format!("arn:aws:events:{region}:000000000000:rule/{rule_name}");
            let mut props = body.clone();
            if let Some(obj) = props.as_object_mut() {
                obj.insert("RuleArn".to_string(), Value::String(arn.clone()));
            }
            let _ = store::create(db, "events", "rule", Some(rule_name), props, region);
            formats::json::success_response(&serde_json::json!({"RuleArn": arn}))
        }
        "DescribeRule" => {
            let rule_name = body.get("Name").and_then(|v| v.as_str()).unwrap_or("");
            match store::lookup(db, "events", "rule", rule_name) {
                Ok(props) => formats::json::success_response(&props),
                Err(e) => store_error_to_json(e),
            }
        }
        "ListRules" => {
            match store::list(db, "events", "rule", &HashMap::new()) {
                Ok(items) => formats::json::success_response(&serde_json::json!({"Rules": items})),
                Err(e) => store_error_to_json(e),
            }
        }
        "DeleteRule" => {
            let rule_name = body.get("Name").and_then(|v| v.as_str()).unwrap_or("");
            let _ = store::delete_by_id(db, "events", "rule", rule_name);
            formats::json::success_response(&serde_json::json!({}))
        }
        "PutTargets" => {
            // Store each target individually, keyed by Rule+TargetId
            let rule = body.get("Rule").and_then(|v| v.as_str()).unwrap_or("default");
            if let Some(targets) = body.get("Targets").and_then(|v| v.as_array()) {
                for t in targets {
                    let target_id = t.get("Id").and_then(|v| v.as_str()).unwrap_or("target");
                    let key = format!("{rule}/{target_id}");
                    let mut props = t.clone();
                    if let Some(obj) = props.as_object_mut() {
                        obj.insert("Rule".to_string(), Value::String(rule.to_string()));
                    }
                    let _ = store::create(db, "events", "target", Some(&key), props, region);
                }
            }
            formats::json::success_response(&serde_json::json!({"FailedEntryCount": 0, "FailedEntries": []}))
        }
        "ListTargetsByRule" => {
            let rule = body.get("Rule").and_then(|v| v.as_str()).unwrap_or("");
            let filters: HashMap<String, String> =
                [("Rule".to_string(), rule.to_string())].into();
            match store::list(db, "events", "target", &filters) {
                Ok(items) => formats::json::success_response(
                    &serde_json::json!({"Targets": items}),
                ),
                Err(e) => store_error_to_json(e),
            }
        }
        "RemoveTargets" => {
            let rule = body.get("Rule").and_then(|v| v.as_str()).unwrap_or("");
            if let Some(ids) = body.get("Ids").and_then(|v| v.as_array()) {
                for id in ids {
                    if let Some(tid) = id.as_str() {
                        let key = format!("{rule}/{tid}");
                        let _ = store::delete_by_id(db, "events", "target", &key);
                    }
                }
            }
            formats::json::success_response(&serde_json::json!({"FailedEntryCount": 0, "FailedEntries": []}))
        }
        "ListTagsForResource" | "TagResource" | "UntagResource" => {
            formats::json::success_response(&serde_json::json!({"Tags": []}))
        }
        _ => {
            // Generic fallback for other EventBridge operations
            let id = extract_json_identifier(&body, "events", "rule");
            let _ = store::create(db, "events", "rule", id.as_deref(), body, region);
            formats::json::success_response(&serde_json::json!({}))
        }
    }
}

fn dispatch_acm(target: &str, body: Value, db: &DbPool, region: &str) -> Response {
    let op = target.rsplit('.').next().unwrap_or(target);
    match op {
        "RequestCertificate" => {
            let domain = body.get("DomainName").and_then(|v| v.as_str()).unwrap_or("example.com");
            let cert_id = Uuid::new_v4().simple().to_string();
            let arn = format!("arn:aws:acm:{region}:000000000000:certificate/{cert_id}");

            // Build DomainValidationOptions from the domain + SANs
            let mut domains = vec![domain.to_string()];
            if let Some(sans) = body.get("SubjectAlternativeNames").and_then(|v| v.as_array()) {
                for san in sans {
                    if let Some(s) = san.as_str() {
                        if s != domain {
                            domains.push(s.to_string());
                        }
                    }
                }
            }
            let validation_options: Vec<Value> = domains.iter().map(|d| {
                // Generate deterministic-ish validation record values
                let hash = format!("{:x}", md5_simple(d.as_bytes()));
                serde_json::json!({
                    "DomainName": d,
                    "ResourceRecord": {
                        "Name": format!("_acm{}.{}", &hash[..8], d.trim_start_matches("*.")),
                        "Type": "CNAME",
                        "Value": format!("_acm{}.acm-validations.aws.", &hash[8..16]),
                    },
                    "ValidationDomain": d,
                    "ValidationStatus": "SUCCESS",
                })
            }).collect();

            let props = serde_json::json!({
                "CertificateArn": arn,
                "DomainName": domain,
                "SubjectAlternativeNames": domains,
                "Status": "ISSUED",
                "Type": "AMAZON_ISSUED",
                "KeyAlgorithm": body.get("KeyAlgorithm").and_then(|v| v.as_str()).unwrap_or("RSA_2048"),
                "DomainValidationOptions": validation_options,
                "InUseBy": [],
                "CreatedAt": Utc::now().timestamp() as f64,
                "IssuedAt": Utc::now().timestamp() as f64,
                "NotBefore": Utc::now().timestamp() as f64,
                "NotAfter": (Utc::now().timestamp() + 365 * 86400) as f64,
                "Serial": Uuid::new_v4().simple().to_string(),
            });

            let _ = store::create(db, "acm", "certificate", Some(&arn), props.clone(), region);

            acm_json_response(&serde_json::json!({
                "CertificateArn": arn,
            }))
        }
        "DescribeCertificate" => {
            let arn = body.get("CertificateArn").and_then(|v| v.as_str()).unwrap_or("");
            match store::lookup(db, "acm", "certificate", arn) {
                Ok(props) => acm_json_response(&serde_json::json!({
                    "Certificate": props,
                })),
                Err(_) => (
                    StatusCode::NOT_FOUND,
                    [("content-type", "application/x-amz-json-1.1")],
                    serde_json::to_string(&serde_json::json!({
                        "__type": "ResourceNotFoundException",
                        "message": format!("Certificate {arn} not found"),
                    })).unwrap_or_default(),
                ).into_response(),
            }
        }
        "AddTagsToCertificate" | "RemoveTagsFromCertificate" => {
            acm_json_response(&serde_json::json!({}))
        }
        "ListTagsForCertificate" => {
            acm_json_response(&serde_json::json!({
                "Tags": [],
            }))
        }
        "DeleteCertificate" => {
            let arn = body.get("CertificateArn").and_then(|v| v.as_str()).unwrap_or("");
            let _ = store::delete_by_id(db, "acm", "certificate", arn);
            acm_json_response(&serde_json::json!({}))
        }
        _ => {
            tracing::warn!("ACM: unhandled operation {op}");
            acm_json_response(&serde_json::json!({}))
        }
    }
}

/// ACM uses application/x-amz-json-1.1 content type.
fn acm_json_response(body: &Value) -> Response {
    (
        StatusCode::OK,
        [("content-type", "application/x-amz-json-1.1")],
        serde_json::to_string(body).unwrap_or_else(|_| "{}".to_string()),
    ).into_response()
}

/// Simple non-cryptographic hash for generating deterministic validation records.
fn md5_simple(data: &[u8]) -> u128 {
    let mut hash: u128 = 0xcbf2_9ce4_8422_2325;
    for &byte in data {
        hash ^= byte as u128;
        hash = hash.wrapping_mul(0x0100_0000_01b3);
    }
    hash
}

fn dispatch_codebuild(target: &str, body: Value, db: &DbPool, region: &str) -> Response {
    let op = target.rsplit('.').next().unwrap_or(target);
    match op {
        "ImportSourceCredentials" => {
            let server_type = body.get("serverType").and_then(|v| v.as_str()).unwrap_or("BITBUCKET");
            let auth_type = body.get("authType").and_then(|v| v.as_str()).unwrap_or("PERSONAL_ACCESS_TOKEN");
            let token = body.get("token").and_then(|v| v.as_str()).unwrap_or("");

            let arn = format!(
                "arn:aws:codebuild:{region}:000000000000:token/{server_type}",
            );

            let props = serde_json::json!({
                "arn": arn,
                "serverType": server_type,
                "authType": auth_type,
                "token": token,
            });

            let _ = store::create(db, "codebuild", "source-credential", Some(server_type), props, region);

            formats::json::success_response(&serde_json::json!({
                "arn": arn,
            }))
        }
        "ListSourceCredentials" => {
            let items = store::list(db, "codebuild", "source-credential", &HashMap::new()).unwrap_or_default();
            let creds: Vec<Value> = items.iter().map(|c| {
                serde_json::json!({
                    "arn": c.get("arn").and_then(|v| v.as_str()).unwrap_or(""),
                    "serverType": c.get("serverType").and_then(|v| v.as_str()).unwrap_or(""),
                    "authType": c.get("authType").and_then(|v| v.as_str()).unwrap_or(""),
                })
            }).collect();
            formats::json::success_response(&serde_json::json!({
                "sourceCredentialsInfos": creds,
            }))
        }
        "DeleteSourceCredentials" => {
            let arn = body.get("arn").and_then(|v| v.as_str()).unwrap_or("");
            // Find and delete by ARN
            if let Ok(items) = store::list(db, "codebuild", "source-credential", &HashMap::new()) {
                for item in &items {
                    if item.get("arn").and_then(|v| v.as_str()) == Some(arn) {
                        if let Some(id) = item.get("ResourceId").and_then(|v| v.as_str()) {
                            let _ = store::delete_by_id(db, "codebuild", "source-credential", id);
                        }
                    }
                }
            }
            formats::json::success_response(&serde_json::json!({}))
        }
        "CreateProject" => {
            let name_raw = body.get("name").and_then(|v| v.as_str()).unwrap_or("unknown");
            // The provider may send a full ARN — extract just the project name
            let name = name_raw.rsplit('/').next().unwrap_or(name_raw);
            let project_arn = format!("arn:aws:codebuild:{region}:000000000000:project/{name}");
            let mut project = body.clone();
            // Ensure required fields exist so the provider doesn't panic
            if let Some(obj) = project.as_object_mut() {
                obj.entry("arn").or_insert(serde_json::Value::String(project_arn.clone()));
                obj.entry("created").or_insert(serde_json::json!(
                    Utc::now().timestamp() as f64
                ));
                obj.entry("lastModified").or_insert(serde_json::json!(
                    Utc::now().timestamp() as f64
                ));
                // Make sure environment, source, artifacts have required nested fields
                if !obj.contains_key("environment") {
                    obj.insert("environment".to_string(), serde_json::json!({
                        "type": "LINUX_CONTAINER",
                        "image": "aws/codebuild/standard:7.0",
                        "computeType": "BUILD_GENERAL1_SMALL",
                    }));
                }
                if !obj.contains_key("source") {
                    obj.insert("source".to_string(), serde_json::json!({
                        "type": "NO_SOURCE",
                    }));
                }
                if !obj.contains_key("artifacts") {
                    obj.insert("artifacts".to_string(), serde_json::json!({
                        "type": "NO_ARTIFACTS",
                    }));
                }
                // The badge field must exist with enabled + url
                obj.entry("badge").or_insert(serde_json::json!({
                    "badgeEnabled": false,
                    "badgeRequestUrl": "",
                }));
            }
            let _ = store::create(db, "codebuild", "project", Some(name), project.clone(), region);
            formats::json::success_response(&serde_json::json!({"project": project}))
        }
        "BatchGetProjects" => {
            let names = body.get("names")
                .and_then(|v| v.as_array())
                .cloned()
                .unwrap_or_default();
            let mut projects = Vec::new();
            let mut not_found = Vec::new();
            for name_val in &names {
                let raw = name_val.as_str().unwrap_or("");
                // The provider may send full ARNs — extract project name
                let name = raw.rsplit('/').next().unwrap_or(raw);
                match store::lookup(db, "codebuild", "project", name) {
                    Ok(props) => projects.push(props),
                    Err(_) => not_found.push(name_val.clone()),
                }
            }
            formats::json::success_response(&serde_json::json!({
                "projects": projects,
                "projectsNotFound": not_found,
            }))
        }
        "UpdateProject" => {
            let name = body.get("name").and_then(|v| v.as_str()).unwrap_or("");
            if !name.is_empty() {
                let _ = store::update_by_lookup(db, "codebuild", "project", name, body.clone());
            }
            match store::lookup(db, "codebuild", "project", name) {
                Ok(props) => formats::json::success_response(&serde_json::json!({"project": props})),
                Err(e) => store_error_to_json(e),
            }
        }
        "DeleteProject" => {
            let name = body.get("name").and_then(|v| v.as_str()).unwrap_or("");
            let _ = store::delete_by_id(db, "codebuild", "project", name);
            formats::json::success_response(&serde_json::json!({}))
        }
        "ListProjects" => {
            let items = store::list(db, "codebuild", "project", &HashMap::new()).unwrap_or_default();
            let names: Vec<&str> = items.iter()
                .filter_map(|p| p.get("name").and_then(|v| v.as_str()))
                .collect();
            formats::json::success_response(&serde_json::json!({"projects": names}))
        }
        _ => {
            formats::json::error_response(
                StatusCode::BAD_REQUEST,
                "UnknownOperation",
                &format!("CodeBuild operation not implemented: {op}"),
            )
        }
    }
}

// --- Classification helpers ---

fn classify_xml_action(action: &str) -> Option<(&str, &str, &str)> {
    // Extract the operation prefix (Create, Describe, Delete, Modify, etc.)
    let operation = extract_operation_prefix(action);
    if operation == "Unknown" {
        return None;
    }

    // Map action suffix to service + resource type
    let rest = action.strip_prefix(operation)?;
    let (service, resource_type) = match rest {
        s if s.starts_with("Vpc") => ("ec2", "vpc"),
        s if s.starts_with("Subnet") => ("ec2", "subnet"),
        s if s.starts_with("SecurityGroup") => ("ec2", "security-group"),
        s if s.starts_with("InternetGateway") => ("ec2", "internet-gateway"),
        s if s.starts_with("NatGateway") => ("ec2", "nat-gateway"),
        s if s.starts_with("RouteTable") => ("ec2", "route-table"),
        s if s.starts_with("Route") && !s.starts_with("RouteTable") => ("ec2", "route"),
        s if s.starts_with("Address") || s.starts_with("ElasticIp") => ("ec2", "elastic-ip"),
        s if s.starts_with("InstanceProfile") => ("ec2", "instance-profile"),
        s if s.starts_with("Instance") => ("ec2", "instance"),
        s if s.starts_with("LaunchTemplate") => ("ec2", "launch-template"),
        s if s.starts_with("Volume") => ("ec2", "volume"),
        s if s.starts_with("Image") => ("ec2", "image"),
        s if s.starts_with("PlacementGroup") => ("ec2", "placement-group"),
        s if s.starts_with("NetworkInterface") => ("ec2", "network-interface"),
        s if s.starts_with("FlowLog") => ("ec2", "flow-log"),
        s if s.starts_with("NetworkAcl") => ("ec2", "network-acl"),
        s if s.starts_with("Tag") => ("ec2", "tag"),
        s if s.starts_with("SpotInstanceRequest") || s.starts_with("SpotInstance") => {
            ("ec2", "spot-instance")
        }
        s if s.starts_with("LoadBalancer") => ("elasticloadbalancing", "loadbalancer"),
        s if s.starts_with("TargetGroup") => ("elasticloadbalancing", "targetgroup"),
        s if s.starts_with("ListenerRule") => ("elasticloadbalancing", "listener-rule"),
        s if s.starts_with("Listener") => ("elasticloadbalancing", "listener"),
        s if s.starts_with("AutoScalingGroup") => ("autoscaling", "group"),
        s if s.starts_with("ScalingPolicy") || s.starts_with("Policy") => {
            ("autoscaling", "policy")
        }
        s if s.starts_with("ScalableTarget") || s.starts_with("ScalingTarget") => {
            ("autoscaling", "target")
        }
        s if s.starts_with("Certificate") => ("acm", "certificate"),
        s if s.starts_with("ResourceRecordSet") || s.starts_with("HostedZone") => {
            ("route53", "record")
        }
        s if s.starts_with("LogGroup") => ("logs", "log-group"),
        s if s.starts_with("Dashboard") => ("cloudwatch", "dashboard"),
        s if s.starts_with("MetricAlarm") || s.starts_with("Alarm") => ("cloudwatch", "alarm"),
        s if s.starts_with("EventRule") => ("events", "rule"),
        s if s.starts_with("EventTarget") => ("events", "target"),
        s if s.starts_with("LifecyclePolicy") => ("dlm", "policy"),
        s if s.starts_with("BackupPlan") => ("backup", "plan"),
        s if s.starts_with("BackupVault") => ("backup", "vault"),
        s if s.starts_with("BackupSelection") => ("backup", "selection"),
        s if s.starts_with("WebAcl") || s.starts_with("WebACL") => ("wafv2", "webacl"),
        // EFS
        s if s.starts_with("FileSystem") => ("efs", "file-system"),
        s if s.starts_with("MountTarget") => ("efs", "mount-target"),
        s if s.starts_with("AccessPoint") => ("efs", "access-point"),
        // Batch
        s if s.starts_with("ComputeEnvironment") => ("batch", "compute-environment"),
        s if s.starts_with("JobQueue") => ("batch", "job-queue"),
        s if s.starts_with("JobDefinition") => ("batch", "job-definition"),
        // EventBridge Pipes
        s if s.starts_with("Pipe") => ("pipes", "pipe"),
        // GuardDuty
        s if s.starts_with("Detector") && !s.starts_with("DetectorFeature") => ("guardduty", "detector"),
        s if s.starts_with("DetectorFeature") => ("guardduty", "detector-feature"),
        // CloudTrail
        s if s.starts_with("Trail") => ("cloudtrail", "trail"),
        // ElastiCache
        s if s.starts_with("ReplicationGroup") => ("elasticache", "replication-group"),
        s if s.starts_with("CacheCluster") => ("elasticache", "cache-cluster"),
        s if s.starts_with("CacheSubnetGroup") || (s.starts_with("SubnetGroup") && !s.starts_with("SubnetGroupId")) => ("elasticache", "subnet-group"),
        // Timestream
        s if s.starts_with("Database") => ("timestream", "database"),
        s if s.starts_with("Table") => ("timestream", "table"),
        // SecurityHub
        s if s.starts_with("Standards") => ("securityhub", "standards-subscription"),
        s if s.starts_with("Hub") || s.starts_with("Account") => ("securityhub", "account"),
        // Inspector
        s if s.starts_with("Enabler") => ("inspector2", "enabler"),
        // Config
        s if s.starts_with("ConfigurationAggregator") => ("config", "aggregator"),
        // Control Tower
        s if s.starts_with("ControlTower") => ("controltower", "control"),
        // Access Analyzer
        s if s.starts_with("Analyzer") => ("accessanalyzer", "analyzer"),
        // Network ACL
        s if s.starts_with("NetworkAcl") => ("ec2", "network-acl"),
        s if s.starts_with("NetworkAclEntry") => ("ec2", "network-acl-entry"),
        // VPC Flow Logs
        s if s.starts_with("FlowLog") => ("ec2", "flow-log"),
        _ => return None,
    };
    Some((service, resource_type, operation))
}

fn classify_json_target(target: &str) -> Option<(&str, &str, &str)> {
    let op = target.rsplit('.').next()?;
    let operation = extract_operation_prefix(op);
    if operation == "Unknown" {
        return None;
    }
    let rest = op.strip_prefix(operation)?;
    if rest.is_empty() {
        return None;
    }

    let prefix = target.split('.').next().unwrap_or("");
    let (service, resource_type) = match prefix {
        p if p.contains("SSM") || p.contains("SimpleSystemsManager") => match rest {
            s if s.contains("Parameter") => ("ssm", "parameter"),
            _ => ("ssm", "parameter"),
        },
        p if p.contains("Lambda") => ("lambda", "function"),
        p if p.contains("SecretsManager") || p.contains("secretsmanager") => match rest {
            s if s.contains("Secret") => ("secretsmanager", "secret"),
            _ => ("secretsmanager", "secret"),
        },
        p if p.contains("KMS") || p.contains("TrentService") => ("kms", "key"),
        p if p.contains("CodeBuild") => match rest {
            s if s.contains("SourceCredential") => ("codebuild", "source-credential"),
            _ => ("codebuild", "project"),
        },
        p if p.contains("WAFv2") || p.contains("AWSWAF") => ("wafv2", "webacl"),
        // EFS (uses JSON protocol via elasticfilesystem target)
        p if p.contains("ElasticFileSystem") => match rest {
            s if s.contains("FileSystem") => ("efs", "file-system"),
            s if s.contains("MountTarget") => ("efs", "mount-target"),
            s if s.contains("AccessPoint") => ("efs", "access-point"),
            _ => ("efs", "file-system"),
        },
        // EventBridge (AWSEvents.PutRule, etc.)
        p if p.contains("AWSEvents") || p.contains("Events") => match rest {
            s if s.contains("Rule") => ("events", "rule"),
            s if s.contains("Target") => ("events", "target"),
            _ => ("events", "rule"),
        },
        // EventBridge Pipes
        p if p.contains("Pipes") => ("pipes", "pipe"),
        // Batch
        p if p.contains("Batch") => match rest {
            s if s.contains("ComputeEnvironment") => ("batch", "compute-environment"),
            s if s.contains("JobQueue") => ("batch", "job-queue"),
            s if s.contains("JobDefinition") => ("batch", "job-definition"),
            _ => ("batch", "job-definition"),
        },
        // SecurityHub
        p if p.contains("SecurityHub") => ("securityhub", "account"),
        // GuardDuty
        p if p.contains("GuardDuty") => ("guardduty", "detector"),
        // CloudTrail
        p if p.contains("CloudTrail") => ("cloudtrail", "trail"),
        // Timestream
        p if p.contains("Timestream") => match rest {
            s if s.contains("Database") => ("timestream", "database"),
            s if s.contains("Table") => ("timestream", "table"),
            _ => ("timestream", "database"),
        },
        // ElastiCache
        p if p.contains("ElastiCache") => match rest {
            s if s.contains("ReplicationGroup") => ("elasticache", "replication-group"),
            s if s.contains("SubnetGroup") => ("elasticache", "subnet-group"),
            _ => ("elasticache", "replication-group"),
        },
        // Inspector
        p if p.contains("Inspector") => ("inspector2", "enabler"),
        // Config
        p if p.contains("Config") || p.contains("StarlingDove") => ("config", "aggregator"),
        // CloudWatch Logs
        p if p.contains("Logs") => match rest {
            s if s.contains("LogGroup") => ("logs", "log-group"),
            s if s.contains("LogStream") => ("logs", "log-stream"),
            _ => ("logs", "log-group"),
        },
        // Access Analyzer
        p if p.contains("AccessAnalyzer") => ("accessanalyzer", "analyzer"),
        // Application Auto Scaling
        p if p.contains("AnyScaleFrontend") || p.contains("ApplicationAutoScaling") => match rest {
            s if s.contains("Target") => ("autoscaling", "target"),
            s if s.contains("Policy") => ("autoscaling", "policy"),
            _ => ("autoscaling", "target"),
        },
        _ => return None,
    };
    Some((service, resource_type, operation))
}

fn extract_operation_prefix(action: &str) -> &str {
    for prefix in &[
        "Describe",
        "Create",
        "Delete",
        "Modify",
        "Update",
        "List",
        "Get",
        "Put",
        "Set",
        "Remove",
        "Release",
        "Attach",
        "Detach",
        "Associate",
        "Disassociate",
        "Register",
        "Deregister",
        "Add",
        "Allocate",
        "Authorize",
        "Revoke",
        "Enable",
        "Disable",
        "Change",
        "Request",
        "Tag",
        "Untag",
        "Import",
        "Run",
        "Terminate",
        "Start",
        "Stop",
        "Reboot",
        "Cancel",
        "Accept",
        "Reject",
        "Monitor",
        "Unmonitor",
        "Replace",
        "Reset",
        "Restore",
        "Copy",
        "Move",
        "Send",
    ] {
        if action.starts_with(prefix) {
            return prefix;
        }
    }
    "Unknown"
}

fn extract_identifier(
    params: &HashMap<String, String>,
    body: &Value,
    service: &str,
    resource_type: &str,
) -> Option<String> {
    // Try common parameter names
    let id_keys: Vec<&str> = match (service, resource_type) {
        ("ec2", "vpc") => vec!["VpcId"],
        ("ec2", "subnet") => vec!["SubnetId"],
        ("ec2", "security-group") => vec!["GroupId", "GroupName"],
        ("ec2", "instance") => vec!["InstanceId"],
        ("ssm", "parameter") => vec!["Name"],
        ("lambda", "function") => vec!["FunctionName"],
        _ => vec!["Name", "Id"],
    };
    for key in id_keys {
        if let Some(v) = params.get(key) {
            return Some(v.clone());
        }
        if let Some(v) = body.get(key).and_then(|v| v.as_str()) {
            return Some(v.to_string());
        }
    }
    None
}

fn extract_id_for_delete(
    params: &HashMap<String, String>,
    body: &Value,
    service: &str,
    resource_type: &str,
) -> Option<String> {
    extract_identifier(params, body, service, resource_type)
}

fn extract_json_identifier(body: &Value, service: &str, resource_type: &str) -> Option<String> {
    let keys: Vec<&str> = match (service, resource_type) {
        ("ssm", "parameter") => vec!["Name"],
        ("lambda", "function") => vec!["FunctionName"],
        ("secretsmanager", "secret") => vec!["Name", "SecretId"],
        ("kms", "key") => vec!["KeyId"],
        ("codebuild", "project") => vec!["name"],
        _ => vec!["Name", "Id", "name", "id"],
    };
    for key in keys {
        if let Some(v) = body.get(key).and_then(|v| v.as_str()) {
            return Some(v.to_string());
        }
    }
    None
}

fn extract_filters(_params: &HashMap<String, String>) -> HashMap<String, String> {
    // Extract Filter.N.Name/Value patterns from query params
    // For now, return empty — filters can be added later
    HashMap::new()
}

fn params_to_json(params: &HashMap<String, String>) -> Value {
    let map: serde_json::Map<String, Value> = params
        .iter()
        .filter(|(k, _)| *k != "Action" && *k != "Version")
        .map(|(k, v)| (k.clone(), Value::String(v.clone())))
        .collect();
    Value::Object(map)
}

fn store_error_to_xml(e: store::StoreError) -> Response {
    match e {
        store::StoreError::NotFound(m) => {
            formats::xml::error_response(StatusCode::NOT_FOUND, "ResourceNotFound", &m)
        }
        store::StoreError::AlreadyExists(m) => {
            formats::xml::error_response(StatusCode::CONFLICT, "ResourceAlreadyExists", &m)
        }
        store::StoreError::Internal(m) => {
            formats::xml::error_response(StatusCode::INTERNAL_SERVER_ERROR, "InternalError", &m)
        }
    }
}

fn store_error_to_json(e: store::StoreError) -> Response {
    match e {
        store::StoreError::NotFound(m) => formats::json::error_response(
            StatusCode::NOT_FOUND,
            "ResourceNotFoundException",
            &m,
        ),
        store::StoreError::AlreadyExists(m) => formats::json::error_response(
            StatusCode::CONFLICT,
            "ResourceAlreadyExistsException",
            &m,
        ),
        store::StoreError::Internal(m) => formats::json::error_response(
            StatusCode::INTERNAL_SERVER_ERROR,
            "InternalError",
            &m,
        ),
    }
}
