use std::borrow::Cow;
use std::ffi::CStr;
use std::fs;
use std::io::{Read, Write};
use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
use std::os::raw::c_char;
use std::path::{Component, Path, PathBuf};
use std::sync::{Mutex, MutexGuard, OnceLock};
use std::time::Duration;

#[cfg(target_os = "android")]
use jni::objects::{JClass, JString};
#[cfg(target_os = "android")]
use jni::JNIEnv;

#[derive(Default)]
struct MobileActionState {
    sync_count: u64,
    preview_count: u64,
    last_action: String,
    last_payload: Option<String>,
    last_result: String,
    last_error: Option<String>,
    auto_scan_started: bool,
}

#[derive(Clone, Copy)]
enum ActionKind {
    Named(ActionHandler),
    Plugin,
}

type ActionHandler =
    fn(&mut MobileActionState, Option<&serde_json::Value>) -> Result<serde_json::Value, String>;

static ACTION_STATE: OnceLock<Mutex<MobileActionState>> = OnceLock::new();
static VIEW_STATE: OnceLock<Mutex<serde_json::Value>> = OnceLock::new();

fn action_state() -> &'static Mutex<MobileActionState> {
    ACTION_STATE.get_or_init(|| Mutex::new(MobileActionState::default()))
}

fn view_state() -> &'static Mutex<serde_json::Value> {
    VIEW_STATE.get_or_init(|| Mutex::new(serde_json::json!({})))
}

fn lock_action_state() -> MutexGuard<'static, MobileActionState> {
    action_state()
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
}

fn lock_view_state() -> MutexGuard<'static, serde_json::Value> {
    view_state()
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
}

pub fn dispatch_action(action: &str) -> bool {
    action_kind(action).is_some()
}

pub fn dispatch_action_json(action: &str) -> String {
    let request = parse_action_request(action);
    let Some(kind) = action_kind(action) else {
        let state = lock_action_state();
        let action_name = legacy_action_name(&request).unwrap_or(action);
        return action_json(false, action_name, None, Some("unknown action"), &state);
    };
    let mut state = lock_action_state();
    let action_name = request_action_name(&request);
    state.last_action = action_name.clone();
    state.last_payload = request_payload(&request).map(serde_json::Value::to_string);
    let result = match kind {
        ActionKind::Named(handler) => handler(&mut state, request_payload(&request)),
        ActionKind::Plugin => plugin_action(&mut state, &request),
    };
    let result = match result {
        Ok(value) => action_json(true, &action_name, Some(value), None, &state),
        Err(error) => action_json(false, &action_name, None, Some(&error), &state),
    };
    state.last_error = parse_action_error(&result);
    state.last_result = result.clone();
    result
}

fn dispatch_change_json(action: &str, bind: &str, value_json: &str) -> String {
    let value = match serde_json::from_str::<serde_json::Value>(value_json) {
        Ok(value) => value,
        Err(error) => {
            let state = lock_action_state();
            return action_json(false, action, None, Some(&format!("invalid change payload: {error}")), &state);
        }
    };
    let mut payload = serde_json::Map::new();
    payload.insert(bind.to_string(), value);
    if let Some((capability, method)) = action.split_once('.') {
        return dispatch_action_json(
            &serde_json::json!({
                "kind": "plugin",
                "capability": capability,
                "method": method,
                "payload": payload,
            })
            .to_string(),
        );
    }
    if !action.is_empty() {
        payload.insert("action".to_string(), serde_json::Value::String(action.to_string()));
    }
    dispatch_action_json(&serde_json::Value::Object(payload).to_string())
}

fn parse_action_error(result: &str) -> Option<String> {
    let payload = serde_json::from_str::<serde_json::Value>(result).ok()?;
    if payload.get("ok").and_then(|value| value.as_bool()) == Some(false) {
        return Some(
            payload
                .get("error")
                .and_then(|value| value.as_str())
                .unwrap_or(result)
                .to_string(),
        );
    }
    None
}

fn action_kind(action: &str) -> Option<ActionKind> {
    let request = parse_action_request(action);
    if request_kind(&request) == Some("plugin") {
        return Some(ActionKind::Plugin);
    }
    match legacy_action_name(&request).unwrap_or(action) {
        "sync" => Some(ActionKind::Named(sync_action)),
        "preview" => Some(ActionKind::Named(preview_action)),
        _ => None,
    }
}

fn sync_action(
    state: &mut MobileActionState,
    payload: Option<&serde_json::Value>,
) -> Result<serde_json::Value, String> {
    state.sync_count += 1;
    Ok(serde_json::json!({
        "message": payload_message(payload).unwrap_or("synced"),
        "syncCount": state.sync_count,
    }))
}

fn preview_action(
    state: &mut MobileActionState,
    payload: Option<&serde_json::Value>,
) -> Result<serde_json::Value, String> {
    state.preview_count += 1;
    Ok(serde_json::json!({
        "message": payload_message(payload).unwrap_or("previewed"),
        "previewCount": state.preview_count,
    }))
}

fn plugin_action(
    state: &mut MobileActionState,
    request: &serde_json::Value,
) -> Result<serde_json::Value, String> {
    let capability = request
        .get("capability")
        .and_then(|value| value.as_str())
        .ok_or_else(|| "plugin request missing capability".to_string())?;
    let method = request
        .get("method")
        .and_then(|value| value.as_str())
        .ok_or_else(|| "plugin request missing method".to_string())?;
    let payload = request.get("payload");
    let value = match capability {
        "preferences" => preferences_backend(method, payload)?,
        "filesystem" => filesystem_backend(method, payload)?,
        "device" => device_backend(method)?,
        "app" => app_backend(state, method)?,
        "network" => network_backend(method, payload)?,
        "http" => http_backend(method, payload)?,
        other => return Err(format!("unsupported capability: {other}")),
    };
    Ok(serde_json::json!({
        "capability": capability,
        "method": method,
        "value": value,
    }))
}

fn preferences_backend(
    method: &str,
    payload: Option<&serde_json::Value>,
) -> Result<serde_json::Value, String> {
    let mut prefs = load_json_map(&preferences_file_path())?;
    match method {
        "get" => {
            let key = payload_string(payload, "key")?;
            Ok(prefs
                .get(&key)
                .cloned()
                .unwrap_or(serde_json::Value::Null))
        }
        "set" => {
            let key = payload_string(payload, "key")?;
            let value = payload
                .and_then(|value| value.get("value"))
                .cloned()
                .ok_or_else(|| "preferences.set requires payload.value".to_string())?;
            prefs.insert(key.clone(), value.clone());
            save_json_map(&preferences_file_path(), &prefs)?;
            Ok(serde_json::json!({ "key": key, "value": value }))
        }
        "remove" => {
            let key = payload_string(payload, "key")?;
            let removed = prefs.remove(&key).is_some();
            save_json_map(&preferences_file_path(), &prefs)?;
            Ok(serde_json::json!({ "key": key, "removed": removed }))
        }
        "keys" => {
            let mut keys = prefs.keys().cloned().collect::<Vec<_>>();
            keys.sort();
            Ok(serde_json::json!(keys))
        }
        "clear" => {
            prefs.clear();
            save_json_map(&preferences_file_path(), &prefs)?;
            Ok(serde_json::json!({ "cleared": true }))
        }
        other => Err(format!("unsupported preferences method: {other}")),
    }
}

