/*
 * conversation.c — PLIP telephone conversation state machine.
 *
 * States: IDLE → DIALTONE → DIALING → RINGBACK | BUSY
 *         RINGBACK (after ~2 s) → GREET → CONNECTED
 *
 * Transitions:
 *   IDLE        + off-hook              → DIALTONE   (tones_dialtone_start)
 *   DIALTONE    + first digit           → DIALING    (tones_stop)
 *   DIALING     + ms_since_last > 3000  → RINGBACK or BUSY (route decision)
 *   RINGBACK    + 2 s elapsed           → GREET      (tones_stop, turn_client_greeting, play WAV)
 *   GREET       + play enqueued         → CONNECTED  (Stage 3: listen loop)
 *   any state   + on-hook              → IDLE        (tones_stop, audio_stop, PA off)
 *
 * Routing table (known numbers → ringback; unknown → busy after 3 s silence):
 *   "12", "3615", "15", "17", "18", "0142738200"
 */

#include "conversation.h"
#include "dialer.h"
#include "tones.h"
#include "audio.h"
#include "phone.h"
#include "slic.h"
#include "turn_client.h"
#include "plip_gamebook.h"
#if CONFIG_PLIP_DIAL_DTMF
#include "dtmf.h"
#endif

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "esp_heap_caps.h"

#include <stdio.h>
#include <string.h>

#if CONFIG_PLIP_VOICE_REPLY
/* Capture buffer sizing.
 * PSRAM target: 8 s of 16kHz mono S16 + 44-byte WAV header.
 *   8 s × 16000 samples/s × 2 bytes = 256000 bytes + 44 = 256044 → round to 256 KB.
 * Fallback (internal heap, 4 s max):
 *   4 s × 16000 × 2 + 44 = 128044 → round to 128 KB.
 */
#define CAPTURE_MAX_PSRAM   (256 * 1024)
#define CAPTURE_MAX_IRAM    (128 * 1024)
#define CAPTURE_MAX_MS_PSRAM  8000
#define CAPTURE_MAX_MS_IRAM   4000
#define CAPTURE_SILENCE_MS     900   /* end-of-speech: needs to ride through brief
                                      * mid-sentence pauses so a whole phrase is
                                      * captured (quiet handset voice). 900 ms. */
#define REPLY_POLL_MS          200   /* interval for checking hook during playback */
#define REPLY_PLAYBACK_EXTRA_MS 500  /* safety margin added to computed WAV duration */
#define BETWEEN_TURNS_MS       300   /* short pause between capture rounds */
#endif /* CONFIG_PLIP_VOICE_REPLY */

#define TAG "conversation"

/* Duration of ringback before picking up and fetching the greeting */
#define RINGBACK_GREET_MS  2000

typedef enum {
    STATE_IDLE,
    STATE_DIALTONE,
    STATE_DIALING,
    STATE_RINGBACK,
    STATE_BUSY,
    STATE_GREET,       /* fetching + playing NPC greeting (Stage 2) */
    STATE_CONNECTED,   /* in-call — Stage 3 will add listen loop     */
    STATE_GAMEBOOK,    /* audio "livre dont vous êtes le héros"      */
} conv_state_t;

static volatile conv_state_t s_state    = STATE_IDLE;
static volatile bool         s_offhook  = false;
static volatile bool         s_hook_changed = false;

/* Ringback start timestamp (µs, from esp_timer_get_time) */
static int64_t s_ringback_start_us = 0;

/* Session ID for the current call (generated at ringback → greet transition) */
static char s_sid[32] = {0};
/* Dialed number LOCKED at routing time. The dialer can keep accumulating
 * spurious rotary pulses (marginal hook contact) during the call, so we must
 * NOT re-read dialer_current() for the greeting/reply — that polluted number
 * would 404 at the gateway. Capture the clean routed number here once. */
static char s_number[16] = {0};
/* Scene this call hints on (e.g. "SCENE_WARNING"), locked at pickup like the
 * number. Empty for player-dialled calls. Drives the local SD hint clip. */
static char s_scene[40] = {0};

