Skip to main content

synadb/
ffi.rs

1// Copyright (c) 2025 SynaDB Contributors
2// Licensed under the SynaDB License. See LICENSE file for details.
3
4//! C-ABI Foreign Function Interface for Syna database.
5//!
6//! This module provides extern "C" functions for cross-language access.
7//! All functions use `catch_unwind` to prevent Rust panics from unwinding
8//! into foreign code.
9
10// FFI functions intentionally take raw pointers without being marked unsafe
11// because they handle null checks and use catch_unwind for safety
12#![allow(clippy::not_unsafe_ptr_arg_deref)]
13// Using slice::from_raw_parts_mut with Box::from_raw is intentional for FFI memory management
14#![allow(clippy::cast_slice_from_raw_parts)]
15
16use std::ffi::CStr;
17use std::os::raw::c_char;
18
19use crate::engine::{close_db, free_tensor, open_db, open_db_with_config, with_db, DbConfig};
20use crate::error::{
21    ERR_GENERIC, ERR_INTERNAL_PANIC, ERR_INVALID_PATH, ERR_KEY_NOT_FOUND, ERR_SUCCESS,
22    ERR_TYPE_MISMATCH,
23};
24use crate::types::Atom;
25
26/// Opens a database at the given path and registers it in the global registry.
27///
28/// # Arguments
29/// * `path` - Null-terminated C string containing the path to the database file
30///
31/// # Returns
32/// * `1` (ERR_SUCCESS) - Database opened successfully or was already open
33/// * `0` (ERR_GENERIC) - Generic error during database open
34/// * `-2` (ERR_INVALID_PATH) - Path is null or invalid UTF-8
35/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
36///
37/// # Safety
38/// * `path` must be a valid null-terminated C string or null
39///
40/// _Requirements: 6.2, 6.3, 9.4_
41#[no_mangle]
42pub extern "C" fn SYNA_open(path: *const c_char) -> i32 {
43    std::panic::catch_unwind(|| {
44        // Check for null pointer
45        if path.is_null() {
46            return ERR_INVALID_PATH;
47        }
48
49        // Convert C string to Rust string
50        let c_str = unsafe { CStr::from_ptr(path) };
51        let path_str = match c_str.to_str() {
52            Ok(s) => s,
53            Err(_) => return ERR_INVALID_PATH,
54        };
55
56        // Open the database
57        match open_db(path_str) {
58            Ok(_) => ERR_SUCCESS,
59            Err(_) => ERR_GENERIC,
60        }
61    })
62    .unwrap_or(ERR_INTERNAL_PANIC)
63}
64
65/// Opens a database with sync_on_write disabled for high-throughput writes.
66///
67/// This is optimized for bulk ingestion scenarios where durability can be
68/// traded for speed. Data is still written to disk but not fsynced after
69/// each write, achieving 100K+ ops/sec instead of ~100 ops/sec.
70///
71/// # Arguments
72/// * `path` - Null-terminated C string containing the path to the database file
73/// * `sync_on_write` - 1 for sync after each write (durable), 0 for no sync (fast)
74///
75/// # Returns
76/// * `1` (ERR_SUCCESS) - Database opened successfully or was already open
77/// * `0` (ERR_GENERIC) - Generic error during database open
78/// * `-2` (ERR_INVALID_PATH) - Path is null or invalid UTF-8
79/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
80///
81/// # Safety
82/// * `path` must be a valid null-terminated C string or null
83#[no_mangle]
84pub extern "C" fn SYNA_open_with_config(path: *const c_char, sync_on_write: i32) -> i32 {
85    std::panic::catch_unwind(|| {
86        // Check for null pointer
87        if path.is_null() {
88            return ERR_INVALID_PATH;
89        }
90
91        // Convert C string to Rust string
92        let c_str = unsafe { CStr::from_ptr(path) };
93        let path_str = match c_str.to_str() {
94            Ok(s) => s,
95            Err(_) => return ERR_INVALID_PATH,
96        };
97
98        // Create config with sync_on_write setting
99        let config = DbConfig {
100            sync_on_write: sync_on_write != 0,
101            ..DbConfig::default()
102        };
103
104        // Open the database with config
105        match open_db_with_config(path_str, config) {
106            Ok(_) => ERR_SUCCESS,
107            Err(_) => ERR_GENERIC,
108        }
109    })
110    .unwrap_or(ERR_INTERNAL_PANIC)
111}
112
113/// Closes a database and removes it from the global registry.
114///
115/// # Arguments
116/// * `path` - Null-terminated C string containing the path to the database file
117///
118/// # Returns
119/// * `1` (ERR_SUCCESS) - Database closed successfully
120/// * `0` (ERR_GENERIC) - Generic error during database close
121/// * `-1` (ERR_DB_NOT_FOUND) - Database not found in registry
122/// * `-2` (ERR_INVALID_PATH) - Path is null or invalid UTF-8
123/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
124///
125/// # Safety
126/// * `path` must be a valid null-terminated C string or null
127///
128/// _Requirements: 6.2_
129#[no_mangle]
130pub extern "C" fn SYNA_close(path: *const c_char) -> i32 {
131    std::panic::catch_unwind(|| {
132        // Check for null pointer
133        if path.is_null() {
134            return ERR_INVALID_PATH;
135        }
136
137        // Convert C string to Rust string
138        let c_str = unsafe { CStr::from_ptr(path) };
139        let path_str = match c_str.to_str() {
140            Ok(s) => s,
141            Err(_) => return ERR_INVALID_PATH,
142        };
143
144        // Close the database
145        match close_db(path_str) {
146            Ok(_) => ERR_SUCCESS,
147            Err(crate::error::SynaError::NotFound(_)) => crate::error::ERR_DB_NOT_FOUND,
148            Err(_) => ERR_GENERIC,
149        }
150    })
151    .unwrap_or(ERR_INTERNAL_PANIC)
152}
153
154/// Writes a float value to the database.
155///
156/// # Arguments
157/// * `path` - Null-terminated C string containing the path to the database file
158/// * `key` - Null-terminated C string containing the key
159/// * `value` - The f64 value to store
160///
161/// # Returns
162/// * Positive value - The byte offset where the entry was written
163/// * `-1` (ERR_DB_NOT_FOUND) - Database not found in registry
164/// * `-2` (ERR_INVALID_PATH) - Path or key is null or invalid UTF-8
165/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
166///
167/// # Safety
168/// * `path` and `key` must be valid null-terminated C strings or null
169///
170/// _Requirements: 6.2, 6.3_
171#[no_mangle]
172pub extern "C" fn SYNA_put_float(path: *const c_char, key: *const c_char, value: f64) -> i64 {
173    std::panic::catch_unwind(|| {
174        // Validate pointers
175        if path.is_null() || key.is_null() {
176            return ERR_INVALID_PATH as i64;
177        }
178
179        // Convert C strings to Rust strings
180        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
181            Ok(s) => s,
182            Err(_) => return ERR_INVALID_PATH as i64,
183        };
184
185        let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
186            Ok(s) => s,
187            Err(_) => return ERR_INVALID_PATH as i64,
188        };
189
190        // Call with_db to append the value
191        match with_db(path_str, |db| db.append(key_str, Atom::Float(value))) {
192            Ok(offset) => offset as i64,
193            Err(crate::error::SynaError::NotFound(_)) => crate::error::ERR_DB_NOT_FOUND as i64,
194            Err(_) => ERR_GENERIC as i64,
195        }
196    })
197    .unwrap_or(ERR_INTERNAL_PANIC as i64)
198}
199
200/// Writes an integer value to the database.
201///
202/// # Arguments
203/// * `path` - Null-terminated C string containing the path to the database file
204/// * `key` - Null-terminated C string containing the key
205/// * `value` - The i64 value to store
206///
207/// # Returns
208/// * Positive value - The byte offset where the entry was written
209/// * `-1` (ERR_DB_NOT_FOUND) - Database not found in registry
210/// * `-2` (ERR_INVALID_PATH) - Path or key is null or invalid UTF-8
211/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
212///
213/// # Safety
214/// * `path` and `key` must be valid null-terminated C strings or null
215///
216/// _Requirements: 6.2, 6.3_
217#[no_mangle]
218pub extern "C" fn SYNA_put_int(path: *const c_char, key: *const c_char, value: i64) -> i64 {
219    std::panic::catch_unwind(|| {
220        // Validate pointers
221        if path.is_null() || key.is_null() {
222            return ERR_INVALID_PATH as i64;
223        }
224
225        // Convert C strings to Rust strings
226        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
227            Ok(s) => s,
228            Err(_) => return ERR_INVALID_PATH as i64,
229        };
230
231        let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
232            Ok(s) => s,
233            Err(_) => return ERR_INVALID_PATH as i64,
234        };
235
236        // Call with_db to append the value
237        match with_db(path_str, |db| db.append(key_str, Atom::Int(value))) {
238            Ok(offset) => offset as i64,
239            Err(crate::error::SynaError::NotFound(_)) => crate::error::ERR_DB_NOT_FOUND as i64,
240            Err(_) => ERR_GENERIC as i64,
241        }
242    })
243    .unwrap_or(ERR_INTERNAL_PANIC as i64)
244}
245
246/// Writes a text value to the database.
247///
248/// # Arguments
249/// * `path` - Null-terminated C string containing the path to the database file
250/// * `key` - Null-terminated C string containing the key
251/// * `value` - Null-terminated C string containing the text value to store
252///
253/// # Returns
254/// * Positive value - The byte offset where the entry was written
255/// * `-1` (ERR_DB_NOT_FOUND) - Database not found in registry
256/// * `-2` (ERR_INVALID_PATH) - Path, key, or value is null or invalid UTF-8
257/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
258///
259/// # Safety
260/// * `path`, `key`, and `value` must be valid null-terminated C strings or null
261///
262/// _Requirements: 6.2, 6.3_
263#[no_mangle]
264pub extern "C" fn SYNA_put_text(
265    path: *const c_char,
266    key: *const c_char,
267    value: *const c_char,
268) -> i64 {
269    std::panic::catch_unwind(|| {
270        // Validate pointers
271        if path.is_null() || key.is_null() || value.is_null() {
272            return ERR_INVALID_PATH as i64;
273        }
274
275        // Convert C strings to Rust strings
276        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
277            Ok(s) => s,
278            Err(_) => return ERR_INVALID_PATH as i64,
279        };
280
281        let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
282            Ok(s) => s,
283            Err(_) => return ERR_INVALID_PATH as i64,
284        };
285
286        let value_str = match unsafe { CStr::from_ptr(value) }.to_str() {
287            Ok(s) => s,
288            Err(_) => return ERR_INVALID_PATH as i64,
289        };
290
291        // Call with_db to append the value
292        match with_db(path_str, |db| {
293            db.append(key_str, Atom::Text(value_str.to_string()))
294        }) {
295            Ok(offset) => offset as i64,
296            Err(crate::error::SynaError::NotFound(_)) => crate::error::ERR_DB_NOT_FOUND as i64,
297            Err(_) => ERR_GENERIC as i64,
298        }
299    })
300    .unwrap_or(ERR_INTERNAL_PANIC as i64)
301}
302
303/// Writes a byte array to the database.
304///
305/// # Arguments
306/// * `path` - Null-terminated C string containing the path to the database file
307/// * `key` - Null-terminated C string containing the key
308/// * `data` - Pointer to the byte array to store
309/// * `len` - Length of the byte array
310///
311/// # Returns
312/// * Positive value - The byte offset where the entry was written
313/// * `-1` (ERR_DB_NOT_FOUND) - Database not found in registry
314/// * `-2` (ERR_INVALID_PATH) - Path, key, or data is null or invalid UTF-8
315/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
316///
317/// # Safety
318/// * `path` and `key` must be valid null-terminated C strings or null
319/// * `data` must be a valid pointer to at least `len` bytes, or null
320///
321/// _Requirements: 6.2, 6.4_
322#[no_mangle]
323pub extern "C" fn SYNA_put_bytes(
324    path: *const c_char,
325    key: *const c_char,
326    data: *const u8,
327    len: usize,
328) -> i64 {
329    std::panic::catch_unwind(|| {
330        // Validate pointers
331        if path.is_null() || key.is_null() || (data.is_null() && len > 0) {
332            return ERR_INVALID_PATH as i64;
333        }
334
335        // Convert C strings to Rust strings
336        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
337            Ok(s) => s,
338            Err(_) => return ERR_INVALID_PATH as i64,
339        };
340
341        let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
342            Ok(s) => s,
343            Err(_) => return ERR_INVALID_PATH as i64,
344        };
345
346        // Create Vec<u8> from raw pointer and length
347        let bytes = if len == 0 {
348            Vec::new()
349        } else {
350            unsafe { std::slice::from_raw_parts(data, len) }.to_vec()
351        };
352
353        // Call with_db to append the value
354        match with_db(path_str, |db| db.append(key_str, Atom::Bytes(bytes))) {
355            Ok(offset) => offset as i64,
356            Err(crate::error::SynaError::NotFound(_)) => crate::error::ERR_DB_NOT_FOUND as i64,
357            Err(_) => ERR_GENERIC as i64,
358        }
359    })
360    .unwrap_or(ERR_INTERNAL_PANIC as i64)
361}
362
363/// Writes multiple float values to the database in a single batch operation.
364///
365/// This is optimized for high-throughput ingestion scenarios. All values are
366/// written under the same key, building up a history that can be extracted
367/// as a tensor with `SYNA_get_history_tensor()`.
368///
369/// # Arguments
370/// * `path` - Null-terminated C string containing the path to the database file
371/// * `key` - Null-terminated C string containing the key
372/// * `values` - Pointer to array of f64 values
373/// * `count` - Number of values in the array
374///
375/// # Returns
376/// * Positive value - Number of values written
377/// * `-1` (ERR_DB_NOT_FOUND) - Database not found in registry
378/// * `-2` (ERR_INVALID_PATH) - Path, key, or values is null or invalid UTF-8
379/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
380///
381/// # Safety
382/// * `path` and `key` must be valid null-terminated C strings or null
383/// * `values` must be a valid pointer to at least `count` f64 values, or null if count is 0
384///
385/// # Performance
386/// This function is significantly faster than calling `SYNA_put_float()` in a loop:
387/// - Single FFI boundary crossing
388/// - Single mutex lock for all writes
389/// - Single fsync at the end (if sync_on_write is enabled)
390#[no_mangle]
391pub extern "C" fn SYNA_put_floats_batch(
392    path: *const c_char,
393    key: *const c_char,
394    values: *const f64,
395    count: usize,
396) -> i64 {
397    std::panic::catch_unwind(|| {
398        // Validate pointers
399        if path.is_null() || key.is_null() || (values.is_null() && count > 0) {
400            return ERR_INVALID_PATH as i64;
401        }
402
403        // Convert C strings to Rust strings
404        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
405            Ok(s) => s,
406            Err(_) => return ERR_INVALID_PATH as i64,
407        };
408
409        let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
410            Ok(s) => s,
411            Err(_) => return ERR_INVALID_PATH as i64,
412        };
413
414        // Create slice from raw pointer
415        let slice = if count == 0 {
416            &[]
417        } else {
418            unsafe { std::slice::from_raw_parts(values, count) }
419        };
420
421        // Call with_db to batch append
422        match with_db(path_str, |db| db.append_floats_batch(key_str, slice)) {
423            Ok(n) => n as i64,
424            Err(crate::error::SynaError::NotFound(_)) => crate::error::ERR_DB_NOT_FOUND as i64,
425            Err(_) => ERR_GENERIC as i64,
426        }
427    })
428    .unwrap_or(ERR_INTERNAL_PANIC as i64)
429}
430
431/// Reads a float value from the database.
432///
433/// # Arguments
434/// * `path` - Null-terminated C string containing the path to the database file
435/// * `key` - Null-terminated C string containing the key
436/// * `out` - Pointer to write the f64 value to
437///
438/// # Returns
439/// * `1` (ERR_SUCCESS) - Value read successfully
440/// * `-1` (ERR_DB_NOT_FOUND) - Database not found in registry
441/// * `-2` (ERR_INVALID_PATH) - Path, key, or out is null or invalid UTF-8
442/// * `-5` (ERR_KEY_NOT_FOUND) - Key not found in database
443/// * `-6` (ERR_TYPE_MISMATCH) - Value exists but is not a Float
444/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
445///
446/// # Safety
447/// * `path` and `key` must be valid null-terminated C strings or null
448/// * `out` must be a valid pointer to an f64
449///
450/// _Requirements: 6.2, 6.4_
451#[no_mangle]
452pub extern "C" fn SYNA_get_float(path: *const c_char, key: *const c_char, out: *mut f64) -> i32 {
453    std::panic::catch_unwind(|| {
454        // Validate pointers
455        if path.is_null() || key.is_null() || out.is_null() {
456            return ERR_INVALID_PATH;
457        }
458
459        // Convert C strings to Rust strings
460        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
461            Ok(s) => s,
462            Err(_) => return ERR_INVALID_PATH,
463        };
464
465        let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
466            Ok(s) => s,
467            Err(_) => return ERR_INVALID_PATH,
468        };
469
470        // Call with_db to get the value
471        match with_db(path_str, |db| db.get(key_str)) {
472            Ok(Some(Atom::Float(f))) => {
473                // Write value to out pointer
474                unsafe { *out = f };
475                ERR_SUCCESS
476            }
477            Ok(Some(_)) => {
478                // Value exists but is not a Float
479                ERR_TYPE_MISMATCH
480            }
481            Ok(None) => {
482                // Key not found
483                ERR_KEY_NOT_FOUND
484            }
485            Err(crate::error::SynaError::NotFound(_)) => crate::error::ERR_DB_NOT_FOUND,
486            Err(_) => ERR_GENERIC,
487        }
488    })
489    .unwrap_or(ERR_INTERNAL_PANIC)
490}
491
492/// Reads an integer value from the database.
493///
494/// # Arguments
495/// * `path` - Null-terminated C string containing the path to the database file
496/// * `key` - Null-terminated C string containing the key
497/// * `out` - Pointer to write the i64 value to
498///
499/// # Returns
500/// * `1` (ERR_SUCCESS) - Value read successfully
501/// * `-1` (ERR_DB_NOT_FOUND) - Database not found in registry
502/// * `-2` (ERR_INVALID_PATH) - Path, key, or out is null or invalid UTF-8
503/// * `-5` (ERR_KEY_NOT_FOUND) - Key not found in database
504/// * `-6` (ERR_TYPE_MISMATCH) - Value exists but is not an Int
505/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
506///
507/// # Safety
508/// * `path` and `key` must be valid null-terminated C strings or null
509/// * `out` must be a valid pointer to an i64
510///
511/// _Requirements: 6.2, 6.4_
512#[no_mangle]
513pub extern "C" fn SYNA_get_int(path: *const c_char, key: *const c_char, out: *mut i64) -> i32 {
514    std::panic::catch_unwind(|| {
515        // Validate pointers
516        if path.is_null() || key.is_null() || out.is_null() {
517            return ERR_INVALID_PATH;
518        }
519
520        // Convert C strings to Rust strings
521        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
522            Ok(s) => s,
523            Err(_) => return ERR_INVALID_PATH,
524        };
525
526        let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
527            Ok(s) => s,
528            Err(_) => return ERR_INVALID_PATH,
529        };
530
531        // Call with_db to get the value
532        match with_db(path_str, |db| db.get(key_str)) {
533            Ok(Some(Atom::Int(i))) => {
534                // Write value to out pointer
535                unsafe { *out = i };
536                ERR_SUCCESS
537            }
538            Ok(Some(_)) => {
539                // Value exists but is not an Int
540                ERR_TYPE_MISMATCH
541            }
542            Ok(None) => {
543                // Key not found
544                ERR_KEY_NOT_FOUND
545            }
546            Err(crate::error::SynaError::NotFound(_)) => crate::error::ERR_DB_NOT_FOUND,
547            Err(_) => ERR_GENERIC,
548        }
549    })
550    .unwrap_or(ERR_INTERNAL_PANIC)
551}
552
553/// Retrieves the complete history of float values for a key as a contiguous array.
554///
555/// This function is designed for AI/ML workloads where you need to feed time-series
556/// data directly to frameworks like PyTorch or TensorFlow.
557///
558/// # Arguments
559/// * `path` - Null-terminated C string containing the path to the database file
560/// * `key` - Null-terminated C string containing the key
561/// * `out_len` - Pointer to write the array length to
562///
563/// # Returns
564/// * Non-null pointer to contiguous f64 array on success
565/// * Null pointer on error (check out_len for error code)
566///
567/// # Error Codes (written to out_len on error)
568/// * `0` - Empty history (no float values for this key)
569///
570/// # Safety
571/// * `path` and `key` must be valid null-terminated C strings or null
572/// * `out_len` must be a valid pointer to a usize
573/// * The returned pointer MUST be freed using `SYNA_free_tensor()` to avoid memory leaks
574///
575/// _Requirements: 4.2, 6.4_
576#[no_mangle]
577pub extern "C" fn SYNA_get_history_tensor(
578    path: *const c_char,
579    key: *const c_char,
580    out_len: *mut usize,
581) -> *mut f64 {
582    std::panic::catch_unwind(|| {
583        // Validate pointers
584        if path.is_null() || key.is_null() || out_len.is_null() {
585            return std::ptr::null_mut();
586        }
587
588        // Convert C strings to Rust strings
589        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
590            Ok(s) => s,
591            Err(_) => return std::ptr::null_mut(),
592        };
593
594        let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
595            Ok(s) => s,
596            Err(_) => return std::ptr::null_mut(),
597        };
598
599        // Call with_db to get the history tensor
600        match with_db(path_str, |db| db.get_history_tensor(key_str)) {
601            Ok((ptr, len)) => {
602                // Write length to out_len pointer
603                unsafe { *out_len = len };
604                ptr
605            }
606            Err(_) => {
607                // Set out_len to 0 on error
608                unsafe { *out_len = 0 };
609                std::ptr::null_mut()
610            }
611        }
612    })
613    .unwrap_or(std::ptr::null_mut())
614}
615
616/// Frees memory allocated by `SYNA_get_history_tensor()`.
617///
618/// # Arguments
619/// * `ptr` - Pointer returned by `SYNA_get_history_tensor()`
620/// * `len` - Length returned by `SYNA_get_history_tensor()`
621///
622/// # Safety
623/// * `ptr` must have been returned by `SYNA_get_history_tensor()`
624/// * `len` must be the length returned alongside the pointer
625/// * This function must only be called once per pointer
626/// * Calling with a null pointer or zero length is safe (no-op)
627///
628/// _Requirements: 4.3, 6.5_
629#[no_mangle]
630pub extern "C" fn SYNA_free_tensor(ptr: *mut f64, len: usize) {
631    std::panic::catch_unwind(|| {
632        // Call internal free_tensor (safe wrapper)
633        unsafe { free_tensor(ptr, len) };
634    })
635    .ok(); // Ignore panic result - we don't want to propagate panics from free
636}
637
638/// Deletes a key from the database by appending a tombstone entry.
639///
640/// # Arguments
641/// * `path` - Null-terminated C string containing the path to the database file
642/// * `key` - Null-terminated C string containing the key to delete
643///
644/// # Returns
645/// * `1` (ERR_SUCCESS) - Key deleted successfully
646/// * `0` (ERR_GENERIC) - Generic error during delete
647/// * `-1` (ERR_DB_NOT_FOUND) - Database not found in registry
648/// * `-2` (ERR_INVALID_PATH) - Path or key is null or invalid UTF-8
649/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
650///
651/// # Safety
652/// * `path` and `key` must be valid null-terminated C strings or null
653///
654/// _Requirements: 10.1_
655#[no_mangle]
656pub extern "C" fn SYNA_delete(path: *const c_char, key: *const c_char) -> i32 {
657    std::panic::catch_unwind(|| {
658        // Validate pointers
659        if path.is_null() || key.is_null() {
660            return ERR_INVALID_PATH;
661        }
662
663        // Convert C strings to Rust strings
664        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
665            Ok(s) => s,
666            Err(_) => return ERR_INVALID_PATH,
667        };
668
669        let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
670            Ok(s) => s,
671            Err(_) => return ERR_INVALID_PATH,
672        };
673
674        // Call with_db to delete the key
675        match with_db(path_str, |db| db.delete(key_str)) {
676            Ok(_) => ERR_SUCCESS,
677            Err(crate::error::SynaError::NotFound(_)) => crate::error::ERR_DB_NOT_FOUND,
678            Err(_) => ERR_GENERIC,
679        }
680    })
681    .unwrap_or(ERR_INTERNAL_PANIC)
682}
683
684/// Checks if a key exists in the database and is not deleted.
685///
686/// # Arguments
687/// * `path` - Null-terminated C string containing the path to the database file
688/// * `key` - Null-terminated C string containing the key to check
689///
690/// # Returns
691/// * `1` - Key exists and is not deleted
692/// * `0` - Key does not exist or is deleted
693/// * `-1` (ERR_DB_NOT_FOUND) - Database not found in registry
694/// * `-2` (ERR_INVALID_PATH) - Path or key is null or invalid UTF-8
695/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
696///
697/// # Safety
698/// * `path` and `key` must be valid null-terminated C strings or null
699///
700/// _Requirements: 10.2_
701#[no_mangle]
702pub extern "C" fn SYNA_exists(path: *const c_char, key: *const c_char) -> i32 {
703    std::panic::catch_unwind(|| {
704        // Validate pointers
705        if path.is_null() || key.is_null() {
706            return ERR_INVALID_PATH;
707        }
708
709        // Convert C strings to Rust strings
710        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
711            Ok(s) => s,
712            Err(_) => return ERR_INVALID_PATH,
713        };
714
715        let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
716            Ok(s) => s,
717            Err(_) => return ERR_INVALID_PATH,
718        };
719
720        // Call with_db to check if key exists
721        match with_db(path_str, |db| Ok(db.exists(key_str))) {
722            Ok(true) => 1,
723            Ok(false) => 0,
724            Err(crate::error::SynaError::NotFound(_)) => crate::error::ERR_DB_NOT_FOUND,
725            Err(_) => ERR_GENERIC,
726        }
727    })
728    .unwrap_or(ERR_INTERNAL_PANIC)
729}
730
731/// Compacts the database by rewriting only the latest non-deleted entries.
732///
733/// This operation reclaims disk space by removing deleted entries and old versions.
734/// After compaction, `get_history()` will only return the latest value for each key.
735///
736/// # Arguments
737/// * `path` - Null-terminated C string containing the path to the database file
738///
739/// # Returns
740/// * `1` (ERR_SUCCESS) - Compaction completed successfully
741/// * `0` (ERR_GENERIC) - Generic error during compaction
742/// * `-1` (ERR_DB_NOT_FOUND) - Database not found in registry
743/// * `-2` (ERR_INVALID_PATH) - Path is null or invalid UTF-8
744/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
745///
746/// # Safety
747/// * `path` must be a valid null-terminated C string or null
748///
749/// _Requirements: 11.1_
750#[no_mangle]
751pub extern "C" fn SYNA_compact(path: *const c_char) -> i32 {
752    std::panic::catch_unwind(|| {
753        // Validate pointer
754        if path.is_null() {
755            return ERR_INVALID_PATH;
756        }
757
758        // Convert C string to Rust string
759        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
760            Ok(s) => s,
761            Err(_) => return ERR_INVALID_PATH,
762        };
763
764        // Call with_db to compact the database
765        match with_db(path_str, |db| db.compact()) {
766            Ok(_) => ERR_SUCCESS,
767            Err(crate::error::SynaError::NotFound(_)) => crate::error::ERR_DB_NOT_FOUND,
768            Err(_) => ERR_GENERIC,
769        }
770    })
771    .unwrap_or(ERR_INTERNAL_PANIC)
772}
773
774/// Returns a list of all non-deleted keys in the database.
775///
776/// # Arguments
777/// * `path` - Null-terminated C string containing the path to the database file
778/// * `out_len` - Pointer to write the number of keys to
779///
780/// # Returns
781/// * Non-null pointer to array of null-terminated C strings on success
782/// * Null pointer on error
783///
784/// # Safety
785/// * `path` must be a valid null-terminated C string or null
786/// * `out_len` must be a valid pointer to a usize
787/// * The returned pointer MUST be freed using `SYNA_free_keys()` to avoid memory leaks
788///
789/// _Requirements: 10.5_
790#[no_mangle]
791pub extern "C" fn SYNA_keys(path: *const c_char, out_len: *mut usize) -> *mut *mut c_char {
792    std::panic::catch_unwind(|| {
793        // Validate pointers
794        if path.is_null() || out_len.is_null() {
795            return std::ptr::null_mut();
796        }
797
798        // Convert C string to Rust string
799        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
800            Ok(s) => s,
801            Err(_) => return std::ptr::null_mut(),
802        };
803
804        // Call with_db to get keys
805        match with_db(path_str, |db| Ok(db.keys())) {
806            Ok(keys) => {
807                let len = keys.len();
808
809                if len == 0 {
810                    // No keys - return null with length 0
811                    unsafe { *out_len = 0 };
812                    return std::ptr::null_mut();
813                }
814
815                // Allocate array of C string pointers
816                let mut c_strings: Vec<*mut c_char> = Vec::with_capacity(len);
817
818                for key in keys {
819                    // Convert each key to a C string
820                    // Add null terminator
821                    let mut bytes = key.into_bytes();
822                    bytes.push(0); // null terminator
823
824                    // Allocate memory for the C string
825                    let c_str = bytes.into_boxed_slice();
826                    let ptr = Box::into_raw(c_str) as *mut c_char;
827                    c_strings.push(ptr);
828                }
829
830                // Write length to out_len
831                unsafe { *out_len = len };
832
833                // Convert Vec to boxed slice and leak for FFI
834                let boxed = c_strings.into_boxed_slice();
835                Box::into_raw(boxed) as *mut *mut c_char
836            }
837            Err(_) => {
838                unsafe { *out_len = 0 };
839                std::ptr::null_mut()
840            }
841        }
842    })
843    .unwrap_or(std::ptr::null_mut())
844}
845
846/// Frees memory allocated by `SYNA_keys()`.
847///
848/// # Arguments
849/// * `keys` - Pointer returned by `SYNA_keys()`
850/// * `len` - Length returned by `SYNA_keys()`
851///
852/// # Safety
853/// * `keys` must have been returned by `SYNA_keys()`
854/// * `len` must be the length returned alongside the pointer
855/// * This function must only be called once per pointer
856/// * Calling with a null pointer or zero length is safe (no-op)
857///
858/// _Requirements: 6.5_
859#[no_mangle]
860pub extern "C" fn SYNA_free_keys(keys: *mut *mut c_char, len: usize) {
861    std::panic::catch_unwind(|| {
862        if keys.is_null() || len == 0 {
863            return;
864        }
865
866        unsafe {
867            // Reconstruct the slice of pointers
868            let key_slice = std::slice::from_raw_parts_mut(keys, len);
869
870            // Free each individual string
871            for key_ptr in key_slice.iter() {
872                if !key_ptr.is_null() {
873                    // Find the length of the C string (including null terminator)
874                    let c_str = CStr::from_ptr(*key_ptr);
875                    let str_len = c_str.to_bytes_with_nul().len();
876
877                    // Reconstruct the box and drop it
878                    let _ =
879                        Box::from_raw(std::slice::from_raw_parts_mut(*key_ptr as *mut u8, str_len));
880                }
881            }
882
883            // Free the array itself
884            let _ = Box::from_raw(std::slice::from_raw_parts_mut(keys, len));
885        }
886    })
887    .ok(); // Ignore panic result - we don't want to propagate panics from free
888}
889
890/// Reads a text value from the database.
891///
892/// # Arguments
893/// * `path` - Null-terminated C string containing the path to the database file
894/// * `key` - Null-terminated C string containing the key
895/// * `out_len` - Pointer to write the string length to (excluding null terminator)
896///
897/// # Returns
898/// * Non-null pointer to null-terminated C string on success
899/// * Null pointer on error
900///
901/// # Error Codes (check return value)
902/// * Null with out_len = 0 - Key not found or type mismatch
903///
904/// # Safety
905/// * `path` and `key` must be valid null-terminated C strings or null
906/// * `out_len` must be a valid pointer to a usize
907/// * The returned pointer MUST be freed using `SYNA_free_text()` to avoid memory leaks
908///
909/// _Requirements: 6.2, 6.4_
910#[no_mangle]
911pub extern "C" fn SYNA_get_text(
912    path: *const c_char,
913    key: *const c_char,
914    out_len: *mut usize,
915) -> *mut c_char {
916    std::panic::catch_unwind(|| {
917        // Validate pointers
918        if path.is_null() || key.is_null() || out_len.is_null() {
919            return std::ptr::null_mut();
920        }
921
922        // Convert C strings to Rust strings
923        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
924            Ok(s) => s,
925            Err(_) => return std::ptr::null_mut(),
926        };
927
928        let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
929            Ok(s) => s,
930            Err(_) => return std::ptr::null_mut(),
931        };
932
933        // Call with_db to get the value
934        match with_db(path_str, |db| db.get(key_str)) {
935            Ok(Some(Atom::Text(s))) => {
936                let len = s.len();
937                unsafe { *out_len = len };
938
939                // Convert to C string with null terminator
940                let mut bytes = s.into_bytes();
941                bytes.push(0); // null terminator
942
943                let c_str = bytes.into_boxed_slice();
944                Box::into_raw(c_str) as *mut c_char
945            }
946            Ok(Some(_)) => {
947                // Value exists but is not Text
948                unsafe { *out_len = 0 };
949                std::ptr::null_mut()
950            }
951            Ok(None) => {
952                // Key not found
953                unsafe { *out_len = 0 };
954                std::ptr::null_mut()
955            }
956            Err(_) => {
957                unsafe { *out_len = 0 };
958                std::ptr::null_mut()
959            }
960        }
961    })
962    .unwrap_or(std::ptr::null_mut())
963}
964
965/// Frees memory allocated by `SYNA_get_text()`.
966///
967/// # Arguments
968/// * `ptr` - Pointer returned by `SYNA_get_text()`
969/// * `len` - Length returned by `SYNA_get_text()` (excluding null terminator)
970///
971/// # Safety
972/// * `ptr` must have been returned by `SYNA_get_text()`
973/// * `len` must be the length returned alongside the pointer
974/// * This function must only be called once per pointer
975/// * Calling with a null pointer is safe (no-op)
976///
977/// _Requirements: 6.5_
978#[no_mangle]
979pub extern "C" fn SYNA_free_text(ptr: *mut c_char, len: usize) {
980    std::panic::catch_unwind(|| {
981        if ptr.is_null() {
982            return;
983        }
984
985        unsafe {
986            // Reconstruct the box (len + 1 for null terminator)
987            let _ = Box::from_raw(std::slice::from_raw_parts_mut(ptr as *mut u8, len + 1));
988        }
989    })
990    .ok();
991}
992
993/// Reads a byte array from the database.
994///
995/// # Arguments
996/// * `path` - Null-terminated C string containing the path to the database file
997/// * `key` - Null-terminated C string containing the key
998/// * `out_len` - Pointer to write the array length to
999///
1000/// # Returns
1001/// * Non-null pointer to byte array on success
1002/// * Null pointer on error
1003///
1004/// # Error Codes (check return value)
1005/// * Null with out_len = 0 - Key not found or type mismatch
1006///
1007/// # Safety
1008/// * `path` and `key` must be valid null-terminated C strings or null
1009/// * `out_len` must be a valid pointer to a usize
1010/// * The returned pointer MUST be freed using `SYNA_free_bytes()` to avoid memory leaks
1011///
1012/// _Requirements: 6.2, 6.4_
1013#[no_mangle]
1014pub extern "C" fn SYNA_get_bytes(
1015    path: *const c_char,
1016    key: *const c_char,
1017    out_len: *mut usize,
1018) -> *mut u8 {
1019    std::panic::catch_unwind(|| {
1020        // Validate pointers
1021        if path.is_null() || key.is_null() || out_len.is_null() {
1022            return std::ptr::null_mut();
1023        }
1024
1025        // Convert C strings to Rust strings
1026        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
1027            Ok(s) => s,
1028            Err(_) => return std::ptr::null_mut(),
1029        };
1030
1031        let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
1032            Ok(s) => s,
1033            Err(_) => return std::ptr::null_mut(),
1034        };
1035
1036        // Call with_db to get the value
1037        match with_db(path_str, |db| db.get(key_str)) {
1038            Ok(Some(Atom::Bytes(bytes))) => {
1039                let len = bytes.len();
1040
1041                if len == 0 {
1042                    unsafe { *out_len = 0 };
1043                    return std::ptr::null_mut();
1044                }
1045
1046                unsafe { *out_len = len };
1047
1048                // Convert to boxed slice and leak for FFI
1049                let boxed = bytes.into_boxed_slice();
1050                Box::into_raw(boxed) as *mut u8
1051            }
1052            Ok(Some(_)) => {
1053                // Value exists but is not Bytes
1054                unsafe { *out_len = 0 };
1055                std::ptr::null_mut()
1056            }
1057            Ok(None) => {
1058                // Key not found
1059                unsafe { *out_len = 0 };
1060                std::ptr::null_mut()
1061            }
1062            Err(_) => {
1063                unsafe { *out_len = 0 };
1064                std::ptr::null_mut()
1065            }
1066        }
1067    })
1068    .unwrap_or(std::ptr::null_mut())
1069}
1070
1071/// Frees memory allocated by `SYNA_get_bytes()`.
1072///
1073/// # Arguments
1074/// * `ptr` - Pointer returned by `SYNA_get_bytes()`
1075/// * `len` - Length returned by `SYNA_get_bytes()`
1076///
1077/// # Safety
1078/// * `ptr` must have been returned by `SYNA_get_bytes()`
1079/// * `len` must be the length returned alongside the pointer
1080/// * This function must only be called once per pointer
1081/// * Calling with a null pointer or zero length is safe (no-op)
1082///
1083/// _Requirements: 6.5_
1084#[no_mangle]
1085pub extern "C" fn SYNA_free_bytes(ptr: *mut u8, len: usize) {
1086    std::panic::catch_unwind(|| {
1087        if ptr.is_null() || len == 0 {
1088            return;
1089        }
1090
1091        unsafe {
1092            let _ = Box::from_raw(std::slice::from_raw_parts_mut(ptr, len));
1093        }
1094    })
1095    .ok();
1096}
1097
1098/* ============================================================================
1099 * Vector Functions (AI/ML Embeddings)
1100 * ============================================================================ */
1101
1102/// Stores a vector (embedding) in the database.
1103///
1104/// Vectors are stored as `Atom::Vector(Vec<f32>, u16)` where the second
1105/// element is the dimensionality. This is optimized for AI/ML embedding
1106/// storage and similarity search.
1107///
1108/// # Arguments
1109/// * `path` - Null-terminated C string containing the path to the database file
1110/// * `key` - Null-terminated C string containing the key
1111/// * `data` - Pointer to f32 array containing the vector data
1112/// * `dimensions` - Number of dimensions (elements) in the vector
1113///
1114/// # Returns
1115/// * Positive value - The byte offset where the entry was written
1116/// * `-1` (ERR_DB_NOT_FOUND) - Database not found in registry
1117/// * `-2` (ERR_INVALID_PATH) - Path, key, or data is null or invalid UTF-8
1118/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
1119///
1120/// # Safety
1121/// * `path` and `key` must be valid null-terminated C strings or null
1122/// * `data` must be a valid pointer to at least `dimensions` f32 values, or null
1123///
1124/// _Requirements: 1.1_
1125#[no_mangle]
1126pub extern "C" fn SYNA_put_vector(
1127    path: *const c_char,
1128    key: *const c_char,
1129    data: *const f32,
1130    dimensions: u16,
1131) -> i64 {
1132    std::panic::catch_unwind(|| {
1133        // Validate pointers
1134        if path.is_null() || key.is_null() || (data.is_null() && dimensions > 0) {
1135            return ERR_INVALID_PATH as i64;
1136        }
1137
1138        // Convert C strings to Rust strings
1139        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
1140            Ok(s) => s,
1141            Err(_) => return ERR_INVALID_PATH as i64,
1142        };
1143
1144        let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
1145            Ok(s) => s,
1146            Err(_) => return ERR_INVALID_PATH as i64,
1147        };
1148
1149        // Create Vec<f32> from raw pointer and dimensions
1150        let vector = if dimensions == 0 {
1151            Vec::new()
1152        } else {
1153            unsafe { std::slice::from_raw_parts(data, dimensions as usize) }.to_vec()
1154        };
1155
1156        // Call with_db to append the vector
1157        match with_db(path_str, |db| {
1158            db.append(key_str, Atom::Vector(vector, dimensions))
1159        }) {
1160            Ok(offset) => offset as i64,
1161            Err(crate::error::SynaError::NotFound(_)) => crate::error::ERR_DB_NOT_FOUND as i64,
1162            Err(_) => ERR_GENERIC as i64,
1163        }
1164    })
1165    .unwrap_or(ERR_INTERNAL_PANIC as i64)
1166}
1167
1168/// Retrieves a vector (embedding) from the database.
1169///
1170/// # Arguments
1171/// * `path` - Null-terminated C string containing the path to the database file
1172/// * `key` - Null-terminated C string containing the key
1173/// * `out_data` - Pointer to store the allocated f32 array pointer
1174/// * `out_dimensions` - Pointer to store the number of dimensions
1175///
1176/// # Returns
1177/// * `1` (ERR_SUCCESS) - Vector retrieved successfully
1178/// * `0` (ERR_GENERIC) - Generic error
1179/// * `-1` (ERR_DB_NOT_FOUND) - Database not found in registry
1180/// * `-2` (ERR_INVALID_PATH) - Path, key, or output pointers are null or invalid UTF-8
1181/// * `-5` (ERR_KEY_NOT_FOUND) - Key not found in database
1182/// * `-6` (ERR_TYPE_MISMATCH) - Value exists but is not a Vector
1183/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
1184///
1185/// # Safety
1186/// * `path` and `key` must be valid null-terminated C strings or null
1187/// * `out_data` must be a valid pointer to a `*mut f32`
1188/// * `out_dimensions` must be a valid pointer to a `u16`
1189/// * The returned data pointer MUST be freed using `SYNA_free_vector()` to avoid memory leaks
1190///
1191/// _Requirements: 1.1_
1192#[no_mangle]
1193pub extern "C" fn SYNA_get_vector(
1194    path: *const c_char,
1195    key: *const c_char,
1196    out_data: *mut *mut f32,
1197    out_dimensions: *mut u16,
1198) -> i32 {
1199    std::panic::catch_unwind(|| {
1200        // Validate pointers
1201        if path.is_null() || key.is_null() || out_data.is_null() || out_dimensions.is_null() {
1202            return ERR_INVALID_PATH;
1203        }
1204
1205        // Convert C strings to Rust strings
1206        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
1207            Ok(s) => s,
1208            Err(_) => return ERR_INVALID_PATH,
1209        };
1210
1211        let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
1212            Ok(s) => s,
1213            Err(_) => return ERR_INVALID_PATH,
1214        };
1215
1216        // Call with_db to get the value
1217        match with_db(path_str, |db| db.get(key_str)) {
1218            Ok(Some(Atom::Vector(vec_data, dims))) => {
1219                // Write dimensions to output
1220                unsafe { *out_dimensions = dims };
1221
1222                if vec_data.is_empty() {
1223                    // Empty vector - return null pointer with 0 dimensions
1224                    unsafe { *out_data = std::ptr::null_mut() };
1225                    return ERR_SUCCESS;
1226                }
1227
1228                // Convert to boxed slice and leak for FFI
1229                let boxed = vec_data.into_boxed_slice();
1230                unsafe { *out_data = Box::into_raw(boxed) as *mut f32 };
1231
1232                ERR_SUCCESS
1233            }
1234            Ok(Some(_)) => {
1235                // Value exists but is not a Vector
1236                unsafe {
1237                    *out_data = std::ptr::null_mut();
1238                    *out_dimensions = 0;
1239                }
1240                ERR_TYPE_MISMATCH
1241            }
1242            Ok(None) => {
1243                // Key not found
1244                unsafe {
1245                    *out_data = std::ptr::null_mut();
1246                    *out_dimensions = 0;
1247                }
1248                ERR_KEY_NOT_FOUND
1249            }
1250            Err(crate::error::SynaError::NotFound(_)) => {
1251                unsafe {
1252                    *out_data = std::ptr::null_mut();
1253                    *out_dimensions = 0;
1254                }
1255                crate::error::ERR_DB_NOT_FOUND
1256            }
1257            Err(_) => {
1258                unsafe {
1259                    *out_data = std::ptr::null_mut();
1260                    *out_dimensions = 0;
1261                }
1262                ERR_GENERIC
1263            }
1264        }
1265    })
1266    .unwrap_or(ERR_INTERNAL_PANIC)
1267}
1268
1269/// Frees memory allocated by `SYNA_get_vector()`.
1270///
1271/// # Arguments
1272/// * `data` - Pointer returned by `SYNA_get_vector()` in `out_data`
1273/// * `dimensions` - Dimensions returned by `SYNA_get_vector()` in `out_dimensions`
1274///
1275/// # Safety
1276/// * `data` must have been returned by `SYNA_get_vector()`
1277/// * `dimensions` must be the dimensions returned alongside the pointer
1278/// * This function must only be called once per pointer
1279/// * Calling with a null pointer or zero dimensions is safe (no-op)
1280///
1281/// _Requirements: 1.1_
1282#[no_mangle]
1283pub extern "C" fn SYNA_free_vector(data: *mut f32, dimensions: u16) {
1284    std::panic::catch_unwind(|| {
1285        if data.is_null() || dimensions == 0 {
1286            return;
1287        }
1288
1289        unsafe {
1290            let _ = Box::from_raw(std::slice::from_raw_parts_mut(data, dimensions as usize));
1291        }
1292    })
1293    .ok(); // Ignore panic result - we don't want to propagate panics from free
1294}
1295
1296// =============================================================================
1297// VectorStore FFI Functions
1298// =============================================================================
1299
1300use std::collections::HashMap;
1301use std::ffi::CString;
1302use std::path::{Path, PathBuf};
1303
1304use once_cell::sync::Lazy;
1305use parking_lot::Mutex;
1306
1307use crate::distance::DistanceMetric;
1308use crate::vector::{VectorConfig, VectorStore};
1309
1310/// Thread-safe global registry for managing open VectorStore instances.
1311/// Uses canonicalized paths as keys to ensure uniqueness.
1312static VECTOR_STORE_REGISTRY: Lazy<Mutex<HashMap<String, VectorStore>>> =
1313    Lazy::new(|| Mutex::new(HashMap::new()));
1314
1315/// Canonicalizes a path to an absolute path string for consistent registry keys.
1316///
1317/// If the path doesn't exist yet (for new databases), we use the parent directory's
1318/// canonical path combined with the filename.
1319fn canonicalize_vector_path(path: &str) -> Option<String> {
1320    let path_buf = PathBuf::from(path);
1321
1322    // Try to canonicalize directly (works if file exists)
1323    if let Ok(canonical) = std::fs::canonicalize(&path_buf) {
1324        return Some(canonical.to_string_lossy().to_string());
1325    }
1326
1327    // File doesn't exist yet - canonicalize parent and append filename
1328    let parent = path_buf.parent().unwrap_or(Path::new("."));
1329    let filename = path_buf.file_name()?;
1330
1331    // Canonicalize parent directory
1332    let canonical_parent = if parent.as_os_str().is_empty() || parent == Path::new(".") {
1333        std::env::current_dir().ok()?
1334    } else {
1335        std::fs::canonicalize(parent).ok()?
1336    };
1337
1338    let canonical = canonical_parent.join(filename);
1339    Some(canonical.to_string_lossy().to_string())
1340}
1341
1342/// Creates a new vector store at the given path.
1343///
1344/// # Arguments
1345/// * `path` - Null-terminated C string containing the path to the database file
1346/// * `dimensions` - Number of dimensions for vectors (64-8192)
1347/// * `metric` - Distance metric: 0=Cosine, 1=Euclidean, 2=DotProduct
1348///
1349/// # Returns
1350/// * `1` (ERR_SUCCESS) - Vector store created successfully
1351/// * `0` (ERR_GENERIC) - Generic error during creation
1352/// * `-2` (ERR_INVALID_PATH) - Path is null or invalid UTF-8
1353/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
1354///
1355/// # Safety
1356/// * `path` must be a valid null-terminated C string or null
1357///
1358/// _Requirements: 8.1_
1359#[no_mangle]
1360pub extern "C" fn SYNA_vector_store_new(path: *const c_char, dimensions: u16, metric: i32) -> i32 {
1361    std::panic::catch_unwind(|| {
1362        // Check for null pointer
1363        if path.is_null() {
1364            return ERR_INVALID_PATH;
1365        }
1366
1367        // Convert C string to Rust string
1368        let c_str = unsafe { CStr::from_ptr(path) };
1369        let path_str = match c_str.to_str() {
1370            Ok(s) => s,
1371            Err(_) => return ERR_INVALID_PATH,
1372        };
1373
1374        // Convert metric integer to DistanceMetric enum
1375        let distance_metric = match metric {
1376            0 => DistanceMetric::Cosine,
1377            1 => DistanceMetric::Euclidean,
1378            2 => DistanceMetric::DotProduct,
1379            _ => DistanceMetric::Cosine, // Default to Cosine for invalid values
1380        };
1381
1382        // Create config
1383        let config = VectorConfig {
1384            dimensions,
1385            metric: distance_metric,
1386            ..Default::default()
1387        };
1388
1389        // Canonicalize path for consistent registry keys
1390        let canonical_path = match canonicalize_vector_path(path_str) {
1391            Some(p) => p,
1392            None => return ERR_INVALID_PATH,
1393        };
1394
1395        // Create the vector store
1396        match VectorStore::new(path_str, config) {
1397            Ok(store) => {
1398                // Register in global registry with canonicalized path
1399                let mut registry = VECTOR_STORE_REGISTRY.lock();
1400                registry.insert(canonical_path, store);
1401                ERR_SUCCESS
1402            }
1403            Err(_) => ERR_GENERIC,
1404        }
1405    })
1406    .unwrap_or(ERR_INTERNAL_PANIC)
1407}
1408
1409/// Creates a new vector store with sync_on_write configuration.
1410///
1411/// # Arguments
1412/// * `path` - Null-terminated C string containing the path to the database file
1413/// * `dimensions` - Number of dimensions for vectors (64-8192)
1414/// * `metric` - Distance metric: 0=Cosine, 1=Euclidean, 2=DotProduct
1415/// * `sync_on_write` - 1 for sync after each write (durable), 0 for no sync (fast)
1416///
1417/// # Returns
1418/// * `1` (ERR_SUCCESS) - Vector store created successfully
1419/// * `0` (ERR_GENERIC) - Generic error during creation
1420/// * `-2` (ERR_INVALID_PATH) - Path is null or invalid UTF-8
1421/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
1422///
1423/// # Safety
1424/// * `path` must be a valid null-terminated C string or null
1425#[no_mangle]
1426pub extern "C" fn SYNA_vector_store_new_with_config(
1427    path: *const c_char,
1428    dimensions: u16,
1429    metric: i32,
1430    sync_on_write: i32,
1431) -> i32 {
1432    std::panic::catch_unwind(|| {
1433        // Check for null pointer
1434        if path.is_null() {
1435            return ERR_INVALID_PATH;
1436        }
1437
1438        // Convert C string to Rust string
1439        let c_str = unsafe { CStr::from_ptr(path) };
1440        let path_str = match c_str.to_str() {
1441            Ok(s) => s,
1442            Err(_) => return ERR_INVALID_PATH,
1443        };
1444
1445        // Convert metric integer to DistanceMetric enum
1446        let distance_metric = match metric {
1447            0 => DistanceMetric::Cosine,
1448            1 => DistanceMetric::Euclidean,
1449            2 => DistanceMetric::DotProduct,
1450            _ => DistanceMetric::Cosine,
1451        };
1452
1453        // Create config with sync_on_write setting
1454        let config = VectorConfig {
1455            dimensions,
1456            metric: distance_metric,
1457            sync_on_write: sync_on_write != 0,
1458            ..Default::default()
1459        };
1460
1461        // Canonicalize path for consistent registry keys
1462        let canonical_path = match canonicalize_vector_path(path_str) {
1463            Some(p) => p,
1464            None => return ERR_INVALID_PATH,
1465        };
1466
1467        // Create the vector store
1468        match VectorStore::new(path_str, config) {
1469            Ok(store) => {
1470                // Register in global registry with canonicalized path
1471                let mut registry = VECTOR_STORE_REGISTRY.lock();
1472                registry.insert(canonical_path, store);
1473                ERR_SUCCESS
1474            }
1475            Err(_) => ERR_GENERIC,
1476        }
1477    })
1478    .unwrap_or(ERR_INTERNAL_PANIC)
1479}
1480
1481/// Inserts a vector into the store.
1482///
1483/// # Arguments
1484/// * `path` - Null-terminated C string containing the path to the database file
1485/// * `key` - Null-terminated C string containing the key
1486/// * `data` - Pointer to the f32 vector data
1487/// * `dimensions` - Number of dimensions in the vector
1488///
1489/// # Returns
1490/// * `1` (ERR_SUCCESS) - Vector inserted successfully
1491/// * `0` (ERR_GENERIC) - Generic error during insertion
1492/// * `-1` (ERR_DB_NOT_FOUND) - Vector store not found in registry
1493/// * `-2` (ERR_INVALID_PATH) - Path, key, or data is null or invalid UTF-8
1494/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
1495///
1496/// # Safety
1497/// * `path` and `key` must be valid null-terminated C strings or null
1498/// * `data` must be a valid pointer to at least `dimensions` f32 values
1499///
1500/// _Requirements: 8.1_
1501#[no_mangle]
1502pub extern "C" fn SYNA_vector_store_insert(
1503    path: *const c_char,
1504    key: *const c_char,
1505    data: *const f32,
1506    dimensions: u16,
1507) -> i32 {
1508    std::panic::catch_unwind(|| {
1509        // Validate pointers
1510        if path.is_null() || key.is_null() || (data.is_null() && dimensions > 0) {
1511            return ERR_INVALID_PATH;
1512        }
1513
1514        // Convert C strings to Rust strings
1515        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
1516            Ok(s) => s,
1517            Err(_) => return ERR_INVALID_PATH,
1518        };
1519
1520        let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
1521            Ok(s) => s,
1522            Err(_) => return ERR_INVALID_PATH,
1523        };
1524
1525        // Canonicalize path for consistent registry keys
1526        let canonical_path = match canonicalize_vector_path(path_str) {
1527            Some(p) => p,
1528            None => return ERR_INVALID_PATH,
1529        };
1530
1531        // Create vector from raw pointer
1532        let vector = if dimensions == 0 {
1533            Vec::new()
1534        } else {
1535            unsafe { std::slice::from_raw_parts(data, dimensions as usize) }.to_vec()
1536        };
1537
1538        // Get the vector store from registry using canonicalized path
1539        let mut registry = VECTOR_STORE_REGISTRY.lock();
1540        match registry.get_mut(&canonical_path) {
1541            Some(store) => match store.insert(key_str, &vector) {
1542                Ok(_) => ERR_SUCCESS,
1543                Err(_) => ERR_GENERIC,
1544            },
1545            None => crate::error::ERR_DB_NOT_FOUND,
1546        }
1547    })
1548    .unwrap_or(ERR_INTERNAL_PANIC)
1549}
1550
1551/// Inserts multiple vectors in a single batch operation.
1552///
1553/// This is significantly faster than calling `SYNA_vector_store_insert()` in a loop:
1554/// - Single FFI boundary crossing for all vectors
1555/// - Deferred index building until after all vectors are inserted
1556/// - Reduced lock contention
1557///
1558/// # Arguments
1559/// * `path` - Null-terminated C string containing the path to the database file
1560/// * `keys` - Array of null-terminated C strings (keys for each vector)
1561/// * `data` - Pointer to contiguous f32 array containing all vectors (row-major)
1562/// * `dimensions` - Number of dimensions per vector
1563/// * `count` - Number of vectors to insert
1564///
1565/// # Returns
1566/// * Non-negative value - Number of vectors successfully inserted
1567/// * `-1` (ERR_DB_NOT_FOUND) - Vector store not found in registry
1568/// * `-2` (ERR_INVALID_PATH) - Path, keys, or data is null or invalid
1569/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
1570///
1571/// # Safety
1572/// * `path` must be a valid null-terminated C string
1573/// * `keys` must be a valid pointer to `count` null-terminated C strings
1574/// * `data` must be a valid pointer to `count * dimensions` f32 values
1575///
1576/// # Example (C)
1577/// ```c
1578/// const char* keys[] = {"doc1", "doc2", "doc3"};
1579/// float data[3 * 768] = { ... };  // 3 vectors of 768 dimensions
1580/// int32_t inserted = SYNA_vector_store_insert_batch("vectors.db", keys, data, 768, 3);
1581/// ```
1582///
1583/// _Requirements: 8.1_
1584#[no_mangle]
1585pub extern "C" fn SYNA_vector_store_insert_batch(
1586    path: *const c_char,
1587    keys: *const *const c_char,
1588    data: *const f32,
1589    dimensions: u16,
1590    count: usize,
1591) -> i32 {
1592    std::panic::catch_unwind(|| {
1593        // Validate pointers
1594        if path.is_null() || keys.is_null() || (data.is_null() && count > 0 && dimensions > 0) {
1595            return ERR_INVALID_PATH;
1596        }
1597
1598        // Convert path
1599        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
1600            Ok(s) => s,
1601            Err(_) => return ERR_INVALID_PATH,
1602        };
1603
1604        // Canonicalize path
1605        let canonical_path = match canonicalize_vector_path(path_str) {
1606            Some(p) => p,
1607            None => return ERR_INVALID_PATH,
1608        };
1609
1610        // Convert keys array
1611        let keys_slice = unsafe { std::slice::from_raw_parts(keys, count) };
1612        let mut key_strings: Vec<&str> = Vec::with_capacity(count);
1613        for key_ptr in keys_slice {
1614            if key_ptr.is_null() {
1615                return ERR_INVALID_PATH;
1616            }
1617            match unsafe { CStr::from_ptr(*key_ptr) }.to_str() {
1618                Ok(s) => key_strings.push(s),
1619                Err(_) => return ERR_INVALID_PATH,
1620            }
1621        }
1622
1623        // Create vector slices from contiguous data
1624        let total_floats = count * dimensions as usize;
1625        let data_slice = if total_floats == 0 {
1626            &[]
1627        } else {
1628            unsafe { std::slice::from_raw_parts(data, total_floats) }
1629        };
1630
1631        // Split into individual vectors
1632        let vectors: Vec<&[f32]> = data_slice.chunks(dimensions as usize).collect();
1633
1634        // Get the vector store from registry
1635        let mut registry = VECTOR_STORE_REGISTRY.lock();
1636        match registry.get_mut(&canonical_path) {
1637            Some(store) => match store.insert_batch(&key_strings, &vectors) {
1638                Ok(n) => n as i32,
1639                Err(_) => ERR_GENERIC,
1640            },
1641            None => crate::error::ERR_DB_NOT_FOUND,
1642        }
1643    })
1644    .unwrap_or(ERR_INTERNAL_PANIC)
1645}
1646
1647/// Inserts multiple vectors without updating the index (maximum write speed).
1648///
1649/// This is the fastest way to bulk-load vectors. Vectors are written to storage
1650/// but NOT added to the HNSW index. Call `SYNA_vector_store_build_index()` after
1651/// all inserts to build the index.
1652///
1653/// # Arguments
1654/// * `path` - Null-terminated C string containing the path to the database file
1655/// * `keys` - Array of null-terminated C strings (keys for each vector)
1656/// * `data` - Pointer to contiguous f32 array containing all vectors (row-major)
1657/// * `dimensions` - Number of dimensions per vector
1658/// * `count` - Number of vectors to insert
1659///
1660/// # Returns
1661/// * Non-negative value - Number of vectors successfully inserted
1662/// * `-1` (ERR_DB_NOT_FOUND) - Vector store not found in registry
1663/// * `-2` (ERR_INVALID_PATH) - Path, keys, or data is null or invalid
1664/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
1665///
1666/// # Performance
1667/// This function achieves 100K+ inserts/sec by skipping index updates.
1668/// After bulk loading, call `SYNA_vector_store_build_index()` to enable fast search.
1669///
1670/// _Requirements: 8.1_
1671#[no_mangle]
1672pub extern "C" fn SYNA_vector_store_insert_batch_fast(
1673    path: *const c_char,
1674    keys: *const *const c_char,
1675    data: *const f32,
1676    dimensions: u16,
1677    count: usize,
1678) -> i32 {
1679    std::panic::catch_unwind(|| {
1680        if path.is_null() || keys.is_null() || (data.is_null() && count > 0 && dimensions > 0) {
1681            return ERR_INVALID_PATH;
1682        }
1683
1684        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
1685            Ok(s) => s,
1686            Err(_) => return ERR_INVALID_PATH,
1687        };
1688
1689        let canonical_path = match canonicalize_vector_path(path_str) {
1690            Some(p) => p,
1691            None => return ERR_INVALID_PATH,
1692        };
1693
1694        let keys_slice = unsafe { std::slice::from_raw_parts(keys, count) };
1695        let mut key_strings: Vec<&str> = Vec::with_capacity(count);
1696        for key_ptr in keys_slice {
1697            if key_ptr.is_null() {
1698                return ERR_INVALID_PATH;
1699            }
1700            match unsafe { CStr::from_ptr(*key_ptr) }.to_str() {
1701                Ok(s) => key_strings.push(s),
1702                Err(_) => return ERR_INVALID_PATH,
1703            }
1704        }
1705
1706        let total_floats = count * dimensions as usize;
1707        let data_slice = if total_floats == 0 {
1708            &[]
1709        } else {
1710            unsafe { std::slice::from_raw_parts(data, total_floats) }
1711        };
1712
1713        let vectors: Vec<&[f32]> = data_slice.chunks(dimensions as usize).collect();
1714
1715        let mut registry = VECTOR_STORE_REGISTRY.lock();
1716        match registry.get_mut(&canonical_path) {
1717            Some(store) => match store.insert_batch_fast(&key_strings, &vectors, false) {
1718                Ok(n) => n as i32,
1719                Err(_) => ERR_GENERIC,
1720            },
1721            None => crate::error::ERR_DB_NOT_FOUND,
1722        }
1723    })
1724    .unwrap_or(ERR_INTERNAL_PANIC)
1725}
1726
1727/// Searches for k nearest neighbors in the vector store.
1728///
1729/// Returns a JSON array of results with the following structure:
1730/// ```json
1731/// [
1732///   {"key": "doc1", "score": 0.123, "vector": [0.1, 0.2, ...]},
1733///   {"key": "doc2", "score": 0.456, "vector": [0.3, 0.4, ...]}
1734/// ]
1735/// ```
1736///
1737/// # Arguments
1738/// * `path` - Null-terminated C string containing the path to the database file
1739/// * `query` - Pointer to the f32 query vector
1740/// * `dimensions` - Number of dimensions in the query vector
1741/// * `k` - Number of nearest neighbors to return
1742/// * `out_json` - Pointer to write the JSON result string to
1743///
1744/// # Returns
1745/// * Non-negative value - Number of results found
1746/// * `-1` (ERR_DB_NOT_FOUND) - Vector store not found in registry
1747/// * `-2` (ERR_INVALID_PATH) - Path, query, or out_json is null or invalid
1748/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
1749///
1750/// # Safety
1751/// * `path` must be a valid null-terminated C string or null
1752/// * `query` must be a valid pointer to at least `dimensions` f32 values
1753/// * `out_json` must be a valid pointer to a `*mut c_char`
1754/// * The returned JSON string MUST be freed using `SYNA_free_json()` to avoid memory leaks
1755///
1756/// _Requirements: 8.1_
1757#[no_mangle]
1758pub extern "C" fn SYNA_vector_store_search(
1759    path: *const c_char,
1760    query: *const f32,
1761    dimensions: u16,
1762    k: usize,
1763    out_json: *mut *mut c_char,
1764) -> i32 {
1765    std::panic::catch_unwind(|| {
1766        // Validate pointers
1767        if path.is_null() || query.is_null() || out_json.is_null() {
1768            return ERR_INVALID_PATH;
1769        }
1770
1771        // Convert C string to Rust string
1772        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
1773            Ok(s) => s,
1774            Err(_) => return ERR_INVALID_PATH,
1775        };
1776
1777        // Canonicalize path for consistent registry keys
1778        let canonical_path = match canonicalize_vector_path(path_str) {
1779            Some(p) => p,
1780            None => return ERR_INVALID_PATH,
1781        };
1782
1783        // Create query vector from raw pointer
1784        let query_vec: Vec<f32> =
1785            unsafe { std::slice::from_raw_parts(query, dimensions as usize) }.to_vec();
1786
1787        // Get the vector store from registry using canonicalized path
1788        let mut registry = VECTOR_STORE_REGISTRY.lock();
1789        match registry.get_mut(&canonical_path) {
1790            Some(store) => match store.search(&query_vec, k) {
1791                Ok(results) => {
1792                    // Convert results to JSON
1793                    let json_results: Vec<serde_json::Value> = results
1794                        .iter()
1795                        .map(|r| {
1796                            serde_json::json!({
1797                                "key": r.key,
1798                                "score": r.score,
1799                                "vector": r.vector
1800                            })
1801                        })
1802                        .collect();
1803
1804                    let json_str =
1805                        serde_json::to_string(&json_results).unwrap_or_else(|_| "[]".to_string());
1806                    let result_count = results.len() as i32;
1807
1808                    // Convert to C string
1809                    match CString::new(json_str) {
1810                        Ok(c_string) => {
1811                            unsafe { *out_json = c_string.into_raw() };
1812                            result_count
1813                        }
1814                        Err(_) => ERR_GENERIC,
1815                    }
1816                }
1817                Err(_) => ERR_GENERIC,
1818            },
1819            None => crate::error::ERR_DB_NOT_FOUND,
1820        }
1821    })
1822    .unwrap_or(ERR_INTERNAL_PANIC)
1823}
1824
1825/// Builds the HNSW index for a vector store.
1826///
1827/// This function manually triggers HNSW index construction for faster search.
1828/// The index is built automatically when vector count exceeds the threshold,
1829/// but this function allows explicit control.
1830///
1831/// # Arguments
1832/// * `path` - Null-terminated C string containing the path to the vector store
1833///
1834/// # Returns
1835/// * `1` (ERR_SUCCESS) - Index built successfully
1836/// * `0` (ERR_GENERIC) - Generic error during build
1837/// * `-1` (ERR_DB_NOT_FOUND) - Vector store not found in registry
1838/// * `-2` (ERR_INVALID_PATH) - Path is null or invalid UTF-8
1839/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
1840///
1841/// # Safety
1842/// * `path` must be a valid null-terminated C string or null
1843///
1844/// _Requirements: 8.1_
1845#[no_mangle]
1846pub extern "C" fn SYNA_vector_store_build_index(path: *const c_char) -> i32 {
1847    std::panic::catch_unwind(|| {
1848        // Check for null pointer
1849        if path.is_null() {
1850            return ERR_INVALID_PATH;
1851        }
1852
1853        // Convert path to Rust string
1854        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
1855            Ok(s) => s,
1856            Err(_) => return ERR_INVALID_PATH,
1857        };
1858
1859        // Canonicalize path for consistent registry keys
1860        let canonical_path = match canonicalize_vector_path(path_str) {
1861            Some(p) => p,
1862            None => return ERR_INVALID_PATH,
1863        };
1864
1865        // Get the vector store from registry
1866        let mut registry = VECTOR_STORE_REGISTRY.lock();
1867        match registry.get_mut(&canonical_path) {
1868            Some(store) => match store.build_index() {
1869                Ok(()) => crate::error::ERR_SUCCESS,
1870                Err(_) => ERR_GENERIC,
1871            },
1872            None => crate::error::ERR_DB_NOT_FOUND,
1873        }
1874    })
1875    .unwrap_or(ERR_INTERNAL_PANIC)
1876}
1877
1878/// Returns whether a vector store has an HNSW index built.
1879///
1880/// # Arguments
1881/// * `path` - Null-terminated C string containing the path to the vector store
1882///
1883/// # Returns
1884/// * `1` - Index is built
1885/// * `0` - Index is not built
1886/// * `-1` (ERR_DB_NOT_FOUND) - Vector store not found in registry
1887/// * `-2` (ERR_INVALID_PATH) - Path is null or invalid UTF-8
1888/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
1889///
1890/// # Safety
1891/// * `path` must be a valid null-terminated C string or null
1892///
1893/// _Requirements: 8.1_
1894#[no_mangle]
1895pub extern "C" fn SYNA_vector_store_has_index(path: *const c_char) -> i32 {
1896    std::panic::catch_unwind(|| {
1897        // Check for null pointer
1898        if path.is_null() {
1899            return ERR_INVALID_PATH;
1900        }
1901
1902        // Convert path to Rust string
1903        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
1904            Ok(s) => s,
1905            Err(_) => return ERR_INVALID_PATH,
1906        };
1907
1908        // Canonicalize path for consistent registry keys
1909        let canonical_path = match canonicalize_vector_path(path_str) {
1910            Some(p) => p,
1911            None => return ERR_INVALID_PATH,
1912        };
1913
1914        // Get the vector store from registry
1915        let registry = VECTOR_STORE_REGISTRY.lock();
1916        match registry.get(&canonical_path) {
1917            Some(store) => {
1918                if store.has_index() {
1919                    1
1920                } else {
1921                    0
1922                }
1923            }
1924            None => crate::error::ERR_DB_NOT_FOUND,
1925        }
1926    })
1927    .unwrap_or(ERR_INTERNAL_PANIC)
1928}
1929
1930/// Closes a vector store and saves any pending changes.
1931///
1932/// This function removes the vector store from the global registry and
1933/// triggers the Drop implementation, which saves any dirty index to disk.
1934///
1935/// # Arguments
1936/// * `path` - Null-terminated C string containing the path to the database file
1937///
1938/// # Returns
1939/// * `1` (ERR_SUCCESS) - Store closed successfully
1940/// * `-1` (ERR_DB_NOT_FOUND) - Store not found in registry
1941/// * `-2` (ERR_INVALID_PATH) - Path is null or invalid UTF-8
1942/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
1943///
1944/// # Safety
1945/// * `path` must be a valid null-terminated C string or null
1946///
1947/// _Requirements: 8.1_
1948#[no_mangle]
1949pub extern "C" fn SYNA_vector_store_close(path: *const c_char) -> i32 {
1950    std::panic::catch_unwind(|| {
1951        // Check for null pointer
1952        if path.is_null() {
1953            return ERR_INVALID_PATH;
1954        }
1955
1956        // Convert C string to Rust string
1957        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
1958            Ok(s) => s,
1959            Err(_) => return ERR_INVALID_PATH,
1960        };
1961
1962        // Canonicalize path for consistent lookup
1963        let canonical_path = match std::fs::canonicalize(path_str) {
1964            Ok(p) => p.to_string_lossy().to_string(),
1965            Err(_) => path_str.to_string(),
1966        };
1967
1968        // Remove from registry (this triggers Drop which saves the index)
1969        let mut registry = VECTOR_STORE_REGISTRY.lock();
1970        match registry.remove(&canonical_path) {
1971            Some(_store) => {
1972                // Store is dropped here, triggering index save
1973                ERR_SUCCESS
1974            }
1975            None => crate::error::ERR_DB_NOT_FOUND,
1976        }
1977    })
1978    .unwrap_or(ERR_INTERNAL_PANIC)
1979}
1980
1981/// Flushes any pending changes to disk without closing the store.
1982///
1983/// This saves the HNSW index if it has unsaved changes.
1984///
1985/// # Arguments
1986/// * `path` - Null-terminated C string containing the path to the database file
1987///
1988/// # Returns
1989/// * `1` (ERR_SUCCESS) - Flush successful
1990/// * `-1` (ERR_DB_NOT_FOUND) - Store not found in registry
1991/// * `-2` (ERR_INVALID_PATH) - Path is null or invalid UTF-8
1992/// * `0` (ERR_GENERIC) - Flush failed
1993/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
1994///
1995/// # Safety
1996/// * `path` must be a valid null-terminated C string or null
1997///
1998/// _Requirements: 8.1_
1999#[no_mangle]
2000pub extern "C" fn SYNA_vector_store_flush(path: *const c_char) -> i32 {
2001    std::panic::catch_unwind(|| {
2002        // Check for null pointer
2003        if path.is_null() {
2004            return ERR_INVALID_PATH;
2005        }
2006
2007        // Convert C string to Rust string
2008        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2009            Ok(s) => s,
2010            Err(_) => return ERR_INVALID_PATH,
2011        };
2012
2013        // Canonicalize path for consistent lookup
2014        let canonical_path = match std::fs::canonicalize(path_str) {
2015            Ok(p) => p.to_string_lossy().to_string(),
2016            Err(_) => path_str.to_string(),
2017        };
2018
2019        // Get store from registry
2020        let mut registry = VECTOR_STORE_REGISTRY.lock();
2021        match registry.get_mut(&canonical_path) {
2022            Some(store) => match store.flush() {
2023                Ok(_) => ERR_SUCCESS,
2024                Err(_) => ERR_GENERIC,
2025            },
2026            None => crate::error::ERR_DB_NOT_FOUND,
2027        }
2028    })
2029    .unwrap_or(ERR_INTERNAL_PANIC)
2030}
2031
2032/// Frees a JSON string allocated by `SYNA_vector_store_search()`.
2033///
2034/// # Arguments
2035/// * `json` - Pointer returned by `SYNA_vector_store_search()` in `out_json`
2036///
2037/// # Safety
2038/// * `json` must have been returned by `SYNA_vector_store_search()`
2039/// * This function must only be called once per pointer
2040/// * Calling with a null pointer is safe (no-op)
2041///
2042/// _Requirements: 8.1_
2043#[no_mangle]
2044pub extern "C" fn SYNA_free_json(json: *mut c_char) {
2045    std::panic::catch_unwind(|| {
2046        if json.is_null() {
2047            return;
2048        }
2049
2050        unsafe {
2051            // Reconstruct the CString and drop it
2052            let _ = CString::from_raw(json);
2053        }
2054    })
2055    .ok(); // Ignore panic result - we don't want to propagate panics from free
2056}
2057
2058// =============================================================================
2059// Model Registry FFI Functions
2060// =============================================================================
2061
2062use crate::model_registry::{ModelRegistry, ModelStage};
2063
2064/// Thread-safe global registry for managing open ModelRegistry instances.
2065static MODEL_REGISTRY: Lazy<Mutex<HashMap<String, ModelRegistry>>> =
2066    Lazy::new(|| Mutex::new(HashMap::new()));
2067
2068/// Opens or creates a model registry at the given path.
2069///
2070/// # Arguments
2071/// * `path` - Null-terminated C string containing the path to the database file
2072///
2073/// # Returns
2074/// * `1` (ERR_SUCCESS) - Registry opened successfully
2075/// * `0` (ERR_GENERIC) - Generic error during open
2076/// * `-2` (ERR_INVALID_PATH) - Path is null or invalid UTF-8
2077/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2078///
2079/// # Safety
2080/// * `path` must be a valid null-terminated C string or null
2081///
2082/// _Requirements: 8.1_
2083#[no_mangle]
2084pub extern "C" fn SYNA_model_registry_open(path: *const c_char) -> i32 {
2085    std::panic::catch_unwind(|| {
2086        if path.is_null() {
2087            return ERR_INVALID_PATH;
2088        }
2089
2090        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2091            Ok(s) => s,
2092            Err(_) => return ERR_INVALID_PATH,
2093        };
2094
2095        match ModelRegistry::new(path_str) {
2096            Ok(registry) => {
2097                let mut reg = MODEL_REGISTRY.lock();
2098                reg.insert(path_str.to_string(), registry);
2099                ERR_SUCCESS
2100            }
2101            Err(_) => ERR_GENERIC,
2102        }
2103    })
2104    .unwrap_or(ERR_INTERNAL_PANIC)
2105}
2106
2107/// Saves a model to the registry with automatic versioning.
2108///
2109/// # Arguments
2110/// * `path` - Null-terminated C string containing the path to the registry
2111/// * `name` - Null-terminated C string containing the model name
2112/// * `data` - Pointer to the model data bytes
2113/// * `data_len` - Length of the model data
2114/// * `metadata_json` - Null-terminated JSON string with metadata (can be null for empty)
2115/// * `out_version` - Pointer to write the assigned version number
2116/// * `out_checksum` - Pointer to write the checksum string (caller must free with SYNA_free_text)
2117/// * `out_checksum_len` - Pointer to write the checksum string length
2118///
2119/// # Returns
2120/// * `1` (ERR_SUCCESS) - Model saved successfully
2121/// * `0` (ERR_GENERIC) - Generic error during save
2122/// * `-1` (ERR_DB_NOT_FOUND) - Registry not found
2123/// * `-2` (ERR_INVALID_PATH) - Invalid arguments
2124/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2125///
2126/// _Requirements: 8.1_
2127#[no_mangle]
2128pub extern "C" fn SYNA_model_save(
2129    path: *const c_char,
2130    name: *const c_char,
2131    data: *const u8,
2132    data_len: usize,
2133    metadata_json: *const c_char,
2134    out_version: *mut u32,
2135    out_checksum: *mut *mut c_char,
2136    out_checksum_len: *mut usize,
2137) -> i64 {
2138    std::panic::catch_unwind(|| {
2139        // Validate required pointers
2140        if path.is_null() || name.is_null() || (data.is_null() && data_len > 0) {
2141            return ERR_INVALID_PATH as i64;
2142        }
2143        if out_version.is_null() || out_checksum.is_null() || out_checksum_len.is_null() {
2144            return ERR_INVALID_PATH as i64;
2145        }
2146
2147        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2148            Ok(s) => s,
2149            Err(_) => return ERR_INVALID_PATH as i64,
2150        };
2151
2152        let name_str = match unsafe { CStr::from_ptr(name) }.to_str() {
2153            Ok(s) => s,
2154            Err(_) => return ERR_INVALID_PATH as i64,
2155        };
2156
2157        // Parse metadata JSON if provided
2158        let metadata: std::collections::HashMap<String, String> = if metadata_json.is_null() {
2159            std::collections::HashMap::new()
2160        } else {
2161            match unsafe { CStr::from_ptr(metadata_json) }.to_str() {
2162                Ok(json_str) => serde_json::from_str(json_str).unwrap_or_default(),
2163                Err(_) => std::collections::HashMap::new(),
2164            }
2165        };
2166
2167        // Create data slice
2168        let model_data = if data_len == 0 {
2169            &[]
2170        } else {
2171            unsafe { std::slice::from_raw_parts(data, data_len) }
2172        };
2173
2174        // Get registry and save model
2175        let mut reg = MODEL_REGISTRY.lock();
2176        match reg.get_mut(path_str) {
2177            Some(registry) => {
2178                match registry.save_model(name_str, model_data, metadata) {
2179                    Ok(version) => {
2180                        unsafe { *out_version = version.version };
2181
2182                        // Return checksum as C string
2183                        let checksum_len = version.checksum.len();
2184                        unsafe { *out_checksum_len = checksum_len };
2185
2186                        let mut bytes = version.checksum.into_bytes();
2187                        bytes.push(0);
2188                        let c_str = bytes.into_boxed_slice();
2189                        unsafe { *out_checksum = Box::into_raw(c_str) as *mut c_char };
2190
2191                        ERR_SUCCESS as i64
2192                    }
2193                    Err(_) => ERR_GENERIC as i64,
2194                }
2195            }
2196            None => crate::error::ERR_DB_NOT_FOUND as i64,
2197        }
2198    })
2199    .unwrap_or(ERR_INTERNAL_PANIC as i64)
2200}
2201
2202/// Loads a model from the registry with checksum verification.
2203///
2204/// # Arguments
2205/// * `path` - Null-terminated C string containing the path to the registry
2206/// * `name` - Null-terminated C string containing the model name
2207/// * `version` - Version number to load (0 for latest)
2208/// * `out_data` - Pointer to write the model data pointer
2209/// * `out_data_len` - Pointer to write the model data length
2210/// * `out_meta_json` - Pointer to write the metadata JSON string
2211/// * `out_meta_len` - Pointer to write the metadata JSON length
2212///
2213/// # Returns
2214/// * `1` (ERR_SUCCESS) - Model loaded successfully
2215/// * `0` (ERR_GENERIC) - Generic error during load
2216/// * `-1` (ERR_DB_NOT_FOUND) - Registry not found
2217/// * `-2` (ERR_INVALID_PATH) - Invalid arguments
2218/// * `-5` (ERR_KEY_NOT_FOUND) - Model not found
2219/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2220///
2221/// _Requirements: 8.1_
2222#[no_mangle]
2223pub extern "C" fn SYNA_model_load(
2224    path: *const c_char,
2225    name: *const c_char,
2226    version: u32,
2227    out_data: *mut *mut u8,
2228    out_data_len: *mut usize,
2229    out_meta_json: *mut *mut c_char,
2230    out_meta_len: *mut usize,
2231) -> i32 {
2232    std::panic::catch_unwind(|| {
2233        if path.is_null() || name.is_null() || out_data.is_null() || out_data_len.is_null() {
2234            return ERR_INVALID_PATH;
2235        }
2236        if out_meta_json.is_null() || out_meta_len.is_null() {
2237            return ERR_INVALID_PATH;
2238        }
2239
2240        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2241            Ok(s) => s,
2242            Err(_) => return ERR_INVALID_PATH,
2243        };
2244
2245        let name_str = match unsafe { CStr::from_ptr(name) }.to_str() {
2246            Ok(s) => s,
2247            Err(_) => return ERR_INVALID_PATH,
2248        };
2249
2250        let version_opt = if version == 0 { None } else { Some(version) };
2251
2252        let mut reg = MODEL_REGISTRY.lock();
2253        match reg.get_mut(path_str) {
2254            Some(registry) => {
2255                match registry.load_model(name_str, version_opt) {
2256                    Ok((data, version_info)) => {
2257                        // Return model data
2258                        let data_len = data.len();
2259                        unsafe { *out_data_len = data_len };
2260
2261                        if data_len > 0 {
2262                            let boxed = data.into_boxed_slice();
2263                            unsafe { *out_data = Box::into_raw(boxed) as *mut u8 };
2264                        } else {
2265                            unsafe { *out_data = std::ptr::null_mut() };
2266                        }
2267
2268                        // Return metadata as JSON
2269                        let meta_json = serde_json::to_string(&version_info)
2270                            .unwrap_or_else(|_| "{}".to_string());
2271                        let meta_len = meta_json.len();
2272                        unsafe { *out_meta_len = meta_len };
2273
2274                        let mut bytes = meta_json.into_bytes();
2275                        bytes.push(0);
2276                        let c_str = bytes.into_boxed_slice();
2277                        unsafe { *out_meta_json = Box::into_raw(c_str) as *mut c_char };
2278
2279                        ERR_SUCCESS
2280                    }
2281                    Err(crate::error::SynaError::ModelNotFound(_)) => ERR_KEY_NOT_FOUND,
2282                    Err(crate::error::SynaError::ChecksumMismatch { .. }) => ERR_GENERIC,
2283                    Err(_) => ERR_GENERIC,
2284                }
2285            }
2286            None => crate::error::ERR_DB_NOT_FOUND,
2287        }
2288    })
2289    .unwrap_or(ERR_INTERNAL_PANIC)
2290}
2291
2292/// Lists all versions of a model.
2293///
2294/// # Arguments
2295/// * `path` - Null-terminated C string containing the path to the registry
2296/// * `name` - Null-terminated C string containing the model name
2297/// * `out_json` - Pointer to write the JSON array of versions
2298/// * `out_len` - Pointer to write the JSON string length
2299///
2300/// # Returns
2301/// * Non-negative value - Number of versions found
2302/// * `-1` (ERR_DB_NOT_FOUND) - Registry not found
2303/// * `-2` (ERR_INVALID_PATH) - Invalid arguments
2304/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2305///
2306/// _Requirements: 8.1_
2307#[no_mangle]
2308pub extern "C" fn SYNA_model_list(
2309    path: *const c_char,
2310    name: *const c_char,
2311    out_json: *mut *mut c_char,
2312    out_len: *mut usize,
2313) -> i32 {
2314    std::panic::catch_unwind(|| {
2315        if path.is_null() || name.is_null() || out_json.is_null() || out_len.is_null() {
2316            return ERR_INVALID_PATH;
2317        }
2318
2319        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2320            Ok(s) => s,
2321            Err(_) => return ERR_INVALID_PATH,
2322        };
2323
2324        let name_str = match unsafe { CStr::from_ptr(name) }.to_str() {
2325            Ok(s) => s,
2326            Err(_) => return ERR_INVALID_PATH,
2327        };
2328
2329        let mut reg = MODEL_REGISTRY.lock();
2330        match reg.get_mut(path_str) {
2331            Some(registry) => match registry.list_versions(name_str) {
2332                Ok(versions) => {
2333                    let count = versions.len() as i32;
2334                    let json =
2335                        serde_json::to_string(&versions).unwrap_or_else(|_| "[]".to_string());
2336
2337                    unsafe { *out_len = json.len() };
2338                    match CString::new(json) {
2339                        Ok(c_string) => {
2340                            unsafe { *out_json = c_string.into_raw() };
2341                            count
2342                        }
2343                        Err(_) => ERR_GENERIC,
2344                    }
2345                }
2346                Err(_) => ERR_GENERIC,
2347            },
2348            None => crate::error::ERR_DB_NOT_FOUND,
2349        }
2350    })
2351    .unwrap_or(ERR_INTERNAL_PANIC)
2352}
2353
2354/// Sets the deployment stage for a model version.
2355///
2356/// # Arguments
2357/// * `path` - Null-terminated C string containing the path to the registry
2358/// * `name` - Null-terminated C string containing the model name
2359/// * `version` - Version number to update
2360/// * `stage` - Stage: 0=Development, 1=Staging, 2=Production, 3=Archived
2361///
2362/// # Returns
2363/// * `1` (ERR_SUCCESS) - Stage updated successfully
2364/// * `0` (ERR_GENERIC) - Generic error
2365/// * `-1` (ERR_DB_NOT_FOUND) - Registry not found
2366/// * `-2` (ERR_INVALID_PATH) - Invalid arguments
2367/// * `-5` (ERR_KEY_NOT_FOUND) - Model/version not found
2368/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2369///
2370/// _Requirements: 8.1_
2371#[no_mangle]
2372pub extern "C" fn SYNA_model_set_stage(
2373    path: *const c_char,
2374    name: *const c_char,
2375    version: u32,
2376    stage: i32,
2377) -> i32 {
2378    std::panic::catch_unwind(|| {
2379        if path.is_null() || name.is_null() {
2380            return ERR_INVALID_PATH;
2381        }
2382
2383        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2384            Ok(s) => s,
2385            Err(_) => return ERR_INVALID_PATH,
2386        };
2387
2388        let name_str = match unsafe { CStr::from_ptr(name) }.to_str() {
2389            Ok(s) => s,
2390            Err(_) => return ERR_INVALID_PATH,
2391        };
2392
2393        let model_stage = match stage {
2394            0 => ModelStage::Development,
2395            1 => ModelStage::Staging,
2396            2 => ModelStage::Production,
2397            3 => ModelStage::Archived,
2398            _ => ModelStage::Development,
2399        };
2400
2401        let mut reg = MODEL_REGISTRY.lock();
2402        match reg.get_mut(path_str) {
2403            Some(registry) => match registry.set_stage(name_str, version, model_stage) {
2404                Ok(_) => ERR_SUCCESS,
2405                Err(crate::error::SynaError::ModelNotFound(_)) => ERR_KEY_NOT_FOUND,
2406                Err(_) => ERR_GENERIC,
2407            },
2408            None => crate::error::ERR_DB_NOT_FOUND,
2409        }
2410    })
2411    .unwrap_or(ERR_INTERNAL_PANIC)
2412}
2413
2414// =============================================================================
2415// Experiment Tracking FFI Functions
2416// =============================================================================
2417
2418use crate::experiment::{ExperimentTracker, RunStatus};
2419
2420/// Thread-safe global registry for managing open ExperimentTracker instances.
2421static EXPERIMENT_REGISTRY: Lazy<Mutex<HashMap<String, ExperimentTracker>>> =
2422    Lazy::new(|| Mutex::new(HashMap::new()));
2423
2424/// Opens or creates an experiment tracker at the given path.
2425///
2426/// # Arguments
2427/// * `path` - Null-terminated C string containing the path to the database file
2428///
2429/// # Returns
2430/// * `1` (ERR_SUCCESS) - Tracker opened successfully
2431/// * `0` (ERR_GENERIC) - Generic error during open
2432/// * `-2` (ERR_INVALID_PATH) - Path is null or invalid UTF-8
2433/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2434///
2435/// _Requirements: 8.1_
2436#[no_mangle]
2437pub extern "C" fn SYNA_exp_tracker_open(path: *const c_char) -> i32 {
2438    std::panic::catch_unwind(|| {
2439        if path.is_null() {
2440            return ERR_INVALID_PATH;
2441        }
2442
2443        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2444            Ok(s) => s,
2445            Err(_) => return ERR_INVALID_PATH,
2446        };
2447
2448        match ExperimentTracker::new(path_str) {
2449            Ok(tracker) => {
2450                let mut reg = EXPERIMENT_REGISTRY.lock();
2451                reg.insert(path_str.to_string(), tracker);
2452                ERR_SUCCESS
2453            }
2454            Err(_) => ERR_GENERIC,
2455        }
2456    })
2457    .unwrap_or(ERR_INTERNAL_PANIC)
2458}
2459
2460/// Starts a new experiment run.
2461///
2462/// # Arguments
2463/// * `path` - Null-terminated C string containing the path to the tracker
2464/// * `experiment` - Null-terminated C string containing the experiment name
2465/// * `tags_json` - Null-terminated JSON array of tags (can be null for empty)
2466/// * `out_run_id` - Pointer to write the run ID string
2467/// * `out_run_id_len` - Pointer to write the run ID length
2468///
2469/// # Returns
2470/// * `1` (ERR_SUCCESS) - Run started successfully
2471/// * `0` (ERR_GENERIC) - Generic error
2472/// * `-1` (ERR_DB_NOT_FOUND) - Tracker not found
2473/// * `-2` (ERR_INVALID_PATH) - Invalid arguments
2474/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2475///
2476/// _Requirements: 8.1_
2477#[no_mangle]
2478pub extern "C" fn SYNA_exp_start_run(
2479    path: *const c_char,
2480    experiment: *const c_char,
2481    tags_json: *const c_char,
2482    out_run_id: *mut *mut c_char,
2483    out_run_id_len: *mut usize,
2484) -> i32 {
2485    std::panic::catch_unwind(|| {
2486        if path.is_null()
2487            || experiment.is_null()
2488            || out_run_id.is_null()
2489            || out_run_id_len.is_null()
2490        {
2491            return ERR_INVALID_PATH;
2492        }
2493
2494        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2495            Ok(s) => s,
2496            Err(_) => return ERR_INVALID_PATH,
2497        };
2498
2499        let exp_str = match unsafe { CStr::from_ptr(experiment) }.to_str() {
2500            Ok(s) => s,
2501            Err(_) => return ERR_INVALID_PATH,
2502        };
2503
2504        let tags: Vec<String> = if tags_json.is_null() {
2505            Vec::new()
2506        } else {
2507            match unsafe { CStr::from_ptr(tags_json) }.to_str() {
2508                Ok(json_str) => serde_json::from_str(json_str).unwrap_or_default(),
2509                Err(_) => Vec::new(),
2510            }
2511        };
2512
2513        let mut reg = EXPERIMENT_REGISTRY.lock();
2514        match reg.get_mut(path_str) {
2515            Some(tracker) => match tracker.start_run(exp_str, tags) {
2516                Ok(run_id) => {
2517                    let run_id_len = run_id.len();
2518                    unsafe { *out_run_id_len = run_id_len };
2519
2520                    let mut bytes = run_id.into_bytes();
2521                    bytes.push(0);
2522                    let c_str = bytes.into_boxed_slice();
2523                    unsafe { *out_run_id = Box::into_raw(c_str) as *mut c_char };
2524
2525                    ERR_SUCCESS
2526                }
2527                Err(_) => ERR_GENERIC,
2528            },
2529            None => crate::error::ERR_DB_NOT_FOUND,
2530        }
2531    })
2532    .unwrap_or(ERR_INTERNAL_PANIC)
2533}
2534
2535/// Logs a parameter for a run.
2536///
2537/// # Arguments
2538/// * `path` - Null-terminated C string containing the path to the tracker
2539/// * `run_id` - Null-terminated C string containing the run ID
2540/// * `key` - Null-terminated C string containing the parameter name
2541/// * `value` - Null-terminated C string containing the parameter value
2542///
2543/// # Returns
2544/// * `1` (ERR_SUCCESS) - Parameter logged successfully
2545/// * `0` (ERR_GENERIC) - Generic error
2546/// * `-1` (ERR_DB_NOT_FOUND) - Tracker not found
2547/// * `-2` (ERR_INVALID_PATH) - Invalid arguments
2548/// * `-5` (ERR_KEY_NOT_FOUND) - Run not found
2549/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2550///
2551/// _Requirements: 8.1_
2552#[no_mangle]
2553pub extern "C" fn SYNA_exp_log_param(
2554    path: *const c_char,
2555    run_id: *const c_char,
2556    key: *const c_char,
2557    value: *const c_char,
2558) -> i32 {
2559    std::panic::catch_unwind(|| {
2560        if path.is_null() || run_id.is_null() || key.is_null() || value.is_null() {
2561            return ERR_INVALID_PATH;
2562        }
2563
2564        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2565            Ok(s) => s,
2566            Err(_) => return ERR_INVALID_PATH,
2567        };
2568
2569        let run_id_str = match unsafe { CStr::from_ptr(run_id) }.to_str() {
2570            Ok(s) => s,
2571            Err(_) => return ERR_INVALID_PATH,
2572        };
2573
2574        let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
2575            Ok(s) => s,
2576            Err(_) => return ERR_INVALID_PATH,
2577        };
2578
2579        let value_str = match unsafe { CStr::from_ptr(value) }.to_str() {
2580            Ok(s) => s,
2581            Err(_) => return ERR_INVALID_PATH,
2582        };
2583
2584        let mut reg = EXPERIMENT_REGISTRY.lock();
2585        match reg.get_mut(path_str) {
2586            Some(tracker) => match tracker.log_param(run_id_str, key_str, value_str) {
2587                Ok(_) => ERR_SUCCESS,
2588                Err(crate::error::SynaError::RunNotFound(_)) => ERR_KEY_NOT_FOUND,
2589                Err(crate::error::SynaError::RunAlreadyEnded(_)) => ERR_GENERIC,
2590                Err(_) => ERR_GENERIC,
2591            },
2592            None => crate::error::ERR_DB_NOT_FOUND,
2593        }
2594    })
2595    .unwrap_or(ERR_INTERNAL_PANIC)
2596}
2597
2598/// Logs a metric value for a run.
2599///
2600/// # Arguments
2601/// * `path` - Null-terminated C string containing the path to the tracker
2602/// * `run_id` - Null-terminated C string containing the run ID
2603/// * `key` - Null-terminated C string containing the metric name
2604/// * `value` - The metric value (f64)
2605/// * `step` - Step number (use -1 for auto-generated timestamp)
2606///
2607/// # Returns
2608/// * `1` (ERR_SUCCESS) - Metric logged successfully
2609/// * `0` (ERR_GENERIC) - Generic error
2610/// * `-1` (ERR_DB_NOT_FOUND) - Tracker not found
2611/// * `-2` (ERR_INVALID_PATH) - Invalid arguments
2612/// * `-5` (ERR_KEY_NOT_FOUND) - Run not found
2613/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2614///
2615/// _Requirements: 8.1_
2616#[no_mangle]
2617pub extern "C" fn SYNA_exp_log_metric(
2618    path: *const c_char,
2619    run_id: *const c_char,
2620    key: *const c_char,
2621    value: f64,
2622    step: i64,
2623) -> i32 {
2624    std::panic::catch_unwind(|| {
2625        if path.is_null() || run_id.is_null() || key.is_null() {
2626            return ERR_INVALID_PATH;
2627        }
2628
2629        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2630            Ok(s) => s,
2631            Err(_) => return ERR_INVALID_PATH,
2632        };
2633
2634        let run_id_str = match unsafe { CStr::from_ptr(run_id) }.to_str() {
2635            Ok(s) => s,
2636            Err(_) => return ERR_INVALID_PATH,
2637        };
2638
2639        let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
2640            Ok(s) => s,
2641            Err(_) => return ERR_INVALID_PATH,
2642        };
2643
2644        let step_opt = if step < 0 { None } else { Some(step as u64) };
2645
2646        let mut reg = EXPERIMENT_REGISTRY.lock();
2647        match reg.get_mut(path_str) {
2648            Some(tracker) => match tracker.log_metric(run_id_str, key_str, value, step_opt) {
2649                Ok(_) => ERR_SUCCESS,
2650                Err(crate::error::SynaError::RunNotFound(_)) => ERR_KEY_NOT_FOUND,
2651                Err(crate::error::SynaError::RunAlreadyEnded(_)) => ERR_GENERIC,
2652                Err(_) => ERR_GENERIC,
2653            },
2654            None => crate::error::ERR_DB_NOT_FOUND,
2655        }
2656    })
2657    .unwrap_or(ERR_INTERNAL_PANIC)
2658}
2659
2660/// Logs an artifact for a run.
2661///
2662/// # Arguments
2663/// * `path` - Null-terminated C string containing the path to the tracker
2664/// * `run_id` - Null-terminated C string containing the run ID
2665/// * `name` - Null-terminated C string containing the artifact name
2666/// * `data` - Pointer to the artifact data
2667/// * `data_len` - Length of the artifact data
2668///
2669/// # Returns
2670/// * `1` (ERR_SUCCESS) - Artifact logged successfully
2671/// * `0` (ERR_GENERIC) - Generic error
2672/// * `-1` (ERR_DB_NOT_FOUND) - Tracker not found
2673/// * `-2` (ERR_INVALID_PATH) - Invalid arguments
2674/// * `-5` (ERR_KEY_NOT_FOUND) - Run not found
2675/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2676///
2677/// _Requirements: 8.1_
2678#[no_mangle]
2679pub extern "C" fn SYNA_exp_log_artifact(
2680    path: *const c_char,
2681    run_id: *const c_char,
2682    name: *const c_char,
2683    data: *const u8,
2684    data_len: usize,
2685) -> i32 {
2686    std::panic::catch_unwind(|| {
2687        if path.is_null() || run_id.is_null() || name.is_null() {
2688            return ERR_INVALID_PATH;
2689        }
2690        if data.is_null() && data_len > 0 {
2691            return ERR_INVALID_PATH;
2692        }
2693
2694        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2695            Ok(s) => s,
2696            Err(_) => return ERR_INVALID_PATH,
2697        };
2698
2699        let run_id_str = match unsafe { CStr::from_ptr(run_id) }.to_str() {
2700            Ok(s) => s,
2701            Err(_) => return ERR_INVALID_PATH,
2702        };
2703
2704        let name_str = match unsafe { CStr::from_ptr(name) }.to_str() {
2705            Ok(s) => s,
2706            Err(_) => return ERR_INVALID_PATH,
2707        };
2708
2709        let artifact_data = if data_len == 0 {
2710            &[]
2711        } else {
2712            unsafe { std::slice::from_raw_parts(data, data_len) }
2713        };
2714
2715        let mut reg = EXPERIMENT_REGISTRY.lock();
2716        match reg.get_mut(path_str) {
2717            Some(tracker) => match tracker.log_artifact(run_id_str, name_str, artifact_data) {
2718                Ok(_) => ERR_SUCCESS,
2719                Err(crate::error::SynaError::RunNotFound(_)) => ERR_KEY_NOT_FOUND,
2720                Err(crate::error::SynaError::RunAlreadyEnded(_)) => ERR_GENERIC,
2721                Err(_) => ERR_GENERIC,
2722            },
2723            None => crate::error::ERR_DB_NOT_FOUND,
2724        }
2725    })
2726    .unwrap_or(ERR_INTERNAL_PANIC)
2727}
2728
2729/// Ends a run with the given status.
2730///
2731/// # Arguments
2732/// * `path` - Null-terminated C string containing the path to the tracker
2733/// * `run_id` - Null-terminated C string containing the run ID
2734/// * `status` - Status: 0=Running, 1=Completed, 2=Failed, 3=Killed
2735///
2736/// # Returns
2737/// * `1` (ERR_SUCCESS) - Run ended successfully
2738/// * `0` (ERR_GENERIC) - Generic error (e.g., run already ended)
2739/// * `-1` (ERR_DB_NOT_FOUND) - Tracker not found
2740/// * `-2` (ERR_INVALID_PATH) - Invalid arguments
2741/// * `-5` (ERR_KEY_NOT_FOUND) - Run not found
2742/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2743///
2744/// _Requirements: 8.1_
2745#[no_mangle]
2746pub extern "C" fn SYNA_exp_end_run(path: *const c_char, run_id: *const c_char, status: i32) -> i32 {
2747    std::panic::catch_unwind(|| {
2748        if path.is_null() || run_id.is_null() {
2749            return ERR_INVALID_PATH;
2750        }
2751
2752        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2753            Ok(s) => s,
2754            Err(_) => return ERR_INVALID_PATH,
2755        };
2756
2757        let run_id_str = match unsafe { CStr::from_ptr(run_id) }.to_str() {
2758            Ok(s) => s,
2759            Err(_) => return ERR_INVALID_PATH,
2760        };
2761
2762        let run_status = match status {
2763            0 => RunStatus::Running,
2764            1 => RunStatus::Completed,
2765            2 => RunStatus::Failed,
2766            3 => RunStatus::Killed,
2767            _ => RunStatus::Completed,
2768        };
2769
2770        let mut reg = EXPERIMENT_REGISTRY.lock();
2771        match reg.get_mut(path_str) {
2772            Some(tracker) => match tracker.end_run(run_id_str, run_status) {
2773                Ok(_) => ERR_SUCCESS,
2774                Err(crate::error::SynaError::RunNotFound(_)) => ERR_KEY_NOT_FOUND,
2775                Err(crate::error::SynaError::RunAlreadyEnded(_)) => ERR_GENERIC,
2776                Err(_) => ERR_GENERIC,
2777            },
2778            None => crate::error::ERR_DB_NOT_FOUND,
2779        }
2780    })
2781    .unwrap_or(ERR_INTERNAL_PANIC)
2782}
2783
2784// =============================================================================
2785// MmapVectorStore FFI Functions (Ultra-High-Throughput)
2786// =============================================================================
2787
2788use crate::mmap_vector::{MmapVectorConfig, MmapVectorStore};
2789
2790/// Thread-safe global registry for managing open MmapVectorStore instances.
2791static MMAP_VECTOR_REGISTRY: Lazy<Mutex<HashMap<String, MmapVectorStore>>> =
2792    Lazy::new(|| Mutex::new(HashMap::new()));
2793
2794/// Creates or opens a memory-mapped vector store at the given path.
2795///
2796/// This is an alternative to `SYNA_vector_store_new()` that uses memory-mapped I/O
2797/// for ultra-high-throughput writes (500K-1M vectors/sec).
2798///
2799/// # Arguments
2800/// * `path` - Null-terminated C string containing the path to the mmap file
2801/// * `dimensions` - Number of dimensions (64-8192)
2802/// * `metric` - Distance metric: 0=Cosine, 1=Euclidean, 2=DotProduct
2803/// * `initial_capacity` - Pre-allocated capacity in number of vectors
2804///
2805/// # Returns
2806/// * `1` (ERR_SUCCESS) - Store opened successfully
2807/// * `0` (ERR_GENERIC) - Generic error during open
2808/// * `-2` (ERR_INVALID_PATH) - Path is null or invalid UTF-8
2809/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2810#[no_mangle]
2811pub extern "C" fn SYNA_mmap_vector_store_new(
2812    path: *const c_char,
2813    dimensions: u16,
2814    metric: i32,
2815    initial_capacity: usize,
2816) -> i32 {
2817    std::panic::catch_unwind(|| {
2818        if path.is_null() {
2819            return ERR_INVALID_PATH;
2820        }
2821
2822        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2823            Ok(s) => s,
2824            Err(_) => return ERR_INVALID_PATH,
2825        };
2826
2827        let distance_metric = match metric {
2828            0 => crate::distance::DistanceMetric::Cosine,
2829            1 => crate::distance::DistanceMetric::Euclidean,
2830            2 => crate::distance::DistanceMetric::DotProduct,
2831            _ => crate::distance::DistanceMetric::Cosine,
2832        };
2833
2834        let config = MmapVectorConfig {
2835            dimensions,
2836            metric: distance_metric,
2837            initial_capacity,
2838            ..Default::default()
2839        };
2840
2841        match MmapVectorStore::new(path_str, config) {
2842            Ok(store) => {
2843                let mut reg = MMAP_VECTOR_REGISTRY.lock();
2844                reg.insert(path_str.to_string(), store);
2845                ERR_SUCCESS
2846            }
2847            Err(_) => ERR_GENERIC,
2848        }
2849    })
2850    .unwrap_or(ERR_INTERNAL_PANIC)
2851}
2852
2853/// Inserts a vector into the mmap vector store.
2854///
2855/// This is an ultra-fast operation (no syscalls, just memcpy).
2856///
2857/// # Arguments
2858/// * `path` - Null-terminated C string containing the path to the store
2859/// * `key` - Null-terminated C string containing the vector key
2860/// * `vector` - Pointer to the vector data (f32 array)
2861/// * `dimensions` - Number of dimensions in the vector
2862///
2863/// # Returns
2864/// * `1` (ERR_SUCCESS) - Vector inserted successfully
2865/// * `0` (ERR_GENERIC) - Generic error
2866/// * `-1` (ERR_DB_NOT_FOUND) - Store not found
2867/// * `-2` (ERR_INVALID_PATH) - Invalid arguments
2868/// * `-6` (ERR_TYPE_MISMATCH) - Dimension mismatch
2869/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2870#[no_mangle]
2871pub extern "C" fn SYNA_mmap_vector_store_insert(
2872    path: *const c_char,
2873    key: *const c_char,
2874    vector: *const f32,
2875    dimensions: u16,
2876) -> i32 {
2877    std::panic::catch_unwind(|| {
2878        if path.is_null() || key.is_null() || vector.is_null() {
2879            return ERR_INVALID_PATH;
2880        }
2881
2882        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2883            Ok(s) => s,
2884            Err(_) => return ERR_INVALID_PATH,
2885        };
2886
2887        let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
2888            Ok(s) => s,
2889            Err(_) => return ERR_INVALID_PATH,
2890        };
2891
2892        let vec_slice = unsafe { std::slice::from_raw_parts(vector, dimensions as usize) };
2893
2894        let mut reg = MMAP_VECTOR_REGISTRY.lock();
2895        match reg.get_mut(path_str) {
2896            Some(store) => match store.insert(key_str, vec_slice) {
2897                Ok(_) => ERR_SUCCESS,
2898                Err(crate::error::SynaError::DimensionMismatch { .. }) => ERR_TYPE_MISMATCH,
2899                Err(_) => ERR_GENERIC,
2900            },
2901            None => crate::error::ERR_DB_NOT_FOUND,
2902        }
2903    })
2904    .unwrap_or(ERR_INTERNAL_PANIC)
2905}
2906
2907/// Inserts multiple vectors in a batch (maximum throughput).
2908///
2909/// This achieves 500K-1M vectors/sec by writing directly to memory.
2910///
2911/// # Arguments
2912/// * `path` - Null-terminated C string containing the path to the store
2913/// * `keys` - Array of null-terminated C strings (vector keys)
2914/// * `vectors` - Contiguous array of vector data (count * dimensions floats)
2915/// * `dimensions` - Number of dimensions per vector
2916/// * `count` - Number of vectors to insert
2917///
2918/// # Returns
2919/// * Non-negative value - Number of vectors inserted
2920/// * `-1` (ERR_DB_NOT_FOUND) - Store not found
2921/// * `-2` (ERR_INVALID_PATH) - Invalid arguments
2922/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2923#[no_mangle]
2924pub extern "C" fn SYNA_mmap_vector_store_insert_batch(
2925    path: *const c_char,
2926    keys: *const *const c_char,
2927    vectors: *const f32,
2928    dimensions: u16,
2929    count: usize,
2930) -> i32 {
2931    std::panic::catch_unwind(|| {
2932        if path.is_null() || keys.is_null() || vectors.is_null() || count == 0 {
2933            return ERR_INVALID_PATH;
2934        }
2935
2936        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2937            Ok(s) => s,
2938            Err(_) => return ERR_INVALID_PATH,
2939        };
2940
2941        // Convert keys
2942        let key_ptrs = unsafe { std::slice::from_raw_parts(keys, count) };
2943        let mut key_strings: Vec<String> = Vec::with_capacity(count);
2944        for &key_ptr in key_ptrs {
2945            if key_ptr.is_null() {
2946                return ERR_INVALID_PATH;
2947            }
2948            match unsafe { CStr::from_ptr(key_ptr) }.to_str() {
2949                Ok(s) => key_strings.push(s.to_string()),
2950                Err(_) => return ERR_INVALID_PATH,
2951            }
2952        }
2953        let key_refs: Vec<&str> = key_strings.iter().map(|s| s.as_str()).collect();
2954
2955        // Convert vectors
2956        let dims = dimensions as usize;
2957        let all_vectors = unsafe { std::slice::from_raw_parts(vectors, count * dims) };
2958        let vec_refs: Vec<&[f32]> = (0..count)
2959            .map(|i| &all_vectors[i * dims..(i + 1) * dims])
2960            .collect();
2961
2962        let mut reg = MMAP_VECTOR_REGISTRY.lock();
2963        match reg.get_mut(path_str) {
2964            Some(store) => match store.insert_batch(&key_refs, &vec_refs) {
2965                Ok(inserted) => inserted as i32,
2966                Err(_) => ERR_GENERIC,
2967            },
2968            None => crate::error::ERR_DB_NOT_FOUND,
2969        }
2970    })
2971    .unwrap_or(ERR_INTERNAL_PANIC)
2972}
2973
2974/// Searches for the k nearest neighbors in the mmap vector store.
2975///
2976/// # Arguments
2977/// * `path` - Null-terminated C string containing the path to the store
2978/// * `query` - Pointer to the query vector (f32 array)
2979/// * `dimensions` - Number of dimensions in the query
2980/// * `k` - Number of results to return
2981/// * `out_json` - Pointer to write the JSON results string
2982/// * `out_len` - Pointer to write the JSON string length
2983///
2984/// # Returns
2985/// * Non-negative value - Number of results found
2986/// * `-1` (ERR_DB_NOT_FOUND) - Store not found
2987/// * `-2` (ERR_INVALID_PATH) - Invalid arguments
2988/// * `-6` (ERR_TYPE_MISMATCH) - Dimension mismatch
2989/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2990#[no_mangle]
2991pub extern "C" fn SYNA_mmap_vector_store_search(
2992    path: *const c_char,
2993    query: *const f32,
2994    dimensions: u16,
2995    k: usize,
2996    out_json: *mut *mut c_char,
2997    out_len: *mut usize,
2998) -> i32 {
2999    std::panic::catch_unwind(|| {
3000        if path.is_null() || query.is_null() || out_json.is_null() || out_len.is_null() {
3001            return ERR_INVALID_PATH;
3002        }
3003
3004        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3005            Ok(s) => s,
3006            Err(_) => return ERR_INVALID_PATH,
3007        };
3008
3009        let query_slice = unsafe { std::slice::from_raw_parts(query, dimensions as usize) };
3010
3011        let reg = MMAP_VECTOR_REGISTRY.lock();
3012        match reg.get(path_str) {
3013            Some(store) => match store.search(query_slice, k) {
3014                Ok(results) => {
3015                    let count = results.len() as i32;
3016
3017                    // Convert results to JSON
3018                    let json_results: Vec<serde_json::Value> = results
3019                        .iter()
3020                        .map(|r| {
3021                            serde_json::json!({
3022                                "key": r.key,
3023                                "score": r.score,
3024                            })
3025                        })
3026                        .collect();
3027
3028                    let json =
3029                        serde_json::to_string(&json_results).unwrap_or_else(|_| "[]".to_string());
3030
3031                    unsafe { *out_len = json.len() };
3032                    match CString::new(json) {
3033                        Ok(c_string) => {
3034                            unsafe { *out_json = c_string.into_raw() };
3035                            count
3036                        }
3037                        Err(_) => ERR_GENERIC,
3038                    }
3039                }
3040                Err(crate::error::SynaError::DimensionMismatch { .. }) => ERR_TYPE_MISMATCH,
3041                Err(_) => ERR_GENERIC,
3042            },
3043            None => crate::error::ERR_DB_NOT_FOUND,
3044        }
3045    })
3046    .unwrap_or(ERR_INTERNAL_PANIC)
3047}
3048
3049/// Builds the HNSW index for the mmap vector store.
3050///
3051/// # Arguments
3052/// * `path` - Null-terminated C string containing the path to the store
3053///
3054/// # Returns
3055/// * `1` (ERR_SUCCESS) - Index built successfully
3056/// * `0` (ERR_GENERIC) - Generic error
3057/// * `-1` (ERR_DB_NOT_FOUND) - Store not found
3058/// * `-2` (ERR_INVALID_PATH) - Invalid path
3059/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
3060#[no_mangle]
3061pub extern "C" fn SYNA_mmap_vector_store_build_index(path: *const c_char) -> i32 {
3062    std::panic::catch_unwind(|| {
3063        if path.is_null() {
3064            return ERR_INVALID_PATH;
3065        }
3066
3067        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3068            Ok(s) => s,
3069            Err(_) => return ERR_INVALID_PATH,
3070        };
3071
3072        let mut reg = MMAP_VECTOR_REGISTRY.lock();
3073        match reg.get_mut(path_str) {
3074            Some(store) => match store.build_index() {
3075                Ok(_) => ERR_SUCCESS,
3076                Err(_) => ERR_GENERIC,
3077            },
3078            None => crate::error::ERR_DB_NOT_FOUND,
3079        }
3080    })
3081    .unwrap_or(ERR_INTERNAL_PANIC)
3082}
3083
3084/// Flushes the mmap vector store to disk.
3085///
3086/// # Arguments
3087/// * `path` - Null-terminated C string containing the path to the store
3088///
3089/// # Returns
3090/// * `1` (ERR_SUCCESS) - Flushed successfully
3091/// * `0` (ERR_GENERIC) - Generic error
3092/// * `-1` (ERR_DB_NOT_FOUND) - Store not found
3093/// * `-2` (ERR_INVALID_PATH) - Invalid path
3094/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
3095#[no_mangle]
3096pub extern "C" fn SYNA_mmap_vector_store_flush(path: *const c_char) -> i32 {
3097    std::panic::catch_unwind(|| {
3098        if path.is_null() {
3099            return ERR_INVALID_PATH;
3100        }
3101
3102        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3103            Ok(s) => s,
3104            Err(_) => return ERR_INVALID_PATH,
3105        };
3106
3107        let mut reg = MMAP_VECTOR_REGISTRY.lock();
3108        match reg.get_mut(path_str) {
3109            Some(store) => match store.flush() {
3110                Ok(_) => ERR_SUCCESS,
3111                Err(_) => ERR_GENERIC,
3112            },
3113            None => crate::error::ERR_DB_NOT_FOUND,
3114        }
3115    })
3116    .unwrap_or(ERR_INTERNAL_PANIC)
3117}
3118
3119/// Closes the mmap vector store and removes it from the registry.
3120///
3121/// # Arguments
3122/// * `path` - Null-terminated C string containing the path to the store
3123///
3124/// # Returns
3125/// * `1` (ERR_SUCCESS) - Closed successfully
3126/// * `-1` (ERR_DB_NOT_FOUND) - Store not found
3127/// * `-2` (ERR_INVALID_PATH) - Invalid path
3128/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
3129#[no_mangle]
3130pub extern "C" fn SYNA_mmap_vector_store_close(path: *const c_char) -> i32 {
3131    std::panic::catch_unwind(|| {
3132        if path.is_null() {
3133            return ERR_INVALID_PATH;
3134        }
3135
3136        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3137            Ok(s) => s,
3138            Err(_) => return ERR_INVALID_PATH,
3139        };
3140
3141        let mut reg = MMAP_VECTOR_REGISTRY.lock();
3142        match reg.remove(path_str) {
3143            Some(_store) => {
3144                // Store is dropped here, which triggers checkpoint
3145                ERR_SUCCESS
3146            }
3147            None => crate::error::ERR_DB_NOT_FOUND,
3148        }
3149    })
3150    .unwrap_or(ERR_INTERNAL_PANIC)
3151}
3152
3153/// Returns the number of vectors in the mmap vector store.
3154///
3155/// # Arguments
3156/// * `path` - Null-terminated C string containing the path to the store
3157///
3158/// # Returns
3159/// * Non-negative value - Number of vectors
3160/// * `-1` (ERR_DB_NOT_FOUND) - Store not found
3161/// * `-2` (ERR_INVALID_PATH) - Invalid path
3162/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
3163#[no_mangle]
3164pub extern "C" fn SYNA_mmap_vector_store_len(path: *const c_char) -> i64 {
3165    std::panic::catch_unwind(|| {
3166        if path.is_null() {
3167            return ERR_INVALID_PATH as i64;
3168        }
3169
3170        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3171            Ok(s) => s,
3172            Err(_) => return ERR_INVALID_PATH as i64,
3173        };
3174
3175        let reg = MMAP_VECTOR_REGISTRY.lock();
3176        match reg.get(path_str) {
3177            Some(store) => store.len() as i64,
3178            None => crate::error::ERR_DB_NOT_FOUND as i64,
3179        }
3180    })
3181    .unwrap_or(ERR_INTERNAL_PANIC as i64)
3182}
3183
3184// =============================================================================
3185// Gravity Well Index (GWI) FFI Functions
3186// =============================================================================
3187
3188use crate::gwi::{GravityWellIndex, GwiConfig};
3189
3190/// Thread-safe global registry for managing open GravityWellIndex instances.
3191static GWI_REGISTRY: Lazy<Mutex<HashMap<String, GravityWellIndex>>> =
3192    Lazy::new(|| Mutex::new(HashMap::new()));
3193
3194/// Create a new Gravity Well Index
3195///
3196/// # Arguments
3197/// * `path` - Path to the index file
3198/// * `dimensions` - Vector dimensions (64-8192)
3199/// * `branching_factor` - Branching factor at each level (default: 16)
3200/// * `num_levels` - Number of hierarchy levels (default: 3)
3201///
3202/// # Returns
3203/// * `1` (ERR_SUCCESS) - Index created successfully
3204/// * `-2` (ERR_INVALID_PATH) - Invalid path
3205/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
3206#[no_mangle]
3207pub extern "C" fn SYNA_gwi_new(
3208    path: *const c_char,
3209    dimensions: u16,
3210    branching_factor: u16,
3211    num_levels: u8,
3212    initial_capacity: usize,
3213) -> i32 {
3214    std::panic::catch_unwind(|| {
3215        if path.is_null() {
3216            return ERR_INVALID_PATH;
3217        }
3218
3219        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3220            Ok(s) => s,
3221            Err(_) => return ERR_INVALID_PATH,
3222        };
3223
3224        let config = GwiConfig {
3225            dimensions,
3226            branching_factor: if branching_factor == 0 {
3227                16
3228            } else {
3229                branching_factor
3230            },
3231            num_levels: if num_levels == 0 { 3 } else { num_levels },
3232            initial_capacity: if initial_capacity == 0 {
3233                10_000
3234            } else {
3235                initial_capacity
3236            },
3237            ..Default::default()
3238        };
3239
3240        match GravityWellIndex::new(path_str, config) {
3241            Ok(index) => {
3242                let mut reg = GWI_REGISTRY.lock();
3243                reg.insert(path_str.to_string(), index);
3244                ERR_SUCCESS
3245            }
3246            Err(_) => ERR_GENERIC,
3247        }
3248    })
3249    .unwrap_or(ERR_INTERNAL_PANIC)
3250}
3251
3252/// Open an existing Gravity Well Index
3253///
3254/// # Arguments
3255/// * `path` - Path to the existing index file
3256///
3257/// # Returns
3258/// * `1` (ERR_SUCCESS) - Index opened successfully
3259/// * `-2` (ERR_INVALID_PATH) - Invalid path or file doesn't exist
3260/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
3261#[no_mangle]
3262pub extern "C" fn SYNA_gwi_open(path: *const c_char) -> i32 {
3263    std::panic::catch_unwind(|| {
3264        if path.is_null() {
3265            return ERR_INVALID_PATH;
3266        }
3267
3268        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3269            Ok(s) => s,
3270            Err(_) => return ERR_INVALID_PATH,
3271        };
3272
3273        match GravityWellIndex::open(path_str) {
3274            Ok(index) => {
3275                let mut reg = GWI_REGISTRY.lock();
3276                reg.insert(path_str.to_string(), index);
3277                ERR_SUCCESS
3278            }
3279            Err(_) => ERR_INVALID_PATH,
3280        }
3281    })
3282    .unwrap_or(ERR_INTERNAL_PANIC)
3283}
3284
3285/// Initialize GWI attractors from sample vectors
3286///
3287/// # Arguments
3288/// * `path` - Path to the index file
3289/// * `vectors` - Pointer to contiguous f32 vector data
3290/// * `num_vectors` - Number of sample vectors
3291/// * `dimensions` - Dimensions per vector
3292///
3293/// # Returns
3294/// * `1` (ERR_SUCCESS) - Attractors initialized
3295/// * `-1` (ERR_DB_NOT_FOUND) - Index not found
3296/// * `-2` (ERR_INVALID_PATH) - Invalid path
3297/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
3298#[no_mangle]
3299pub extern "C" fn SYNA_gwi_initialize(
3300    path: *const c_char,
3301    vectors: *const f32,
3302    num_vectors: usize,
3303    dimensions: u16,
3304) -> i32 {
3305    std::panic::catch_unwind(|| {
3306        if path.is_null() || vectors.is_null() {
3307            return ERR_INVALID_PATH;
3308        }
3309
3310        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3311            Ok(s) => s,
3312            Err(_) => return ERR_INVALID_PATH,
3313        };
3314
3315        let total_floats = num_vectors * dimensions as usize;
3316        let data_slice = unsafe { std::slice::from_raw_parts(vectors, total_floats) };
3317
3318        let sample_vectors: Vec<&[f32]> = data_slice.chunks(dimensions as usize).collect();
3319
3320        let mut reg = GWI_REGISTRY.lock();
3321        match reg.get_mut(path_str) {
3322            Some(index) => match index.initialize_attractors(&sample_vectors) {
3323                Ok(()) => ERR_SUCCESS,
3324                Err(_) => ERR_GENERIC,
3325            },
3326            None => crate::error::ERR_DB_NOT_FOUND,
3327        }
3328    })
3329    .unwrap_or(ERR_INTERNAL_PANIC)
3330}
3331
3332/// Insert a vector into the GWI
3333///
3334/// # Arguments
3335/// * `path` - Path to the index file
3336/// * `key` - Null-terminated key string
3337/// * `vector` - Pointer to f32 vector data
3338/// * `dimensions` - Vector dimensions
3339///
3340/// # Returns
3341/// * `1` (ERR_SUCCESS) - Vector inserted
3342/// * `-1` (ERR_DB_NOT_FOUND) - Index not found
3343/// * `-2` (ERR_INVALID_PATH) - Invalid path/key
3344/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
3345#[no_mangle]
3346pub extern "C" fn SYNA_gwi_insert(
3347    path: *const c_char,
3348    key: *const c_char,
3349    vector: *const f32,
3350    dimensions: u16,
3351) -> i32 {
3352    std::panic::catch_unwind(|| {
3353        if path.is_null() || key.is_null() || vector.is_null() {
3354            return ERR_INVALID_PATH;
3355        }
3356
3357        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3358            Ok(s) => s,
3359            Err(_) => return ERR_INVALID_PATH,
3360        };
3361
3362        let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
3363            Ok(s) => s,
3364            Err(_) => return ERR_INVALID_PATH,
3365        };
3366
3367        let vector_slice = unsafe { std::slice::from_raw_parts(vector, dimensions as usize) };
3368
3369        let mut reg = GWI_REGISTRY.lock();
3370        match reg.get_mut(path_str) {
3371            Some(index) => match index.insert(key_str, vector_slice) {
3372                Ok(()) => ERR_SUCCESS,
3373                Err(_) => ERR_GENERIC,
3374            },
3375            None => crate::error::ERR_DB_NOT_FOUND,
3376        }
3377    })
3378    .unwrap_or(ERR_INTERNAL_PANIC)
3379}
3380
3381/// Batch insert vectors into the GWI
3382///
3383/// # Arguments
3384/// * `path` - Path to the index file
3385/// * `keys` - Array of null-terminated key strings
3386/// * `vectors` - Pointer to contiguous f32 vector data
3387/// * `dimensions` - Dimensions per vector
3388/// * `count` - Number of vectors to insert
3389///
3390/// # Returns
3391/// * Non-negative - Number of vectors inserted
3392/// * `-1` (ERR_DB_NOT_FOUND) - Index not found
3393/// * `-2` (ERR_INVALID_PATH) - Invalid path
3394/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
3395#[no_mangle]
3396pub extern "C" fn SYNA_gwi_insert_batch(
3397    path: *const c_char,
3398    keys: *const *const c_char,
3399    vectors: *const f32,
3400    dimensions: u16,
3401    count: usize,
3402) -> i32 {
3403    std::panic::catch_unwind(|| {
3404        if path.is_null() || keys.is_null() || (vectors.is_null() && count > 0) {
3405            return ERR_INVALID_PATH;
3406        }
3407
3408        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3409            Ok(s) => s,
3410            Err(_) => return ERR_INVALID_PATH,
3411        };
3412
3413        let keys_slice = unsafe { std::slice::from_raw_parts(keys, count) };
3414        let mut key_strings: Vec<&str> = Vec::with_capacity(count);
3415        for key_ptr in keys_slice {
3416            if key_ptr.is_null() {
3417                return ERR_INVALID_PATH;
3418            }
3419            match unsafe { CStr::from_ptr(*key_ptr) }.to_str() {
3420                Ok(s) => key_strings.push(s),
3421                Err(_) => return ERR_INVALID_PATH,
3422            }
3423        }
3424
3425        let total_floats = count * dimensions as usize;
3426        let data_slice = unsafe { std::slice::from_raw_parts(vectors, total_floats) };
3427        let vector_refs: Vec<&[f32]> = data_slice.chunks(dimensions as usize).collect();
3428
3429        let mut reg = GWI_REGISTRY.lock();
3430        match reg.get_mut(path_str) {
3431            Some(index) => match index.insert_batch(&key_strings, &vector_refs) {
3432                Ok(n) => n as i32,
3433                Err(_) => ERR_GENERIC,
3434            },
3435            None => crate::error::ERR_DB_NOT_FOUND,
3436        }
3437    })
3438    .unwrap_or(ERR_INTERNAL_PANIC)
3439}
3440
3441/// Search for k nearest neighbors in the GWI
3442///
3443/// # Arguments
3444/// * `path` - Path to the index file
3445/// * `query` - Pointer to f32 query vector
3446/// * `dimensions` - Query vector dimensions
3447/// * `k` - Number of neighbors to return
3448/// * `out_json` - Pointer to write JSON result string
3449///
3450/// # Returns
3451/// * Non-negative - Number of results found
3452/// * `-1` (ERR_DB_NOT_FOUND) - Index not found
3453/// * `-2` (ERR_INVALID_PATH) - Invalid path
3454/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
3455#[no_mangle]
3456pub extern "C" fn SYNA_gwi_search(
3457    path: *const c_char,
3458    query: *const f32,
3459    dimensions: u16,
3460    k: usize,
3461    out_json: *mut *mut c_char,
3462) -> i32 {
3463    // Default nprobe
3464    SYNA_gwi_search_nprobe(path, query, dimensions, k, 3, out_json)
3465}
3466
3467/// Search for k nearest neighbors with custom nprobe
3468///
3469/// Higher nprobe = better recall but slower search.
3470/// - nprobe=3: Fast, ~5-15% recall
3471/// - nprobe=10: Balanced, ~30-50% recall
3472/// - nprobe=30: High quality, ~70-90% recall
3473/// - nprobe=100: Near-exact, ~95%+ recall
3474///
3475/// # Arguments
3476/// * `path` - Path to the index file
3477/// * `query` - Query vector
3478/// * `dimensions` - Number of dimensions
3479/// * `k` - Number of results to return
3480/// * `nprobe` - Number of clusters to probe
3481/// * `out_json` - Output JSON string with results
3482///
3483/// # Returns
3484/// * Number of results on success
3485/// * `-1` (ERR_DB_NOT_FOUND) - Index not found
3486/// * `-2` (ERR_INVALID_PATH) - Invalid path
3487/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
3488#[no_mangle]
3489pub extern "C" fn SYNA_gwi_search_nprobe(
3490    path: *const c_char,
3491    query: *const f32,
3492    dimensions: u16,
3493    k: usize,
3494    nprobe: usize,
3495    out_json: *mut *mut c_char,
3496) -> i32 {
3497    std::panic::catch_unwind(|| {
3498        if path.is_null() || query.is_null() || out_json.is_null() {
3499            return ERR_INVALID_PATH;
3500        }
3501
3502        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3503            Ok(s) => s,
3504            Err(_) => return ERR_INVALID_PATH,
3505        };
3506
3507        let query_slice = unsafe { std::slice::from_raw_parts(query, dimensions as usize) };
3508
3509        let reg = GWI_REGISTRY.lock();
3510        match reg.get(path_str) {
3511            Some(index) => match index.search_with_nprobe(query_slice, k, nprobe) {
3512                Ok(results) => {
3513                    let json_results: Vec<serde_json::Value> = results
3514                        .iter()
3515                        .map(|r| {
3516                            serde_json::json!({
3517                                "key": r.key,
3518                                "score": r.score,
3519                            })
3520                        })
3521                        .collect();
3522
3523                    let json_str = serde_json::to_string(&json_results).unwrap_or_default();
3524                    let c_str = CString::new(json_str).unwrap_or_default();
3525                    unsafe {
3526                        *out_json = c_str.into_raw();
3527                    }
3528                    results.len() as i32
3529                }
3530                Err(_) => ERR_GENERIC,
3531            },
3532            None => crate::error::ERR_DB_NOT_FOUND,
3533        }
3534    })
3535    .unwrap_or(ERR_INTERNAL_PANIC)
3536}
3537
3538/// Flush GWI changes to disk
3539///
3540/// # Arguments
3541/// * `path` - Path to the index file
3542///
3543/// # Returns
3544/// * `1` (ERR_SUCCESS) - Flushed successfully
3545/// * `-1` (ERR_DB_NOT_FOUND) - Index not found
3546/// * `-2` (ERR_INVALID_PATH) - Invalid path
3547/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
3548#[no_mangle]
3549pub extern "C" fn SYNA_gwi_flush(path: *const c_char) -> i32 {
3550    std::panic::catch_unwind(|| {
3551        if path.is_null() {
3552            return ERR_INVALID_PATH;
3553        }
3554
3555        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3556            Ok(s) => s,
3557            Err(_) => return ERR_INVALID_PATH,
3558        };
3559
3560        let reg = GWI_REGISTRY.lock();
3561        match reg.get(path_str) {
3562            Some(index) => match index.flush() {
3563                Ok(()) => ERR_SUCCESS,
3564                Err(_) => ERR_GENERIC,
3565            },
3566            None => crate::error::ERR_DB_NOT_FOUND,
3567        }
3568    })
3569    .unwrap_or(ERR_INTERNAL_PANIC)
3570}
3571
3572/// Close a GWI and remove from registry
3573///
3574/// # Arguments
3575/// * `path` - Path to the index file
3576///
3577/// # Returns
3578/// * `1` (ERR_SUCCESS) - Closed successfully
3579/// * `-1` (ERR_DB_NOT_FOUND) - Index not found
3580/// * `-2` (ERR_INVALID_PATH) - Invalid path
3581/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
3582#[no_mangle]
3583pub extern "C" fn SYNA_gwi_close(path: *const c_char) -> i32 {
3584    std::panic::catch_unwind(|| {
3585        if path.is_null() {
3586            return ERR_INVALID_PATH;
3587        }
3588
3589        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3590            Ok(s) => s,
3591            Err(_) => return ERR_INVALID_PATH,
3592        };
3593
3594        let mut reg = GWI_REGISTRY.lock();
3595        match reg.remove(path_str) {
3596            Some(mut index) => {
3597                let _ = index.close();
3598                ERR_SUCCESS
3599            }
3600            None => crate::error::ERR_DB_NOT_FOUND,
3601        }
3602    })
3603    .unwrap_or(ERR_INTERNAL_PANIC)
3604}
3605
3606/// Get number of vectors in the GWI
3607///
3608/// # Arguments
3609/// * `path` - Path to the index file
3610///
3611/// # Returns
3612/// * Non-negative - Number of vectors
3613/// * `-1` (ERR_DB_NOT_FOUND) - Index not found
3614/// * `-2` (ERR_INVALID_PATH) - Invalid path
3615/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
3616#[no_mangle]
3617pub extern "C" fn SYNA_gwi_len(path: *const c_char) -> i64 {
3618    std::panic::catch_unwind(|| {
3619        if path.is_null() {
3620            return ERR_INVALID_PATH as i64;
3621        }
3622
3623        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3624            Ok(s) => s,
3625            Err(_) => return ERR_INVALID_PATH as i64,
3626        };
3627
3628        let reg = GWI_REGISTRY.lock();
3629        match reg.get(path_str) {
3630            Some(index) => index.len() as i64,
3631            None => crate::error::ERR_DB_NOT_FOUND as i64,
3632        }
3633    })
3634    .unwrap_or(ERR_INTERNAL_PANIC as i64)
3635}
3636
3637// =============================================================================
3638// Cascade Index FFI Functions
3639// =============================================================================
3640
3641use crate::cascade::{CascadeConfig, CascadeIndex};
3642
3643/// Global registry for Cascade Index instances
3644static CASCADE_REGISTRY: Lazy<Mutex<HashMap<String, CascadeIndex>>> =
3645    Lazy::new(|| Mutex::new(HashMap::new()));
3646
3647/// Creates a new Cascade Index.
3648///
3649/// # Arguments
3650/// * `path` - Path to the index file
3651/// * `dimensions` - Vector dimensions (64-8192)
3652///
3653/// # Returns
3654/// * `1` (ERR_SUCCESS) - Index created successfully
3655/// * `-2` (ERR_INVALID_PATH) - Invalid path
3656/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic
3657#[no_mangle]
3658pub extern "C" fn SYNA_cascade_new(path: *const c_char, dimensions: u16) -> i32 {
3659    std::panic::catch_unwind(|| {
3660        if path.is_null() {
3661            return ERR_INVALID_PATH;
3662        }
3663
3664        let c_str = unsafe { CStr::from_ptr(path) };
3665        let path_str = match c_str.to_str() {
3666            Ok(s) => s,
3667            Err(_) => return ERR_INVALID_PATH,
3668        };
3669
3670        let config = CascadeConfig {
3671            dimensions,
3672            ..Default::default()
3673        };
3674
3675        match CascadeIndex::new(path_str, config) {
3676            Ok(index) => {
3677                let mut registry = CASCADE_REGISTRY.lock();
3678                registry.insert(path_str.to_string(), index);
3679                ERR_SUCCESS
3680            }
3681            Err(_) => ERR_GENERIC,
3682        }
3683    })
3684    .unwrap_or(ERR_INTERNAL_PANIC)
3685}
3686
3687/// Inserts a vector into the Cascade Index.
3688///
3689/// # Arguments
3690/// * `path` - Path to the index
3691/// * `key` - Key for the vector
3692/// * `vector` - Pointer to vector data
3693/// * `dimensions` - Vector dimensions
3694///
3695/// # Returns
3696/// * `1` (ERR_SUCCESS) - Vector inserted
3697/// * `-2` (ERR_INVALID_PATH) - Invalid path
3698/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic
3699#[no_mangle]
3700pub extern "C" fn SYNA_cascade_insert(
3701    path: *const c_char,
3702    key: *const c_char,
3703    vector: *const f32,
3704    dimensions: u16,
3705) -> i32 {
3706    std::panic::catch_unwind(|| {
3707        if path.is_null() || key.is_null() || vector.is_null() {
3708            return ERR_INVALID_PATH;
3709        }
3710
3711        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3712            Ok(s) => s,
3713            Err(_) => return ERR_INVALID_PATH,
3714        };
3715
3716        let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
3717            Ok(s) => s,
3718            Err(_) => return ERR_INVALID_PATH,
3719        };
3720
3721        let vec_slice = unsafe { std::slice::from_raw_parts(vector, dimensions as usize) };
3722
3723        let mut registry = CASCADE_REGISTRY.lock();
3724        if let Some(index) = registry.get_mut(path_str) {
3725            match index.insert(key_str, vec_slice) {
3726                Ok(_) => ERR_SUCCESS,
3727                Err(_) => ERR_GENERIC,
3728            }
3729        } else {
3730            ERR_GENERIC
3731        }
3732    })
3733    .unwrap_or(ERR_INTERNAL_PANIC)
3734}
3735
3736/// Inserts multiple vectors into the Cascade Index.
3737///
3738/// # Arguments
3739/// * `path` - Path to the index
3740/// * `keys` - Array of key pointers
3741/// * `vectors` - Pointer to flattened vector data
3742/// * `dimensions` - Vector dimensions
3743/// * `count` - Number of vectors
3744///
3745/// # Returns
3746/// * `1` (ERR_SUCCESS) - Vectors inserted
3747/// * `-2` (ERR_INVALID_PATH) - Invalid path
3748/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic
3749#[no_mangle]
3750pub extern "C" fn SYNA_cascade_insert_batch(
3751    path: *const c_char,
3752    keys: *const *const c_char,
3753    vectors: *const f32,
3754    dimensions: u16,
3755    count: usize,
3756) -> i32 {
3757    std::panic::catch_unwind(|| {
3758        if path.is_null() || keys.is_null() || vectors.is_null() || count == 0 {
3759            return ERR_INVALID_PATH;
3760        }
3761
3762        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3763            Ok(s) => s,
3764            Err(_) => return ERR_INVALID_PATH,
3765        };
3766
3767        let dims = dimensions as usize;
3768        let mut registry = CASCADE_REGISTRY.lock();
3769
3770        if let Some(index) = registry.get_mut(path_str) {
3771            for i in 0..count {
3772                let key_ptr = unsafe { *keys.add(i) };
3773                if key_ptr.is_null() {
3774                    continue;
3775                }
3776
3777                let key_str = match unsafe { CStr::from_ptr(key_ptr) }.to_str() {
3778                    Ok(s) => s,
3779                    Err(_) => continue,
3780                };
3781
3782                let vec_start = i * dims;
3783                let vec_slice = unsafe { std::slice::from_raw_parts(vectors.add(vec_start), dims) };
3784
3785                if index.insert(key_str, vec_slice).is_err() {
3786                    return ERR_GENERIC;
3787                }
3788            }
3789            ERR_SUCCESS
3790        } else {
3791            ERR_GENERIC
3792        }
3793    })
3794    .unwrap_or(ERR_INTERNAL_PANIC)
3795}
3796
3797/// Searches the Cascade Index for nearest neighbors.
3798///
3799/// # Arguments
3800/// * `path` - Path to the index
3801/// * `query` - Query vector
3802/// * `dimensions` - Vector dimensions
3803/// * `k` - Number of results
3804/// * `out_json` - Output JSON string pointer
3805///
3806/// # Returns
3807/// * `1` (ERR_SUCCESS) - Search completed
3808/// * `-2` (ERR_INVALID_PATH) - Invalid path
3809/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic
3810#[no_mangle]
3811pub extern "C" fn SYNA_cascade_search(
3812    path: *const c_char,
3813    query: *const f32,
3814    dimensions: u16,
3815    k: usize,
3816    out_json: *mut *mut c_char,
3817) -> i32 {
3818    // Use good defaults matching CascadeConfig::default()
3819    SYNA_cascade_search_params(path, query, dimensions, k, 16, 80, out_json)
3820}
3821
3822/// Searches with custom parameters.
3823#[no_mangle]
3824pub extern "C" fn SYNA_cascade_search_params(
3825    path: *const c_char,
3826    query: *const f32,
3827    dimensions: u16,
3828    k: usize,
3829    num_probes: usize,
3830    ef_search: usize,
3831    out_json: *mut *mut c_char,
3832) -> i32 {
3833    std::panic::catch_unwind(|| {
3834        if path.is_null() || query.is_null() || out_json.is_null() {
3835            return ERR_INVALID_PATH;
3836        }
3837
3838        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3839            Ok(s) => s,
3840            Err(_) => return ERR_INVALID_PATH,
3841        };
3842
3843        let query_slice = unsafe { std::slice::from_raw_parts(query, dimensions as usize) };
3844
3845        let registry = CASCADE_REGISTRY.lock();
3846        if let Some(index) = registry.get(path_str) {
3847            match index.search_with_params(query_slice, k, num_probes, ef_search) {
3848                Ok(results) => {
3849                    let json_results: Vec<serde_json::Value> = results
3850                        .iter()
3851                        .map(|r| {
3852                            serde_json::json!({
3853                                "key": r.key,
3854                                "score": r.score
3855                            })
3856                        })
3857                        .collect();
3858
3859                    let json_str = serde_json::to_string(&json_results).unwrap_or_default();
3860                    let c_string = std::ffi::CString::new(json_str).unwrap_or_default();
3861                    unsafe {
3862                        *out_json = c_string.into_raw();
3863                    }
3864                    ERR_SUCCESS
3865                }
3866                Err(_) => ERR_GENERIC,
3867            }
3868        } else {
3869            ERR_GENERIC
3870        }
3871    })
3872    .unwrap_or(ERR_INTERNAL_PANIC)
3873}
3874
3875/// Flushes the Cascade Index to disk.
3876#[no_mangle]
3877pub extern "C" fn SYNA_cascade_flush(path: *const c_char) -> i32 {
3878    std::panic::catch_unwind(|| {
3879        if path.is_null() {
3880            return ERR_INVALID_PATH;
3881        }
3882
3883        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3884            Ok(s) => s,
3885            Err(_) => return ERR_INVALID_PATH,
3886        };
3887
3888        let registry = CASCADE_REGISTRY.lock();
3889        if let Some(index) = registry.get(path_str) {
3890            match index.flush() {
3891                Ok(_) => ERR_SUCCESS,
3892                Err(_) => ERR_GENERIC,
3893            }
3894        } else {
3895            ERR_GENERIC
3896        }
3897    })
3898    .unwrap_or(ERR_INTERNAL_PANIC)
3899}
3900
3901/// Closes the Cascade Index.
3902#[no_mangle]
3903pub extern "C" fn SYNA_cascade_close(path: *const c_char) -> i32 {
3904    std::panic::catch_unwind(|| {
3905        if path.is_null() {
3906            return ERR_INVALID_PATH;
3907        }
3908
3909        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3910            Ok(s) => s,
3911            Err(_) => return ERR_INVALID_PATH,
3912        };
3913
3914        let mut registry = CASCADE_REGISTRY.lock();
3915        if registry.remove(path_str).is_some() {
3916            ERR_SUCCESS
3917        } else {
3918            ERR_GENERIC
3919        }
3920    })
3921    .unwrap_or(ERR_INTERNAL_PANIC)
3922}
3923
3924/// Returns the number of vectors in the Cascade Index.
3925#[no_mangle]
3926pub extern "C" fn SYNA_cascade_len(path: *const c_char) -> i64 {
3927    std::panic::catch_unwind(|| {
3928        if path.is_null() {
3929            return -1;
3930        }
3931
3932        let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3933            Ok(s) => s,
3934            Err(_) => return -1,
3935        };
3936
3937        let registry = CASCADE_REGISTRY.lock();
3938        if let Some(index) = registry.get(path_str) {
3939            index.len() as i64
3940        } else {
3941            -1
3942        }
3943    })
3944    .unwrap_or(-1)
3945}