fn filesystem_backend(
    method: &str,
    payload: Option<&serde_json::Value>,
) -> Result<serde_json::Value, String> {
    match method {
        "readText" => {
            let path = scoped_data_path(&payload_string(payload, "path")?)?;
            let text = fs::read_to_string(&path)
                .map_err(|error| format!("read {}: {error}", path.display()))?;
            Ok(serde_json::json!({ "path": path_string(&path), "text": text }))
        }
        "writeText" => {
            let path = scoped_data_path(&payload_string(payload, "path")?)?;
            let text = payload_string(payload, "text")?;
            ensure_parent_dir(&path)?;
            fs::write(&path, text.as_bytes())
                .map_err(|error| format!("write {}: {error}", path.display()))?;
            Ok(file_stat_json(&path)?)
        }
        "delete" => {
            let path = scoped_data_path(&payload_string(payload, "path")?)?;
            let deleted = if path.is_dir() {
                fs::remove_dir_all(&path).is_ok()
            } else {
                fs::remove_file(&path).is_ok()
            };
            Ok(serde_json::json!({ "path": path_string(&path), "deleted": deleted }))
        }
        "mkdir" => {
            let path = scoped_data_path(&payload_string(payload, "path")?)?;
            fs::create_dir_all(&path)
                .map_err(|error| format!("mkdir {}: {error}", path.display()))?;
            Ok(file_stat_json(&path)?)
        }
        "list" => {
            let path = scoped_data_path(&payload_string(payload, "path")?)?;
            let mut entries = fs::read_dir(&path)
                .map_err(|error| format!("list {}: {error}", path.display()))?
                .map(|entry| {
                    let entry = entry.map_err(|error| error.to_string())?;
                    let entry_path = entry.path();
                    let metadata = entry
                        .metadata()
                        .map_err(|error| format!("stat {}: {error}", entry_path.display()))?;
                    Ok(serde_json::json!({
                        "name": entry.file_name().to_string_lossy().to_string(),
                        "path": path_string(&entry_path),
                        "isDir": metadata.is_dir(),
                        "bytes": metadata.len(),
                    }))
                })
                .collect::<Result<Vec<_>, String>>()?;
            entries.sort_by(|left, right| left["name"].as_str().cmp(&right["name"].as_str()));
            Ok(serde_json::json!({ "path": path_string(&path), "entries": entries }))
        }
        "stat" => {
            let path = scoped_data_path(&payload_string(payload, "path")?)?;
            file_stat_json(&path)
        }
        other => Err(format!("unsupported filesystem method: {other}")),
    }
}

fn device_backend(method: &str) -> Result<serde_json::Value, String> {
    match method {
        "info" => Ok(serde_json::json!({
            "targetOs": std::env::consts::OS,
            "targetArch": std::env::consts::ARCH,
            "targetFamily": std::env::consts::FAMILY,
            "tempDir": path_string(&data_root_dir()),
        })),
        other => Err(format!("unsupported device method: {other}")),
    }
}

fn app_backend(state: &MobileActionState, method: &str) -> Result<serde_json::Value, String> {
    match method {
        "info" => Ok(serde_json::json!({
            "syncCount": state.sync_count,
            "previewCount": state.preview_count,
            "lastAction": state.last_action,
            "lastPayload": state.last_payload,
            "dataRoot": path_string(&data_root_dir()),
        })),
        other => Err(format!("unsupported app method: {other}")),
    }
}

fn network_backend(
    method: &str,
    payload: Option<&serde_json::Value>,
) -> Result<serde_json::Value, String> {
    match method {
        "status" => {
            let host = payload
                .and_then(|value| value.get("host"))
                .and_then(|value| value.as_str())
                .unwrap_or("1.1.1.1");
            let port = payload
                .and_then(|value| value.get("port"))
                .and_then(|value| value.as_u64())
                .unwrap_or(53) as u16;
            let timeout_ms = payload
                .and_then(|value| value.get("timeoutMs"))
                .and_then(|value| value.as_u64())
                .unwrap_or(1000);
            let reachable = probe_socket(host, port, timeout_ms);
            Ok(serde_json::json!({
                "host": host,
                "port": port,
                "reachable": reachable,
            }))
        }
        other => Err(format!("unsupported network method: {other}")),
    }
}

fn http_backend(
    method: &str,
    payload: Option<&serde_json::Value>,
) -> Result<serde_json::Value, String> {
    let payload = payload.ok_or_else(|| "http request requires payload".to_string())?;
    let url = payload_string(Some(payload), "url")?;
    let verb = match method {
        "get" => "GET",
        "post" => "POST",
        "put" => "PUT",
        "delete" => "DELETE",
        "request" => payload
            .get("verb")
            .and_then(|value| value.as_str())
            .unwrap_or("GET"),
        other => return Err(format!("unsupported http method: {other}")),
    };
    let parsed = parse_http_url(&url)?;
    let body = payload
        .get("body")
        .map(|body| match body {
            serde_json::Value::String(text) => Ok(text.clone()),
            other => serde_json::to_string(&other).map_err(|error| error.to_string()),
        })
        .transpose()?
        .unwrap_or_default();
    let response = send_http_request(verb, &parsed, payload.get("headers"), &body)?;
    Ok(serde_json::json!({
        "status": response.status,
        "ok": (200..=299).contains(&response.status),
        "text": response.body,
    }))
}

fn payload_message(payload: Option<&serde_json::Value>) -> Option<&str> {
    payload
        .and_then(|value| value.get("message"))
        .and_then(|value| value.as_str())
}

fn parse_action_request(input: &str) -> serde_json::Value {
    serde_json::from_str(input)
        .ok()
        .filter(|value: &serde_json::Value| {
            value.get("action").is_some() || value.get("kind").is_some()
        })
        .unwrap_or_else(|| serde_json::json!({ "action": input }))
}

fn legacy_action_name<'a>(request: &'a serde_json::Value) -> Option<&'a str> {
    request.get("action").and_then(|value| value.as_str())
}

fn request_kind(request: &serde_json::Value) -> Option<&str> {
    request.get("kind").and_then(|value| value.as_str())
}

fn request_action_name(request: &serde_json::Value) -> String {
    if let Some(action) = legacy_action_name(request) {
        return action.to_string();
    }
    if request_kind(request) == Some("plugin") {
        let capability = request
            .get("capability")
            .and_then(|value| value.as_str())
            .unwrap_or("plugin");
        let method = request
            .get("method")
            .and_then(|value| value.as_str())
            .unwrap_or("call");
        return format!("{capability}.{method}");
    }
    request_kind(request).unwrap_or("").to_string()
}

