use serde_json::{Map, Value};

use super::error::ProvisioningError;

/// Ensure object input and set key if absent.
pub fn json_object_insert_if_missing(
    input: Option<Value>,
    key: &str,
    value: Value,
) -> Result<Value, ProvisioningError> {
    let mut object: Map<String, Value> = if let Some(input) = input {
        match input {
            Value::Object(map) => map,
            _ => {
                return Err(ProvisioningError::InvalidInput(format!(
                    "input must be a JSON object for key '{}'",
                    key
                )));
            }
        }
    } else {
        Map::new()
    };

    object.entry(key.to_string()).or_insert(value);
    Ok(Value::Object(object))
}

pub(crate) fn extract_connection_uri(value: &Value) -> Option<String> {
    let preferred_pointers = [
        "/uri",
        "/connection_uri",
        "/data/uri",
        "/data/connection_uri",
        "/data/variables/DATABASE_URL",
        "/data/variables/DATABASE_PRIVATE_URL",
        "/data/variables/DATABASE_PUBLIC_URL",
        "/data/variables/POSTGRES_URL",
        "/data/variables/PGDATABASE_URL",
    ];

    for pointer in preferred_pointers {
        if let Some(found) = value.pointer(pointer).and_then(Value::as_str)
            && is_postgres_uri(found)
        {
            return Some(found.to_string());
        }
    }

    extract_from_variables_payload(value).or_else(|| recursive_find_postgres_uri(value))
}

pub(crate) fn extract_render_service_id(value: &Value) -> Option<String> {
    let pointers = [
        "/id",
        "/service/id",
        "/postgres/id",
        "/database/id",
        "/data/id",
        "/data/service/id",
    ];
    for pointer in pointers {
        if let Some(id) = value.pointer(pointer).and_then(Value::as_str)
            && !id.trim().is_empty()
        {
            return Some(id.to_string());
        }
    }
    None
}

pub(crate) fn extract_render_connection_uri(value: &Value) -> Option<String> {
    let pointers = [
        "/connectionString",
        "/databaseUrl",
        "/externalConnectionString",
        "/internalConnectionString",
        "/info/connectionString",
        "/info/databaseUrl",
        "/connectionInfo/connectionString",
        "/connectionInfo/databaseUrl",
        "/postgres/connectionString",
        "/database/connectionString",
        "/data/connectionString",
        "/data/databaseUrl",
    ];
    for pointer in pointers {
        if let Some(candidate) = value.pointer(pointer).and_then(Value::as_str)
            && is_postgres_uri(candidate)
        {
            return Some(candidate.to_string());
        }
    }
    None
}

fn extract_from_variables_payload(value: &Value) -> Option<String> {
    let variables = value
        .pointer("/data/variables")
        .or_else(|| value.get("variables"))?;
    match variables {
        Value::Object(map) => {
            let preferred_keys = [
                "DATABASE_URL",
                "DATABASE_PRIVATE_URL",
                "DATABASE_PUBLIC_URL",
                "POSTGRES_URL",
                "PGDATABASE_URL",
                "DATABASE_CONNECTION_URL",
            ];
            for key in preferred_keys {
                if let Some(candidate) = map.get(key).and_then(Value::as_str)
                    && is_postgres_uri(candidate)
                {
                    return Some(candidate.to_string());
                }
            }
            for candidate in map.values().filter_map(Value::as_str) {
                if is_postgres_uri(candidate) {
                    return Some(candidate.to_string());
                }
            }
            None
        }
        Value::Array(items) => {
            for item in items {
                let name = item.get("name").and_then(Value::as_str).unwrap_or_default();
                let candidate = item
                    .get("value")
                    .or_else(|| item.get("rawValue"))
                    .and_then(Value::as_str);
                if let Some(candidate) = candidate
                    && (name.eq_ignore_ascii_case("DATABASE_URL") || is_postgres_uri(candidate))
                {
                    return Some(candidate.to_string());
                }
            }
            None
        }
        Value::String(text) => {
            if is_postgres_uri(text) {
                Some(text.to_string())
            } else {
                None
            }
        }
        _ => None,
    }
}

fn recursive_find_postgres_uri(value: &Value) -> Option<String> {
    match value {
        Value::String(text) if is_postgres_uri(text) => Some(text.to_string()),
        Value::Array(items) => items.iter().find_map(recursive_find_postgres_uri),
        Value::Object(map) => map.values().find_map(recursive_find_postgres_uri),
        _ => None,
    }
}

fn is_postgres_uri(value: &str) -> bool {
    let lower = value.to_lowercase();
    lower.starts_with("postgres://") || lower.starts_with("postgresql://")
}

#[cfg(test)]
mod tests {
    use super::{extract_connection_uri, extract_render_service_id};
    use serde_json::json;

    #[test]
    fn extract_render_service_id_supports_common_shapes() {
        let payload = json!({ "service": { "id": "srv-123" } });
        assert_eq!(
            extract_render_service_id(&payload),
            Some("srv-123".to_string())
        );
    }

    #[test]
    fn extract_connection_uri_supports_render_connection_payload_shape() {
        let payload = json!({
            "connectionInfo": {
                "connectionString": "postgres://user:pass@host:5432/db"
            }
        });
        assert_eq!(
            extract_connection_uri(&payload),
            Some("postgres://user:pass@host:5432/db".to_string())
        );
    }
}