/* Incoming-call mode: an NPC "calls" the phone. conversation_arm_incoming()
 * stores the caller's persona number (and optional scene) and rings; when the
 * handset is then picked up from IDLE, we skip the outgoing dialtone/dial/
 * ringback and go straight to GREET with this number (the NPC speaks first). */
static char s_incoming_number[16] = {0};
static char s_incoming_scene[40] = {0};
static volatile bool s_incoming_armed = false;

/* Known numbers: ringback when dialed */
static const char *KNOWN[] = {
    "12", "3615", "15", "17", "18", "0142738200", NULL
};

static bool is_known(const char *num)
{
    for (int i = 0; KNOWN[i] != NULL; i++) {
        if (strcmp(num, KNOWN[i]) == 0) return true;
    }
    return false;
}

/* "SCENE_WARNING" → "warning": strip the SCENE_ prefix and lowercase, matching
 * the SD pack filenames from tools/tts/generate_plip_sd_pack.py. */
static void scene_slug(const char *scene, char *out, size_t cap)
{
    const char *p = scene;
    if (strncmp(p, "SCENE_", 6) == 0) p += 6;
    size_t i = 0;
    for (; p[i] && i + 1 < cap; i++) {
        char c = p[i];
        out[i] = (c >= 'A' && c <= 'Z') ? (char)(c - 'A' + 'a') : c;
    }
    out[i] = '\0';
}

static void go_idle(void)
{
    tones_stop();
    audio_stop();
    audio_pa_set(false);
    dialer_reset();
    plip_gamebook_end();   /* no-op if the gamebook wasn't running */
#if CONFIG_PLIP_DIAL_DTMF
    dtmf_stop();
#endif
    s_scene[0] = '\0';   /* clear scripted-call scene so a later dial-out call
                          * doesn't inherit it (CONNECTED would skip listening). */
    s_state = STATE_IDLE;
    ESP_LOGI(TAG, "-> IDLE");
}

/* Begin an answered INCOMING call: stop ringing, lock the persona number, and
 * jump straight to GREET (the NPC speaks first). Drives the conversation's own
 * s_offhook directly because phone.c's debounced GPIO detection is unreliable
 * during ringing — we trust the SLIC's raw off-hook reading instead. */
static void enter_incoming_greet(void)
{
    s_incoming_armed = false;
    phone_ring_stop();
    slic_ring_stop();   /* belt-and-suspenders: kill the physical bell directly in
                         * case phone.c's s_ringing flag desynced from slic.c's
                         * (otherwise phone_ring_stop early-returns and the bell
                         * keeps ringing through the answered call). */
    tones_stop();
    dialer_reset();
    s_offhook = true;
    snprintf(s_number, sizeof(s_number), "%s", s_incoming_number);
    snprintf(s_scene, sizeof(s_scene), "%s", s_incoming_scene);
    snprintf(s_sid, sizeof(s_sid), "%lld", (long long)esp_timer_get_time());
    audio_pa_set(true);
    s_state = STATE_GREET;
    ESP_LOGI(TAG, "INCOMING pickup -> GREET num=%s sid=%s", s_number, s_sid);
}

