
/*
 * Copyright (c) 2026 Nordic Semiconductor ASA
 *
 * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
 *
 * Behaviour:
 *   - First connection from any phone  → passkey shown, bond stored
 *   - Subsequent connections from same phone → auto-reconnect, no passkey
 *   - Multiple phones each bond once and reconnect silently thereafter
 *   - Each advertising window: connect → send packet → disconnect
 *   - New/unknown phone while bonded phones exist → pairs with passkey,
 *     adds its bond alongside the existing ones (up to CONFIG_BT_MAX_PAIRED)
 */

#include "ble_gatt.h"
#include "app_threads.h"
#include "user_app.h"
#include "user_led.h"
#include "main.h"
#include "user_util.h"
#include "fota.h"
#include "npm1300.h"

#include <zephyr/sys/util.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/gatt.h>
#include <zephyr/bluetooth/hci.h>
#include <zephyr/bluetooth/uuid.h>
#include <zephyr/settings/settings.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/logging/log.h>
#include <zephyr/device.h>
#include <zephyr/kernel.h>
#include <zephyr/kvss/zms.h>

LOG_MODULE_REGISTER(ble_gatt, LOG_LEVEL_INF);

/*
 * Maximum time (ms) to wait for encryption + CCCD after connection.
 * A bonded phone re-encrypts in < 1 s; a fresh pairing takes up to
 * ~10 s depending on the phone.  15 s covers both cases.
 */
#define BLE_SECURITY_TIMEOUT_MS  15000U

/* ------------------------------------------------------------------ */
/* Private state                                                        */
/* ------------------------------------------------------------------ */

static char   ble_rx_buffer[BLE_RX_BUFFER_SIZE];
static size_t ble_rx_len;

static uint32_t        ble_passkey = BLE_DEFAULT_PASSKEY;
static struct bt_conn *auth_conn;
static bool            passkey_requested;

static bool ble_timeout_active;
bool        ble_connected;

static bool                    power_on_ble_sequence_active;
static struct k_work_delayable power_on_ble_retry_work;
static void power_on_ble_retry_handler(struct k_work *work);

static struct k_work_delayable ble_disconnect_work;
static struct k_work_delayable ble_adv_timeout_work;
static struct k_work_delayable ble_connected_work;

static ble_ready_callback_t ble_ready_callback;
static bool                 ble_sequence_active;
static uint8_t              ble_argument;

static bool ble_available;
static K_SEM_DEFINE(ble_availability_sem, 0, 1);

static ble_data_received_cb_t user_data_received_callback;
static bool                   ccc_enabled;
static volatile bool          ble_secure;
static struct bt_conn        *current_conn;

/* Elapsed ms inside the ready-state poller */
static uint32_t security_wait_ms;

/*
 * FOTA mode (see fota.c).  While set: connectable advertising with the SMP
 * UUID is kept running, and the short advertising / ready-state / post-data
 * disconnect timers below are suspended so a slow image upload survives.
 */
static volatile bool           fota_mode;
static struct k_work_delayable fota_adv_work;
static struct k_work_delayable deferred_seq_work;
static ble_ready_callback_t    deferred_seq_cb;
static uint8_t                 deferred_seq_arg;
static bool                    deferred_seq_valid;
static void fota_adv_work_handler(struct k_work *work);
static void deferred_seq_work_handler(struct k_work *work);

/* ------------------------------------------------------------------ */
/* Advertising data                                                     */
/* ------------------------------------------------------------------ */

const struct bt_data ad[] = {
	BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
	BT_DATA_BYTES(BT_DATA_UUID16_ALL, BT_UUID_16_ENCODE(BT_UUID_BAS_VAL)),
};

const struct bt_data sd[] = {
	BT_DATA(BT_DATA_NAME_COMPLETE,
		CONFIG_BT_DEVICE_NAME,
		sizeof(CONFIG_BT_DEVICE_NAME) - 1),
};

/* ------------------------------------------------------------------ */
/* GATT service                                                         */
/* ------------------------------------------------------------------ */

#define NEUTON_SERVICE_UUID \
	BT_UUID_128_ENCODE(0xa5d4f351, 0x9d11, 0x419f, 0x9f1b, 0x3dcdf0a15f4d)

#define NEUTON_OUT_CHARACTERISTIC_UUID \
	BT_UUID_128_ENCODE(0x516a51c4, 0xb1e1, 0x47fa, 0x8327, 0x8acaeb3399eb)

#define NEUTON_IN_CHARACTERISTIC_UUID \
	BT_UUID_128_ENCODE(0x516a51c4, 0xb1e1, 0x47fa, 0x8327, 0x8acaeb3399ec)

#define BT_UUID_NEUTON_SERVICE  BT_UUID_DECLARE_128(NEUTON_SERVICE_UUID)
#define BT_UUID_NEUTON_CHAR_OUT BT_UUID_DECLARE_128(NEUTON_OUT_CHARACTERISTIC_UUID)
#define BT_UUID_NEUTON_CHAR_IN  BT_UUID_DECLARE_128(NEUTON_IN_CHARACTERISTIC_UUID)

static ssize_t on_data_received(struct bt_conn *conn,
				const struct bt_gatt_attr *attr,
				const void *buf,
				uint16_t len,
				uint16_t offset,
				uint8_t flags);

static void on_cccd_changed(const struct bt_gatt_attr *attr, uint16_t value)
{
	ARG_UNUSED(attr);

	LOG_INF("CCCD changed: value=0x%04x connected=%d conn=%p",
		value, ble_connected, current_conn);

	ccc_enabled = (value == BT_GATT_CCC_NOTIFY);
	LOG_INF("Notifications %s", ccc_enabled ? "ENABLED" : "DISABLED");
}

