use crate::core::dispatch;
use crate::core::message::{Destination, Envelope, Message};
use anyhow::{anyhow, Context, Result};
use dbus::arg::{PropMap, Variant};
use dbus::blocking::stdintf::org_freedesktop_dbus::{ObjectManager, Properties};
use dbus::blocking::Connection;
use dbus::channel::MatchingReceiver;
use dbus::channel::Sender;
use dbus::message::{MatchRule, Message as DbusMessage, MessageType};
use dbus::Path;
use dbus_crossroads::{Crossroads, IfaceBuilder, IfaceToken, MethodErr};
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{self, Receiver, TryRecvError};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};

const BUS_NAME: &str = "io.showen.BleProvisioning";
const BLUEZ_SERVICE: &str = "org.bluez";
const ADAPTER_IFACE: &str = "org.bluez.Adapter1";
const GATT_MANAGER_IFACE: &str = "org.bluez.GattManager1";
const LE_ADVERTISING_MANAGER_IFACE: &str = "org.bluez.LEAdvertisingManager1";
const APP_PATH: &str = "/org/showen/ble";
const SERVICE_PATH: &str = "/org/showen/ble/service0";
const CHAR_SSID_PATH: &str = "/org/showen/ble/service0/char0";
const CHAR_PASSWORD_PATH: &str = "/org/showen/ble/service0/char1";
const CHAR_COMMAND_PATH: &str = "/org/showen/ble/service0/char2";
const CHAR_STATUS_PATH: &str = "/org/showen/ble/service0/char3";
const ADV_PATH: &str = "/org/showen/ble/advertisement0";
const SERVICE_UUID: &str = "12345678-1234-5678-1234-56789abcdef0";
const CHAR_SSID_UUID: &str = "12345678-1234-5678-1234-56789abcdef1";
const CHAR_PASSWORD_UUID: &str = "12345678-1234-5678-1234-56789abcdef2";
const CHAR_COMMAND_UUID: &str = "12345678-1234-5678-1234-56789abcdef3";
const CHAR_STATUS_UUID: &str = "12345678-1234-5678-1234-56789abcdef4";
const SERVER_TIMEOUT: Duration = Duration::from_millis(250);
const PROXY_TIMEOUT: Duration = Duration::from_secs(10);

type ManagedObjects = HashMap<Path<'static>, HashMap<String, PropMap>>;

#[derive(Default)]
struct RegistrationReplies {
    gatt_serial: Option<u32>,
    advertisement_serial: Option<u32>,
    gatt: Option<Result<()>>,
    advertisement: Option<Result<()>>,
}

pub enum BleControl {
    UpdateStatus(String),
}

#[derive(Clone)]
struct SharedState {
    tx: mpsc::Sender<Envelope>,
    ssid: Arc<Mutex<String>>,
    password: Arc<Mutex<String>>,
    status: Arc<Mutex<String>>,
    notifying: Arc<AtomicBool>,
    pending_notify: Arc<AtomicBool>,
}