fn request_payload(request: &serde_json::Value) -> Option<&serde_json::Value> {
    request.get("payload")
}

fn payload_string(
    payload: Option<&serde_json::Value>,
    key: &str,
) -> Result<String, String> {
    payload
        .and_then(|value| value.get(key))
        .and_then(|value| value.as_str())
        .map(str::to_string)
        .ok_or_else(|| format!("payload.{key} must be a string"))
}

fn data_root_dir() -> PathBuf {
    if let Some(path) = std::env::var_os("CREPUS_NATIVE_STATE_DIR") {
        return PathBuf::from(path);
    }
    std::env::temp_dir().join("crepus_mobile_actions")
}

fn probe_socket(host: &str, port: u16, timeout_ms: u64) -> bool {
    resolve_socket_addrs(host, port)
        .into_iter()
        .any(|address| TcpStream::connect_timeout(&address, Duration::from_millis(timeout_ms)).is_ok())
}

fn resolve_socket_addrs(host: &str, port: u16) -> Vec<SocketAddr> {
    (host, port)
        .to_socket_addrs()
        .map(|addresses| addresses.collect())
        .unwrap_or_default()
}

struct ParsedHttpUrl {
    host: String,
    port: u16,
    path: String,
}

struct HttpResponse {
    status: u16,
    body: String,
}

fn parse_http_url(url: &str) -> Result<ParsedHttpUrl, String> {
    let rest = url
        .strip_prefix("http://")
        .ok_or_else(|| "only http:// URLs are supported".to_string())?;
    let (authority, path) = match rest.split_once('/') {
        Some((authority, path)) => (authority, format!("/{}", path)),
        None => (rest, "/".to_string()),
    };
    let (host, port) = match authority.rsplit_once(':') {
        Some((host, port)) if !host.contains(']') => {
            let port = port
                .parse::<u16>()
                .map_err(|_| format!("invalid port in URL: {url}"))?;
            (host.to_string(), port)
        }
        _ => (authority.to_string(), 80),
    };
    if host.is_empty() {
        return Err("URL host must not be empty".to_string());
    }
    Ok(ParsedHttpUrl { host, port, path })
}

fn send_http_request(
    verb: &str,
    url: &ParsedHttpUrl,
    headers: Option<&serde_json::Value>,
    body: &str,
) -> Result<HttpResponse, String> {
    let address = resolve_socket_addrs(&url.host, url.port)
        .into_iter()
        .next()
        .ok_or_else(|| format!("failed to resolve {}:{}", url.host, url.port))?;
    let mut stream = TcpStream::connect_timeout(&address, Duration::from_secs(2))
        .map_err(|error| format!("connect {}: {error}", address))?;
    stream
        .set_read_timeout(Some(Duration::from_secs(2)))
        .map_err(|error| error.to_string())?;
    stream
        .set_write_timeout(Some(Duration::from_secs(2)))
        .map_err(|error| error.to_string())?;

    let mut request = format!(
        "{verb} {} HTTP/1.1\r\nHost: {}:{}\r\nConnection: close\r\n",
        url.path, url.host, url.port
    );
    if let Some(headers) = headers.and_then(|value| value.as_object()) {
        for (key, value) in headers {
            let Some(value) = value.as_str() else {
                return Err(format!("header {key} must be a string"));
            };
            request.push_str(&format!("{key}: {value}\r\n"));
        }
    }
    if !body.is_empty() {
        request.push_str(&format!("Content-Length: {}\r\n", body.len()));
    }
    request.push_str("\r\n");
    request.push_str(body);

    stream
        .write_all(request.as_bytes())
        .map_err(|error| format!("write request: {error}"))?;
    let mut response = String::new();
    stream
        .read_to_string(&mut response)
        .map_err(|error| format!("read response: {error}"))?;
    parse_http_response(&response)
}

fn parse_http_response(response: &str) -> Result<HttpResponse, String> {
    let (head, body) = response
        .split_once("\r\n\r\n")
        .ok_or_else(|| "invalid HTTP response".to_string())?;
    let mut lines = head.lines();
    let status_line = lines.next().ok_or_else(|| "missing status line".to_string())?;
    let status = status_line
        .split_whitespace()
        .nth(1)
        .ok_or_else(|| "missing status code".to_string())?
        .parse::<u16>()
        .map_err(|_| "invalid status code".to_string())?;
    Ok(HttpResponse {
        status,
        body: body.to_string(),
    })
}

fn preferences_file_path() -> PathBuf {
    data_root_dir().join("preferences.json")
}

fn load_json_map(
    path: &Path,
) -> Result<serde_json::Map<String, serde_json::Value>, String> {
    if !path.exists() {
        return Ok(serde_json::Map::new());
    }
    let text =
        fs::read_to_string(path).map_err(|error| format!("read {}: {error}", path.display()))?;
    let value: serde_json::Value = serde_json::from_str(&text)
        .map_err(|error| format!("parse {}: {error}", path.display()))?;
    value
        .as_object()
        .cloned()
        .ok_or_else(|| format!("{} is not a JSON object", path.display()))
}

fn save_json_map(
    path: &Path,
    map: &serde_json::Map<String, serde_json::Value>,
) -> Result<(), String> {
    ensure_parent_dir(path)?;
    let json = serde_json::to_vec_pretty(map).map_err(|error| error.to_string())?;
    fs::write(path, json).map_err(|error| format!("write {}: {error}", path.display()))
}

fn ensure_parent_dir(path: &Path) -> Result<(), String> {
    let Some(parent) = path.parent() else {
        return Ok(());
    };
    fs::create_dir_all(parent)
        .map_err(|error| format!("mkdir {}: {error}", parent.display()))
}

fn scoped_data_path(raw: &str) -> Result<PathBuf, String> {
    let relative = sanitize_relative_path(raw)?;
    Ok(data_root_dir().join(relative))
}

fn sanitize_relative_path(raw: &str) -> Result<PathBuf, String> {
    let path = Path::new(raw);
    if path.is_absolute() {
        return Err("absolute paths are not allowed".to_string());
    }
    let mut clean = PathBuf::new();
    for component in path.components() {
        match component {
            Component::Normal(part) => clean.push(part),
            Component::CurDir => {}
            Component::ParentDir => return Err("parent segments are not allowed".to_string()),
            _ => return Err("unsupported path component".to_string()),
        }
    }
    if clean.as_os_str().is_empty() {
        return Err("path must not be empty".to_string());
    }
    Ok(clean)
}

fn file_stat_json(path: &Path) -> Result<serde_json::Value, String> {
    let metadata = fs::metadata(path).map_err(|error| format!("stat {}: {error}", path.display()))?;
    Ok(serde_json::json!({
        "path": path_string(path),
        "isDir": metadata.is_dir(),
        "bytes": metadata.len(),
    }))
}

