#include "audio_backend.h"

#include <algorithm>
#include <array>
#include <cmath>
#include <atomic>
#include <mutex>
#ifdef CONFIG_DEVICE_LAB_SPEECH_TRACE
#include <memory>
#include "speech_diagnostics.h"
#include "esp_timer.h"
#endif

#include "driver/i2s_std.h"
#include "esp_codec_dev.h"
#include "esp_codec_dev_defaults.h"
#include "esp_log.h"
#include "esp_heap_caps.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/idf_additions.h"
#include "freertos/task.h"

namespace {

constexpr char TAG[] = "AUDIO";
constexpr i2s_port_t I2S_PORT = I2S_NUM_0;
constexpr gpio_num_t I2S_MCLK_PIN = GPIO_NUM_18;
constexpr gpio_num_t I2S_BCLK_PIN = GPIO_NUM_17;
constexpr gpio_num_t I2S_DADC_IN_PIN = GPIO_NUM_16;
constexpr gpio_num_t I2S_LRCK_PIN = GPIO_NUM_15;
constexpr gpio_num_t I2S_DDAC_OUT_PIN = GPIO_NUM_21;
constexpr int AUDIO_SAMPLE_RATE = 44100;
constexpr int AUDIO_VOLUME = 90;
constexpr int16_t TONE_AMPLITUDE = 8192;
constexpr size_t PCM_CHUNK_SAMPLES = 256;
constexpr float TWO_PI = 6.28318530717958647692F;

i2s_chan_handle_t s_tx_handle;
i2s_chan_handle_t s_rx_handle;
const audio_codec_data_if_t *s_data_if;
const audio_codec_ctrl_if_t *s_ctrl_if;
const audio_codec_gpio_if_t *s_gpio_if;
const audio_codec_if_t *s_codec_if;
esp_codec_dev_handle_t s_codec_dev;
QueueHandle_t s_notification_queue;
bool s_ready;
std::mutex s_audio_mode_lock; // codec has exactly one owner: playback or capture
QueueHandle_t s_capture_queue;
TaskHandle_t s_capture_task;
std::atomic<bool> s_recording{false}, s_stop{false}, s_cancel{false};
uint64_t s_capture_id;
struct SpeechFrame { uint32_t id; size_t size; bool end; uint8_t pcm[AUDIO_SPEECH_BYTES]; };
QueueHandle_t s_speech_queue;
std::atomic<uint32_t> s_speech_id{0};
std::atomic<bool> s_approval_tone{false};
std::mutex s_speech_lock;
TickType_t s_speech_progress_at;
#ifdef CONFIG_DEVICE_LAB_SPEECH_TRACE
// One allocation per stream; an in-flight write keeps its OLD generation alive
// through stop/replacement. No extra mutex or per-512-byte locking/logging.
std::shared_ptr<SpeechPlaybackMetrics> s_metrics;
void speech_metrics_log(const SpeechPlaybackMetrics &m, const char *reason) {
    const auto dropped=m.rejected+m.discarded;
    ESP_LOGI(TAG,"[SPEECH_QUEUE] generation=%u reason=%s at_ms=%lld queue_depth=%u accepted=%u played=%u dropped=%u rejected=%u discarded=%u high_water=%u bytes_accepted=%llu bytes_played=%llu in_flight=%u final=%d",
             m.generation,reason,static_cast<long long>(esp_timer_get_time()/1000),
             s_speech_queue && s_metrics && s_metrics->generation==m.generation ? static_cast<unsigned>(uxQueueMessagesWaiting(s_speech_queue)) : 0,
             m.accepted,m.played,dropped,m.rejected,m.discarded,m.high_water,
             static_cast<unsigned long long>(m.bytes_accepted),static_cast<unsigned long long>(m.bytes_written),
             m.in_flight,m.closed && !m.in_flight);
    ESP_LOGI(TAG,"[SPEECH_PLAY] generation=%u reason=%s at_ms=%lld bytes_received=%llu bytes_written=%llu write_calls=%u max_write_us=%llu total_write_us=%llu slow_writes=%u write_errors=%u first_write_ms=%lld last_write_ms=%lld in_flight=%u final=%d stack_free_min_bytes=%u",
             m.generation,reason,static_cast<long long>(esp_timer_get_time()/1000),
             static_cast<unsigned long long>(m.bytes_accepted),static_cast<unsigned long long>(m.bytes_written),
             m.write_calls,static_cast<unsigned long long>(m.max_write_us),static_cast<unsigned long long>(m.write_us),
             m.slow_writes,m.write_errors,static_cast<long long>(m.first_write_us/1000),
             static_cast<long long>(m.last_write_us/1000),m.in_flight,m.closed && !m.in_flight,
             static_cast<unsigned>(uxTaskGetStackHighWaterMark(nullptr)));
}
void speech_metrics_close(const char *reason) {
    if (!s_metrics || s_metrics->closed) return;
    s_metrics->closed=true;
    s_metrics->discarded=s_metrics->accepted-s_metrics->played-s_metrics->in_flight;
    speech_metrics_log(*s_metrics,reason);
}
#endif



esp_err_t initialize_i2s()
{
    i2s_chan_config_t channel_config = I2S_CHANNEL_DEFAULT_CONFIG(I2S_PORT, I2S_ROLE_MASTER);
    // The DMA ring otherwise replays old tone/speech samples during TX idle.
    // Capture keeps I2S enabled for RX, so stopping TX is not an alternative.
    channel_config.auto_clear_after_cb = true;
    ESP_LOGI(TAG, "[AUDIO] TX idle silence auto_clear=1 dma_desc=%lu dma_frames=%lu",
             static_cast<unsigned long>(channel_config.dma_desc_num),
             static_cast<unsigned long>(channel_config.dma_frame_num));
    i2s_std_config_t standard_config = {
        .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(AUDIO_SAMPLE_RATE),
        .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(
            I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
        .gpio_cfg = {
            .mclk = I2S_MCLK_PIN,
            .bclk = I2S_BCLK_PIN,
            .ws = I2S_LRCK_PIN,
            .dout = I2S_DDAC_OUT_PIN,
            .din = I2S_DADC_IN_PIN,
            .invert_flags = {},
        },
    };

    esp_err_t result = i2s_new_channel(&channel_config, &s_tx_handle, &s_rx_handle);
    if (result != ESP_OK) {
        return result;
    }
    result = i2s_channel_init_std_mode(s_tx_handle, &standard_config);
    if (result != ESP_OK) {
        return result;
    }
    result = i2s_channel_init_std_mode(s_rx_handle, &standard_config);
    if (result != ESP_OK) {
        return result;
    }
    result = i2s_channel_enable(s_tx_handle);
    if (result != ESP_OK) {
        return result;
    }
    return i2s_channel_enable(s_rx_handle);
}

esp_err_t write_silence(uint32_t duration_ms)
{
    std::array<int16_t, PCM_CHUNK_SAMPLES> samples = {};
    size_t remaining = static_cast<size_t>(AUDIO_SAMPLE_RATE) * duration_ms / 1000;
    while (remaining > 0) {
        const size_t count = std::min(remaining, samples.size());
        const esp_err_t result = esp_codec_dev_write(
            s_codec_dev, samples.data(), count * sizeof(int16_t));
        if (result != ESP_OK) {
            return result;
        }
        remaining -= count;
    }
    return ESP_OK;
}

esp_err_t write_tone(float frequency_hz, uint32_t duration_ms)
{
    ESP_LOGI(TAG, "[AUDIO] tone begin hz=%.0f duration_ms=%lu", frequency_hz,
             static_cast<unsigned long>(duration_ms));
    std::array<int16_t, PCM_CHUNK_SAMPLES> samples = {};
    const size_t total = static_cast<size_t>(AUDIO_SAMPLE_RATE) * duration_ms / 1000;
    const size_t fade = std::min<size_t>(AUDIO_SAMPLE_RATE / 200, total / 2);
    const float step = TWO_PI * frequency_hz / static_cast<float>(AUDIO_SAMPLE_RATE);
    float phase = 0.0F;
    size_t offset = 0;
    while (offset < total) {
        const size_t count = std::min(total - offset, samples.size());
        for (size_t index = 0; index < count; ++index) {
            const size_t absolute = offset + index;
            float envelope = 1.0F;
            if (fade > 0 && absolute < fade) {
                envelope = static_cast<float>(absolute) / static_cast<float>(fade);
            } else if (fade > 0 && absolute >= total - fade) {
                envelope = static_cast<float>(total - absolute) / static_cast<float>(fade);
            }
            samples[index] = static_cast<int16_t>(
                static_cast<float>(TONE_AMPLITUDE) * envelope * std::sinf(phase));
            phase += step;
            if (phase >= TWO_PI) {
                phase -= TWO_PI;
            }
        }
        const esp_err_t result = esp_codec_dev_write(
            s_codec_dev, samples.data(), count * sizeof(int16_t));
        if (result != ESP_OK) {
            return result;
        }
        offset += count;
    }
    ESP_LOGI(TAG, "[AUDIO] tone queued hz=%.0f", frequency_hz);
    return ESP_OK;
}

void play_notification(audio_notification_t kind)
{
    esp_err_t result = ESP_OK;
    if (kind == audio_notification_t::APPROVAL) {
        result = write_tone(1000.0F, 100);
        if (result == ESP_OK) result = write_silence(100);
        if (result == ESP_OK) result = write_tone(1000.0F, 100);
        if (result == ESP_OK) result = write_silence(100);
        if (result == ESP_OK) result = write_tone(1400.0F, 160);
    } else if (kind == audio_notification_t::DONE) {
        result = write_tone(880.0F, 120);
        if (result == ESP_OK) result = write_silence(60);
        if (result == ESP_OK) result = write_tone(1200.0F, 180);
    } else {
        result = write_tone(600.0F, 180);
        if (result == ESP_OK) result = write_silence(70);
        if (result == ESP_OK) result = write_tone(400.0F, 250);
    }
    if (result == ESP_OK) {
        result = write_silence(80);
    }
    if (result != ESP_OK) {
        ESP_LOGW(TAG, "[AUDIO] tone playback failed: %s (%d)",
                 esp_err_to_name(result), result);
    }
}

void audio_task(void *)
{
    uint8_t queued_kind = 0;
    while (true) {
        if (xQueueReceive(s_notification_queue, &queued_kind, portMAX_DELAY) == pdTRUE) {
            std::lock_guard<std::mutex> lock(s_audio_mode_lock);
            const auto kind = static_cast<audio_notification_t>(queued_kind);
            if (kind == audio_notification_t::APPROVAL || (!s_recording && !s_speech_id && !s_approval_tone))
                play_notification(kind);
            if (kind == audio_notification_t::APPROVAL) s_approval_tone = false;
        }
    }
}

void speech_task(void *)
{
    auto *frame = new SpeechFrame{};
    const TickType_t idle_timeout = pdMS_TO_TICKS(3000);
    while (true) {
        TickType_t wait = idle_timeout;
        {
            std::lock_guard<std::mutex> guard(s_speech_lock);
            if (s_speech_id) {
                const TickType_t age = xTaskGetTickCount() - s_speech_progress_at;
                wait = age >= idle_timeout ? 1 : idle_timeout - age;
            }
        }
        if (xQueueReceive(s_speech_queue, frame, wait) != pdTRUE) {
            // Missing end or prolonged network stall releases ownership.
            // A new stream can start during an old queue wait. Only its own
            // start/last accepted PCM timestamp may expire that stream.
            std::lock_guard<std::mutex> guard(s_speech_lock);
            const TickType_t age = xTaskGetTickCount() - s_speech_progress_at;
            if (s_speech_id && age >= idle_timeout && uxQueueMessagesWaiting(s_speech_queue)==0) {
                ESP_LOGW(TAG,"[SPEECH] receive idle timeout generation=%u elapsed_ms=%u",
                         static_cast<unsigned>(s_speech_id.load()),static_cast<unsigned>(age*portTICK_PERIOD_MS));
#ifdef CONFIG_DEVICE_LAB_SPEECH_TRACE
                speech_metrics_close("timeout");
#endif
                s_speech_id=0;
            }
            continue;
        }
        std::lock_guard<std::mutex> lock(s_audio_mode_lock);
        if (s_speech_id != frame->id || s_recording || s_approval_tone) continue;
#ifdef CONFIG_DEVICE_LAB_SPEECH_TRACE
        std::shared_ptr<SpeechPlaybackMetrics> metrics;
        {
            std::lock_guard<std::mutex> guard(s_speech_lock);
            if (s_metrics && s_metrics->generation==frame->id && !s_metrics->closed) {
                metrics=s_metrics;
                metrics->in_flight=frame->size ? 1 : 0;
            }
        }
        unsigned calls=0, slow=0, errors=0;
        uint64_t written=0, total_us=0, max_us=0;
        int64_t first_us=0, last_us=0;
#endif
        bool ok = true;
        for (size_t offset=0; offset<frame->size && s_speech_id==frame->id && !s_recording && !s_approval_tone; offset+=512) {
            const auto bytes=std::min<size_t>(512,frame->size-offset);
#ifdef CONFIG_DEVICE_LAB_SPEECH_TRACE
            const int64_t started=esp_timer_get_time();
            if (!calls) first_us=started;
#endif
            const auto result=esp_codec_dev_write(s_codec_dev, frame->pcm+offset, bytes);
#ifdef CONFIG_DEVICE_LAB_SPEECH_TRACE
            last_us=esp_timer_get_time();
            const uint64_t duration=last_us-started;
            ++calls; total_us+=duration; max_us=std::max(max_us,duration); slow+=duration>20000;
            if (result==ESP_OK) written+=bytes; else ++errors;
#endif
            if (result!=ESP_OK) { ok=false; break; }
        }
#ifdef CONFIG_DEVICE_LAB_SPEECH_TRACE
        if (metrics) {
            std::lock_guard<std::mutex> guard(s_speech_lock);
            metrics->in_flight=0;
            metrics->writes(calls,written,total_us,max_us,slow,errors,first_us,last_us,
                            frame->size && written==frame->size);
            if (metrics->closed) {
                metrics->discarded=metrics->accepted-metrics->played;
                speech_metrics_log(*metrics,"settled");
            } else if (!ok || frame->end) {
                metrics->closed=true;
                metrics->discarded=metrics->accepted-metrics->played;
                speech_metrics_log(*metrics,ok ? "end" : "write-error");
            }
        }
#endif
        if (!ok || frame->end) {
            uint32_t expected=frame->id;
            s_speech_id.compare_exchange_strong(expected,0);
        }
    }
}

void capture_task(void *)
{
    auto *frame = new AudioCaptureFrame{};
    while (true) {
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
        std::lock_guard<std::mutex> lock(s_audio_mode_lock);
        frame->id = s_capture_id;
        ESP_LOGI(TAG,"[VOICE] capture-started id=%016llx internal_free=%u dma_largest=%u",static_cast<unsigned long long>(frame->id),
                 static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_INTERNAL)),
                 static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_DMA)));
        frame->size = 0;
        frame->kind = CaptureKind::START;
        bool ok = !s_cancel && write_tone(1200, 45) == ESP_OK && write_silence(30) == ESP_OK;
        esp_codec_dev_set_out_mute(s_codec_dev, true);
        // Drain RX data accumulated during playback, before taking new speech.
        for (int i=0; ok && i<3 && !s_cancel; ++i)
            ok = esp_codec_dev_read(s_codec_dev, frame->pcm, AUDIO_CAPTURE_BYTES) == ESP_OK;
        ok = ok && !s_cancel && xQueueSend(s_capture_queue, frame, 0) == pdTRUE;
        size_t total = 0;
        unsigned chunks = 0;
        const char *failure = ok ? "none" : "start-tone/drain/queue";
        while (ok && !s_stop && !s_cancel && total < 44100U*2*20) {
            frame->kind = CaptureKind::PCM;
            frame->size = AUDIO_CAPTURE_BYTES;
            ok = esp_codec_dev_read(s_codec_dev, frame->pcm, frame->size) == ESP_OK;
            if (!ok) failure="codec-read";
            if (ok && !s_cancel) {
                ok = xQueueSend(s_capture_queue, frame, 0) == pdTRUE;
                if (!ok) failure="capture-queue-full";
                else ++chunks;
            }
            total += frame->size;
        }
        esp_codec_dev_set_out_mute(s_codec_dev, false);
        const bool cancelled = !ok || s_cancel;
        if (!cancelled) { write_tone(800, 45); write_silence(30); }
        frame->kind = cancelled ? CaptureKind::CANCEL : CaptureKind::END;
        frame->size = 0;
        if (cancelled) xQueueReset(s_capture_queue);
        if (xQueueSend(s_capture_queue, frame, 0) != pdTRUE) {
            xQueueReset(s_capture_queue);
            frame->kind = CaptureKind::CANCEL;
            failure="terminal-queue-full";
            xQueueSend(s_capture_queue, frame, 0);
        }
        ESP_LOGI(TAG,"[VOICE] capture-finished id=%016llx chunks=%u read_bytes=%u terminal=%s stop=%d cancel=%d failure=%s",
                 static_cast<unsigned long long>(frame->id),chunks,static_cast<unsigned>(total),
                 frame->kind==CaptureKind::END?"END":"CANCEL",s_stop.load(),s_cancel.load(),failure);
        ESP_LOGI(TAG,"[VOICE] capture-memory internal_free=%u dma_largest=%u",
                 static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_INTERNAL)),
                 static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_DMA)));
        s_recording = false;
    }
}

}  // namespace

