// Bitbucket Data Center MCP server — generated by mcpify. Do not hand-edit.
//
// jsonschema-based input/output validation. Schemas themselves are NOT
// embedded directly in this file's logic — they're loaded from a
// per-version co-located generated_schemas(_v<label>).json.zst asset
// (written directly by the Rust generator, not templated, keeping this
// file's size independent of how many operations the source spec
// declares) via `include_bytes!` per known version, so each is baked into
// the compiled binary rather than read from disk at runtime. Because of
// that, a project rebuild (`cargo build`) is required after `mcpify
// add-version` for a new version's schemas to actually take effect here.
//
// The asset is zstd-compressed: every operation's schema embeds a full
// copy of the spec's `$defs` library (a deliberate generator tradeoff —
// see `mcpify`'s `openapi::schema_resolve` — simpler than cross-schema
// `$ref` resolution, at the cost of file size), and a long-distance-
// matching compressor collapses that duplication (measured ~190 MB down
// to tens of KB on a real spec) without requiring any change to the JSON
// Schema shape itself. Decompressed once per version, lazily, into the
// same cache below.

use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};

use serde::Deserialize;
use serde_json::Value;

use crate::core::errors::McpifyError;

#[derive(Debug, Deserialize)]
struct OperationSchemas {
    #[serde(rename = "inputSchema")]
    input_schema: Value,
    #[serde(rename = "outputSchema")]
    output_schema: Value,
}

// mcpify:versions:begin
fn schemas_zst_for(api_version: &str) -> Option<&'static [u8]> {
    match api_version {
        "10.3" => Some(include_bytes!("generated_schemas.json.zst")),
        "10.2" => Some(include_bytes!("generated_schemas_v10.2.json.zst")),
        "10.0" => Some(include_bytes!("generated_schemas_v10.0.json.zst")),
        "9.6" => Some(include_bytes!("generated_schemas_v9.6.json.zst")),
        "9.5" => Some(include_bytes!("generated_schemas_v9.5.json.zst")),
        "9.4" => Some(include_bytes!("generated_schemas_v9.4.json.zst")),
        "9.3" => Some(include_bytes!("generated_schemas_v9.3.json.zst")),
        "9.2" => Some(include_bytes!("generated_schemas_v9.2.json.zst")),
        "9.1" => Some(include_bytes!("generated_schemas_v9.1.json.zst")),
        "9.0" => Some(include_bytes!("generated_schemas_v9.0.json.zst")),
        "8.19" => Some(include_bytes!("generated_schemas_v8.19.json.zst")),
        "8.18" => Some(include_bytes!("generated_schemas_v8.18.json.zst")),
        "8.17" => Some(include_bytes!("generated_schemas_v8.17.json.zst")),
        "8.16" => Some(include_bytes!("generated_schemas_v8.16.json.zst")),
        "8.15" => Some(include_bytes!("generated_schemas_v8.15.json.zst")),
        "8.14" => Some(include_bytes!("generated_schemas_v8.14.json.zst")),
        "8.13" => Some(include_bytes!("generated_schemas_v8.13.json.zst")),
        "8.12" => Some(include_bytes!("generated_schemas_v8.12.json.zst")),
        "8.11" => Some(include_bytes!("generated_schemas_v8.11.json.zst")),
        "8.10" => Some(include_bytes!("generated_schemas_v8.10.json.zst")),
        "8.9" => Some(include_bytes!("generated_schemas_v8.9.json.zst")),
        "8.8" => Some(include_bytes!("generated_schemas_v8.8.json.zst")),
        "8.7" => Some(include_bytes!("generated_schemas_v8.7.json.zst")),
        "8.6" => Some(include_bytes!("generated_schemas_v8.6.json.zst")),
        "8.5" => Some(include_bytes!("generated_schemas_v8.5.json.zst")),
        "8.4" => Some(include_bytes!("generated_schemas_v8.4.json.zst")),
        "8.3" => Some(include_bytes!("generated_schemas_v8.3.json.zst")),
        "8.2" => Some(include_bytes!("generated_schemas_v8.2.json.zst")),
        "8.1" => Some(include_bytes!("generated_schemas_v8.1.json.zst")),
        "8.0" => Some(include_bytes!("generated_schemas_v8.0.json.zst")),
        _ => None,
    }
}
// mcpify:versions:end