fn path_string(path: &Path) -> String {
    path.to_string_lossy().to_string()
}

fn action_json(
    ok: bool,
    action: &str,
    value: Option<serde_json::Value>,
    error: Option<&str>,
    state: &MobileActionState,
) -> String {
    let mut out = serde_json::json!({
        "ok": ok,
        "action": action,
        "state": {
            "syncCount": state.sync_count,
            "previewCount": state.preview_count,
            "lastAction": state.last_action,
            "lastPayload": state.last_payload,
        }
    });
    if let Some(value) = value {
        out["value"] = value;
    }
    if let Some(error) = error {
        out["error"] = serde_json::Value::String(error.to_string());
    }
    out.to_string()
}

#[no_mangle]
pub extern "C" fn crepus_mobile_dispatch(action_ptr: *const c_char, action_len: usize) -> bool {
    action_from_ffi(action_ptr, action_len)
        .map(|action| dispatch_action(action.as_ref()))
        .unwrap_or(false)
}

#[no_mangle]
pub extern "C" fn crepus_mobile_dispatch_json(
    action_ptr: *const c_char,
    action_len: usize,
    output_ptr: *mut c_char,
    output_len: usize,
) -> usize {
    let result = action_from_ffi(action_ptr, action_len)
        .map(|action| dispatch_action_json(action.as_ref()))
        .unwrap_or_else(|| {
            action_json(
                false,
                "",
                None,
                Some("invalid action pointer"),
                &MobileActionState::default(),
            )
        });
    copy_json_to_output(&result, output_ptr, output_len)
}

#[no_mangle]
pub extern "C" fn crepus_mobile_dispatch_and_store_json(
    action_ptr: *const c_char,
    action_len: usize,
    output_ptr: *mut c_char,
    output_len: usize,
) -> usize {
    crepus_mobile_dispatch_json(action_ptr, action_len, output_ptr, output_len)
}

#[no_mangle]
pub extern "C" fn crepus_mobile_dispatch_change_json(
    action_ptr: *const c_char,
    action_len: usize,
    bind_ptr: *const c_char,
    bind_len: usize,
    value_ptr: *const c_char,
    value_len: usize,
    output_ptr: *mut c_char,
    output_len: usize,
) -> usize {
    let Some(action) = action_from_ffi(action_ptr, action_len) else {
        let result = action_json(false, "", None, Some("invalid action pointer"), &MobileActionState::default());
        return copy_json_to_output(&result, output_ptr, output_len);
    };
    let Some(bind) = action_from_ffi(bind_ptr, bind_len) else {
        let result = action_json(false, action.as_ref(), None, Some("invalid bind pointer"), &MobileActionState::default());
        return copy_json_to_output(&result, output_ptr, output_len);
    };
    let Some(value_json) = action_from_ffi(value_ptr, value_len) else {
        let result = action_json(false, action.as_ref(), None, Some("invalid value pointer"), &MobileActionState::default());
        return copy_json_to_output(&result, output_ptr, output_len);
    };
    copy_json_to_output(
        &dispatch_change_json(action.as_ref(), bind.as_ref(), value_json.as_ref()),
        output_ptr,
        output_len,
    )
}

#[no_mangle]
pub extern "C" fn crepus_mobile_start_auto_scan() -> *mut c_char {
    let mut state = lock_action_state();
    if !state.auto_scan_started {
        state.auto_scan_started = true;
        let result = dispatch_action_json("preview");
        return alloc_c_string(&result);
    }
    alloc_c_string(&state.last_result)
}

#[no_mangle]
pub extern "C" fn crepus_mobile_last_result() -> *mut c_char {
    let state = lock_action_state();
    alloc_c_string(&state.last_result)
}

#[no_mangle]
pub extern "C" fn crepus_mobile_last_error() -> *mut c_char {
    let state = lock_action_state();
    alloc_c_string(state.last_error.as_deref().unwrap_or(""))
}

#[no_mangle]
pub unsafe extern "C" fn crepus_mobile_free_string(ptr: *mut c_char) {
    if !ptr.is_null() {
        libc::free(ptr.cast());
    }
}

fn alloc_c_string(result: &str) -> *mut c_char {
    let bytes = result.as_bytes();
    let ptr = unsafe { libc::malloc(bytes.len() + 1) as *mut c_char };
    if ptr.is_null() {
        return ptr;
    }
    unsafe {
        std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.cast::<u8>(), bytes.len());
        *ptr.add(bytes.len()) = 0;
    }
    ptr
}

fn action_from_ffi(action_ptr: *const c_char, action_len: usize) -> Option<Cow<'static, str>> {
    if action_ptr.is_null() {
        return None;
    }
    Some(
        if action_len == 0 {
            unsafe { CStr::from_ptr(action_ptr) }.to_string_lossy()
        } else {
            let bytes = unsafe { std::slice::from_raw_parts(action_ptr.cast::<u8>(), action_len) };
            String::from_utf8_lossy(bytes)
        }
        .into_owned()
        .into(),
    )
}

fn copy_json_to_output(result: &str, output_ptr: *mut c_char, output_len: usize) -> usize {
    let bytes = result.as_bytes();
    if output_ptr.is_null() || output_len == 0 {
        return bytes.len();
    }
    let copy_len = bytes.len().min(output_len.saturating_sub(1));
    unsafe {
        std::ptr::copy_nonoverlapping(bytes.as_ptr(), output_ptr.cast::<u8>(), copy_len);
        *output_ptr.add(copy_len) = 0;
    }
    bytes.len()
}

fn store_view_state(raw: &str) -> bool {
    match serde_json::from_str::<serde_json::Value>(raw) {
        Ok(value) => {
            *lock_view_state() = value;
            true
        }
        Err(_) => false,
    }
}

fn eval_text(expr: &str, scope_name: Option<&str>, scope_json: Option<&str>) -> String {
    stringify_value(&resolve_expr(expr, scope_name, scope_json))
}

fn eval_bool(expr: &str, scope_name: Option<&str>, scope_json: Option<&str>) -> bool {
    truthy_value(&resolve_expr(expr, scope_name, scope_json))
}

fn eval_number(expr: &str, scope_name: Option<&str>, scope_json: Option<&str>) -> f64 {
    number_value(&resolve_expr(expr, scope_name, scope_json)).unwrap_or(0.0)
}

fn eval_items_json(expr: &str, scope_name: Option<&str>, scope_json: Option<&str>) -> String {
    match resolve_path(expr, scope_name, scope_json) {
        serde_json::Value::Array(items) => serde_json::Value::Array(items).to_string(),
        _ => "[]".to_string(),
    }
}