esp_err_t audio_backend_init(i2c_master_bus_handle_t i2c_bus)
{
    if (i2c_bus == nullptr) {
        return ESP_ERR_INVALID_ARG;
    }
    esp_err_t result = initialize_i2s();
    if (result != ESP_OK) {
        return result;
    }

    audio_codec_i2s_cfg_t i2s_config = {};
    i2s_config.rx_handle = s_rx_handle;
    i2s_config.tx_handle = s_tx_handle;
    s_data_if = audio_codec_new_i2s_data(&i2s_config);
    audio_codec_i2c_cfg_t i2c_config = {};
    i2c_config.addr = ES8311_CODEC_DEFAULT_ADDR;
    i2c_config.bus_handle = i2c_bus;
    s_ctrl_if = audio_codec_new_i2c_ctrl(&i2c_config);
    s_gpio_if = audio_codec_new_gpio();
    if (s_data_if == nullptr || s_ctrl_if == nullptr || s_gpio_if == nullptr) {
        return ESP_ERR_NO_MEM;
    }

    es8311_codec_cfg_t codec_config = {};
    codec_config.ctrl_if = s_ctrl_if;
    codec_config.gpio_if = s_gpio_if;
    codec_config.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH;
    codec_config.pa_pin = GPIO_NUM_NC;
    codec_config.pa_reverted = false;
    codec_config.use_mclk = true;
    s_codec_if = es8311_codec_new(&codec_config);
    if (s_codec_if == nullptr) {
        return ESP_ERR_NO_MEM;
    }

    esp_codec_dev_cfg_t device_config = {};
    device_config.dev_type = ESP_CODEC_DEV_TYPE_IN_OUT;
    device_config.codec_if = s_codec_if;
    device_config.data_if = s_data_if;
    s_codec_dev = esp_codec_dev_new(&device_config);
    if (s_codec_dev == nullptr) {
        return ESP_ERR_NO_MEM;
    }

    esp_codec_dev_sample_info_t sample_info = {};
    sample_info.bits_per_sample = 16;
    sample_info.channel = 1;
    sample_info.sample_rate = AUDIO_SAMPLE_RATE;
    result = esp_codec_dev_open(s_codec_dev, &sample_info);
    if (result != ESP_OK) {
        return result;
    }
    result = esp_codec_dev_set_out_vol(s_codec_dev, AUDIO_VOLUME);
    if (result != ESP_OK) {
        return result;
    }

    result = esp_codec_dev_set_in_gain(s_codec_dev, 30.0F);
    if (result != ESP_OK) return result;
    // PCM queue storage does not feed DMA directly. Keep these large buffers
    // in PSRAM so TCP traffic cannot exhaust the display/audio DMA heap.
    s_capture_queue = xQueueCreateWithCaps(VOICE_CAPTURE_QUEUE_FRAMES, sizeof(AudioCaptureFrame), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
    if (!s_capture_queue || xTaskCreate(capture_task, "mic_capture", 4096, nullptr, 5, &s_capture_task) != pdPASS) return ESP_ERR_NO_MEM;
    s_speech_queue = xQueueCreateWithCaps(AUDIO_SPEECH_QUEUE_FRAMES, sizeof(SpeechFrame), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
    if (!s_speech_queue || xTaskCreate(speech_task, "speech_play", 4096, nullptr, 5, nullptr)!=pdPASS) return ESP_ERR_NO_MEM;
    s_notification_queue = xQueueCreate(4, sizeof(uint8_t));
    if (s_notification_queue == nullptr) {
        return ESP_ERR_NO_MEM;
    }
    if (xTaskCreate(audio_task, "audio_notify", 4096, nullptr, 5, nullptr) != pdPASS) {
        return ESP_ERR_NO_MEM;
    }
    s_ready = true;
    ESP_LOGI(TAG,"[AUDIO] PCM queues in PSRAM bytes=%u internal_free=%u dma_largest=%u",
             static_cast<unsigned>(VOICE_CAPTURE_QUEUE_FRAMES*sizeof(AudioCaptureFrame)+AUDIO_SPEECH_QUEUE_FRAMES*sizeof(SpeechFrame)),
             static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_INTERNAL)),
             static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_DMA)));
    return ESP_OK;
}