BT_GATT_SERVICE_DEFINE(neuton_gatt,
	BT_GATT_PRIMARY_SERVICE(BT_UUID_NEUTON_SERVICE),
	BT_GATT_CHARACTERISTIC(BT_UUID_NEUTON_CHAR_OUT,
		BT_GATT_CHRC_NOTIFY,
		BT_GATT_PERM_NONE,
		NULL, NULL, NULL),
	BT_GATT_CCC(on_cccd_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
	BT_GATT_CHARACTERISTIC(BT_UUID_NEUTON_CHAR_IN,
		BT_GATT_CHRC_WRITE_WITHOUT_RESP,
		BT_GATT_PERM_WRITE,
		NULL, on_data_received, NULL)
);

/* ------------------------------------------------------------------ */
/* Connection callbacks                                                 */
/* ------------------------------------------------------------------ */

static void connected(struct bt_conn *conn, uint8_t err)
{
	char addr[BT_ADDR_LE_STR_LEN];

	bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));

	if (err) {
		LOG_ERR("Failed to connect to %s (%u)", addr, err);
		return;
	}

	LOG_INF("BLE CONNECTED: %s", addr);

	if (current_conn != NULL) {
		bt_conn_unref(current_conn);
		current_conn = NULL;
	}

	current_conn     = bt_conn_ref(conn);
	ble_connected    = true;
	ble_secure       = false;
	ccc_enabled      = false;
	security_wait_ms = 0;

	ble_common_set_connected(true);

	/*
	 * Request encryption.
	 *
	 * Bonded phone  → stack restores the LTK and re-encrypts
	 *                 automatically; security_changed() fires with
	 *                 the restored level (usually L2 or L4).
	 *
	 * Unknown phone → stack starts SMP pairing; auth_passkey_display()
	 *                 fires and we log the passkey; phone prompts user.
	 *
	 * We use BT_SECURITY_L2 (encrypt, no forced MITM) so that both
	 * "Just Works" and passkey-entry phones succeed without err=2.
	 */
	int sec_err = bt_conn_set_security(conn, BT_SECURITY_L2);

	if (sec_err && sec_err != -EALREADY) {
		LOG_ERR("bt_conn_set_security L2 failed: %d", sec_err);
	}

	/* Stop advertising window timer — connection has arrived */
	k_work_cancel_delayable(&ble_adv_timeout_work);

	/*
	 * Start the ready-state poller.
	 * It checks every 100 ms for encryption + CCCD, with a
	 * BLE_SECURITY_TIMEOUT_MS ceiling.
	 */
	k_work_cancel_delayable(&ble_connected_work);
	if (!fota_mode) {
		k_work_schedule_for_queue(&ble_workq, &ble_connected_work,
					  K_MSEC(BLE_CONNECTED_DELAY_MS));
	}

	ble_available = true;
	k_sem_give(&ble_availability_sem);

	LOG_INF("Waiting for encryption + CCCD (timeout %u ms)",
		BLE_SECURITY_TIMEOUT_MS);
}

static void disconnected(struct bt_conn *conn, uint8_t reason)
{
	char addr[BT_ADDR_LE_STR_LEN];

	bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
	LOG_INF("Disconnected from %s (reason 0x%02x)", addr, reason);

	ccc_enabled   = false;
	ble_connected = false;
	ble_secure    = false;

	k_work_cancel_delayable(&ble_connected_work);
	k_work_cancel_delayable(&ble_disconnect_work);

	if (current_conn) {
		bt_conn_unref(current_conn);
		current_conn = NULL;
	}

	ble_common_set_connected(false);
	ble_available = false;

	/*
	 * Only clear the bond when authentication actually failed
	 * (wrong passkey, key mismatch after deliberate unpairing on
	 * the phone side).  Normal disconnects keep the bond so the
	 * phone can reconnect silently next time.
	 */
	if (reason == BT_HCI_ERR_AUTH_FAIL ||
	    reason == BT_HCI_ERR_PIN_OR_KEY_MISSING) {
		LOG_INF("Auth failure — removing bond for this peer");
		bt_unpair(BT_ID_DEFAULT, bt_conn_get_dst(conn));
	}

	if (fota_mode) {
		/* phone dropped (or is resetting us): keep the FOTA window advertising */
		k_work_reschedule_for_queue(&ble_workq, &fota_adv_work, K_MSEC(300));
	} else if (deferred_seq_valid) {
		/* a BLE sequence preempted FOTA while a link was up: run it now */
		k_work_reschedule_for_queue(&ble_workq, &deferred_seq_work, K_MSEC(200));
	}
}

static void security_changed(struct bt_conn *conn,
			     bt_security_t level,
			     enum bt_security_err err)
{
	char addr[BT_ADDR_LE_STR_LEN];

	bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));

	if (err) {
		/*
		 * Common causes:
		 *   err=2 (AUTH_REQUIREMENT) — phone tried to reconnect with
		 *          a bond we no longer have (e.g. after a firmware
		 *          flash that wiped settings).
		 *   err=5 (UNSPECIFIED)      — phone rejected the pairing.
		 *
		 * In both cases remove the stale local bond (if any) and
		 * let the ready-state poller time out, which will then
		 * disconnect and let the user try again.  The next
		 * advertising window will have no bond on the nRF side,
		 * so the phone will be asked to pair fresh.
		 */
		LOG_WRN("Security failed for %s: level %u err %d",
			addr, level, err);

		bt_unpair(BT_ID_DEFAULT, bt_conn_get_dst(conn));
		ble_secure = false;

		/*
		 * Cancel the poller so it does not keep waiting — the
		 * timeout ceiling would handle it eventually, but
		 * cancelling here gives a faster disconnect.
		 * The security_wait_ms ceiling check in the poller fires
		 * on the next reschedule; by setting it to max we make it
		 * disconnect on the very next tick.
		 */
		security_wait_ms = BLE_SECURITY_TIMEOUT_MS;
		return;
	}

	LOG_INF("Security changed for %s: level %u", addr, level);

	if (level >= BT_SECURITY_L2) {
		ble_secure = true;
		LOG_INF("BLE link encrypted (level %u)", level);
	}
}

BT_CONN_CB_DEFINE(conn_callbacks) = {
	.connected        = connected,
	.disconnected     = disconnected,
	.security_changed = security_changed,
};

/* ------------------------------------------------------------------ */
/* Authentication callbacks                                             */
/* ------------------------------------------------------------------ */

/*
 * Called only for NEW (unbonded) phones during the passkey-entry phase.
 * Bonded phones never trigger this — they re-encrypt with the stored LTK.
 */
static void auth_passkey_display(struct bt_conn *conn, unsigned int passkey)
{
	char addr[BT_ADDR_LE_STR_LEN];

	bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
	LOG_INF("*** PASSKEY for %s: %06u ***", addr, passkey);
	LOG_INF("Enter this code on the phone to complete pairing");
}

static void auth_cancel(struct bt_conn *conn)
{
	ARG_UNUSED(conn);
	LOG_INF("Bluetooth authentication cancelled by phone");
}

static void pairing_complete(struct bt_conn *conn, bool bonded)
{
	char addr[BT_ADDR_LE_STR_LEN];

	bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
	LOG_INF("Pairing complete: %s bonded=%d "
		"(phone will connect silently from now on)",
		addr, bonded);
}

static void pairing_failed(struct bt_conn *conn,
			   enum bt_security_err reason)
{
	char addr[BT_ADDR_LE_STR_LEN];

	bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
	LOG_ERR("Pairing failed: %s reason=%d", addr, reason);

	/* Remove any partial bond so a clean retry is possible */
	bt_unpair(BT_ID_DEFAULT, bt_conn_get_dst(conn));
}

static struct bt_conn_auth_cb auth_cb = {
	.passkey_display = auth_passkey_display,
	.cancel          = auth_cancel,
};

static struct bt_conn_auth_info_cb auth_info_cb = {
	.pairing_complete = pairing_complete,
	.pairing_failed   = pairing_failed,
};

/* ------------------------------------------------------------------ */
/* Passkey settings (stored in flash)                                   */
/* ------------------------------------------------------------------ */

static int passkey_settings_set(const char *name,
				size_t len,
				settings_read_cb read_cb,
				void *cb_arg)
{
	if (strcmp(name, "value") != 0) {
		return -ENOENT;
	}

	if (len != sizeof(ble_passkey)) {
		LOG_ERR("Invalid stored passkey size: %u", (unsigned int)len);
		return -EINVAL;
	}

	int ret = read_cb(cb_arg, &ble_passkey, sizeof(ble_passkey));

	if (ret < 0) {
		LOG_ERR("Failed to read stored passkey: %d", ret);
		return ret;
	}

	if (ble_passkey > PASSKEY_MAX) {
		LOG_ERR("Invalid stored passkey: %06u — resetting", ble_passkey);
		ble_passkey = BLE_DEFAULT_PASSKEY;
		return -EINVAL;
	}

	LOG_INF("Passkey loaded from flash: %06u", ble_passkey);
	return 0;
}

static int passkey_settings_commit(void)
{
	LOG_INF("Passkey settings committed: %06u", ble_passkey);
	return 0;
}

static struct settings_handler passkey_settings_handler = {
	.name     = "ble_passkey",
	.h_set    = passkey_settings_set,
	.h_commit = passkey_settings_commit,
};

/* ------------------------------------------------------------------ */
/* bt_enable callback                                                   */
/* ------------------------------------------------------------------ */

void bt_ready(int err)
{
	if (err) {
		LOG_ERR("Bluetooth init failed (err %d)", err);
		return;
	}

	LOG_INF("Bluetooth initialized");

	err = settings_register(&passkey_settings_handler);
	if (err) {
		LOG_ERR("Failed to register passkey settings: %d", err);
	}

	/*
	 * Load ALL settings — this restores both:
	 *   1. The passkey value (ble_passkey/value)
	 *   2. All stored bonds (bt/keys/...)
	 *
	 * Bonds MUST be loaded so bonded phones can reconnect without
	 * a passkey prompt.  Do NOT call bt_unpair() here.
	 */
	if (IS_ENABLED(CONFIG_SETTINGS)) {
		settings_load();
	}

	int ret = bt_passkey_set(ble_passkey);

	if (ret) {
		LOG_ERR("Failed to set BLE passkey: %d", ret);
	} else {
		LOG_INF("BLE fixed passkey set: %06u", ble_passkey);
	}
}

/* ------------------------------------------------------------------ */
/* GATT data callbacks                                                  */
/* ------------------------------------------------------------------ */