fn resolve_expr(expr: &str, scope_name: Option<&str>, scope_json: Option<&str>) -> serde_json::Value {
    let trimmed = expr.trim();
    if let Some(rest) = trimmed.strip_prefix('!') {
        return serde_json::Value::Bool(!truthy_value(&resolve_expr(rest, scope_name, scope_json)));
    }
    for op in [">=", "<=", "==", "!=", ">", "<"] {
        if let Some(index) = trimmed.find(op) {
            let left = resolve_path(trimmed[..index].trim(), scope_name, scope_json);
            let right = resolve_path(trimmed[index + op.len()..].trim(), scope_name, scope_json);
            return serde_json::Value::Bool(compare_values(&left, &right, op));
        }
    }
    resolve_path(trimmed, scope_name, scope_json)
}

fn resolve_path(expr: &str, scope_name: Option<&str>, scope_json: Option<&str>) -> serde_json::Value {
    if let Some(literal) = literal_value(expr) {
        return literal;
    }
    if let Some(scope) = scope_json.and_then(|value| serde_json::from_str(value).ok()) {
        if let Some(scope_name) = scope_name {
            if expr == scope_name {
                return scope;
            }
            if let Some(path) = expr
                .strip_prefix(scope_name)
                .and_then(|rest| rest.strip_prefix('.'))
            {
                return lookup_path(path, &scope).unwrap_or(serde_json::Value::Null);
            }
        }
        return lookup_path(expr, &scope)
            .or_else(|| {
                let state = lock_view_state();
                lookup_path(expr, &state)
            })
            .unwrap_or(serde_json::Value::Null);
    }
    let state = lock_view_state();
    lookup_path(expr, &state).unwrap_or(serde_json::Value::Null)
}

fn lookup_path(path: &str, root: &serde_json::Value) -> Option<serde_json::Value> {
    if path.is_empty() {
        return Some(root.clone());
    }
    let mut current = root;
    for segment in path.split('.') {
        match current {
            serde_json::Value::Object(map) => current = map.get(segment)?,
            serde_json::Value::Array(items) => current = items.get(segment.parse::<usize>().ok()?)?,
            _ => return None,
        }
    }
    Some(current.clone())
}

fn literal_value(expr: &str) -> Option<serde_json::Value> {
    match expr {
        "true" => Some(serde_json::Value::Bool(true)),
        "false" => Some(serde_json::Value::Bool(false)),
        "null" => Some(serde_json::Value::Null),
        _ if expr.starts_with('"') && expr.ends_with('"') && expr.len() >= 2 => Some(
            serde_json::Value::String(expr[1..expr.len() - 1].to_string()),
        ),
        _ => expr
            .parse::<i64>()
            .map(|value| serde_json::json!(value))
            .or_else(|_| expr.parse::<f64>().map(|value| serde_json::json!(value)))
            .ok(),
    }
}

fn compare_values(left: &serde_json::Value, right: &serde_json::Value, op: &str) -> bool {
    if let (Some(left), Some(right)) = (number_value(left), number_value(right)) {
        return match op {
            ">=" => left >= right,
            "<=" => left <= right,
            ">" => left > right,
            "<" => left < right,
            "==" => left == right,
            "!=" => left != right,
            _ => false,
        };
    }
    let left = stringify_value(left);
    let right = stringify_value(right);
    match op {
        "==" => left == right,
        "!=" => left != right,
        _ => false,
    }
}

fn number_value(value: &serde_json::Value) -> Option<f64> {
    match value {
        serde_json::Value::Number(number) => number.as_f64(),
        serde_json::Value::String(text) => text.parse::<f64>().ok(),
        _ => None,
    }
}

fn truthy_value(value: &serde_json::Value) -> bool {
    match value {
        serde_json::Value::Null => false,
        serde_json::Value::Bool(value) => *value,
        serde_json::Value::Number(number) => number.as_f64().unwrap_or_default() != 0.0,
        serde_json::Value::String(text) => !text.is_empty(),
        serde_json::Value::Array(items) => !items.is_empty(),
        serde_json::Value::Object(map) => !map.is_empty(),
    }
}

fn stringify_value(value: &serde_json::Value) -> String {
    match value {
        serde_json::Value::Null => String::new(),
        serde_json::Value::Bool(value) => value.to_string(),
        serde_json::Value::String(text) => text.clone(),
        serde_json::Value::Number(number) => number.to_string(),
        _ => value.to_string(),
    }
}

#[no_mangle]
pub extern "C" fn crepus_mobile_store_result_json(
    json_ptr: *const c_char,
    json_len: usize,
) -> bool {
    action_from_ffi(json_ptr, json_len)
        .map(|json| store_view_state(json.as_ref()))
        .unwrap_or(false)
}

#[no_mangle]
pub extern "C" fn crepus_mobile_eval_text(
    expr_ptr: *const c_char,
    expr_len: usize,
    scope_name_ptr: *const c_char,
    scope_name_len: usize,
    scope_ptr: *const c_char,
    scope_len: usize,
    output_ptr: *mut c_char,
    output_len: usize,
) -> usize {
    let result = action_from_ffi(expr_ptr, expr_len)
        .map(|expr| {
            let scope_name = action_from_ffi(scope_name_ptr, scope_name_len);
            let scope = action_from_ffi(scope_ptr, scope_len);
            eval_text(expr.as_ref(), scope_name.as_deref(), scope.as_deref())
        })
        .unwrap_or_default();
    copy_json_to_output(&result, output_ptr, output_len)
}

#[no_mangle]
pub extern "C" fn crepus_mobile_eval_bool(
    expr_ptr: *const c_char,
    expr_len: usize,
    scope_name_ptr: *const c_char,
    scope_name_len: usize,
    scope_ptr: *const c_char,
    scope_len: usize,
) -> bool {
    action_from_ffi(expr_ptr, expr_len)
        .map(|expr| {
            let scope_name = action_from_ffi(scope_name_ptr, scope_name_len);
            let scope = action_from_ffi(scope_ptr, scope_len);
            eval_bool(expr.as_ref(), scope_name.as_deref(), scope.as_deref())
        })
        .unwrap_or(false)
}

#[no_mangle]
pub extern "C" fn crepus_mobile_eval_number(
    expr_ptr: *const c_char,
    expr_len: usize,
    scope_name_ptr: *const c_char,
    scope_name_len: usize,
    scope_ptr: *const c_char,
    scope_len: usize,
) -> f64 {
    action_from_ffi(expr_ptr, expr_len)
        .map(|expr| {
            let scope_name = action_from_ffi(scope_name_ptr, scope_name_len);
            let scope = action_from_ffi(scope_ptr, scope_len);
            eval_number(expr.as_ref(), scope_name.as_deref(), scope.as_deref())
        })
        .unwrap_or(0.0)
}