bool audio_backend_notify(audio_notification_t kind)
{
    if (!s_ready || s_notification_queue == nullptr) {
        return false;
    }
    if (kind == audio_notification_t::APPROVAL) {
        s_approval_tone = true;
        audio_backend_speech_stop();
        audio_backend_record_stop(true);
        xQueueReset(s_notification_queue);
    } else if (s_recording || s_speech_id || s_approval_tone) return false;
    const uint8_t queued_kind = static_cast<uint8_t>(kind);
    return xQueueSend(s_notification_queue, &queued_kind, 0) == pdTRUE;
}

bool audio_backend_ready(void)
{
    return s_ready;
}

bool audio_backend_record_start(uint64_t id)
{
    bool expected = false;
    if (!s_ready || !s_recording.compare_exchange_strong(expected, true)) return false;
    audio_backend_speech_stop();
    xQueueReset(s_notification_queue);
    xQueueReset(s_capture_queue);
    s_capture_id = id;
    s_stop = false; s_cancel = false;
    xTaskNotifyGive(s_capture_task);
    return true;
}
void audio_backend_record_stop(bool cancel)
{
    if (s_recording && (cancel ? !s_cancel.load() : !s_stop.load()))
        ESP_LOGI(TAG,"[VOICE] record-stop cancel=%d",cancel);
    if (cancel) s_cancel = true;
    s_stop = true;
}
bool audio_backend_take_capture(AudioCaptureFrame *frame)
{
    return s_capture_queue && xQueueReceive(s_capture_queue, frame, 0) == pdTRUE;
}

