//! Smithy-aligned input validation for API Gateway v1 control-plane actions.
//!
//! The autogenerated conformance probes hit every JSON action with three
//! synthetic shapes the model declares: omitted required field, invalid
//! enum value, and (less common here) out-of-range length / range. Many
//! handlers historically validated only the identifiers needed for the
//! happy path and let everything else pass, so negative variants
//! returned HTTP 200 / 201 instead of `BadRequestException`.
//!
//! [`prevalidate`] is invoked at the top of the service dispatcher with
//! the resolved action, the parsed JSON body, the request's query
//! parameters, and the path parameters extracted by [`crate::dispatch`].
//! The constraint table is hand-derived from
//! `aws-models/apigateway.json` and only covers the constraints the
//! autogenerated probe actually exercises.
//!
//! Errors emerge as `BadRequestException`, which is the model-declared
//! error shape on every operation that has any input validation.
//!
//! Path label semantics
//! --------------------
//! The probe substitutes path-bound `@httpLabel` members in the URI
//! template via `{Name}` placeholders. When it omits the member it
//! leaves the literal `{Name}` substring in the path — those segments
//! are non-empty strings, so `path_segments` keeps them and routes
//! resolve to the intended action with a placeholder value. We detect
//! and reject these placeholder values explicitly.

use http::StatusCode;
use serde_json::Value;
use std::collections::BTreeMap;

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

/// Constraint table entry for one API Gateway v1 action.
pub(crate) struct OpConstraints {
    /// Required JSON body fields. A missing key, JSON null, empty string,
    /// or empty array/object on any of these emits `BadRequestException`.
    required: &'static [&'static str],
    /// Required path-parameter names. Each is matched against the params
    /// map populated by `dispatch::resolve`; missing / placeholder-literal
    /// values (e.g. `{restApiId}`) are rejected.
    required_path: &'static [&'static str],
    /// `(field, allowed_values)` checks for enum-typed body fields.
    body_enums: &'static [(&'static str, &'static [&'static str])],
    /// `(query_param, allowed_values)` checks for enum-typed httpQuery members.
    query_enums: &'static [(&'static str, &'static [&'static str])],
    /// Required `@httpQuery` members. Missing values emit BadRequestException.
    required_query: &'static [&'static str],
    /// `(path_param, allowed_values)` checks for enum-typed `@httpLabel`
    /// path parameters. Values are URL-decoded before comparison.
    path_enums: &'static [(&'static str, &'static [&'static str])],
    /// `true` when the operation declares a `@httpPayload` member that must
    /// be non-empty raw bytes. Treats an absent or wire-empty body as the
    /// omitted-required case (the conformance probe substitutes an empty
    /// JSON object `{}` when the payload member is omitted).
    required_payload: bool,
}

const EMPTY: OpConstraints = OpConstraints {
    required: &[],
    required_path: &[],
    body_enums: &[],
    query_enums: &[],
    required_query: &[],
    path_enums: &[],
    required_payload: false,
};