impl SharedState {
    fn new(tx: mpsc::Sender<Envelope>) -> Self {
        Self {
            tx,
            ssid: Arc::new(Mutex::new(String::new())),
            password: Arc::new(Mutex::new(String::new())),
            status: Arc::new(Mutex::new(r#"{"ok":true,"action":"idle"}"#.to_string())),
            notifying: Arc::new(AtomicBool::new(false)),
            pending_notify: Arc::new(AtomicBool::new(false)),
        }
    }

    fn read_status(&self) -> Vec<u8> {
        self.status.lock().unwrap().as_bytes().to_vec()
    }

    fn read_value(&self, kind: &CharacteristicKind) -> Vec<u8> {
        match kind {
            CharacteristicKind::Status => self.read_status(),
            CharacteristicKind::Ssid
            | CharacteristicKind::Password
            | CharacteristicKind::Command => Vec::new(),
        }
    }

    fn set_ssid(&self, value: &[u8]) {
        *self.ssid.lock().unwrap() = bytes_to_string(value);
    }

    fn set_password(&self, value: &[u8]) {
        *self.password.lock().unwrap() = bytes_to_string(value);
    }

    fn set_status(&self, status: impl Into<String>) {
        *self.status.lock().unwrap() = status.into();
        self.pending_notify.store(true, Ordering::SeqCst);
    }

    fn set_notifying(&self, notifying: bool) {
        self.notifying.store(notifying, Ordering::SeqCst);
        if notifying {
            self.pending_notify.store(true, Ordering::SeqCst);
        }
    }

    fn is_notifying(&self) -> bool {
        self.notifying.load(Ordering::SeqCst)
    }

    fn take_pending_notification(&self) -> bool {
        self.pending_notify.swap(false, Ordering::SeqCst)
    }

    fn dispatch_command(&self, raw: &[u8]) -> Result<()> {
        let command = bytes_to_string(raw);
        let ssid = self.ssid.lock().unwrap().clone();
        let password = self.password.lock().unwrap().clone();

        match dispatch::parse_command(&command, "ble", &ssid, &password) {
            Ok(result) => {
                self.tx
                    .send(result.envelope)
                    .context("failed to send command from BLE")?;
                self.set_status(format!(
                    r#"{{"ok":true,"action":"{}","state":"queued"}}"#,
                    command
                ));
                Ok(())
            }
            Err(error) => {
                self.set_status(format!(
                    r#"{{"ok":false,"action":"{}","error":"{}"}}"#,
                    command,
                    error.replace('"', "\\\"")
                ));
                Err(anyhow!("BLE command error: {}", error))
            }
        }
    }
}

struct AppData;

struct GattServiceData {
    uuid: String,
    primary: bool,
    characteristics: Vec<Path<'static>>,
}

enum CharacteristicKind {
    Ssid,
    Password,
    Command,
    Status,
}

struct GattCharacteristicData {
    uuid: String,
    service: Path<'static>,
    flags: Vec<String>,
    kind: CharacteristicKind,
    shared: SharedState,
}

struct AdvertisementData {
    advertisement_type: String,
    service_uuids: Vec<String>,
    local_name: String,
    includes: Vec<String>,
}

pub fn run_ble_service(
    device_name: String,
    tx: mpsc::Sender<Envelope>,
    control_rx: &Receiver<BleControl>,
    stop: &Arc<AtomicBool>,
) -> Result<()> {
    let shared = SharedState::new(tx.clone());

    eprintln!("[BLE] connecting to system bus...");
    let conn = Connection::new_system().context("failed to connect to system bus for BLE")?;
    conn.request_name(BUS_NAME, false, true, false)
        .context("failed to request BLE D-Bus name")?;
    eprintln!("[BLE] D-Bus name acquired");

    // 构建 Crossroads 并注册所有 GATT/Advertisement objects
    let mut cr = Crossroads::new();
    let object_manager = register_object_manager_iface(&mut cr);
    let service_iface = register_service_iface(&mut cr);
    let characteristic_iface = register_characteristic_iface(&mut cr);
    let advertisement_iface = register_advertisement_iface(&mut cr);

    cr.insert(APP_PATH, &[object_manager], AppData);
    cr.insert(
        SERVICE_PATH,
        &[service_iface],
        GattServiceData {
            uuid: SERVICE_UUID.to_string(),
            primary: true,
            characteristics: vec![
                Path::from(CHAR_SSID_PATH.to_string()),
                Path::from(CHAR_PASSWORD_PATH.to_string()),
                Path::from(CHAR_COMMAND_PATH.to_string()),
                Path::from(CHAR_STATUS_PATH.to_string()),
            ],
        },
    );
    cr.insert(
        Path::from(CHAR_SSID_PATH.to_string()),
        &[characteristic_iface],
        GattCharacteristicData {
            uuid: CHAR_SSID_UUID.to_string(),
            service: Path::from(SERVICE_PATH.to_string()),
            flags: vec!["write".to_string()],
            kind: CharacteristicKind::Ssid,
            shared: shared.clone(),
        },
    );
    cr.insert(
        Path::from(CHAR_PASSWORD_PATH.to_string()),
        &[characteristic_iface],
        GattCharacteristicData {
            uuid: CHAR_PASSWORD_UUID.to_string(),
            service: Path::from(SERVICE_PATH.to_string()),
            flags: vec!["write".to_string()],
            kind: CharacteristicKind::Password,
            shared: shared.clone(),
        },
    );
    cr.insert(
        Path::from(CHAR_COMMAND_PATH.to_string()),
        &[characteristic_iface],
        GattCharacteristicData {
            uuid: CHAR_COMMAND_UUID.to_string(),
            service: Path::from(SERVICE_PATH.to_string()),
            flags: vec!["write".to_string()],
            kind: CharacteristicKind::Command,
            shared: shared.clone(),
        },
    );
    cr.insert(
        Path::from(CHAR_STATUS_PATH.to_string()),
        &[characteristic_iface],
        GattCharacteristicData {
            uuid: CHAR_STATUS_UUID.to_string(),
            service: Path::from(SERVICE_PATH.to_string()),
            flags: vec!["read".to_string(), "notify".to_string()],
            kind: CharacteristicKind::Status,
            shared: shared.clone(),
        },
    );
    // BlueZ LEAdvertisement1 约束（5.55+）：
    // Includes 里若含 "local-name"，就禁止再设 LocalName 属性，否则
    // parse_local_name 报 "Local name already included" →
    // "Failed to parse advertisement"。
    // 我们需要自定义设备名，因此只用 LocalName，Includes 仅保留 tx-power。
    cr.insert(
        ADV_PATH,
        &[advertisement_iface],
        AdvertisementData {
            advertisement_type: "peripheral".to_string(),
            service_uuids: vec![SERVICE_UUID.to_string()],
            local_name: device_name.clone(),
            includes: vec!["tx-power".to_string()],
        },
    );

    // 注册 Crossroads 消息处理（必须在 RegisterApplication 之前，
    // 因为 BlueZ 会在注册过程中回调 GetManagedObjects）
    let shared_cr = Arc::new(Mutex::new(cr));
    let cr_for_handler = Arc::clone(&shared_cr);
    conn.start_receive(
        MatchRule::new_method_call(),
        Box::new(move |msg, conn| {
            if cr_for_handler
                .lock()
                .unwrap()
                .handle_message(msg, conn)
                .is_err()
            {
                eprintln!("[BLE] crossroads dispatch error");
            }
            true
        }),
    );

    let registration_replies = Arc::new(Mutex::new(RegistrationReplies::default()));
    let replies_for_success = Arc::clone(&registration_replies);
    let mut success_rule = MatchRule::new();
    success_rule.msg_type = Some(MessageType::MethodReturn);
    conn.start_receive(
        success_rule,
        Box::new(move |msg, _conn| {
            record_registration_reply(&replies_for_success, &msg, Ok(()));
            true
        }),
    );

    let replies_for_error = Arc::clone(&registration_replies);
    let mut error_rule = MatchRule::new();
    error_rule.msg_type = Some(MessageType::Error);
    conn.start_receive(
        error_rule,
        Box::new(move |msg, _conn| {
            let error_message = msg.get1::<String>().unwrap_or_default();
            let error = if error_message.is_empty() {
                anyhow!("{msg:?}")
            } else {
                anyhow!("{error_message}")
            };
            record_registration_reply(&replies_for_error, &msg, Err(error));
            true
        }),
    );

    // 配置 adapter
    let adapter_path = find_adapter(&conn)?;
    configure_adapter(&conn, &adapter_path, &device_name)?;

    // 先尝试清理上一次进程残留的注册（防止崩溃后 BlueZ 状态残留）
    let _ = unregister_ble_objects(&conn, &adapter_path);
    // 给 BlueZ 一点时间释放残留 GATT/Adv 实例
    for _ in 0..5 {
        if stop.load(Ordering::SeqCst) {
            return Ok(());
        }
        conn.process(Duration::from_millis(50)).ok();
    }

    // 串行注册：先 GATT，再 Advertisement。并行时若 Adv 成功、GATT 失败，
    // 残留 ActiveInstances 会导致后续 "Failed to create entry in database"。
    let gatt_serial = send_register_gatt_app(&conn, &adapter_path)?;
    if let Ok(mut replies) = registration_replies.lock() {
        replies.gatt_serial = Some(gatt_serial);
    }
    eprintln!("[BLE] RegisterApplication sent, waiting...");
    if let Err(error) =
        wait_for_one_registration_reply(&conn, &registration_replies, "gatt", Duration::from_secs(10))
    {
        let _ = unregister_ble_objects(&conn, &adapter_path);
        return Err(error);
    }

    let ad_serial = send_register_advertisement(&conn, &adapter_path)?;
    if let Ok(mut replies) = registration_replies.lock() {
        replies.advertisement_serial = Some(ad_serial);
    }
    eprintln!("[BLE] RegisterAdvertisement sent, waiting...");
    if let Err(error) = wait_for_one_registration_reply(
        &conn,
        &registration_replies,
        "advertisement",
        Duration::from_secs(10),
    ) {
        let _ = unregister_ble_objects(&conn, &adapter_path);
        return Err(error);
    }

    eprintln!("[BLE] GATT application and advertisement registered");

    tx.send(Envelope {
        from: "ble".to_string(),
        to: Destination::Manager,
        message: Message::PluginReady("ble".to_string()),
    })
    .context("failed to report BLE plugin readiness")?;

    eprintln!("[BLE] ready, entering main loop");

    // 主循环：处理 D-Bus 消息 + control 消息
    while !stop.load(Ordering::SeqCst) {
        if shared.is_notifying() && shared.take_pending_notification() {
            emit_status_notification(&conn, &shared)?;
        }
        drain_control_messages(&shared, &control_rx)?;
        conn.process(SERVER_TIMEOUT)
            .context("BLE connection process loop failed")?;
    }

    drain_control_messages(&shared, &control_rx)?;

    // 清理
    unregister_ble_objects(&conn, &adapter_path)?;

    Ok(())
}

fn register_object_manager_iface(cr: &mut Crossroads) -> IfaceToken<AppData> {
    cr.register(
        "org.freedesktop.DBus.ObjectManager",
        |b: &mut IfaceBuilder<AppData>| {
            b.method("GetManagedObjects", (), ("objects",), |_, _, ()| {
                Ok((build_managed_objects(),))
            });
        },
    )
}

fn register_service_iface(cr: &mut Crossroads) -> dbus_crossroads::IfaceToken<GattServiceData> {
    cr.register(
        "org.bluez.GattService1",
        |b: &mut IfaceBuilder<GattServiceData>| {
            b.property::<String, _>("UUID")
                .get(|_, data| Ok(data.uuid.clone()));
            b.property::<bool, _>("Primary")
                .get(|_, data| Ok(data.primary));
            b.property::<Vec<Path<'static>>, _>("Characteristics")
                .get(|_, data| Ok(data.characteristics.clone()));
        },
    )
}

fn register_characteristic_iface(
    cr: &mut Crossroads,
) -> dbus_crossroads::IfaceToken<GattCharacteristicData> {
    cr.register(
        "org.bluez.GattCharacteristic1",
        |b: &mut IfaceBuilder<GattCharacteristicData>| {
            b.property::<String, _>("UUID")
                .get(|_, data| Ok(data.uuid.clone()));
            b.property::<Path<'static>, _>("Service")
                .get(|_, data| Ok(data.service.clone()));
            b.property::<Vec<String>, _>("Flags")
                .get(|_, data| Ok(data.flags.clone()));
            b.property::<Vec<Path<'static>>, _>("Descriptors")
                .get(|_, _| Ok(Vec::new()));
            b.property::<Vec<u8>, _>("Value")
                .get(|_, data| Ok(data.shared.read_value(&data.kind)));
            b.property::<bool, _>("Notifying").get(|_, data| {
                Ok(matches!(data.kind, CharacteristicKind::Status) && data.shared.is_notifying())
            });

            b.method(
                "ReadValue",
                ("options",),
                ("value",),
                |_, data, (_options,): (PropMap,)| {
                    let value = data.shared.read_value(&data.kind);
                    Ok((value,))
                },
            );

            b.method(
                "WriteValue",
                ("value", "options"),
                (),
                |_, data, (value, _options): (Vec<u8>, PropMap)| -> Result<(), MethodErr> {
                    match data.kind {
                        CharacteristicKind::Ssid => data.shared.set_ssid(&value),
                        CharacteristicKind::Password => data.shared.set_password(&value),
                        CharacteristicKind::Command => data
                            .shared
                            .dispatch_command(&value)
                            .map_err(|e| MethodErr::failed(&e.to_string()))?,
                        CharacteristicKind::Status => {
                            return Err(MethodErr::failed("status characteristic is read-only"));
                        }
                    }
                    Ok(())
                },
            );

            b.method("StartNotify", (), (), |_, data, ()| match data.kind {
                CharacteristicKind::Status => {
                    data.shared.set_notifying(true);
                    Ok(())
                }
                _ => Err(MethodErr::failed(
                    "notify is only supported on status characteristic",
                )),
            });
            b.method("StopNotify", (), (), |_, data, ()| match data.kind {
                CharacteristicKind::Status => {
                    data.shared.set_notifying(false);
                    Ok(())
                }
                _ => Err(MethodErr::failed(
                    "notify is only supported on status characteristic",
                )),
            });
        },
    )
}

fn register_advertisement_iface(
    cr: &mut Crossroads,
) -> dbus_crossroads::IfaceToken<AdvertisementData> {
    cr.register(
        "org.bluez.LEAdvertisement1",
        |b: &mut IfaceBuilder<AdvertisementData>| {
            b.property::<String, _>("Type")
                .get(|_, data| Ok(data.advertisement_type.clone()));
            b.property::<Vec<String>, _>("ServiceUUIDs")
                .get(|_, data| Ok(data.service_uuids.clone()));
            b.property::<String, _>("LocalName")
                .get(|_, data| Ok(data.local_name.clone()));
            b.property::<Vec<String>, _>("Includes")
                .get(|_, data| Ok(data.includes.clone()));
            b.method("Release", (), (), |_, _, ()| Ok(()));
        },
    )
}

fn build_managed_objects() -> ManagedObjects {
    let mut objects = ManagedObjects::new();

    let mut service_props = PropMap::new();
    service_props.insert("UUID".into(), Variant(Box::new(SERVICE_UUID.to_string())));
    service_props.insert("Primary".into(), Variant(Box::new(true)));
    service_props.insert(
        "Characteristics".into(),
        Variant(Box::new(vec![
            Path::from(CHAR_SSID_PATH),
            Path::from(CHAR_PASSWORD_PATH),
            Path::from(CHAR_COMMAND_PATH),
            Path::from(CHAR_STATUS_PATH),
        ])),
    );

    let mut service_ifaces = HashMap::new();
    service_ifaces.insert("org.bluez.GattService1".to_string(), service_props);
    objects.insert(Path::from(SERVICE_PATH), service_ifaces);

    for (path, uuid, flags) in [
        (CHAR_SSID_PATH, CHAR_SSID_UUID, vec!["write".to_string()]),
        (
            CHAR_PASSWORD_PATH,
            CHAR_PASSWORD_UUID,
            vec!["write".to_string()],
        ),
        (
            CHAR_COMMAND_PATH,
            CHAR_COMMAND_UUID,
            vec!["write".to_string()],
        ),
        (
            CHAR_STATUS_PATH,
            CHAR_STATUS_UUID,
            vec!["read".to_string(), "notify".to_string()],
        ),
    ] {
        let mut props = PropMap::new();
        props.insert(
            "Service".into(),
            Variant(Box::new(Path::from(SERVICE_PATH))),
        );
        props.insert("UUID".into(), Variant(Box::new(uuid.to_string())));
        let value = if path == CHAR_STATUS_PATH {
            r#"{"ok":true,"action":"idle"}"#.as_bytes().to_vec()
        } else {
            Vec::new()
        };
        props.insert("Flags".into(), Variant(Box::new(flags)));
        props.insert("Value".into(), Variant(Box::new(value)));
        props.insert("Notifying".into(), Variant(Box::new(false)));
        props.insert(
            "Descriptors".into(),
            Variant(Box::new(Vec::<Path<'static>>::new())),
        );

        let mut ifaces = HashMap::new();
        ifaces.insert("org.bluez.GattCharacteristic1".to_string(), props);
        objects.insert(Path::from(path), ifaces);
    }

    objects
}

fn record_registration_reply(
    replies: &Arc<Mutex<RegistrationReplies>>,
    msg: &DbusMessage,
    result: Result<()>,
) {
    let Some(reply_serial) = msg.get_reply_serial() else {
        return;
    };

    if let Ok(mut replies) = replies.lock() {
        match replies_for_serial(&mut replies, reply_serial) {
            Some(slot) if slot.is_none() => *slot = Some(result),
            _ => {}
        }
    }
}

fn replies_for_serial(
    replies: &mut RegistrationReplies,
    reply_serial: u32,
) -> Option<&mut Option<Result<()>>> {
    if replies.gatt_serial == Some(reply_serial) {
        Some(&mut replies.gatt)
    } else if replies.advertisement_serial == Some(reply_serial) {
        Some(&mut replies.advertisement)
    } else {
        None
    }
}

fn wait_for_one_registration_reply(
    conn: &Connection,
    replies: &Arc<Mutex<RegistrationReplies>>,
    which: &str,
    timeout: Duration,
) -> Result<()> {
    let deadline = Instant::now() + timeout;
    let op = match which {
        "gatt" => "RegisterApplication",
        "advertisement" => "RegisterAdvertisement",
        other => other,
    };

    loop {
        if let Ok(mut replies) = replies.lock() {
            let slot = match which {
                "gatt" => &mut replies.gatt,
                "advertisement" => &mut replies.advertisement,
                _ => return Err(anyhow!("unknown BLE registration target: {which}")),
            };
            if let Some(()) = reply_status(slot, op)? {
                return Ok(());
            }
        }

        let now = Instant::now();
        if now >= deadline {
            return Err(anyhow!("timed out waiting for BLE {op} reply"));
        }

        conn.process(Duration::from_millis(100).min(deadline.saturating_duration_since(now)))
            .context("BLE connection process failed during registration")?;
    }
}

fn reply_status(reply: &mut Option<Result<()>>, operation: &str) -> Result<Option<()>> {
    match reply {
        Some(Ok(())) => Ok(Some(())),
        Some(Err(error)) => Err(anyhow!("{operation} failed: {error}")),
        None => Ok(None),
    }
}

fn send_register_gatt_app(conn: &Connection, adapter_path: &str) -> Result<u32> {
    let msg = dbus::Message::method_call(
        &BLUEZ_SERVICE.into(),
        &Path::from(adapter_path.to_string()),
        &GATT_MANAGER_IFACE.into(),
        &"RegisterApplication".into(),
    )
    .append2(Path::from(APP_PATH), PropMap::new());

    conn.send(msg)
        .map_err(|_| anyhow!("failed to send RegisterApplication"))
}

fn send_register_advertisement(conn: &Connection, adapter_path: &str) -> Result<u32> {
    let msg = dbus::Message::method_call(
        &BLUEZ_SERVICE.into(),
        &Path::from(adapter_path.to_string()),
        &LE_ADVERTISING_MANAGER_IFACE.into(),
        &"RegisterAdvertisement".into(),
    )
    .append2(Path::from(ADV_PATH), PropMap::new());

    conn.send(msg)
        .map_err(|_| anyhow!("failed to send RegisterAdvertisement"))
}

fn find_adapter(conn: &Connection) -> Result<String> {
    let proxy = conn.with_proxy(BLUEZ_SERVICE, "/", PROXY_TIMEOUT);
    let objects = proxy
        .get_managed_objects()
        .context("failed to enumerate BlueZ managed objects")?;

    for (path, interfaces) in objects {
        if interfaces.contains_key(GATT_MANAGER_IFACE)
            && interfaces.contains_key(LE_ADVERTISING_MANAGER_IFACE)
        {
            return Ok(path.to_string());
        }
    }

    Err(anyhow!(
        "BLE adapter with GATT and advertising support not found"
    ))
}

fn configure_adapter(conn: &Connection, adapter_path: &str, device_name: &str) -> Result<()> {
    let adapter = conn.with_proxy(BLUEZ_SERVICE, adapter_path, PROXY_TIMEOUT);
    adapter
        .set(ADAPTER_IFACE, "Powered", true)
        .context("failed to power on BLE adapter")?;
    adapter
        .set(ADAPTER_IFACE, "Alias", device_name.to_string())
        .context("failed to set BLE adapter alias")?;
    adapter
        .set(ADAPTER_IFACE, "Discoverable", true)
        .context("failed to set BLE adapter discoverable")?;
    // DiscoverableTimeout=0 表示永久可发现
    adapter
        .set(ADAPTER_IFACE, "DiscoverableTimeout", 0u32)
        .context("failed to set DiscoverableTimeout")?;
    adapter
        .set(ADAPTER_IFACE, "Pairable", true)
        .context("failed to set BLE adapter pairable")?;
    // PairableTimeout=0 表示永久可配对
    adapter
        .set(ADAPTER_IFACE, "PairableTimeout", 0u32)
        .context("failed to set PairableTimeout")?;
    Ok(())
}

fn unregister_ble_objects(conn: &Connection, adapter_path: &str) -> Result<()> {
    let adv_manager = conn.with_proxy(BLUEZ_SERVICE, adapter_path, PROXY_TIMEOUT);
    let _ = adv_manager.method_call::<(), _, _, _>(
        LE_ADVERTISING_MANAGER_IFACE,
        "UnregisterAdvertisement",
        (Path::from(ADV_PATH.to_string()),),
    );

    let gatt_manager = conn.with_proxy(BLUEZ_SERVICE, adapter_path, PROXY_TIMEOUT);
    let _ = gatt_manager.method_call::<(), _, _, _>(
        GATT_MANAGER_IFACE,
        "UnregisterApplication",
        (Path::from(APP_PATH.to_string()),),
    );

    Ok(())
}

fn bytes_to_string(value: &[u8]) -> String {
    String::from_utf8_lossy(value)
        .trim_matches(|c: char| c == '\0' || c.is_whitespace())
        .to_string()
}

fn drain_control_messages(shared: &SharedState, control_rx: &Receiver<BleControl>) -> Result<()> {
    loop {
        match control_rx.try_recv() {
            Ok(BleControl::UpdateStatus(payload)) => shared.set_status(payload),
            Err(TryRecvError::Empty) => return Ok(()),
            Err(TryRecvError::Disconnected) => return Ok(()),
        }
    }
}

fn emit_status_notification(conn: &Connection, shared: &SharedState) -> Result<()> {
    let mut changed = PropMap::new();
    changed.insert("Value".into(), Variant(Box::new(shared.read_status())));
    changed.insert("Notifying".into(), Variant(Box::new(shared.is_notifying())));

    let signal = dbus::Message::signal(
        &Path::from(CHAR_STATUS_PATH),
        &"org.freedesktop.DBus.Properties".into(),
        &"PropertiesChanged".into(),
    )
    .append3(
        "org.bluez.GattCharacteristic1".to_string(),
        changed,
        Vec::<String>::new(),
    );

    conn.send(signal)
        .map_err(|_| anyhow!("failed to emit BLE status notification"))?;

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::message::WifiCommand;

    #[test]
    fn bytes_to_string_trims_nulls_and_surrounding_whitespace() {
        assert_eq!(bytes_to_string(b"  demo-ssid\0\0  "), "demo-ssid");
    }

    #[test]
    fn dispatch_command_uses_cached_wifi_credentials() {
        let (tx, rx) = mpsc::channel();
        let shared = SharedState::new(tx);
        shared.set_ssid(br#"Cafe \"Guest\""#);
        shared.set_password(br#"pa\\ss word"#);

        shared
            .dispatch_command(b"connect")
            .expect("connect command should dispatch");

        let envelope = rx.recv().expect("command should be forwarded to core");
        match envelope.message {
            Message::WifiCommand(WifiCommand::Connect { ssid, password }) => {
                assert_eq!(ssid, r#"Cafe \"Guest\""#);
                assert_eq!(password, r#"pa\\ss word"#);
            }
            other => panic!("unexpected forwarded BLE command: {:?}", other),
        }

        assert_eq!(
            String::from_utf8(shared.read_status()).expect("status should be utf8"),
            r#"{"ok":true,"action":"connect","state":"queued"}"#
        );
    }

    #[test]
    fn dispatch_command_reports_invalid_command_in_status() {
        let (tx, _rx) = mpsc::channel();
        let shared = SharedState::new(tx);

        let error = shared
            .dispatch_command(b"bad-command")
            .expect_err("invalid command should fail");

        assert!(error
            .to_string()
            .contains("unsupported command: bad-command"));

        let status = String::from_utf8(shared.read_status()).expect("status should be utf8");
        assert!(status.contains(r#""ok":false"#));
        assert!(status.contains(r#""action":"bad-command""#));
        assert!(status.contains(r#"unsupported command: bad-command"#));
    }

    #[test]
    fn drain_control_messages_updates_status_without_dbus() {
        let (tx, _rx) = mpsc::channel();
        let shared = SharedState::new(tx);
        let (control_tx, control_rx) = mpsc::channel();

        control_tx
            .send(BleControl::UpdateStatus(
                r#"{"ok":true,"action":"status"}"#.to_string(),
            ))
            .expect("status update should send");

        drain_control_messages(&shared, &control_rx).expect("control queue should drain");

        assert_eq!(
            String::from_utf8(shared.read_status()).expect("status should be utf8"),
            r#"{"ok":true,"action":"status"}"#
        );
        assert!(shared.take_pending_notification());
    }
}
