/// Utilities for masking sensitive or verbose content in logs
use serde_json::Value;

/// Truncate long strings to a reasonable length for logging
const MAX_LOG_LENGTH: usize = 100;

/// Extract filename from a file path (handles both Unix and Windows paths)
fn extract_filename(path: &str) -> &str {
    // Try Unix-style path separator first, then Windows
    if path.contains('/') {
        path.split('/').last().unwrap_or("file")
    } else if path.contains('\\') {
        path.split('\\').last().unwrap_or("file")
    } else {
        // No path separator, just a filename
        path
    }
}

/// Mask text content in JSON values for concise logging
///
/// This function recursively processes JSON and replaces long text fields
/// with truncated versions or placeholders, while preserving structure
/// and non-text metadata.
pub fn mask_content(value: &Value) -> Value {
    match value {
        Value::String(s) => {
            if s.len() > MAX_LOG_LENGTH {
                Value::String(format!("... ({} chars)", s.len()))
            } else {
                Value::String(s.clone())
            }
        }
        Value::Array(arr) => {
            Value::Array(arr.iter().map(mask_content).collect())
        }
        Value::Object(obj) => {
            let mut masked = serde_json::Map::new();
            for (k, v) in obj {
                // Mask known content fields (only if they're strings)
                if k == "text" || k == "content_md" || k == "message" || k == "thinking" {
                    if let Value::String(s) = v {
                        if s.len() > 50 {
                            masked.insert(k.clone(), Value::String(format!("... ({} chars)", s.len())));
                        } else if s.len() > 0 {
                            masked.insert(k.clone(), Value::String("...".to_string()));
                        } else {
                            masked.insert(k.clone(), Value::String("".to_string()));
                        }
                    } else {
                        // Not a string, recurse (e.g., content as object)
                        masked.insert(k.clone(), mask_content(v));
                    }
                }
                // Mask raw input/output fields (large blobs of data)
                else if k == "rawOutput" || k == "rawInput" || k == "raw_output" || k == "raw_input" {
                    if let Value::String(s) = v {
                        if s.len() > 50 {
                            masked.insert(k.clone(), Value::String(format!("... ({} chars)", s.len())));
                        } else if s.len() > 0 {
                            masked.insert(k.clone(), Value::String("...".to_string()));
                        } else {
                            masked.insert(k.clone(), Value::String("".to_string()));
                        }
                    } else {
                        // Not a string, recurse
                        masked.insert(k.clone(), mask_content(v));
                    }
                }
                // Mask filename/path fields
                else if k == "filename" || k == "file" || k == "path" || k == "file_path" || k == "filepath" {
                    if let Value::String(s) = v {
                        // Extract just the filename from path for debugging context
                        let filename = extract_filename(s);
                        masked.insert(k.clone(), Value::String(format!("<{}>", filename)));
                    } else if let Value::Array(arr) = v {
                        // Array of filenames
                        let masked_arr: Vec<Value> = arr.iter().map(|item| {
                            if let Value::String(s) = item {
                                let filename = extract_filename(s);
                                Value::String(format!("<{}>", filename))
                            } else {
                                item.clone()
                            }
                        }).collect();
                        masked.insert(k.clone(), Value::Array(masked_arr));
                    } else {
                        masked.insert(k.clone(), mask_content(v));
                    }
                }
                // Mask arrays of filenames
                else if k == "filenames" || k == "files" || k == "paths" {
                    if let Value::Array(arr) = v {
                        let masked_arr: Vec<Value> = arr.iter().map(|item| {
                            if let Value::String(s) = item {
                                let filename = extract_filename(s);
                                Value::String(format!("<{}>", filename))
                            } else {
                                mask_content(item)
                            }
                        }).collect();
                        masked.insert(k.clone(), Value::Array(masked_arr));
                    } else {
                        masked.insert(k.clone(), mask_content(v));
                    }
                }
                else {
                    masked.insert(k.clone(), mask_content(v));
                }
            }
            Value::Object(masked)
        }
        _ => value.clone(),
    }
}