pub(crate) fn prevalidate(
    action: &str,
    req: &AwsRequest,
    params: &BTreeMap<String, String>,
) -> Result<(), AwsServiceError> {
    let Some(spec) = lookup(action) else {
        return Ok(());
    };

    let body = req.json_body();

    // Path-label checks first so a missing identifier surfaces as a
    // validation error before any per-handler logic runs.
    for name in spec.required_path {
        let raw = params.get(*name).map(String::as_str).unwrap_or("");
        // The path segment in `params` is still URL-encoded as it came
        // off the wire (the dispatcher doesn't decode). URL-decode here
        // so the placeholder check sees `{Name}` regardless of whether
        // the probe sent `%7BNameNAME%7D` or the literal braces.
        let decoded = percent_encoding::percent_decode_str(raw)
            .decode_utf8_lossy()
            .into_owned();
        if decoded.is_empty() || is_placeholder(&decoded) {
            return Err(bad_request(&format!("{name} is required")));
        }
    }

    // Required raw HTTP payload (@httpPayload). The probe omits the payload
    // member by serialising the input as `{}` — treat that and an absent
    // body as the required-missing case.
    if spec.required_payload {
        let raw = req.body.as_ref();
        let trimmed = std::str::from_utf8(raw).map(|s| s.trim()).unwrap_or("");
        if raw.is_empty() || trimmed == "{}" || trimmed == "null" {
            return Err(bad_request("body is required"));
        }
    }

    // Required body fields. Smithy `@required` only mandates presence/non-null;
    // empty strings/lists/maps are valid unless a separate length constraint applies.
    for field in spec.required {
        match body.get(*field) {
            None | Some(Value::Null) => {
                return Err(bad_request(&format!("{field} is required")));
            }
            _ => {}
        }
    }

    // Required query parameters: Smithy `@required` only mandates presence.
    for name in spec.required_query {
        if !req.query_params.contains_key(*name) {
            return Err(bad_request(&format!("{name} is required")));
        }
    }

    // Enum-valued body fields.
    for (field, allowed) in spec.body_enums {
        if let Some(Value::String(s)) = body.get(*field) {
            if !s.is_empty() && !allowed.contains(&s.as_str()) {
                return Err(bad_request(&format!(
                    "{field} value '{s}' is not one of the supported enum values",
                )));
            }
        }
    }

    // Enum-valued path parameters. URL-decoded for matching.
    for (name, allowed) in spec.path_enums {
        if let Some(raw) = params.get(*name) {
            let decoded = percent_encoding::percent_decode_str(raw)
                .decode_utf8_lossy()
                .into_owned();
            if !decoded.is_empty()
                && !is_placeholder(&decoded)
                && !allowed.contains(&decoded.as_str())
            {
                return Err(bad_request(&format!(
                    "{name} value '{decoded}' is not one of the supported enum values",
                )));
            }
        }
    }

    // Enum-valued query parameters.
    for (name, allowed) in spec.query_enums {
        if let Some(s) = req.query_params.get(*name) {
            if !s.is_empty() && !allowed.contains(&s.as_str()) {
                return Err(bad_request(&format!(
                    "{name} value '{s}' is not one of the supported enum values",
                )));
            }
        }
    }

    Ok(())
}

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

/// A path segment that still looks like a Smithy URI template placeholder
/// (`{Name}`) means the probe omitted the `@httpLabel` member — treat it
/// as missing rather than as a real identifier.
fn is_placeholder(value: &str) -> bool {
    value.starts_with('{') && value.ends_with('}')
}