static ssize_t on_data_received(struct bt_conn *conn,
				const struct bt_gatt_attr *attr,
				const void *buf,
				uint16_t len,
				uint16_t offset,
				uint8_t flags)
{
	ARG_UNUSED(conn);
	ARG_UNUSED(attr);
	ARG_UNUSED(offset);
	ARG_UNUSED(flags);

	if (user_data_received_callback) {
		user_data_received_callback((const char *)buf, len);
	} else {
		LOG_ERR("Data received but no callback registered");
	}

	return len;
}

/* ------------------------------------------------------------------ */
/* RSSI helper                                                          */
/* ------------------------------------------------------------------ */

static int read_conn_rssi(struct bt_conn *conn, int8_t *out_rssi)
{
	if (!conn || !out_rssi) {
		return -EINVAL;
	}

	uint16_t handle;
	int err = bt_hci_get_conn_handle(conn, &handle);

	if (err) {
		LOG_ERR("Failed to get HCI handle: %d", err);
		return err;
	}

	struct bt_hci_cp_read_rssi cp = {
		.handle = sys_cpu_to_le16(handle),
	};

	struct net_buf *buf = bt_hci_cmd_alloc(K_FOREVER);

	if (!buf) {
		return -ENOMEM;
	}

	net_buf_add_mem(buf, &cp, sizeof(cp));

	struct net_buf *rsp_buf;
	int ret = bt_hci_cmd_send_sync(BT_HCI_OP_READ_RSSI, buf, &rsp_buf);

	if (ret) {
		LOG_ERR("HCI RSSI request failed: %d", ret);
		return ret;
	}

	struct bt_hci_rp_read_rssi *rp =
		(struct bt_hci_rp_read_rssi *)rsp_buf->data;

	if (rp->status == 0) {
		*out_rssi = rp->rssi;
	} else {
		LOG_ERR("HCI RSSI status error: 0x%02x", rp->status);
	}

	net_buf_unref(rsp_buf);
	return rp->status;
}

/* ------------------------------------------------------------------ */
/* Public API                                                           */
/* ------------------------------------------------------------------ */

int ble_gatt_init(ble_data_received_cb_t data_received_cb)
{
	int err;

	user_data_received_callback = data_received_cb;

	err = bt_conn_auth_cb_register(&auth_cb);
	if (err) {
		LOG_ERR("Failed to register auth callbacks: %d", err);
		return err;
	}

	err = bt_conn_auth_info_cb_register(&auth_info_cb);
	if (err) {
		LOG_ERR("Failed to register auth info callbacks: %d", err);
		return err;
	}

	k_work_init_delayable(&power_on_ble_retry_work, power_on_ble_retry_handler);
	k_work_init_delayable(&ble_adv_timeout_work,    ble_adv_timeout_handler);
	k_work_init_delayable(&ble_connected_work,      ble_connected_work_handler);
	k_work_init_delayable(&ble_disconnect_work,     ble_disconnect_work_handler);
	k_work_init_delayable(&fota_adv_work,           fota_adv_work_handler);
	k_work_init_delayable(&deferred_seq_work,       deferred_seq_work_handler);

	err = bt_enable(bt_ready);
	if (err) {
		LOG_ERR("Bluetooth init failed: %d", err);
		return err;
	}

	return 0;
}

int ble_gatt_send_raw_data(const uint8_t *data, size_t len)
{
	if (!data || len == 0) {
		LOG_ERR("BLE TX: invalid data");
		return -EINVAL;
	}

	if (!current_conn || !ble_connected) {
		LOG_ERR("BLE TX: not connected");
		return -ENOTCONN;
	}

	const struct bt_gatt_attr *attr = &neuton_gatt.attrs[1];

	bool subscribed = bt_gatt_is_subscribed(current_conn,
						attr,
						BT_GATT_CCC_NOTIFY);

	LOG_INF("BLE TX: conn=%p handle=%u len=%u subscribed=%d",
		current_conn, attr->handle, (unsigned int)len, subscribed);

	if (!subscribed) {
		LOG_ERR("BLE TX: mobile has NOT subscribed");
		return -EACCES;
	}

	LOG_HEXDUMP_INF(data, len, "BLE TX:");

	int ret = bt_gatt_notify(current_conn, attr, data, len);

	if (ret) {
		LOG_ERR("bt_gatt_notify failed: %d", ret);
	} else {
		LOG_INF("bt_gatt_notify SUCCESS");
	}

	return ret;
}

int ble_gatt_start_advertising(void)
{
	return ble_start_advertising_with_timeout();
}

int ble_gatt_get_rssi(int8_t *out_rssi)
{
	if (!current_conn) {
		LOG_ERR("No current connection");
		return -EINVAL;
	}

	int8_t rssi = 0;
	int res = read_conn_rssi(current_conn, &rssi);

	if (res == 0 && out_rssi) {
		*out_rssi = rssi;
	}

	return res;
}

void ble_data_received(const char *data, size_t len)
{
	if (len >= BLE_RX_BUFFER_SIZE) {
		len = BLE_RX_BUFFER_SIZE - 1;
	}

	memcpy(ble_rx_buffer, data, len);
	ble_rx_buffer[len] = '\0';
	ble_rx_len = len;

	LOG_INF("BLE RX: %s", ble_rx_buffer);

	frame_data_received(ble_rx_buffer, ble_rx_len);
}

int passkey_from_payload(const uint8_t *payload, uint32_t *passkey)
{
	uint32_t value = 0;

	for (int i = 0; i < 6; i++) {
		if (payload[i] < '0' || payload[i] > '9') {
			return -EINVAL;
		}
		value = (value * 10) + (payload[i] - '0');
	}

	*passkey = value;
	return 0;
}

