#include "lamp_guard.h"
#include <limits.h>
#include <stddef.h>
#include <string.h>

static void clear_output(lg_guard *g) {
    g->output_enable = false;
    g->frame_active = false;
    memset(g->pwm, 0, sizeof(g->pwm));
}

static void disarm(lg_guard *g, lg_mode mode) {
    clear_output(g);
    g->armed = false;
    g->pending_ticket = false;
    g->hand_present = false;
    g->vision_status_seen = false;
    g->mode = mode;
}

static bool interlocks_ok(const lg_guard *g) {
    return g->inputs.temperature_ok && g->inputs.supply_ok && g->inputs.led_driver_ok &&
           g->inputs.hardware_interlock_ok;
}

/* Intervals must be below 2^31 ms. Poll gap policing prevents ambiguity across
   rollover as long as poll is serviced; a hardware watchdog covers CPU stalls. */
static bool service(lg_guard *g, uint32_t now) {
    if (!g || !g->configured) return false;
    uint32_t gap = now - g->last_poll_ms;
    if (gap > g->cfg.max_poll_gap_ms) {
        disarm(g, LG_FAULT);
        return false;
    }
    if (g->armed && (uint32_t)(now - g->run_started_ms) >= g->run_duration_ms) {
        disarm(g, LG_OFF);
    }
    if (g->pending_ticket &&
        (uint32_t)(now - g->ticket_issued_ms) >= g->cfg.ticket_lifetime_ms) {
        g->pending_ticket = false;
    }
    if (g->vision_status_seen &&
        (uint32_t)(now - g->vision_status_ms) >= g->cfg.ticket_lifetime_ms) {
        g->vision_status_seen = false;
        g->hand_present = false;
        clear_output(g);
    }
    if (g->frame_active && g->mode == LG_AI &&
        (uint32_t)(now - g->frame_issued_ms) >= g->cfg.ticket_lifetime_ms) {
        clear_output(g);
    }
    return g->armed && interlocks_ok(g);
}

bool lg_init(lg_guard *g, const lg_config *cfg, uint64_t session,
             uint32_t now) {
    if (!g) return false;
    /* cfg must not alias storage inside g. */
    lg_config copy;
    if (cfg) copy = *cfg;
    memset(g, 0, sizeof(*g));
    g->mode = LG_FAULT;
    if (!cfg || session == 0 || copy.channels == 0 ||
        copy.channels > LG_MAX_CHANNELS || copy.calibration_id == 0 ||
        copy.max_run_ms == 0 || copy.max_run_ms >= INT32_MAX ||
        copy.ticket_lifetime_ms == 0 || copy.ticket_lifetime_ms >= INT32_MAX ||
        copy.max_poll_gap_ms == 0 || copy.max_poll_gap_ms >= INT32_MAX ||
        copy.max_poll_gap_ms > copy.ticket_lifetime_ms ||
        copy.input_budget_mw <= copy.fixed_input_mw) return false;
    for (uint16_t i = 0; i < copy.channels; ++i) {
        if (copy.channel_input_mw[i] == 0) return false;
    }
    g->cfg = copy;
    g->configured = true;
    g->session_id = session;
    g->last_poll_ms = now;
    g->mode = LG_OFF;
    return true;
}

void lg_stop(lg_guard *g) {
    if (!g) return;
    /* A user stop must not clear a fault latch. */
    disarm(g, g->mode == LG_FAULT ? LG_FAULT : LG_OFF);
}

void lg_poll(lg_guard *g, uint32_t now, lg_inputs inputs) {
    if (!g || !g->configured) return;
    /* Service before advancing last_poll to detect stalls. */
    (void)service(g, now);
    g->last_poll_ms = now;
    g->inputs = inputs;
    if (!inputs.infrared_hand_present) {
        /* Removal is normal absence, not a fault. Do not reset the run timer.
         * Cancel outstanding vision permission, so a late packet cannot relight. */
        clear_output(g);
        g->pending_ticket = false;
        g->hand_present = false;
        g->vision_status_seen = false;
    }
    if (!inputs.temperature_ok || !inputs.supply_ok || !inputs.led_driver_ok ||
        !inputs.hardware_interlock_ok) {
        disarm(g, LG_FAULT);
    }
}

bool lg_arm(lg_guard *g, lg_mode mode, uint32_t duration, uint32_t now) {
    if (!g || !g->configured) return false;
    (void)service(g, now);
    uint32_t mode_limit = mode == LG_AI ? LG_AI_MAX_RUN_MS :
                          mode == LG_MANUAL ? LG_MANUAL_MAX_RUN_MS : 0u;
    if (g->mode == LG_FAULT || g->armed || !interlocks_ok(g) ||
        (mode != LG_AI && mode != LG_MANUAL) || duration == 0 ||
        duration > g->cfg.max_run_ms || duration > mode_limit) return false;
    clear_output(g);
    g->pending_ticket = false;
    g->mode = mode;
    g->armed = true;
    g->run_started_ms = now;
    g->run_duration_ms = duration;
    return true;
}