fn schemas_by_operation(api_version: &str) -> &'static HashMap<String, OperationSchemas> {
    static SCHEMAS: OnceLock<Mutex<HashMap<String, &'static HashMap<String, OperationSchemas>>>> =
        OnceLock::new();
    static EMPTY: OnceLock<HashMap<String, OperationSchemas>> = OnceLock::new();
    let empty = EMPTY.get_or_init(HashMap::new);

    let cache = SCHEMAS.get_or_init(|| Mutex::new(HashMap::new()));
    let mut cache = cache.lock().unwrap();
    if let Some(schemas) = cache.get(api_version) {
        return schemas;
    }

    let Some(compressed) = schemas_zst_for(api_version) else {
        return empty;
    };
    let parsed: HashMap<String, OperationSchemas> = zstd::decode_all(compressed)
        .ok()
        .and_then(|json| serde_json::from_slice(&json).ok())
        .unwrap_or_default();
    let leaked: &'static HashMap<String, OperationSchemas> = Box::leak(Box::new(parsed));
    cache.insert(api_version.to_string(), leaked);
    leaked
}

fn validate_against(
    cache: &Mutex<HashMap<String, jsonschema::Validator>>,
    operation_id: &str,
    schema: &Value,
    data: &Value,
) -> Result<(), Vec<String>> {
    let mut cache = cache.lock().unwrap();
    let validator = cache.entry(operation_id.to_string()).or_insert_with(|| {
        jsonschema::validator_for(schema).unwrap_or_else(|_| {
            jsonschema::validator_for(&Value::Object(Default::default())).unwrap()
        })
    });

    if validator.is_valid(data) {
        return Ok(());
    }
    let errors = validator
        .iter_errors(data)
        .map(|error| error.to_string())
        .collect::<Vec<_>>();
    Err(errors)
}

/// Returns `Err` if `data` doesn't satisfy `operation_id`'s input schema,
/// for the given `api_version`.
pub fn validate_input(
    api_version: &str,
    operation_id: &str,
    data: &Value,
) -> Result<(), McpifyError> {
    static CACHE: OnceLock<Mutex<HashMap<String, jsonschema::Validator>>> = OnceLock::new();
    let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));

    let empty = Value::Object(Default::default());
    let schema = schemas_by_operation(api_version)
        .get(operation_id)
        .map(|schemas| &schemas.input_schema)
        .unwrap_or(&empty);

    let cache_key = format!("{api_version} {operation_id}");
    validate_against(cache, &cache_key, schema, data).map_err(|errors| McpifyError::Validation {
        message: format!("invalid input for '{operation_id}'"),
        details: Some(serde_json::json!(errors)),
    })
}

/// Returns `Err` if `data` doesn't satisfy `operation_id`'s output schema
/// — surfaces upstream API drift as a structured error rather than
/// silently returning a mismatched response (architecture.md's `call`
/// pipeline). Validates against the given `api_version`'s schema.
pub fn validate_output(
    api_version: &str,
    operation_id: &str,
    data: &Value,
) -> Result<(), McpifyError> {
    static CACHE: OnceLock<Mutex<HashMap<String, jsonschema::Validator>>> = OnceLock::new();
    let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));

    let empty = Value::Object(Default::default());
    let schema = schemas_by_operation(api_version)
        .get(operation_id)
        .map(|schemas| &schemas.output_schema)
        .unwrap_or(&empty);

    let cache_key = format!("{api_version} {operation_id}");
    validate_against(cache, &cache_key, schema, data).map_err(|errors| McpifyError::Validation {
        message: format!("unexpected response shape for '{operation_id}'"),
        details: Some(serde_json::json!(errors)),
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    // These tests deliberately use an operationId that generated_schemas.json
    // never declares — they exercise the "no schema found" fallback (an
    // always-valid `{}` schema), rather than any spec-dependent content
    // this project's own generation happened to produce.

    #[test]
    fn an_unknown_operation_id_falls_back_to_an_always_valid_schema() {
        assert!(
            validate_input(
                "10.3",
                "__unknown_operation__",
                &serde_json::json!({"anything": true})
            )
            .is_ok()
        );
        assert!(validate_output("10.3", "__unknown_operation__", &serde_json::json!(42)).is_ok());
    }

    #[test]
    fn an_unknown_api_version_also_falls_back_to_an_always_valid_schema() {
        assert!(
            validate_input(
                "__unknown_version__",
                "__unknown_operation__",
                &serde_json::json!({"anything": true})
            )
            .is_ok()
        );
    }

    #[test]
    fn validate_against_reports_every_schema_violation() {
        static CACHE: OnceLock<Mutex<HashMap<String, jsonschema::Validator>>> = OnceLock::new();
        let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
        let schema = serde_json::json!({
            "type": "object",
            "required": ["name"],
            "properties": { "name": { "type": "string" } },
        });

        let ok = validate_against(
            cache,
            "test_op_ok",
            &schema,
            &serde_json::json!({"name": "widget"}),
        );
        assert!(ok.is_ok());

        let err = validate_against(cache, "test_op_missing", &schema, &serde_json::json!({}));
        assert!(err.is_err());
    }
}
