// plip_virtual — implementation. See plip_virtual.h for the contract.
//
// Threading: the ring tone runs in its own task (synthesised sine bursts on
// the shared speaker I2S channel, French cadence 1.5 s on / 3.5 s off); hook
// reports run in a tiny worker so the button/touch paths never block on
// HTTP. The UI callback fires from whichever context transitions the state
// (httpd, button task, LVGL touch) — the UI does its own locking.

#include "plip_virtual.h"

#include <math.h>
#include <string.h>

#include "cJSON.h"
#include "esp_http_client.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"

#include "board_config.h"

#ifndef CONFIG_ZACUS_MASTER_URL
#define CONFIG_ZACUS_MASTER_URL "http://10.2.5.42"
#endif

#define TAG "plip_virtual"

#define RING_FREQ_HZ      440.0f
#define RING_ON_MS        1500      /* French cadence */
#define RING_OFF_MS       3500
#define RING_AMPLITUDE    14000.0f
#define HOOK_QUEUE_DEPTH  4
#define DIAL_MAX          20

typedef struct {
    char state[8];    /* "off" | "on" */
    char reason[32];  /* "pickup" | "hangup" | "ring_timeout" | "dial:<n>" */
} hook_event_t;

static i2s_chan_handle_t        s_spk;
static volatile plip_hook_state_t s_state = PLIP_HOOK_ON;
static volatile bool            s_ring_stop = false;
static TaskHandle_t             s_ring_task = NULL;
static QueueHandle_t            s_hook_queue = NULL;
static plip_state_cb_t          s_state_cb = NULL;
static char                     s_dialed[DIAL_MAX + 1] = "";

static void set_state(plip_hook_state_t st)
{
    s_state = st;
    if (s_state_cb) s_state_cb(st);
}

/* ---------- hook reporting (mirrors PLIP's zacus_hook_client) ---------- */

static void hook_report(const char *state, const char *reason)
{
    if (!s_hook_queue) return;
    hook_event_t ev;
    strlcpy(ev.state, state, sizeof(ev.state));
    strlcpy(ev.reason, reason, sizeof(ev.reason));
    if (xQueueSend(s_hook_queue, &ev, 0) != pdTRUE) {
        ESP_LOGW(TAG, "hook queue full, dropping (%s/%s)", state, reason);
    }
}

static void hook_worker_task(void *arg)
{
    (void) arg;
    hook_event_t ev;
    char url[160];
    snprintf(url, sizeof(url), "%s/voice/hook", CONFIG_ZACUS_MASTER_URL);

    for (;;) {
        if (xQueueReceive(s_hook_queue, &ev, portMAX_DELAY) != pdTRUE) continue;

        char body[96];
        snprintf(body, sizeof(body), "{\"state\":\"%s\",\"reason\":\"%s\"}",
                 ev.state, ev.reason);

        esp_http_client_config_t cfg = {
            .url = url,
            .method = HTTP_METHOD_POST,
            .timeout_ms = 3000,
        };
        esp_http_client_handle_t client = esp_http_client_init(&cfg);
        if (!client) continue;
        esp_http_client_set_header(client, "Content-Type", "application/json");
        esp_http_client_set_post_field(client, body, strlen(body));
        esp_err_t err = esp_http_client_perform(client);
        if (err == ESP_OK) {
            ESP_LOGI(TAG, "hook POST %s -> %d (%s/%s)", url,
                     esp_http_client_get_status_code(client),
                     ev.state, ev.reason);
        } else {
            ESP_LOGW(TAG, "hook POST failed: %s (%s/%s)",
                     esp_err_to_name(err), ev.state, ev.reason);
        }
        esp_http_client_cleanup(client);
    }
}

/* ---------- ring tone ---------- */