#[no_mangle]
pub extern "C" fn crepus_mobile_eval_items_json(
    expr_ptr: *const c_char,
    expr_len: usize,
    scope_name_ptr: *const c_char,
    scope_name_len: usize,
    scope_ptr: *const c_char,
    scope_len: usize,
    output_ptr: *mut c_char,
    output_len: usize,
) -> usize {
    let result = action_from_ffi(expr_ptr, expr_len)
        .map(|expr| {
            let scope_name = action_from_ffi(scope_name_ptr, scope_name_len);
            let scope = action_from_ffi(scope_ptr, scope_len);
            eval_items_json(expr.as_ref(), scope_name.as_deref(), scope.as_deref())
        })
        .unwrap_or_else(|| "[]".to_string());
    copy_json_to_output(&result, output_ptr, output_len)
}

#[cfg(test)]
fn reset_action_state() {
    *lock_action_state() = MobileActionState::default();
}

#[cfg(test)]
fn test_lock() -> std::sync::MutexGuard<'static, ()> {
    static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
    TEST_LOCK
        .get_or_init(|| Mutex::new(()))
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
}

#[cfg(test)]
fn reset_test_data_root() {
    let root = data_root_dir();
    let _ = fs::remove_dir_all(root);
}

#[cfg(target_os = "android")]
#[no_mangle]
pub extern "system" fn Java_dev_crepuscularity_nativeshell_CrepusRustActions_dispatchAction(
    mut env: JNIEnv<'_>,
    _class: JClass<'_>,
    action: JString<'_>,
) -> bool {
    match env.get_string(&action) {
        Ok(action) => dispatch_action(action.to_string_lossy().as_ref()),
        Err(_) => false,
    }
}

#[cfg(target_os = "android")]
#[no_mangle]
pub extern "system" fn Java_dev_crepuscularity_nativeshell_CrepusRustActions_dispatchActionJson<'a>(
    mut env: JNIEnv<'a>,
    _class: JClass<'a>,
    action: JString<'a>,
) -> JString<'a> {
    let result = match env.get_string(&action) {
        Ok(action) => dispatch_action_json(action.to_string_lossy().as_ref()),
        Err(_) => "{\"ok\":false,\"action\":\"\",\"error\":\"invalid action string\",\"state\":{\"syncCount\":0,\"previewCount\":0,\"lastAction\":\"\"}}".to_string(),
    };
    env.new_string(result).unwrap()
}

#[cfg(target_os = "android")]
#[no_mangle]
pub extern "system" fn Java_dev_crepuscularity_nativeshell_CrepusRustActions_dispatchAndStoreJson<
    'a,
>(
    mut env: JNIEnv<'a>,
    _class: JClass<'a>,
    action: JString<'a>,
) -> JString<'a> {
    let result = match env.get_string(&action) {
        Ok(action) => dispatch_action_json(action.to_string_lossy().as_ref()),
        Err(_) => action_json(
            false,
            "",
            None,
            Some("invalid action pointer"),
            &MobileActionState::default(),
        ),
    };
    env.new_string(result).unwrap()
}

#[cfg(target_os = "android")]
#[no_mangle]
pub extern "system" fn Java_dev_crepuscularity_nativeshell_CrepusRustActions_dispatchChangeJson<'a>(
    mut env: JNIEnv<'a>,
    _class: JClass<'a>,
    action: JString<'a>,
    bind: JString<'a>,
    value_json: JString<'a>,
) -> JString<'a> {
    let result = match (
        env.get_string(&action),
        env.get_string(&bind),
        env.get_string(&value_json),
    ) {
        (Ok(action), Ok(bind), Ok(value_json)) => dispatch_change_json(
            action.to_string_lossy().as_ref(),
            bind.to_string_lossy().as_ref(),
            value_json.to_string_lossy().as_ref(),
        ),
        _ => action_json(
            false,
            "",
            None,
            Some("invalid change payload"),
            &MobileActionState::default(),
        ),
    };
    env.new_string(result).unwrap()
}

#[cfg(target_os = "android")]
#[no_mangle]
pub extern "system" fn Java_dev_crepuscularity_nativeshell_CrepusRustActions_storeResultJson(
    mut env: JNIEnv<'_>,
    _class: JClass<'_>,
    json: JString<'_>,
) -> bool {
    match env.get_string(&json) {
        Ok(json) => store_view_state(json.to_string_lossy().as_ref()),
        Err(_) => false,
    }
}

#[cfg(target_os = "android")]
#[no_mangle]
pub extern "system" fn Java_dev_crepuscularity_nativeshell_CrepusRustActions_evalText<'a>(
    mut env: JNIEnv<'a>,
    _class: JClass<'a>,
    expr: JString<'a>,
    scope_name: JString<'a>,
    scope: JString<'a>,
) -> JString<'a> {
    let expr = env
        .get_string(&expr)
        .map(|value| value.to_string_lossy().into_owned())
        .unwrap_or_default();
    let scope_name = env
        .get_string(&scope_name)
        .ok()
        .map(|value| value.to_string_lossy().into_owned())
        .filter(|value| !value.is_empty());
    let scope = env
        .get_string(&scope)
        .ok()
        .map(|value| value.to_string_lossy().into_owned())
        .filter(|value| !value.is_empty());
    env.new_string(eval_text(&expr, scope_name.as_deref(), scope.as_deref())).unwrap()
}

#[cfg(target_os = "android")]
#[no_mangle]
pub extern "system" fn Java_dev_crepuscularity_nativeshell_CrepusRustActions_evalBool(
    mut env: JNIEnv<'_>,
    _class: JClass<'_>,
    expr: JString<'_>,
    scope_name: JString<'_>,
    scope: JString<'_>,
) -> bool {
    let expr = env
        .get_string(&expr)
        .map(|value| value.to_string_lossy().into_owned())
        .unwrap_or_default();
    let scope_name = env
        .get_string(&scope_name)
        .ok()
        .map(|value| value.to_string_lossy().into_owned())
        .filter(|value| !value.is_empty());
    let scope = env
        .get_string(&scope)
        .ok()
        .map(|value| value.to_string_lossy().into_owned())
        .filter(|value| !value.is_empty());
    eval_bool(&expr, scope_name.as_deref(), scope.as_deref())
}

#[cfg(target_os = "android")]
#[no_mangle]
pub extern "system" fn Java_dev_crepuscularity_nativeshell_CrepusRustActions_evalNumber(
    mut env: JNIEnv<'_>,
    _class: JClass<'_>,
    expr: JString<'_>,
    scope_name: JString<'_>,
    scope: JString<'_>,
) -> f64 {
    let expr = env
        .get_string(&expr)
        .map(|value| value.to_string_lossy().into_owned())
        .unwrap_or_default();
    let scope_name = env
        .get_string(&scope_name)
        .ok()
        .map(|value| value.to_string_lossy().into_owned())
        .filter(|value| !value.is_empty());
    let scope = env
        .get_string(&scope)
        .ok()
        .map(|value| value.to_string_lossy().into_owned())
        .filter(|value| !value.is_empty());
    eval_number(&expr, scope_name.as_deref(), scope.as_deref())
}