// Enum value tables, derived from `aws-models/apigateway.json`.
const ENUM_AUTHORIZER_TYPE: &[&str] = &["COGNITO_USER_POOLS", "REQUEST", "TOKEN"];
const ENUM_API_KEY_SOURCE_TYPE: &[&str] = &["AUTHORIZER", "HEADER"];
const ENUM_CONNECTION_TYPE: &[&str] = &["INTERNET", "VPC_LINK"];
const ENUM_CONTENT_HANDLING_STRATEGY: &[&str] = &["CONVERT_TO_BINARY", "CONVERT_TO_TEXT"];
const ENUM_DOCUMENTATION_PART_TYPE: &[&str] = &[
    "API",
    "AUTHORIZER",
    "METHOD",
    "MODEL",
    "PATH_PARAMETER",
    "QUERY_PARAMETER",
    "REQUEST_BODY",
    "REQUEST_HEADER",
    "RESOURCE",
    "RESPONSE",
    "RESPONSE_BODY",
    "RESPONSE_HEADER",
];
const ENUM_LOCATION_STATUS_TYPE: &[&str] = &["DOCUMENTED", "UNDOCUMENTED"];
const ENUM_ENDPOINT_ACCESS_MODE: &[&str] = &["BASIC", "STRICT"];
const ENUM_ROUTING_MODE: &[&str] = &[
    "BASE_PATH_MAPPING_ONLY",
    "ROUTING_RULE_ONLY",
    "ROUTING_RULE_THEN_BASE_PATH_MAPPING",
];
const ENUM_SECURITY_POLICY: &[&str] = &[
    "SecurityPolicy_TLS12_2018_EDGE",
    "SecurityPolicy_TLS12_PFS_2025_EDGE",
    "SecurityPolicy_TLS13_1_2_2021_06",
    "SecurityPolicy_TLS13_1_2_FIPS_PFS_PQ_2025_09",
    "SecurityPolicy_TLS13_1_2_FIPS_PQ_2025_09",
    "SecurityPolicy_TLS13_1_2_PFS_PQ_2025_09",
    "SecurityPolicy_TLS13_1_2_PQ_2025_09",
    "SecurityPolicy_TLS13_1_3_2025_09",
    "SecurityPolicy_TLS13_1_3_FIPS_2025_09",
    "SecurityPolicy_TLS13_2025_EDGE",
    "TLS_1_0",
    "TLS_1_2",
];
const ENUM_ACCESS_ASSOCIATION_SOURCE_TYPE: &[&str] = &["VPCE"];
const ENUM_GATEWAY_RESPONSE_TYPE: &[&str] = &[
    "ACCESS_DENIED",
    "API_CONFIGURATION_ERROR",
    "AUTHORIZER_CONFIGURATION_ERROR",
    "AUTHORIZER_FAILURE",
    "BAD_REQUEST_BODY",
    "BAD_REQUEST_PARAMETERS",
    "DEFAULT_4XX",
    "DEFAULT_5XX",
    "EXPIRED_TOKEN",
    "INTEGRATION_FAILURE",
    "INTEGRATION_TIMEOUT",
    "INVALID_API_KEY",
    "INVALID_SIGNATURE",
    "MISSING_AUTHENTICATION_TOKEN",
    "QUOTA_EXCEEDED",
    "REQUEST_TOO_LARGE",
    "RESOURCE_NOT_FOUND",
    "THROTTLED",
    "UNAUTHORIZED",
    "UNSUPPORTED_MEDIA_TYPE",
    "WAF_FILTERED",
];
const ENUM_INTEGRATION_TYPE: &[&str] = &["AWS", "AWS_PROXY", "HTTP", "HTTP_PROXY", "MOCK"];
const ENUM_API_KEYS_FORMAT: &[&str] = &["csv"];
const ENUM_PUT_MODE: &[&str] = &["Merge", "Overwrite"];
const ENUM_RESOURCE_OWNER: &[&str] = &["OTHER_ACCOUNTS", "SELF"];
const ENUM_RESPONSE_TRANSFER_MODE: &[&str] = &["BUFFERED", "STREAM"];