static void conv_task(void *arg)
{
    (void)arg;
    ESP_LOGI(TAG, "conversation task ready");

    for (;;) {
        vTaskDelay(pdMS_TO_TICKS(50));

        /* Handle hook change events */
        if (s_hook_changed) {
            s_hook_changed = false;
            if (!s_offhook) {
                /* On-hook: always go idle regardless of current state */
                if (s_state != STATE_IDLE) {
                    ESP_LOGI(TAG, "on-hook -> IDLE");
                    go_idle();
                }
                continue;
            } else {
                /* Off-hook from idle */
                if (s_state == STATE_IDLE) {
                    if (s_incoming_armed) {
                        /* INCOMING call answered (phone.c detected the edge). */
                        enter_incoming_greet();
                    } else if (plip_gamebook_available()) {
                        /* Gamebook phone: a story pack is on the SD → pick up
                         * launches the audio "livre dont vous êtes le héros".
                         * Digits dialed choose the story then the answers. */
                        dialer_reset();
#if CONFIG_PLIP_DIAL_DTMF
                        dtmf_start();
#endif
                        plip_gamebook_begin();
                        s_state = STATE_GAMEBOOK;
                        ESP_LOGI(TAG, "off-hook -> GAMEBOOK");
                    } else {
                        /* Outgoing call: dial tone, wait for digits. */
                        dialer_reset();
                        tones_dialtone_start();
#if CONFIG_PLIP_DIAL_DTMF
                        dtmf_start();
#endif
                        s_state = STATE_DIALTONE;
                        ESP_LOGI(TAG, "off-hook -> DIALTONE");
                    }
                }
                continue;
            }
        }

        /* State machine polling */
        switch (s_state) {
        case STATE_IDLE:
            /* Incoming-call pickup: phone.c's debounced edge detection is
             * unreliable while the bell rings, so poll the SLIC's raw off-hook
             * line directly — it reads the pickup cleanly mid-ring. */
            if (s_incoming_armed && slic_is_offhook()) {
                enter_incoming_greet();
            }
            break;

        case STATE_DIALTONE:
            if (!s_offhook) {
                go_idle();
                break;
            }
            /* First digit received → stop dialtone, enter dialing */
            if (!dialer_idle()) {
                tones_stop();
                s_state = STATE_DIALING;
                ESP_LOGI(TAG, "first digit -> DIALING");
            }
            break;

        case STATE_GAMEBOOK:
            if (!s_offhook) {
                go_idle();
                break;
            }
            /* Each dialed digit drives one choice. Consume a single digit at a
             * time (take the first, reset) so the player can dial again right
             * away — even over the narration, which gets interrupted. */
            if (!dialer_idle()) {
                int d = dialer_current()[0] - '0';
                dialer_reset();
                plip_gamebook_feed_digit(d);
            }
            break;

        case STATE_DIALING:
            if (!s_offhook) {
                go_idle();
                break;
            }
            /* Wait for 3 s of silence after last digit, then route */
            if (dialer_ms_since_last() > 3000) {
                const char *num = dialer_current();
                if (is_known(num)) {
                    ESP_LOGI(TAG, "route %s -> known (ringback)", num);
                    /* Lock the routed number now — the dialer may pick up
                     * spurious pulses later and we must keep posting "17". */
                    snprintf(s_number, sizeof(s_number), "%s", num);
                    tones_ringback_start();
                    s_ringback_start_us = esp_timer_get_time();
                    s_state = STATE_RINGBACK;
                } else {
                    ESP_LOGI(TAG, "route %s -> unknown (busy)", num);
                    tones_busy_start();
                    s_state = STATE_BUSY;
                }
            }
            break;

        case STATE_RINGBACK:
            if (!s_offhook) {
                go_idle();
                break;
            }
            {
                int64_t elapsed_ms =
                    (esp_timer_get_time() - s_ringback_start_us) / 1000;
                if (elapsed_ms >= RINGBACK_GREET_MS) {
                    /* Stop ringback tone synchronously before fetching */
                    tones_stop();
#if CONFIG_PLIP_DIAL_DTMF
                    /* Disarm DTMF before entering voice-capture phase */
                    dtmf_stop();
#endif
                    /* Generate a session ID from timer ticks */
                    snprintf(s_sid, sizeof(s_sid), "%lld",
                             (long long)esp_timer_get_time());
                    ESP_LOGI(TAG, "ringback done -> GREET (sid=%s num=%s)",
                             s_sid, dialer_current());
                    s_state = STATE_GREET;
                }
            }
            break;

        case STATE_GREET:
            if (!s_offhook) {
                go_idle();
                break;
            }
            /* Prefer the local SD voice pack (no gateway, no model in RAM):
             * play /sdcard/voice/greet_<number>.wav when present. Fall back to
             * the live gateway greeting only when the pack has no clip for this
             * NPC number (see tools/tts/generate_plip_sd_pack.py). */
            {
                char sd_greet[64];
                snprintf(sd_greet, sizeof(sd_greet),
                         "/sdcard/voice/greet_%s.wav", s_number);
                FILE *tf = NULL;
                if (audio_ensure_sd() && (tf = fopen(sd_greet, "rb")) != NULL) {
                    fclose(tf);
                    audio_play_async(sd_greet);
                    ESP_LOGI(TAG, "GREET: local SD pack %s", sd_greet);
                } else if (turn_client_greeting(s_sid, s_number,
                                                "/spiffs/turn.wav")) {
                    audio_play_async("/spiffs/turn.wav");
                } else {
                    ESP_LOGW(TAG, "no SD clip + gateway greeting failed — silent");
                }
            }
            /* Scripted offline call: chain this scene's hint from the SD pack
             * right after the greeting (the audio queue plays them back-to-back).
             * Fully local — no gateway, no model. */
            if (s_scene[0] != '\0' && audio_ensure_sd()) {
                char slug[40], hint[80];
                scene_slug(s_scene, slug, sizeof(slug));
                snprintf(hint, sizeof(hint),
                         "/sdcard/voice/hint_%s_l1_0.wav", slug);
                FILE *hf = fopen(hint, "rb");
                if (hf != NULL) {
                    fclose(hf);
                    audio_play_async(hint);
                    ESP_LOGI(TAG, "GREET: chained SD hint %s", hint);
                } else {
                    ESP_LOGI(TAG, "GREET: no SD hint for scene %s (%s)",
                             s_scene, hint);
                }
            }
            s_state = STATE_CONNECTED;
            ESP_LOGI(TAG, "-> CONNECTED");
            break;

        case STATE_CONNECTED:
            /* Scripted offline call (greeting + SD hint already queued): there's
             * no live two-way conversation to run — just let the clips play out
             * and wait for the player to hang up. Skips the gateway/model listen
             * loop entirely. The global on-hook check returns us to IDLE. */
            if (s_scene[0] != '\0') {
                vTaskDelay(pdMS_TO_TICKS(REPLY_POLL_MS));
                break;
            }
#if CONFIG_PLIP_VOICE_REPLY
            /*
             * Stage 3 — LISTEN loop.
             *
             * Allocate capture buffer once from PSRAM (preferred) or internal
             * heap.  Then loop: capture → POST reply → play → wait → repeat.
             * Exit on any on-hook event.  Buffer freed before leaving.
             */
            {
                /* --- Allocate capture buffer -------------------------------- */
                uint8_t *cap_buf   = NULL;
                size_t   cap_max   = 0;
                int      cap_ms    = 0;

                cap_buf = heap_caps_malloc(CAPTURE_MAX_PSRAM, MALLOC_CAP_SPIRAM);
                if (cap_buf) {
                    cap_max = CAPTURE_MAX_PSRAM;
                    cap_ms  = CAPTURE_MAX_MS_PSRAM;
                    ESP_LOGI(TAG, "listen: cap_buf %zu B from PSRAM", cap_max);
                } else {
                    cap_buf = malloc(CAPTURE_MAX_IRAM);
                    if (cap_buf) {
                        cap_max = CAPTURE_MAX_IRAM;
                        cap_ms  = CAPTURE_MAX_MS_IRAM;
                        ESP_LOGW(TAG, "listen: PSRAM unavail, cap_buf %zu B from heap (max %d s)",
                                 cap_max, cap_ms / 1000);
                    } else {
                        ESP_LOGE(TAG, "listen: cap_buf alloc failed — staying silent");
                        /* Remain in CONNECTED without looping */
                        if (!s_offhook) go_idle();
                        break;
                    }
                }

                /* --- LISTEN loop ------------------------------------------- */
                ESP_LOGI(TAG, "listen: entering loop (max %d s / silence %d ms)",
                         cap_ms, CAPTURE_SILENCE_MS);

                while (s_offhook) {
                    /* HALF-DUPLEX: a telephone handset couples the earpiece into
                     * the mic. Never capture while anything is playing, or the
                     * playback feeds back and the line saturates. Wait for the
                     * greeting/filler/reply to finish, then let the line settle. */
                    vTaskDelay(pdMS_TO_TICKS(120));   /* let a just-queued clip start (was 250) */
                    while (s_offhook && audio_is_playing()) {
                        vTaskDelay(pdMS_TO_TICKS(50));
                    }
                    if (!s_offhook) break;
                    vTaskDelay(pdMS_TO_TICKS(200));   /* let the I2S DMA tail drain */

                    /* Capture the caller utterance with the PA LEFT ON. The loop
                     * already waits for any playback to finish above, so nothing
                     * is driving the earpiece during capture — there is no echo to
                     * mute. Cutting the PA here was found to collapse the mic level
                     * (captures came back near-silent / DC only), so keep it on. */
                    int n = audio_capture_wav(cap_buf, cap_max,
                                              cap_ms, CAPTURE_SILENCE_MS);
                    if (!s_offhook) break;   /* hung up during capture */

                    if (n <= 44) {
                        ESP_LOGD(TAG, "listen: no voice (n=%d)", n);
                        continue;
                    }

                    ESP_LOGI(TAG, "listen: captured %d bytes, posting to gateway", n);

                    /* Filler "un instant, je traite votre demande" plays while the
                     * reply is synthesised (the POST blocks for several seconds). */
                    audio_play_async("/spiffs/wait.wav");

                    esp_err_t ret = turn_client_reply(s_sid, s_number,
                                                      cap_buf, (size_t)n,
                                                      "/spiffs/reply.wav");
                    if (!s_offhook) break;   /* hung up during HTTP round-trip */

                    if (ret != ESP_OK) {
                        ESP_LOGW(TAG, "listen: turn_client_reply failed (%s) — skipping",
                                 esp_err_to_name(ret));
                        continue;
                    }

                    /* Let the filler finish before the reply (no overlap), then play
                     * the reply. The loop top waits for it to end before re-capturing. */
                    while (s_offhook && audio_is_playing()) {
                        vTaskDelay(pdMS_TO_TICKS(50));
                    }
                    if (!s_offhook) break;
                    audio_play_async("/spiffs/reply.wav");
                }

                /* --- Cleanup ----------------------------------------------- */
                free(cap_buf);
                ESP_LOGI(TAG, "listen: loop exited (offhook=%d)", (int)s_offhook);

                if (!s_offhook) go_idle();
            }
#else
            /* Stage 3 disabled — STATE_CONNECTED is terminal */
            if (!s_offhook) {
                go_idle();
            }
#endif /* CONFIG_PLIP_VOICE_REPLY */
            break;

        case STATE_BUSY:
            if (!s_offhook) {
                go_idle();
            }
            break;
        }
    }
}