bool audio_backend_speech_start(uint32_t id) {
    std::lock_guard<std::mutex> guard(s_speech_lock);
    if (!s_ready || !id || s_recording || s_approval_tone) return false;
#ifdef CONFIG_DEVICE_LAB_SPEECH_TRACE
    speech_metrics_close("replaced");
    s_metrics=std::make_shared<SpeechPlaybackMetrics>();
    s_metrics->generation=id; s_metrics->start_us=esp_timer_get_time();
#endif
    s_speech_id=0;
    xQueueReset(s_speech_queue); xQueueReset(s_notification_queue);
    s_speech_progress_at=xTaskGetTickCount();
    s_speech_id=id;
#ifdef CONFIG_DEVICE_LAB_SPEECH_TRACE
    speech_metrics_log(*s_metrics,"start");
#endif
    return true;
}
bool audio_backend_speech_push(uint32_t id, const uint8_t *pcm, size_t size, bool end) {
    std::lock_guard<std::mutex> guard(s_speech_lock);
    if (!s_ready || !id || s_speech_id!=id) return false;
    if (size>AUDIO_SPEECH_BYTES || size%2 || (!end && !size) || (size && !pcm)) {
#ifdef CONFIG_DEVICE_LAB_SPEECH_TRACE
        if (s_metrics) ++s_metrics->rejected;
        speech_metrics_close("push-failure");
#endif
        s_speech_id=0; xQueueReset(s_speech_queue); return false;
    }
    // Static storage is protected by s_speech_lock, avoiding a 7 KiB callback stack allocation.
    static SpeechFrame frame;
    frame.id=id; frame.size=size; frame.end=end;
    if (size) std::copy(pcm,pcm+size,frame.pcm);
#ifdef CONFIG_DEVICE_LAB_SPEECH_TRACE
    const unsigned depth_before=uxQueueMessagesWaiting(s_speech_queue);
#endif
    if (xQueueSend(s_speech_queue,&frame,0)!=pdTRUE) {
#ifdef CONFIG_DEVICE_LAB_SPEECH_TRACE
        if (s_metrics) ++s_metrics->rejected;
        speech_metrics_close("push-failure");
#endif
        s_speech_id=0; xQueueReset(s_speech_queue); return false;
    }
    s_speech_progress_at=xTaskGetTickCount();
#ifdef CONFIG_DEVICE_LAB_SPEECH_TRACE
    // Producer's enqueue occupancy upper bound: consumer can dequeue concurrently.
    if (s_metrics) {
        s_metrics->accept(size,std::min<unsigned>(AUDIO_SPEECH_QUEUE_FRAMES,depth_before+1));
        if (end) speech_metrics_log(*s_metrics,"end-received");
    }
#endif
    return true;
}
void audio_backend_speech_stop() {
    std::lock_guard<std::mutex> guard(s_speech_lock);
#ifdef CONFIG_DEVICE_LAB_SPEECH_TRACE
    speech_metrics_close("stop");
#endif
    s_speech_id=0;
    if (s_speech_queue) xQueueReset(s_speech_queue);
}
bool audio_backend_speech_active(uint32_t id) { return id && s_speech_id==id; }

unsigned audio_backend_speech_queue_depth() {
#ifdef CONFIG_DEVICE_LAB_SPEECH_TRACE
    return s_speech_queue ? uxQueueMessagesWaiting(s_speech_queue) : 0;
#else
    return 0;
#endif
}
uint64_t audio_backend_speech_bytes_written() {
#ifdef CONFIG_DEVICE_LAB_SPEECH_TRACE
    std::lock_guard<std::mutex> guard(s_speech_lock);
    return s_metrics ? s_metrics->bytes_written : 0;
#else
    return 0;
#endif
}