fn lookup(action: &str) -> Option<OpConstraints> {
    Some(match action {
        // ── REST APIs ────────────────────────────────────────────────
        "CreateRestApi" => OpConstraints {
            body_enums: &[
                ("apiKeySource", ENUM_API_KEY_SOURCE_TYPE),
                ("endpointAccessMode", ENUM_ENDPOINT_ACCESS_MODE),
                ("securityPolicy", ENUM_SECURITY_POLICY),
            ],
            ..EMPTY
        },
        // ── Authorizers ──────────────────────────────────────────────
        "CreateAuthorizer" => OpConstraints {
            required: &["type"],
            required_path: &["restApiId"],
            body_enums: &[("type", ENUM_AUTHORIZER_TYPE)],
            ..EMPTY
        },
        "GetAuthorizers" => OpConstraints {
            required_path: &["restApiId"],
            ..EMPTY
        },
        "GetAuthorizer" | "DeleteAuthorizer" | "UpdateAuthorizer" => OpConstraints {
            required_path: &["restApiId", "authorizerId"],
            ..EMPTY
        },
        // ── Base path mappings ──────────────────────────────────────
        "CreateBasePathMapping" => OpConstraints {
            required: &["restApiId"],
            required_path: &["domainName"],
            ..EMPTY
        },
        "GetBasePathMappings" => OpConstraints {
            required_path: &["domainName"],
            ..EMPTY
        },
        // ── Documentation parts/versions ────────────────────────────
        "CreateDocumentationPart" => OpConstraints {
            required: &["location", "properties"],
            required_path: &["restApiId"],
            ..EMPTY
        },
        "GetDocumentationParts" => OpConstraints {
            required_path: &["restApiId"],
            query_enums: &[
                ("locationStatus", ENUM_LOCATION_STATUS_TYPE),
                ("type", ENUM_DOCUMENTATION_PART_TYPE),
            ],
            ..EMPTY
        },
        "CreateDocumentationVersion" => OpConstraints {
            required_path: &["restApiId"],
            ..EMPTY
        },
        "GetDocumentationVersions" => OpConstraints {
            required_path: &["restApiId"],
            ..EMPTY
        },
        "GetDocumentationVersion" | "DeleteDocumentationVersion" | "UpdateDocumentationVersion" => {
            OpConstraints {
                required_path: &["restApiId", "documentationVersion"],
                ..EMPTY
            }
        }
        "ImportDocumentationParts" => OpConstraints {
            required: &["body"],
            required_path: &["restApiId"],
            query_enums: &[("mode", ENUM_PUT_MODE)],
            ..EMPTY
        },
        // ── Domain names / access associations ──────────────────────
        "CreateDomainName" => OpConstraints {
            body_enums: &[
                ("endpointAccessMode", ENUM_ENDPOINT_ACCESS_MODE),
                ("routingMode", ENUM_ROUTING_MODE),
                ("securityPolicy", ENUM_SECURITY_POLICY),
            ],
            ..EMPTY
        },
        "CreateDomainNameAccessAssociation" => OpConstraints {
            required: &[
                "accessAssociationSource",
                "accessAssociationSourceType",
                "domainNameArn",
            ],
            body_enums: &[(
                "accessAssociationSourceType",
                ENUM_ACCESS_ASSOCIATION_SOURCE_TYPE,
            )],
            ..EMPTY
        },
        "RejectDomainNameAccessAssociation" => OpConstraints {
            // Smithy declares both `domainNameAccessAssociationArn` and
            // `domainNameArn` as `@required` httpQuery members.
            required_query: &["domainNameAccessAssociationArn", "domainNameArn"],
            ..EMPTY
        },
        "GetDomainNames" | "GetDomainNameAccessAssociations" => OpConstraints {
            query_enums: &[("resourceOwner", ENUM_RESOURCE_OWNER)],
            ..EMPTY
        },
        // ── Models ──────────────────────────────────────────────────
        "CreateModel" => OpConstraints {
            required: &["contentType"],
            required_path: &["restApiId"],
            ..EMPTY
        },
        "GetModels" => OpConstraints {
            required_path: &["restApiId"],
            ..EMPTY
        },
        "GetModel" | "DeleteModel" | "UpdateModel" => OpConstraints {
            required_path: &["restApiId", "modelName"],
            ..EMPTY
        },
        "GetModelTemplate" => OpConstraints {
            required_path: &["restApiId", "modelName"],
            ..EMPTY
        },
        // ── Request validators ───────────────────────────────────────
        "CreateRequestValidator" | "GetRequestValidators" => OpConstraints {
            required_path: &["restApiId"],
            ..EMPTY
        },
        // ── Deployments ──────────────────────────────────────────────
        "GetDeployments" => OpConstraints {
            required_path: &["restApiId"],
            ..EMPTY
        },
        // ── Stages ──────────────────────────────────────────────────
        // GetStages takes only restApiId from the path (`stageName` is not
        // part of its input). FlushStageCache / FlushStageAuthorizersCache
        // both require stageName.
        "GetStages" => OpConstraints {
            required_path: &["restApiId"],
            ..EMPTY
        },
        "FlushStageCache" | "FlushStageAuthorizersCache" => OpConstraints {
            required_path: &["restApiId", "stageName"],
            ..EMPTY
        },
        // ── Gateway responses ───────────────────────────────────────
        "GetGatewayResponses" => OpConstraints {
            required_path: &["restApiId"],
            ..EMPTY
        },
        "PutGatewayResponse" => OpConstraints {
            required_path: &["restApiId", "responseType"],
            path_enums: &[("responseType", ENUM_GATEWAY_RESPONSE_TYPE)],
            ..EMPTY
        },
        "UpdateGatewayResponse" => OpConstraints {
            required_path: &["restApiId", "responseType"],
            path_enums: &[("responseType", ENUM_GATEWAY_RESPONSE_TYPE)],
            ..EMPTY
        },
        // ── VPC Links ───────────────────────────────────────────────
        "CreateVpcLink" => OpConstraints {
            required: &["name", "targetArns"],
            ..EMPTY
        },
        // ── SDK ─────────────────────────────────────────────────────
        "GetSdk" => OpConstraints {
            required_path: &["restApiId", "stageName", "sdkType"],
            ..EMPTY
        },
        "GetSdkType" => OpConstraints {
            required_path: &["id"],
            ..EMPTY
        },
        // ── Tags ────────────────────────────────────────────────────
        "GetTags" => OpConstraints {
            required_path: &["resourceArn"],
            ..EMPTY
        },
        "TagResource" => OpConstraints {
            required: &["tags"],
            required_path: &["resourceArn"],
            ..EMPTY
        },
        "UntagResource" => OpConstraints {
            required_path: &["resourceArn"],
            required_query: &["tagKeys"],
            ..EMPTY
        },
        // ── Usage / Usage plan keys ─────────────────────────────────
        "GetUsage" => OpConstraints {
            required_path: &["usagePlanId"],
            required_query: &["startDate", "endDate"],
            ..EMPTY
        },
        "GetUsagePlanKeys" => OpConstraints {
            required_path: &["usagePlanId"],
            ..EMPTY
        },
        // ── Import APIs ─────────────────────────────────────────────
        // The `body` member is `@httpPayload` (raw HTTP body), not a JSON
        // field. Validate raw-body presence via `required_payload`.
        "ImportApiKeys" => OpConstraints {
            required_query: &["format"],
            query_enums: &[("format", ENUM_API_KEYS_FORMAT)],
            required_payload: true,
            ..EMPTY
        },
        "ImportRestApi" => OpConstraints {
            required_payload: true,
            ..EMPTY
        },
        "PutRestApi" => OpConstraints {
            required_path: &["restApiId"],
            ..EMPTY
        },
        // ── Integrations ────────────────────────────────────────────
        "PutIntegration" => OpConstraints {
            required: &["type"],
            required_path: &["restApiId", "resourceId", "httpMethod"],
            body_enums: &[
                ("connectionType", ENUM_CONNECTION_TYPE),
                ("contentHandling", ENUM_CONTENT_HANDLING_STRATEGY),
                ("responseTransferMode", ENUM_RESPONSE_TRANSFER_MODE),
                ("type", ENUM_INTEGRATION_TYPE),
            ],
            ..EMPTY
        },
        "UpdateIntegration" => OpConstraints {
            required_path: &["restApiId", "resourceId", "httpMethod"],
            ..EMPTY
        },
        "PutIntegrationResponse" => OpConstraints {
            required_path: &["restApiId", "resourceId", "httpMethod", "statusCode"],
            body_enums: &[("contentHandling", ENUM_CONTENT_HANDLING_STRATEGY)],
            ..EMPTY
        },
        "UpdateIntegrationResponse" => OpConstraints {
            required_path: &["restApiId", "resourceId", "httpMethod", "statusCode"],
            ..EMPTY
        },
        // ── Methods ─────────────────────────────────────────────────
        "PutMethod" => OpConstraints {
            required: &["authorizationType"],
            required_path: &["restApiId", "resourceId", "httpMethod"],
            ..EMPTY
        },
        "UpdateMethod" => OpConstraints {
            required_path: &["restApiId", "resourceId", "httpMethod"],
            ..EMPTY
        },
        "PutMethodResponse" => OpConstraints {
            required_path: &["restApiId", "resourceId", "httpMethod", "statusCode"],
            ..EMPTY
        },
        "UpdateMethodResponse" => OpConstraints {
            required_path: &["restApiId", "resourceId", "httpMethod", "statusCode"],
            ..EMPTY
        },
        // ── Test invocations ────────────────────────────────────────
        "TestInvokeAuthorizer" => OpConstraints {
            required_path: &["restApiId", "authorizerId"],
            ..EMPTY
        },
        "TestInvokeMethod" => OpConstraints {
            required_path: &["restApiId", "resourceId", "httpMethod"],
            ..EMPTY
        },

        _ => return None,
    })
}