int ble_hid_set_passkey(uint32_t passkey)
{
	if (passkey > PASSKEY_MAX) {
		LOG_ERR("Invalid passkey: %u", passkey);
		return -EINVAL;
	}

	ble_passkey = passkey;

	int err = settings_save_one("ble_passkey/value",
				    &ble_passkey, sizeof(ble_passkey));

	if (err) {
		LOG_ERR("Failed to save passkey to flash: %d", err);
		return err;
	}

	LOG_INF("Passkey saved to flash: %06u", ble_passkey);

	err = bt_passkey_set(ble_passkey);
	if (err) {
		LOG_ERR("bt_passkey_set failed: %d", err);
	}

	return err;
}

int ble_user_entered_passkey(uint32_t passkey)
{
	if (!current_conn) {
		LOG_ERR("No active BLE connection");
		return -ENOTCONN;
	}

	if (!auth_conn || !passkey_requested) {
		LOG_ERR("No pending BLE passkey request");
		return -EINVAL;
	}

	if (passkey > PASSKEY_MAX) {
		LOG_ERR("Invalid passkey: %06u", passkey);
		return -EINVAL;
	}

	LOG_INF("User entered passkey: %06u", passkey);

	int err = bt_conn_auth_passkey_entry(auth_conn, passkey);

	if (err) {
		LOG_ERR("Failed to submit passkey: %d", err);
		return err;
	}

	LOG_INF("Passkey submitted successfully");

	passkey_requested = false;
	bt_conn_unref(auth_conn);
	auth_conn = NULL;

	return 0;
}

int ble_availability_check(void)
{
	if (ble_common_is_connected() && current_conn != NULL) {
		LOG_INF("BLE already connected — AVAILABLE");
		ble_available = true;
		return 0;
	}

	while (k_sem_take(&ble_availability_sem, K_NO_WAIT) == 0) {
	}

	ble_available = false;

	LOG_INF("Checking BLE availability...");

	int ret = k_sem_take(&ble_availability_sem,
			     K_MSEC(BLE_AVAILABILITY_TIMEOUT_MS));

	if (ret == 0 && ble_available) {
		LOG_INF("BLE AVAILABLE");
		return 0;
	}

	LOG_INF("BLE NOT AVAILABLE (timeout)");
	return -ETIMEDOUT;
}

int ble_reset_passkey_to_default(void)
{
	ble_passkey = BLE_DEFAULT_PASSKEY;

	int err = settings_save_one("ble_passkey/value",
				    &ble_passkey, sizeof(ble_passkey));

	if (err) {
		LOG_ERR("Failed to reset passkey: %d", err);
		return err;
	}

	err = bt_passkey_set(ble_passkey);
	if (err) {
		LOG_ERR("bt_passkey_set failed after reset: %d", err);
	}

	LOG_INF("BLE passkey reset to default: %06u", ble_passkey);
	return err;
}

/*
 * Remove bonds for ALL phones and force fresh pairing on next connect.
 * Call this only when the user explicitly wants to "forget all phones"
 * (e.g. long-press factory reset).
 */
int ble_forget_all_bonds(void)
{
	int err = bt_unpair(BT_ID_DEFAULT, BT_ADDR_LE_ANY);

	if (err) {
		LOG_ERR("bt_unpair (all) failed: %d", err);
	} else {
		LOG_INF("All bonds removed — every phone must re-pair");
	}

	return err;
}

int ble_gatt_disconnect_and_stop_advertising(void)
{
	if (fota_mode) {
		LOG_INF("FOTA active - disconnect/stop request ignored");
		return 0;
	}

	int err = bt_le_adv_stop();

	if (err && err != -EALREADY) {
		LOG_ERR("Failed to stop advertising: %d", err);
	}

	if (current_conn) {
		err = bt_conn_disconnect(current_conn,
					 BT_HCI_ERR_REMOTE_USER_TERM_CONN);
		if (err) {
			LOG_ERR("Failed to disconnect: %d", err);
			return err;
		}
		LOG_INF("BLE disconnect requested");
	} else {
		LOG_INF("No active BLE connection");
	}

	return 0;
}

bool ble_gatt_is_ready(void)
{
	return ble_connected && ccc_enabled && (current_conn != NULL);
}

bool ble_notifications_enabled(void)
{
	return ccc_enabled;
}

bool ble_is_ready_for_data(void)
{
	return ble_connected &&
	       current_conn != NULL &&
	       ccc_enabled &&
	       ble_secure;
}

int ble_wait_for_notifications(uint32_t timeout_ms)
{
	uint32_t elapsed = 0U;

	LOG_INF("Waiting for notification subscription...");

	while (elapsed < timeout_ms) {
		if (!ble_connected || current_conn == NULL) {
			LOG_ERR("BLE connection lost");
			return -ENOTCONN;
		}

		if (ccc_enabled) {
			LOG_INF("BLE notifications ENABLED");
			return 0;
		}

		k_sleep(K_MSEC(100));
		elapsed += 100U;
	}

	LOG_ERR("Notification subscription timeout");
	return -ETIMEDOUT;
}

int ble_wait_until_ready_for_data(uint32_t timeout_ms)
{
	uint32_t elapsed = 0U;

	LOG_INF("Waiting for BLE data-ready state...");

	while (elapsed < timeout_ms) {
		if (!ble_connected || current_conn == NULL) {
			LOG_ERR("BLE connection lost");
			return -ENOTCONN;
		}

		if (ccc_enabled && ble_secure) {
			LOG_INF("BLE READY: encryption + CCCD done");
			return 0;
		}

		k_sleep(K_MSEC(50));
		elapsed += 50U;
	}

	LOG_ERR("BLE data-ready timeout (connected=%d ccc=%d secure=%d)",
		ble_connected, ccc_enabled, ble_secure);

	return -ETIMEDOUT;
}

