/*
    lft_pathwatch.c — J1 path-change alert heuristic for watch mode (-W)

    Pure state machine classifying per-TTL hop observations across watch
    cycles.  No ncurses, session, or network dependencies — unit-testable
    standalone (tests/pathwatch_test.c).  Design and rule table:
    Output/LFT/j1-path-change-heuristic-design.md.

    This file is part of LFT.
*/
#include <stdio.h>
#include <string.h>
#include "lft_pathwatch.h"

/* An observation is "real" when it is a concrete IP, not a cloaked hop. */
static int pw_real(const char *ip)
{
    return ip && ip[0] && ip[0] != '*';
}

static int pw_in_set(const watch_pathwatch_t *pw, const char *ip)
{
    int i;
    for (i = 0; i < pw->n_known; i++)
        if (strcmp(pw->known_ips[i], ip) == 0)
            return 1;
    return 0;
}

void pw_note_ip(watch_pathwatch_t *pw, const char *ip)
{
    if (!pw_real(ip) || pw_in_set(pw, ip))
        return;
    if (pw->n_known >= PW_KNOWN_MAX)
        return;                         /* set full: membership frozen      */
    snprintf(pw->known_ips[pw->n_known], PW_IPLEN, "%s", ip);
    pw->n_known++;
}

int pw_update_armed(int armed, int stable_cycles, int cycle)
{
    if (armed)
        return 1;                       /* latch: never disarm mid-session  */
    return (stable_cycles >= 3 || cycle >= 8) ? 1 : 0;
}

void pw_reset(watch_pathwatch_t *pw)
{
    memset(pw, 0, sizeof(*pw));
}

pw_verdict_t pw_observe(watch_pathwatch_t *pw, char *confirmed,
                        const char *cur, int armed, char *flap_was)
{
    /* Rule 1: cloaked/empty observation.  Pending is deliberately
     * preserved: rate-limited hops blink, and hysteresis counts observed
     * sightings, not consecutive cycles.                                   */
    if (!pw_real(cur))
        return PW_IGNORE;

    /* Rules 2 + 3: new TTL slot, or still learning — accept silently.      */
    if (!pw_real(confirmed) || !armed) {
        snprintf(confirmed, PW_IPLEN, "%s", cur);
        pw_note_ip(pw, cur);
        return PW_DISCOVERY;
    }

    if (strcmp(cur, confirmed) == 0) {
        /* Rule 5: reverted to confirmed while a change was pending.        */
        if (pw->pending_ip[0]) {
            if (flap_was)
                snprintf(flap_was, PW_IPLEN, "%s", pw->pending_ip);
            pw->pending_ip[0] = '\0';
            pw->pending_cycles = 0;
            return PW_FLAP;
        }
        /* Rule 4: steady state.                                            */
        return PW_STABLE;
    }

    /* Rule 6: second observed sighting of the pending IP — confirmed path
     * change.  MUST be tested before set membership (rule 7): the pending
     * IP was noted into the set at first sighting, and membership-first
     * would misclassify every genuine reroute as an ECMP shift.            */
    if (pw->pending_ip[0] && strcmp(cur, pw->pending_ip) == 0) {
        pw->pending_cycles++;
        if (pw->pending_cycles >= 2) {
            snprintf(confirmed, PW_IPLEN, "%s", cur);
            pw->pending_ip[0] = '\0';
            pw->pending_cycles = 0;
            /* Reset the known set to {cur}: a later reversion to the old
             * path is a route restore and must alert, not read as ECMP.    */
            pw->n_known = 0;
            pw_note_ip(pw, cur);
            return PW_CONFIRMED;
        }
        return PW_PENDING;              /* unreachable at threshold 2, but
                                         * keeps the counter honest if the
                                         * threshold is ever raised         */
    }

    /* Rule 7: previously-seen IP at this TTL — multipath member shifting,
     * not a reroute.  Quiet; caller rate-caps the log via ecmp_logged.     */
    if (pw_in_set(pw, cur)) {
        snprintf(confirmed, PW_IPLEN, "%s", cur);
        pw->ecmp_logged++;
        pw->pending_ip[0] = '\0';
        pw->pending_cycles = 0;
        return PW_ECMP_SHIFT;
    }

    /* Rule 8: brand-new IP — start (or rebind) hysteresis.                 */
    snprintf(pw->pending_ip, PW_IPLEN, "%s", cur);
    pw->pending_cycles = 1;
    pw_note_ip(pw, cur);
    return PW_PENDING;
}
