#include "lamp_ui_dispatch.h"

#include <string.h>

static bool mode_is_selectable(lg_mode mode) {
    return mode == LG_OFF || mode == LG_AI || mode == LG_MANUAL;
}

static bool sequence_is_newer(uint32_t candidate, uint32_t previous) {
    const uint32_t delta = candidate - previous;
    return delta != 0u && delta < 0x80000000u;
}

static void fill_live_ack(const lamp_ui_dispatcher *dispatcher,
                          uint32_t sequence, lamp_ui_ack_status status,
                          bool duplicate, uint32_t now_ms,
                          lamp_ui_ack *ack) {
    const lg_guard *guard = dispatcher->guard;
    ack->sequence = sequence;
    ack->status = status;
    ack->selected_mode = dispatcher->selected_mode;
    ack->running = guard->armed && guard->mode != LG_FAULT;
    ack->remaining_ms = ack->running ? lg_remaining_ms(guard, now_ms) : 0u;
    ack->duplicate = duplicate;
}

bool lamp_ui_dispatcher_init(lamp_ui_dispatcher *dispatcher, lg_guard *guard,
                             uint64_t session_id, lg_mode selected_mode) {
    if (dispatcher == NULL || guard == NULL || session_id == 0u ||
        !mode_is_selectable(selected_mode)) {
        return false;
    }
    memset(dispatcher, 0, sizeof(*dispatcher));
    dispatcher->guard = guard;
    dispatcher->session_id = session_id;
    dispatcher->selected_mode = selected_mode;
    dispatcher->last_button = LUI_BUTTON_INVALID;
    return true;
}

bool lamp_ui_dispatcher_handle(lamp_ui_dispatcher *dispatcher,
                              const lamp_ui_event *event, uint32_t now_ms,
                              lamp_ui_ack *ack) {
    if (dispatcher == NULL || event == NULL || ack == NULL ||
        dispatcher->guard == NULL || dispatcher->session_id == 0u ||
        event->sequence == 0u) {
        return false;
    }

    if (event->session_id != dispatcher->session_id) {
        fill_live_ack(dispatcher, event->sequence, LUID_ACK_BAD_SESSION,
                      false, now_ms, ack);
        return true;
    }

    if (dispatcher->sequence_seen && event->sequence == dispatcher->last_sequence) {
        if (event->button != dispatcher->last_button) {
            fill_live_ack(dispatcher, event->sequence,
                          LUID_ACK_SEQUENCE_COLLISION, false, now_ms, ack);
            return true;
        }
        fill_live_ack(dispatcher, event->sequence, dispatcher->last_ack.status,
                      true, now_ms, ack);
        return true;
    }
    if (dispatcher->sequence_seen &&
        !sequence_is_newer(event->sequence, dispatcher->last_sequence)) {
        fill_live_ack(dispatcher, event->sequence, LUID_ACK_STALE_SEQUENCE,
                      false, now_ms, ack);
        return true;
    }

    /* Consume this fresh sequence before dispatch. A rejected request cannot
       be replayed later after readiness or fault state changes. */
    dispatcher->sequence_seen = true;
    dispatcher->last_sequence = event->sequence;
    dispatcher->last_button = event->button;

    const lg_mode effective_mode = dispatcher->guard->mode == LG_FAULT
                                       ? LG_FAULT
                                       : dispatcher->selected_mode;
    lamp_ui_command command;
    lamp_ui_ack_status status = LUID_ACK_INVALID_COMMAND;
    if (lamp_ui_resolve_button(effective_mode, event->button, &command)) {
        if (command.kind == LUI_COMMAND_STOP) {
            lg_stop(dispatcher->guard);
            status = LUID_ACK_ACCEPTED;
        } else if (command.kind == LUI_COMMAND_SET_MODE &&
                   command.mode == LG_MANUAL) {
            /* Stop first; mode acknowledgement must never leave an old run active. */
            lg_stop(dispatcher->guard);
            dispatcher->selected_mode = LG_MANUAL;
            status = LUID_ACK_ACCEPTED;
        } else if (command.kind == LUI_COMMAND_START_RECIPE) {
            /* A fresh preset replaces the active timed run. Stop first so the
               target guard's single-run invariant does not turn valid preset
               changes into NOT_READY; if the new arm fails, the old run stays off. */
            if (dispatcher->guard->armed) {
                lg_stop(dispatcher->guard);
            }
            if (lg_arm(dispatcher->guard, command.mode,
                       command.duration_ms, now_ms)) {
                dispatcher->selected_mode = command.mode;
                status = LUID_ACK_ACCEPTED;
            } else {
                status = LUID_ACK_NOT_READY;
            }
        }
    }

    fill_live_ack(dispatcher, event->sequence, status, false, now_ms, ack);
    dispatcher->last_ack = *ack;
    return true;
}