/* ------------------------------------------------------------------ */
/* Work handlers                                                        */
/* ------------------------------------------------------------------ */

/* No connection within advertising window */
void ble_adv_timeout_handler(struct k_work *work)
{
	ARG_UNUSED(work);

	if (fota_mode || (ble_connected && current_conn != NULL)) {
		return;
	}

	LOG_INF("BLE: No connection within advertising window — stopping");

	int err = bt_le_adv_stop();

	if (err != 0 && err != -EALREADY) {
		LOG_ERR("Failed to stop advertising: %d", err);
	} else {
		LOG_INF("BLE advertising stopped");
	}

	ble_timeout_active = false;

	if (power_on_ble_sequence_active) {
		LOG_INF("BLE: Power-on pairing attempt failed");

		led_blink_led0(270, 730);
		led_blink_led0(270, 730);
		led_off();

		LOG_INF("BLE: Next attempt in 30 minutes");

		k_work_schedule_for_queue(&ble_workq, &power_on_ble_retry_work,
				K_MSEC(BLE_POWER_ON_RETRY_MS));
	}
}

/*
 * Poll every 100 ms for encryption + CCCD subscription.
 *
 * Bonded phone:  re-encrypts in < 500 ms, CCCD restored automatically
 *                (if CONFIG_BT_GATT_CACHING=y) or written by the app.
 *
 * New phone:     SMP pairing + passkey entry takes several seconds.
 *
 * BLE_SECURITY_TIMEOUT_MS covers both cases.
 * security_changed() sets security_wait_ms = BLE_SECURITY_TIMEOUT_MS
 * to trigger an immediate timeout and disconnect on security errors.
 */
void ble_connected_work_handler(struct k_work *work)
{
	ARG_UNUSED(work);

	if (!ble_connected || current_conn == NULL) {
		LOG_INF("BLE: Connection disappeared during wait");
		return;
	}

	security_wait_ms += 100U;

	/* ---- timeout ceiling ---- */
	if (security_wait_ms >= BLE_SECURITY_TIMEOUT_MS) {
		/*
		 * A phone that is bonded + authenticated (level >= 3) but never
		 * subscribes to our data characteristic is a DFU client (nRF Device
		 * Manager).  On the charger, keep it connected as a FOTA session
		 * instead of cutting the link.
		 */
		if (IS_ENABLED(CONFIG_APP_FOTA) && npm1300_vbus_connected() &&
		    bt_conn_get_security(current_conn) >= BT_SECURITY_L3) {
			LOG_INF("BLE: authenticated client without CCCD - treating as FOTA session");
			ble_sequence_active = false;
			ble_ready_callback  = NULL;
			fota_request_now();
			return;
		}

		LOG_ERR("BLE: Ready-state timeout after %u ms — disconnecting",
			security_wait_ms);

		ble_sequence_active = false;
		ble_ready_callback  = NULL;

		ble_gatt_disconnect_and_stop_advertising();
		return;
	}

	/* ---- check encryption ---- */
	bt_security_t sec = bt_conn_get_security(current_conn);

	if (sec < BT_SECURITY_L2) {
		/* Limit log spam: print only every 2 s */
		if ((security_wait_ms % 2000U) < 100U) {
			LOG_INF("BLE: Waiting for encryption "
				"(level=%d, %u/%u ms)",
				sec, security_wait_ms,
				BLE_SECURITY_TIMEOUT_MS);
		}
		k_work_schedule_for_queue(&ble_workq, &ble_connected_work, K_MSEC(100));
		return;
	}

	ble_secure = true;

	/* ---- check CCCD subscription ---- */
	bool subscribed = bt_gatt_is_subscribed(current_conn,
						&neuton_gatt.attrs[1],
						BT_GATT_CCC_NOTIFY);

	if (!subscribed) {
		if ((security_wait_ms % 2000U) < 100U) {
			LOG_INF("BLE: Waiting for CCCD (%u/%u ms)",
				security_wait_ms, BLE_SECURITY_TIMEOUT_MS);
		}
		k_work_schedule_for_queue(&ble_workq, &ble_connected_work, K_MSEC(100));
		return;
	}

	ccc_enabled = true;

	LOG_INF("BLE READY — encrypted (level=%d), CCCD subscribed "
		"(took %u ms)", sec, security_wait_ms);

	/* ---- invoke application callback ---- */
	if (ble_ready_callback != NULL) {
		int ret = ble_ready_callback(ble_argument);

		if (ret != 0) {
			LOG_ERR("BLE: Ready callback failed: %d", ret);
			ble_sequence_active = false;
			ble_ready_callback  = NULL;
			ble_gatt_disconnect_and_stop_advertising();
			return;
		}
	}

	ble_sequence_active = false;
	ble_ready_callback  = NULL;

	/*
	 * Keep the connection alive for 5 s so the notification has time
	 * to reach the phone before we disconnect.
	 */
	k_work_schedule_for_queue(&ble_workq, &ble_disconnect_work, K_MSEC(5000));
}