uint32_t lg_remaining_ms(const lg_guard *g, uint32_t now) {
    if (!g || !g->configured || !g->armed || g->run_duration_ms == 0u)
        return 0u;
    uint32_t elapsed = (uint32_t)(now - g->run_started_ms);
    if (elapsed >= g->run_duration_ms) return 0u;
    return g->run_duration_ms - elapsed;
}

bool lg_issue_ticket(lg_guard *g, uint32_t now, lg_frame *ticket) {
    if (!g || !ticket || !service(g, now) ||
        !g->inputs.infrared_hand_present ||
        (g->mode != LG_AI && g->mode != LG_MANUAL)) return false;
    if (g->ticket_counter == UINT32_MAX ||
        (g->source_frame_seen && g->last_source_frame_seq == UINT64_MAX)) {
        disarm(g, LG_FAULT); /* Never wrap and accept an old ticket. */
        return false;
    }
    ++g->ticket_counter;
    g->ticket_issued_ms = now;
    g->pending_ticket = true;
    memset(ticket, 0, sizeof(*ticket));
    ticket->session_id = g->session_id;
    ticket->ticket_id = g->ticket_counter;
    g->ticket_min_source_frame_seq = g->source_frame_seen
        ? g->last_source_frame_seq + 1u : 1u;
    ticket->min_source_frame_seq = g->ticket_min_source_frame_seq;
    ticket->calibration_id = g->cfg.calibration_id;
    ticket->channels = g->cfg.channels;
    return true;
}

static bool power_within_budget(const lg_guard *g, const uint8_t *pwm) {
    uint64_t numerator = (uint64_t)g->cfg.fixed_input_mw * 255u;
    for (uint16_t i = 0; i < g->cfg.channels; ++i) {
        numerator += (uint64_t)g->cfg.channel_input_mw[i] * pwm[i];
    }
    return numerator <= (uint64_t)g->cfg.input_budget_mw * 255u;
}

static void apply(lg_guard *g, const uint8_t *pwm) {
    memcpy(g->pwm, pwm, g->cfg.channels);
    memset(g->pwm + g->cfg.channels, 0, LG_MAX_CHANNELS - g->cfg.channels);
    g->output_enable = false;
    for (uint16_t i = 0; i < g->cfg.channels; ++i) {
        if (g->pwm[i] != 0) g->output_enable = true;
    }
}

bool lg_accept_ai(lg_guard *g, const lg_frame *f, uint32_t now) {
    if (!g || !f || !service(g, now) ||
        !g->inputs.infrared_hand_present ||
        (g->mode != LG_AI && g->mode != LG_MANUAL)) return false;
    const uint8_t zero_pwm[LG_MAX_CHANNELS] = {0};
    const bool pwm_is_zero = memcmp(f->pwm, zero_pwm, g->cfg.channels) == 0;
    if (!g->pending_ticket || f->session_id != g->session_id ||
        f->ticket_id != g->ticket_counter || f->source_frame_seq == 0u ||
        f->source_frame_seq < g->ticket_min_source_frame_seq ||
        f->source_frame_seq <= f->capture_baseline_seq ||
        (g->source_frame_seen && f->capture_baseline_seq < g->last_source_frame_seq) ||
        (g->source_frame_seen && f->source_frame_seq <= g->last_source_frame_seq) ||
        f->channels != g->cfg.channels ||
        f->calibration_id != g->cfg.calibration_id ||
        !power_within_budget(g, f->pwm) ||
        (!pwm_is_zero && g->mode == LG_MANUAL) ||
        (!f->hand_present && !pwm_is_zero)) {
        clear_output(g);
        g->hand_present = false;
        g->vision_status_seen = false;
        /* A rejected packet cannot leave presence or activation permission live. */
        g->pending_ticket = false;
        return false;
    }
    g->hand_present = f->hand_present;
    g->vision_status_seen = true;
    /* Like AI PWM, presence freshness is anchored to ticket issuance. */
    g->vision_status_ms = g->ticket_issued_ms;
    if (f->hand_present && g->mode == LG_AI) {
        apply(g, f->pwm);
        g->frame_active = true;
    } else if (!f->hand_present) {
        clear_output(g);
    }
    g->source_frame_seen = true;
    g->last_source_frame_seq = f->source_frame_seq;
    /* Deadline starts at MCU ticket issuance, not at delayed packet receipt. */
    g->frame_issued_ms = g->ticket_issued_ms;
    g->pending_ticket = false;
    return true;
}

bool lg_set_manual(lg_guard *g, const uint8_t *pwm, uint16_t count,
                   uint32_t now) {
    if (!g || !pwm || !service(g, now) || g->mode != LG_MANUAL) return false;
    if (!g->inputs.infrared_hand_present || !g->hand_present ||
        count != g->cfg.channels ||
        !power_within_budget(g, pwm)) {
        clear_output(g);
        return false;
    }
    apply(g, pwm);
    return true;
}