#[cfg(target_os = "android")]
#[no_mangle]
pub extern "system" fn Java_dev_crepuscularity_nativeshell_CrepusRustActions_evalItemsJson<'a>(
    mut env: JNIEnv<'a>,
    _class: JClass<'a>,
    expr: JString<'a>,
    scope_name: JString<'a>,
    scope: JString<'a>,
) -> JString<'a> {
    let expr = env
        .get_string(&expr)
        .map(|value| value.to_string_lossy().into_owned())
        .unwrap_or_default();
    let scope_name = env
        .get_string(&scope_name)
        .ok()
        .map(|value| value.to_string_lossy().into_owned())
        .filter(|value| !value.is_empty());
    let scope = env
        .get_string(&scope)
        .ok()
        .map(|value| value.to_string_lossy().into_owned())
        .filter(|value| !value.is_empty());
    env.new_string(eval_items_json(&expr, scope_name.as_deref(), scope.as_deref())).unwrap()
}

#[cfg(target_os = "android")]
#[no_mangle]
pub extern "system" fn Java_dev_crepuscularity_nativeshell_CrepusRustActions_startAutoScan<'a>(
    env: JNIEnv<'a>,
    _class: JClass<'a>,
) -> JString<'a> {
    let ptr = crepus_mobile_start_auto_scan();
    let result = if ptr.is_null() {
        String::new()
    } else {
        let result = unsafe { CStr::from_ptr(ptr) }.to_string_lossy().into_owned();
        unsafe { crepus_mobile_free_string(ptr) };
        result
    };
    env.new_string(result).unwrap()
}

#[cfg(target_os = "android")]
#[no_mangle]
pub extern "system" fn Java_dev_crepuscularity_nativeshell_CrepusRustActions_lastResult<'a>(
    env: JNIEnv<'a>,
    _class: JClass<'a>,
) -> JString<'a> {
    let ptr = crepus_mobile_last_result();
    let result = if ptr.is_null() {
        String::new()
    } else {
        let result = unsafe { CStr::from_ptr(ptr) }.to_string_lossy().into_owned();
        unsafe { crepus_mobile_free_string(ptr) };
        result
    };
    env.new_string(result).unwrap()
}

#[cfg(target_os = "android")]
#[no_mangle]
pub extern "system" fn Java_dev_crepuscularity_nativeshell_CrepusRustActions_lastError<'a>(
    env: JNIEnv<'a>,
    _class: JClass<'a>,
) -> JString<'a> {
    let ptr = crepus_mobile_last_error();
    let result = if ptr.is_null() {
        String::new()
    } else {
        let result = unsafe { CStr::from_ptr(ptr) }.to_string_lossy().into_owned();
        unsafe { crepus_mobile_free_string(ptr) };
        result
    };
    env.new_string(result).unwrap()
}

#[cfg(target_os = "android")]
#[no_mangle]
pub extern "system" fn Java_dev_crepuscularity_nativeshell_CrepusRustActions_initAndroid<'a>(
    _env: JNIEnv<'a>,
    _class: JClass<'a>,
    _context: jni::objects::JObject<'a>,
) {
}

#[cfg(target_os = "android")]
#[no_mangle]
pub extern "system" fn Java_dev_crepuscularity_nativeshell_CrepusRustActions_shutdownAndroid<
    'a,