static void ring_burst(int duration_ms)
{
    const int total = AUDIO_SAMPLE_RATE * duration_ms / 1000;
    int16_t buffer[256];
    size_t written = 0;
    int idx = 0;
    while (idx < total && !s_ring_stop) {
        int chunk = (total - idx < 256) ? (total - idx) : 256;
        for (int i = 0; i < chunk; i++) {
            float t = (float) (idx + i) / (float) AUDIO_SAMPLE_RATE;
            buffer[i] = (int16_t) (RING_AMPLITUDE *
                                   sinf(2.0f * (float) M_PI * RING_FREQ_HZ * t));
        }
        i2s_channel_write(s_spk, buffer, chunk * sizeof(int16_t), &written,
                          pdMS_TO_TICKS(500));
        idx += chunk;
    }
}

static void ring_task(void *arg)
{
    int duration_ms = (int) (intptr_t) arg;
    int elapsed = 0;

    ESP_LOGI(TAG, "ring start (%d ms)", duration_ms);
    while (elapsed < duration_ms && !s_ring_stop) {
        ring_burst(RING_ON_MS);
        elapsed += RING_ON_MS;
        if (elapsed >= duration_ms || s_ring_stop) break;
        for (int w = 0; w < RING_OFF_MS && !s_ring_stop; w += 100) {
            vTaskDelay(pdMS_TO_TICKS(100));
        }
        elapsed += RING_OFF_MS;
    }

    if (s_state == PLIP_HOOK_RINGING) {
        set_state(PLIP_HOOK_ON);    /* nobody picked up */
        if (!s_ring_stop) hook_report("on", "ring_timeout");
    }
    ESP_LOGI(TAG, "ring end (state=%d)", (int) s_state);
    s_ring_task = NULL;
    vTaskDelete(NULL);
}

static void ring_stop(void)
{
    s_ring_stop = true;
    /* The task observes the flag within one 100 ms slice and self-deletes. */
    for (int i = 0; i < 20 && s_ring_task; i++) vTaskDelay(pdMS_TO_TICKS(50));
    s_ring_stop = false;
}

/* ---------- REST handlers ---------- */

static esp_err_t send_json(httpd_req_t *req, const char *status, const char *body)
{
    httpd_resp_set_status(req, status);
    httpd_resp_set_type(req, "application/json");
    return httpd_resp_sendstr(req, body);
}

static esp_err_t handle_ring_post(httpd_req_t *req)
{
    int duration_ms = 4000;
    if (req->content_len > 0 && req->content_len < 128) {
        char body[128];
        int got = httpd_req_recv(req, body, req->content_len);
        if (got > 0) {
            body[got] = '\0';
            cJSON *root = cJSON_Parse(body);
            const cJSON *d = root ? cJSON_GetObjectItem(root, "duration_ms") : NULL;
            if (cJSON_IsNumber(d) && d->valueint > 0 && d->valueint <= 60000) {
                duration_ms = d->valueint;
            }
            cJSON_Delete(root);
        }
    }

    if (s_state == PLIP_HOOK_OFF) {
        return send_json(req, "409 Conflict", "{\"error\":\"off-hook\"}");
    }
    if (s_ring_task) ring_stop();

    set_state(PLIP_HOOK_RINGING);
    if (xTaskCreate(ring_task, "plip_ring", 4096,
                    (void *) (intptr_t) duration_ms, 4, &s_ring_task) != pdPASS) {
        set_state(PLIP_HOOK_ON);
        return send_json(req, "500 Internal Server Error",
                         "{\"error\":\"ring task failed\"}");
    }
    char resp[64];
    snprintf(resp, sizeof(resp), "{\"ok\":true,\"duration_ms\":%d}", duration_ms);
    return send_json(req, "200 OK", resp);
}

static esp_err_t handle_stop_post(httpd_req_t *req)
{
    if (s_ring_task) ring_stop();
    if (s_state == PLIP_HOOK_RINGING) set_state(PLIP_HOOK_ON);
    return send_json(req, "200 OK", "{\"ok\":true}");
}