/* Disconnect 5 s after application data delivered */
void ble_disconnect_work_handler(struct k_work *work)
{
	ARG_UNUSED(work);

	if (fota_mode) {
		return;
	}

	LOG_INF("BLE: Post-data delay expired — disconnecting");

	if (current_conn != NULL) {
		int err = bt_conn_disconnect(current_conn,
					     BT_HCI_ERR_REMOTE_USER_TERM_CONN);
		if (err) {
			LOG_ERR("BLE disconnect failed: %d", err);
		} else {
			LOG_INF("BLE disconnect requested");
		}
	}

	int err = bt_le_adv_stop();

	if (err != 0 && err != -EALREADY) {
		LOG_ERR("BLE advertising stop failed: %d", err);
	} else {
		LOG_INF("BLE advertising stopped");
	}
}

/* 30-minute power-on retry */
static void power_on_ble_retry_handler(struct k_work *work)
{
	ARG_UNUSED(work);

	if (!power_on_ble_sequence_active) {
		return;
	}

	LOG_INF("BLE: 30-minute power-on retry");

	if (ble_common_is_connected()) {
		LOG_INF("BLE already connected — cancelling retry");
		power_on_ble_sequence_active = false;
		return;
	}

	int ret = ble_gatt_start_advertising();

	if (ret != 0) {
		LOG_ERR("BLE retry advertising failed: %d", ret);
		k_work_schedule_for_queue(&ble_workq, &power_on_ble_retry_work,
				K_MSEC(BLE_POWER_ON_RETRY_MS));
	} else {
		LOG_INF("BLE: Retry advertising started");
	}
}

/* Legacy simple timeout handler */
void ble_timeout_work_handler(struct k_work *work)
{
	ARG_UNUSED(work);

	if (fota_mode) {
		return;
	}

	if (ble_connected && current_conn != NULL) {
		bt_conn_disconnect(current_conn,
				   BT_HCI_ERR_REMOTE_USER_TERM_CONN);
		return;
	}

	bt_le_adv_stop();
	ble_timeout_active = false;

	if (power_on_ble_sequence_active) {
		led_blink_led0(270, 730);
		led_blink_led0(270, 730);
		led_off();

		k_work_schedule_for_queue(&ble_workq, &power_on_ble_retry_work,
				K_MSEC(BLE_POWER_ON_RETRY_MS));
	}
}

/* ------------------------------------------------------------------ */
/* Advertising with timeout (NO bond-clear)                             */
/* ------------------------------------------------------------------ */

int ble_start_advertising_with_timeout(void)
{
	if (fota_mode) {
		return -EBUSY;
	}

	if (current_conn != NULL || ble_connected) {
		LOG_INF("BLE already connected");
		return -EBUSY;
	}

	k_work_cancel_delayable(&ble_adv_timeout_work);
	k_work_cancel_delayable(&ble_connected_work);
	k_work_cancel_delayable(&ble_disconnect_work);

	ccc_enabled      = false;
	ble_secure       = false;
	security_wait_ms = 0;

	/*
	 * Do NOT call bt_unpair() here.
	 *
	 * Bonds are kept so that already-paired phones reconnect
	 * automatically without seeing a passkey prompt again.
	 * Only new/unknown phones will go through the pairing ceremony.
	 */

	/* Stop any leftover advertising */
	int err = bt_le_adv_stop();

	if (err != 0 && err != -EALREADY) {
		LOG_ERR("Failed to stop old advertising: %d", err);
	}

	err = bt_le_adv_start(BT_LE_ADV_CONN_FAST_1,
			      ad, ARRAY_SIZE(ad),
			      sd, ARRAY_SIZE(sd));

	if (err) {
		LOG_ERR("Advertising failed to start: %d", err);
		return err;
	}

	ble_timeout_active = true;

	k_work_schedule_for_queue(&ble_workq, &ble_adv_timeout_work, K_MSEC(BLE_ADV_TIMEOUT_MS));

	LOG_INF("BLE advertising started — %u ms window "
		"(bonded phones connect silently, new phones will pair)",
		BLE_ADV_TIMEOUT_MS);

	return 0;
}

/* ------------------------------------------------------------------ */
/* Power-on sequence helpers                                            */
/* ------------------------------------------------------------------ */

void ble_power_on_retry_start(void)
{
	power_on_ble_sequence_active = true;
	LOG_INF("BLE: Power-on retry mechanism enabled");
}

void ble_power_on_retry_cancel(void)
{
	power_on_ble_sequence_active = false;
	k_work_cancel_delayable(&power_on_ble_retry_work);
	LOG_INF("BLE: Power-on retry mechanism cancelled");
}

void power_on_seq(void)
{
	LOG_INF("BLE POWER-ON SEQUENCE START");

	power_on_led_blinks();

	ble_power_on_retry_start();

	int ret = ble_start_sequence(send_power_on_status, 0x00);

	if (ret != 0) {
		LOG_ERR("BLE: Failed to start power-on sequence: %d", ret);

		led_blink_led0(270, 730);
		led_blink_led0(270, 730);
		led_off();

		k_work_schedule_for_queue(&ble_workq, &power_on_ble_retry_work,
				K_MSEC(BLE_POWER_ON_RETRY_MS));
		return;
	}

	LOG_INF("BLE: Power-on advertising started");
}

/* ------------------------------------------------------------------ */
/* Sequence API                                                         */
/* ------------------------------------------------------------------ */