/// Format a Value for logging with content masked
pub fn format_for_log(value: &Value) -> String {
    let masked = mask_content(value);
    serde_json::to_string(&masked).unwrap_or_else(|_| "{}".to_string())
}

/// Mask content in a JSON string, returning a masked JSON string
pub fn mask_json_string(json_str: &str) -> String {
    match serde_json::from_str::<Value>(json_str) {
        Ok(value) => format_for_log(&value),
        Err(_) => {
            // If not valid JSON, just truncate
            if json_str.len() > MAX_LOG_LENGTH {
                format!("{}... ({} bytes)", &json_str[..MAX_LOG_LENGTH], json_str.len())
            } else {
                json_str.to_string()
            }
        }
    }
}

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

    #[test]
    fn test_mask_short_string() {
        let value = Value::String("short".to_string());
        let masked = mask_content(&value);
        assert_eq!(masked, Value::String("short".to_string()));
    }

    #[test]
    fn test_mask_long_string() {
        let long_str = "a".repeat(200);
        let value = Value::String(long_str.clone());
        let masked = mask_content(&value);
        if let Value::String(s) = masked {
            assert!(s.contains("(200 chars)"));
            assert!(s.len() < long_str.len());
        } else {
            panic!("Expected string");
        }
    }

    #[test]
    fn test_mask_text_field() {
        let value = json!({
            "text": "This is some long message content that should be masked",
            "message_id": "123",
            "role": "user"
        });
        let masked = mask_content(&value);
        assert_eq!(masked["message_id"], "123");
        assert_eq!(masked["role"], "user");
        // Text is 56 chars, which is > 50, so should be masked with char count
        assert_eq!(masked["text"], "... (55 chars)");
    }

    #[test]
    fn test_mask_nested_content() {
        let value = json!({
            "sessionId": "abc-123",
            "update": {
                "content": [{
                    "type": "content",
                    "content": {
                        "type": "text",
                        "text": "A".repeat(500)
                    }
                }],
                "sessionUpdate": "tool_call_update",
                "status": "completed"
            }
        });
        let masked = mask_content(&value);
        assert_eq!(masked["sessionId"], "abc-123");
        assert_eq!(masked["update"]["sessionUpdate"], "tool_call_update");
        assert_eq!(masked["update"]["status"], "completed");

        // Check that the text field is masked
        let text_value = &masked["update"]["content"][0]["content"]["text"];
        if let Value::String(s) = text_value {
            assert!(s.contains("(500 chars)"));
        } else {
            panic!("Expected text to be masked");
        }
    }

    #[test]
    fn test_mask_content_as_object() {
        // Ensure "content" as object is NOT masked, only strings
        let value = json!({
            "content": {
                "type": "text",
                "text": "Some message"
            },
            "message_id": "123"
        });
        let masked = mask_content(&value);
        assert_eq!(masked["message_id"], "123");
        assert_eq!(masked["content"]["type"], "text");
        assert_eq!(masked["content"]["text"], "..."); // text field is masked
    }

    #[test]
    fn test_mask_filename() {
        let value = json!({
            "filename": "/Users/name/Projects/dirigent/packages/web/src/main.rs",
            "operation": "read"
        });
        let masked = mask_content(&value);
        assert_eq!(masked["operation"], "read");
        assert_eq!(masked["filename"], "<main.rs>");
    }

    #[test]
    fn test_mask_filenames_array() {
        let value = json!({
            "filenames": [
                "/Users/name/Projects/dirigent/packages/web/src/main.rs",
                "/Users/name/Projects/dirigent/packages/api/src/core.rs",
                "C:\\Users\\name\\Documents\\file.txt"
            ],
            "count": 3
        });
        let masked = mask_content(&value);
        assert_eq!(masked["count"], 3);
        assert_eq!(masked["filenames"][0], "<main.rs>");
        assert_eq!(masked["filenames"][1], "<core.rs>");
        assert_eq!(masked["filenames"][2], "<file.txt>");
    }
}