static esp_err_t handle_play_post(httpd_req_t *req)
{
    /* Audio on the BOX-3 flows through the voice-bridge WS TTS path; a file
     * player duplicating that would lie about capabilities. */
    return send_json(req, "501 Not Implemented",
                     "{\"error\":\"use the voice bridge TTS path\"}");
}

static esp_err_t handle_status_get(httpd_req_t *req)
{
    char resp[128];
    snprintf(resp, sizeof(resp),
             "{\"off_hook\":%s,\"ringing\":%s,\"playing\":false,"
             "\"dialed\":\"%s\"}",
             s_state == PLIP_HOOK_OFF ? "true" : "false",
             s_state == PLIP_HOOK_RINGING ? "true" : "false",
             s_dialed);
    return send_json(req, "200 OK", resp);
}

/* ---------- public API ---------- */

void plip_virtual_pickup(void)
{
    if (s_state != PLIP_HOOK_RINGING) return;
    ring_stop();
    s_dialed[0] = '\0';
    set_state(PLIP_HOOK_OFF);
    ESP_LOGI(TAG, "virtual pickup");
    hook_report("off", "pickup");
}

void plip_virtual_hangup(void)
{
    if (s_state != PLIP_HOOK_OFF) return;
    set_state(PLIP_HOOK_ON);
    ESP_LOGI(TAG, "virtual hangup");
    hook_report("on", "hangup");
}

void plip_virtual_dial(const char *number)
{
    if (s_state != PLIP_HOOK_OFF || !number || !*number) return;
    strlcpy(s_dialed, number, sizeof(s_dialed));
    char reason[32];
    snprintf(reason, sizeof(reason), "dial:%s", s_dialed);
    ESP_LOGI(TAG, "dialed \"%s\"", s_dialed);
    hook_report("off", reason);
}

bool plip_virtual_button_press(void)
{
    switch (s_state) {
    case PLIP_HOOK_RINGING:
        plip_virtual_pickup();
        return true;
    case PLIP_HOOK_OFF:
        plip_virtual_hangup();
        return true;
    default:
        return false;   /* on-hook, not ringing: not ours */
    }
}

void plip_virtual_register_state_cb(plip_state_cb_t cb)
{
    s_state_cb = cb;
}

plip_hook_state_t plip_virtual_state(void)
{
    return s_state;
}

esp_err_t plip_virtual_init(httpd_handle_t server, i2s_chan_handle_t spk)
{
    if (!server || !spk) return ESP_ERR_INVALID_ARG;
    s_spk = spk;

    s_hook_queue = xQueueCreate(HOOK_QUEUE_DEPTH, sizeof(hook_event_t));
    if (!s_hook_queue) return ESP_ERR_NO_MEM;
    if (xTaskCreate(hook_worker_task, "plip_hook", 4096, NULL, 3, NULL) != pdPASS) {
        vQueueDelete(s_hook_queue);
        s_hook_queue = NULL;
        return ESP_FAIL;
    }

    static const httpd_uri_t uri_ring = {
        .uri = "/ring", .method = HTTP_POST,
        .handler = handle_ring_post, .user_ctx = NULL,
    };
    static const httpd_uri_t uri_stop = {
        .uri = "/stop", .method = HTTP_POST,
        .handler = handle_stop_post, .user_ctx = NULL,
    };
    static const httpd_uri_t uri_play = {
        .uri = "/play", .method = HTTP_POST,
        .handler = handle_play_post, .user_ctx = NULL,
    };
    static const httpd_uri_t uri_status = {
        .uri = "/status", .method = HTTP_GET,
        .handler = handle_status_get, .user_ctx = NULL,
    };
    httpd_register_uri_handler(server, &uri_ring);
    httpd_register_uri_handler(server, &uri_stop);
    httpd_register_uri_handler(server, &uri_play);
    httpd_register_uri_handler(server, &uri_status);

    ESP_LOGI(TAG, "virtual PLIP up (POST /ring /stop /play, GET /status; "
                  "BOOT button = hook switch; master=%s)", CONFIG_ZACUS_MASTER_URL);
    return ESP_OK;
}