>(
    _env: JNIEnv<'a>,
    _class: JClass<'a>,
) {
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::ffi::CString;
    use std::net::TcpListener;
    use std::thread;

    #[test]
    fn dispatch_action_json_mutates_state() {
        let _guard = test_lock();
        reset_action_state();
        let first = dispatch_action_json("sync");
        let second = dispatch_action_json("preview");

        let first: serde_json::Value = serde_json::from_str(&first).expect("json");
        let second: serde_json::Value = serde_json::from_str(&second).expect("json");
        assert_eq!(first["ok"], true);
        assert_eq!(first["action"], "sync");
        assert_eq!(first["state"]["syncCount"], 1);
        assert_eq!(second["ok"], true);
        assert_eq!(second["action"], "preview");
        assert_eq!(second["state"]["syncCount"], 1);
        assert_eq!(second["state"]["previewCount"], 1);
        assert_eq!(second["state"]["lastAction"], "preview");
    }

    #[test]
    fn dispatch_action_json_reports_unknown_actions() {
        let _guard = test_lock();
        reset_action_state();
        let result = dispatch_action_json("missing");

        let result: serde_json::Value = serde_json::from_str(&result).expect("json");
        assert_eq!(result["ok"], false);
        assert_eq!(result["action"], "missing");
        assert_eq!(result["error"], "unknown action");
    }

    #[test]
    fn dispatch_known_action_checks_registry_without_mutation() {
        let _guard = test_lock();
        reset_action_state();
        assert!(dispatch_action("sync"));
        assert!(dispatch_action("preview"));
        assert!(dispatch_action(r#"{"kind":"plugin","capability":"device","method":"info"}"#));
        let state = lock_action_state();
        assert_eq!(state.sync_count, 0);
        assert_eq!(state.preview_count, 0);
    }

    #[test]
    fn dispatch_action_json_accepts_typed_payloads() {
        let _guard = test_lock();
        reset_action_state();
        let result = dispatch_action_json(r#"{"action":"sync","payload":{"message":"hydrate"}}"#);

        let result: serde_json::Value = serde_json::from_str(&result).expect("json");
        assert_eq!(result["ok"], true);
        assert_eq!(result["action"], "sync");
        assert_eq!(result["value"]["message"], "hydrate");
        assert_eq!(result["state"]["lastPayload"], r#"{"message":"hydrate"}"#);
    }

    #[test]
    fn dispatch_change_json_builds_action_payload_in_rust() {
        let _guard = test_lock();
        reset_action_state();
        let result = dispatch_change_json("sync", "enabled", "true");

        let result: serde_json::Value = serde_json::from_str(&result).expect("json");
        assert_eq!(result["ok"], true);
        assert_eq!(result["action"], "sync");
        assert_eq!(result["value"]["enabled"], true);
        assert_eq!(result["state"]["lastPayload"], r#"{"enabled":true}"#);
    }

    #[test]
    fn preferences_round_trip_persists_values() {
        let _guard = test_lock();
        reset_action_state();
        reset_test_data_root();
        let set = dispatch_action_json(
            r#"{"kind":"plugin","capability":"preferences","method":"set","payload":{"key":"theme","value":"light"}}"#,
        );
        let get = dispatch_action_json(
            r#"{"kind":"plugin","capability":"preferences","method":"get","payload":{"key":"theme"}}"#,
        );
        let keys =
            dispatch_action_json(r#"{"kind":"plugin","capability":"preferences","method":"keys"}"#);

        let set: serde_json::Value = serde_json::from_str(&set).expect("json");
        let get: serde_json::Value = serde_json::from_str(&get).expect("json");
        let keys: serde_json::Value = serde_json::from_str(&keys).expect("json");
        assert_eq!(set["ok"], true);
        assert_eq!(get["value"]["value"], "light");
        assert_eq!(keys["value"]["value"][0], "theme");
    }

    #[test]
    fn filesystem_round_trip_handles_text_files() {
        let _guard = test_lock();
        reset_action_state();
        reset_test_data_root();
        let write = dispatch_action_json(
            r#"{"kind":"plugin","capability":"filesystem","method":"writeText","payload":{"path":"notes/hello.txt","text":"hi"}}"#,
        );
        let read = dispatch_action_json(
            r#"{"kind":"plugin","capability":"filesystem","method":"readText","payload":{"path":"notes/hello.txt"}}"#,
        );
        let list = dispatch_action_json(
            r#"{"kind":"plugin","capability":"filesystem","method":"list","payload":{"path":"notes"}}"#,
        );

        let write: serde_json::Value = serde_json::from_str(&write).expect("json");
        let read: serde_json::Value = serde_json::from_str(&read).expect("json");
        let list: serde_json::Value = serde_json::from_str(&list).expect("json");
        assert_eq!(write["ok"], true);
        assert_eq!(read["value"]["value"]["text"], "hi");
        assert_eq!(list["value"]["value"]["entries"][0]["name"], "hello.txt");
    }

    #[test]
    fn filesystem_rejects_parent_escape() {
        let _guard = test_lock();
        reset_action_state();
        let result = dispatch_action_json(
            r#"{"kind":"plugin","capability":"filesystem","method":"readText","payload":{"path":"../secret.txt"}}"#,
        );

        let result: serde_json::Value = serde_json::from_str(&result).expect("json");
        assert_eq!(result["ok"], false);
        assert_eq!(result["error"], "parent segments are not allowed");
    }

    #[test]
    fn device_and_app_requests_return_real_values() {
        let _guard = test_lock();
        reset_action_state();
        let device =
            dispatch_action_json(r#"{"kind":"plugin","capability":"device","method":"info"}"#);
        let app = dispatch_action_json(r#"{"kind":"plugin","capability":"app","method":"info"}"#);

        let device: serde_json::Value = serde_json::from_str(&device).expect("json");
        let app: serde_json::Value = serde_json::from_str(&app).expect("json");
        assert_eq!(device["ok"], true);
        assert!(device["value"]["value"]["targetOs"].is_string());
        assert_eq!(app["ok"], true);
        assert_eq!(app["value"]["value"]["syncCount"], 0);
    }

    #[test]
    fn start_auto_scan_tracks_last_result() {
        let _guard = test_lock();
        reset_action_state();
        let first_ptr = crepus_mobile_start_auto_scan();
        let first = unsafe { CStr::from_ptr(first_ptr) }.to_string_lossy().into_owned();
        unsafe { crepus_mobile_free_string(first_ptr) };
        let second_ptr = crepus_mobile_start_auto_scan();
        let second = unsafe { CStr::from_ptr(second_ptr) }
            .to_string_lossy()
            .into_owned();
        unsafe { crepus_mobile_free_string(second_ptr) };
        let first: serde_json::Value = serde_json::from_str(&first).expect("json");
        let second: serde_json::Value = serde_json::from_str(&second).expect("json");
        assert_eq!(first["ok"], true);
        assert_eq!(second["ok"], true);
    }

    #[test]
    fn network_status_reports_reachable_loopback_port() {
        let _guard = test_lock();
        let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
        let port = listener.local_addr().expect("addr").port();
        let server = thread::spawn(move || {
            let _ = listener.accept();
        });

        let result = dispatch_action_json(&format!(
            r#"{{"kind":"plugin","capability":"network","method":"status","payload":{{"host":"127.0.0.1","port":{port},"timeoutMs":100}}}}"#
        ));
        server.join().expect("server thread");
        let result: serde_json::Value = serde_json::from_str(&result).expect("json");
        assert_eq!(result["ok"], true);
        assert_eq!(result["value"]["value"]["reachable"], true);
    }

    #[test]
    fn http_get_returns_text_from_loopback_server() {
        let _guard = test_lock();
        let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
        let port = listener.local_addr().expect("addr").port();
        let server = thread::spawn(move || {
            let (mut stream, _) = listener.accept().expect("accept");
            let mut buffer = [0_u8; 1024];
            let _ = stream.read(&mut buffer).expect("read request");
            stream
                .write_all(
                    b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\nContent-Type: text/plain\r\n\r\nhello",
                )
                .expect("write response");
        });

        let result = dispatch_action_json(&format!(
            r#"{{"kind":"plugin","capability":"http","method":"get","payload":{{"url":"http://127.0.0.1:{port}/ping"}}}}"#
        ));
        server.join().expect("server thread");

        let result: serde_json::Value = serde_json::from_str(&result).expect("json");
        assert_eq!(result["ok"], true);
        assert_eq!(result["value"]["value"]["status"], 200);
        assert_eq!(result["value"]["value"]["text"], "hello");
    }

    #[test]
    fn dispatch_action_json_recovers_from_poisoned_state_lock() {
        let _guard = test_lock();
        reset_action_state();
        let _ = std::thread::spawn(|| {
            let _state = action_state().lock().unwrap();
            panic!("poison state lock");
        })
        .join();

        let result = dispatch_action_json("sync");

        let result: serde_json::Value = serde_json::from_str(&result).expect("json");
        assert_eq!(result["ok"], true);
        assert_eq!(result["action"], "sync");
        assert_eq!(result["state"]["syncCount"], 1);
        reset_action_state();
    }

    #[test]
    fn c_abi_json_null_pointer_reports_full_state_shape() {
        let _guard = test_lock();
        reset_action_state();
        let mut output = [0 as c_char; 256];

        let written =
            crepus_mobile_dispatch_json(std::ptr::null(), 0, output.as_mut_ptr(), output.len());

        assert!(written > 0);
        let result = unsafe { CStr::from_ptr(output.as_ptr()) }.to_string_lossy();
        let result: serde_json::Value = serde_json::from_str(&result).expect("json");
        assert_eq!(result["ok"], false);
        assert_eq!(result["error"], "invalid action pointer");
        assert_eq!(result["state"]["syncCount"], 0);
        assert_eq!(result["state"]["previewCount"], 0);
        assert_eq!(result["state"]["lastAction"], "");
        assert!(result["state"].get("lastPayload").is_some());
    }

    #[test]
    fn c_abi_accepts_len_prefixed_action() {
        let _guard = test_lock();
        reset_action_state();
        let action = "sync";
        assert!(crepus_mobile_dispatch(
            action.as_ptr().cast::<c_char>(),
            action.len()
        ));
    }

    #[test]
    fn c_abi_accepts_c_string_action() {
        let _guard = test_lock();
        reset_action_state();
        let action = CString::new("preview").unwrap();
        assert!(crepus_mobile_dispatch(action.as_ptr(), 0));
    }

    #[test]
    fn unknown_action_returns_false() {
        let _guard = test_lock();
        reset_action_state();
        assert!(!dispatch_action("missing"));
    }
}
