use serde_json::{json, Map, Value};
use sha2::{Digest, Sha256};

pub fn now_iso() -> String {
    chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
}

pub fn millis_now() -> i64 {
    chrono::Utc::now().timestamp_millis()
}

pub fn error_json(error: &(dyn std::error::Error + 'static)) -> Value {
    json!({ "name": "Error", "message": error.to_string() })
}

pub fn compact_json(value: &Value) -> Value {
    compact_json_depth(value, 0)
}

fn compact_json_depth(value: &Value, depth: usize) -> Value {
    match value {
        Value::Null | Value::Bool(_) | Value::Number(_) => value.clone(),
        Value::String(text) => {
            if text.len() > 600 {
                Value::String(format!(
                    "{}...<truncated:{}>",
                    &text[..600.min(text.len())],
                    text.len()
                ))
            } else {
                value.clone()
            }
        }
        Value::Array(items) => {
            let mut result: Vec<Value> = items
                .iter()
                .take(20)
                .map(|item| compact_json_depth(item, depth + 1))
                .collect();
            if items.len() > 20 {
                result.push(json!({ "truncatedItems": items.len() - 20 }));
            }
            Value::Array(result)
        }
        Value::Object(object) => {
            if depth >= 4 {
                return Value::String("<truncated:depth>".to_string());
            }
            let mut result = Map::new();
            for (key, item) in object.iter().take(30) {
                result.insert(key.clone(), compact_json_depth(item, depth + 1));
            }
            if object.len() > 30 {
                result.insert("truncatedKeys".to_string(), json!(object.len() - 30));
            }
            Value::Object(result)
        }
    }
}

fn sha256_text(value: &str) -> String {
    let mut hasher = Sha256::new();
    hasher.update(value.as_bytes());
    format!("{:x}", hasher.finalize())
}

fn bounded_string(value: &str, limit: usize) -> Value {
    if value.len() <= limit {
        return Value::String(value.to_string());
    }
    json!({
        "omitted": true,
        "omittedReason": "string too large for database inline JSON storage",
        "chars": value.len(),
        "preview": &value[..limit.min(value.len())],
        "sha256": sha256_text(value),
    })
}

fn compact_body_text(value: &str) -> Value {
    const BODY_INLINE_LIMIT: usize = 16_384;
    const BODY_PREVIEW_LIMIT: usize = 2_000;
    if value.len() <= BODY_INLINE_LIMIT {
        return Value::String(value.to_string());
    }
    json!({
        "omitted": true,
        "omittedReason": "microservice HTTP body is stored as metadata only",
        "bodyPreview": &value[..BODY_PREVIEW_LIMIT.min(value.len())],
        "bodyTextChars": value.len(),
        "bodyTextSha256": sha256_text(value),
    })
}

pub fn compact_json_for_storage(value: &Value) -> Value {
    compact_json_for_storage_inner(value, 0, "")
}

fn compact_json_for_storage_inner(value: &Value, depth: usize, key: &str) -> Value {
    const DEFAULT_STRING_LIMIT: usize = 8_000;
    const MAX_ARRAY_ITEMS: usize = 100;
    const MAX_OBJECT_KEYS: usize = 100;
    const MAX_DEPTH: usize = 8;
    match value {
        Value::Null | Value::Bool(_) | Value::Number(_) => value.clone(),
        Value::String(text) => {
            if key == "bodyText" {
                compact_body_text(text)
            } else {
                bounded_string(text, DEFAULT_STRING_LIMIT)
            }
        }
        Value::Array(items) => {
            let mut result: Vec<Value> = items
                .iter()
                .take(MAX_ARRAY_ITEMS)
                .map(|item| compact_json_for_storage_inner(item, depth + 1, ""))
                .collect();
            if items.len() > MAX_ARRAY_ITEMS {
                result.push(json!({ "truncatedItems": items.len() - MAX_ARRAY_ITEMS }));
            }
            Value::Array(result)
        }
        Value::Object(object) => {
            if depth >= MAX_DEPTH {
                return Value::String("<truncated:depth>".to_string());
            }
            let mut result = Map::new();
            for (child_key, item) in object.iter().take(MAX_OBJECT_KEYS) {
                if child_key == "bodyText" {
                    if let Some(text) = item.as_str() {
                        if let Value::Object(map) = compact_body_text(text) {
                            for (key, value) in map {
                                result.insert(key, value);
                            }
                            continue;
                        }
                    }
                }
                result.insert(
                    child_key.clone(),
                    compact_json_for_storage_inner(item, depth + 1, child_key),
                );
            }
            if object.len() > MAX_OBJECT_KEYS {
                result.insert(
                    "truncatedKeys".to_string(),
                    json!(object.len() - MAX_OBJECT_KEYS),
                );
            }
            Value::Object(result)
        }
    }
}

pub fn nested_number(value: &Value, object_key: &str, number_key: &str) -> f64 {
    value
        .get(object_key)
        .and_then(|object| object.get(number_key))
        .and_then(|value| {
            value
                .as_f64()
                .or_else(|| value.as_str().and_then(|text| text.parse::<f64>().ok()))
        })
        .unwrap_or(0.0)
}

pub fn redact_database_url(value: &str) -> String {
    match url::Url::parse(value) {
        Ok(mut url) => {
            if url.password().is_some() {
                let _ = url.set_password(Some("***"));
            }
            url.to_string()
        }
        Err(_) => "<invalid-url>".to_string(),
    }
}

pub fn is_plain_record(value: &Value) -> bool {
    matches!(value, Value::Object(_))
}

pub fn truncate_text(value: &str, max_chars: usize) -> String {
    if value.len() <= max_chars {
        value.to_string()
    } else {
        value[..max_chars.min(value.len())].to_string()
    }
}

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

    #[test]
    fn compact_json_for_storage_replaces_large_body_text_with_metadata() {
        let body_text = "x".repeat(20_000);
        let compacted = compact_json_for_storage(&json!({
            "status": 200,
            "bodyText": body_text,
        }));

        assert_eq!(compacted.get("status").and_then(Value::as_i64), Some(200));
        assert!(compacted.get("bodyText").is_none());
        assert_eq!(
            compacted.get("omitted").and_then(Value::as_bool),
            Some(true)
        );
        assert_eq!(
            compacted.get("bodyTextChars").and_then(Value::as_u64),
            Some(20_000)
        );
        assert_eq!(
            compacted
                .get("bodyPreview")
                .and_then(Value::as_str)
                .map(str::len),
            Some(2_000)
        );
        assert_eq!(
            compacted
                .get("bodyTextSha256")
                .and_then(Value::as_str)
                .map(str::len),
            Some(64)
        );
    }

    #[test]
    fn compact_json_for_storage_bounds_large_regular_strings() {
        let compacted = compact_json_for_storage(&json!({
            "large": "a".repeat(9_000),
        }));
        let large = compacted.get("large").expect("large field");

        assert_eq!(large.get("omitted").and_then(Value::as_bool), Some(true));
        assert_eq!(large.get("chars").and_then(Value::as_u64), Some(9_000));
        assert_eq!(
            large.get("preview").and_then(Value::as_str).map(str::len),
            Some(8_000)
        );
    }
}