static int start_sequence(ble_ready_callback_t callback, uint8_t arg, bool urgent)
{
	if (callback == NULL) {
		return -EINVAL;
	}

	if (fota_mode && !urgent) {
		/* power-on / battery / reset packets wait; they are retried after FOTA */
		LOG_INF("BLE sequence postponed: FOTA in progress");
		return -EBUSY;
	}

	if (fota_mode) {
		/*
		 * An urgent sequence (fall alert) always wins over an update: close
		 * the FOTA window.  If a phone is still connected the sequence starts
		 * as soon as that link is gone.  MCUmgr uploads resume from the last
		 * written offset, so nothing is lost.
		 */
		LOG_WRN("Urgent BLE sequence requested during FOTA - preempting FOTA");
		ble_gatt_fota_exit();

		if (ble_connected || current_conn != NULL) {
			deferred_seq_cb    = callback;
			deferred_seq_arg   = arg;
			deferred_seq_valid = true;
			return 0;
		}
	}

	if (ble_connected || current_conn != NULL) {
		LOG_INF("BLE already connected");
		return -EBUSY;
	}

	ble_ready_callback  = callback;
	ble_sequence_active = true;
	ble_argument        = arg;

	return ble_gatt_start_advertising();
}

int ble_start_sequence(ble_ready_callback_t callback, uint8_t arg)
{
	return start_sequence(callback, arg, false);
}

int ble_start_sequence_urgent(ble_ready_callback_t callback, uint8_t arg)
{
	return start_sequence(callback, arg, true);
}

void ble_sequence_cancel(void)
{
	ble_sequence_active = false;
	ble_ready_callback  = NULL;

	k_work_cancel_delayable(&ble_connected_work);
	k_work_cancel_delayable(&ble_disconnect_work);
	k_work_cancel_delayable(&ble_adv_timeout_work);

	ble_gatt_disconnect_and_stop_advertising();
}

/* ------------------------------------------------------------------ */
/* FOTA window                                                          */
/* ------------------------------------------------------------------ */

/* MCUmgr SMP service UUID 8D53DC1D-1DB7-4CD3-868B-8A527460AA84 */
#define FOTA_SMP_UUID_VAL \
	BT_UUID_128_ENCODE(0x8d53dc1d, 0x1db7, 0x4cd3, 0x868b, 0x8a527460aa84)

static const struct bt_data fota_ad[] = {
	BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
	BT_DATA_BYTES(BT_DATA_UUID128_ALL, FOTA_SMP_UUID_VAL),
};

/* 100-150 ms: responsive enough for a phone, far cheaper than FAST_1 over minutes */
static const struct bt_le_adv_param fota_adv_param =
	BT_LE_ADV_PARAM_INIT(BT_LE_ADV_OPT_CONN,
			     BT_GAP_ADV_FAST_INT_MIN_2,
			     BT_GAP_ADV_FAST_INT_MAX_2,
			     NULL);

bool ble_gatt_fota_mode(void)
{
	return fota_mode;
}

bool ble_gatt_sequence_busy(void)
{
	/* a normal sequence is advertising (window timer running) or has a link up */
	return ble_connected || current_conn != NULL ||
	       k_work_delayable_is_pending(&ble_adv_timeout_work);
}

static void fota_adv_work_handler(struct k_work *work)
{
	ARG_UNUSED(work);

	if (!fota_mode || current_conn != NULL) {
		return;
	}

	int err = bt_le_adv_start(&fota_adv_param, fota_ad, ARRAY_SIZE(fota_ad),
				  sd, ARRAY_SIZE(sd));

	if (err == -EALREADY) {
		return;
	}
	if (err) {
		LOG_ERR("FOTA advertising failed: %d - retrying", err);
		k_work_reschedule_for_queue(&ble_workq, &fota_adv_work, K_SECONDS(1));
		return;
	}
	LOG_INF("FOTA advertising started (SMP service)");
}

static void deferred_seq_work_handler(struct k_work *work)
{
	ARG_UNUSED(work);

	if (!deferred_seq_valid) {
		return;
	}
	deferred_seq_valid = false;

	int ret = ble_start_sequence_urgent(deferred_seq_cb, deferred_seq_arg);

	if (ret != 0) {
		LOG_ERR("Deferred BLE sequence failed to start: %d", ret);
	}
}

int ble_gatt_fota_enter(void)
{
	/* Suspend the short-lived timers of the normal sequence. */
	k_work_cancel_delayable(&ble_adv_timeout_work);
	k_work_cancel_delayable(&ble_connected_work);
	k_work_cancel_delayable(&ble_disconnect_work);
	k_work_cancel_delayable(&power_on_ble_retry_work);

	ble_sequence_active = false;
	ble_ready_callback  = NULL;
	fota_mode           = true;

	if (current_conn == NULL) {
		/* drop any normal-mode advertising so the FOTA ad data replaces it */
		(void)bt_le_adv_stop();
		k_work_reschedule_for_queue(&ble_workq, &fota_adv_work, K_NO_WAIT);
	}

	return 0;
}

void ble_gatt_fota_exit(void)
{
	if (!fota_mode) {
		return;
	}

	fota_mode = false;
	k_work_cancel_delayable(&fota_adv_work);

	int err = bt_le_adv_stop();

	if (err != 0 && err != -EALREADY) {
		LOG_ERR("FOTA: failed to stop advertising: %d", err);
	}

	if (current_conn != NULL) {
		(void)bt_conn_disconnect(current_conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN);
	}

	if (power_on_ble_sequence_active) {
		/* the power-on status was held back by FOTA: send it now, not in 30 min */
		k_work_schedule_for_queue(&ble_workq, &power_on_ble_retry_work, K_SECONDS(5));
	}

	LOG_INF("FOTA window closed");
}