void conversation_init(void)
{
    s_state        = STATE_IDLE;
    s_offhook      = false;
    s_hook_changed = false;
    /* Stack: STATE_GREET needs 6144 (esp_http_client + file I/O).
     * STATE_CONNECTED listen loop (Stage 3) adds turn_client_reply (~1100 B
     * locals) + stat() call → bump to 8192 when Stage 3 is compiled in. */
#if CONFIG_PLIP_VOICE_REPLY
    xTaskCreate(conv_task, "conv", 8192, NULL, 4, NULL);
#else
    xTaskCreate(conv_task, "conv", 6144, NULL, 4, NULL);
#endif
    ESP_LOGI(TAG, "conversation init");
}

void conversation_on_hook_change(bool offhook)
{
    s_offhook      = offhook;
    s_hook_changed = true;
}

void conversation_arm_incoming(const char *number, const char *scene)
{
    snprintf(s_incoming_number, sizeof(s_incoming_number), "%s",
             (number && number[0]) ? number : "17");
    snprintf(s_incoming_scene, sizeof(s_incoming_scene), "%s",
             (scene && scene[0]) ? scene : "");
    s_incoming_armed = true;
    phone_ring_start();
    ESP_LOGI(TAG, "incoming call armed (num=%s scene=%s) — ringing until pickup",
             s_incoming_number, s_incoming_scene[0] ? s_incoming_scene : "-");
}
