// ============================================================================
// gps_module.cpp — see gps_module.h for design notes.
// ============================================================================

#include "gps_module.h"
#include "config.h"   // board-conditional pin map (TDECK_GPS_RX/TX, PDL_BOARD_TDECK)

#ifdef ARDUINO

#include <Arduino.h>
#include <HardwareSerial.h>
#include <TinyGPS++.h>

// ###########################################################################
// THESE PINS ARE HELTEC **V4** ONLY — THEY DO NOT WORK ON A V3.
//
// RX=39 / TX=38 are the V4's integrated JST-SH GNSS connector. The V3 has no
// GNSS connector and does NOT break GPIO 38/39 out on its header at all (V3
// header exposes: 1-21, 35, 36, 41-44, 48).
//
// This comment previously said only "CONFIRMED WORKING (Spencer01)" without
// naming the board. Spencer01 is a V4. On 2026-08-02 that ambiguity caused a
// NEO-6M to be hand-wired to 38/39 on a V3 (Spencer03) — it read 0 bytes for
// 949 s across ~8 full scan sweeps, because those pins simply are not present.
//
// For a hand-wired GPS on a **V3**, use a pair that is BOTH on the header and
// in kGpsPinCandidates below. Valid options: {5,6}, {3,4}, {7,15}, {2,48}.
// Prefer {5,6}: GPIO3 is a JTAG strapping pin and GPIO48 is LED_BUILTIN.
// ###########################################################################
//
// CONFIRMED WORKING 2026-07-26 on real HELTEC V4 hardware (Spencer01): ESP32
// RX=GPIO39, TX=GPIO38, GPS_EN=GPIO34 held LOW. Found via direct multimeter
// continuity from the V4's JST-SH connector's wires to board pads (green=38,
// blue=39, black=GND), then verified live: 184 bytes of sustained UART traffic on
// first try at this pairing, settling to ~3000 bytes processed with only 2
// checksum failures (i.e. genuine, correctly-framed NMEA 0183 — not noise).
// None of the earlier doc-sourced guesses (GPIO34/33 "V3 GPS pins", GPIO48/47
// per two independent Heltec/Meshtastic wiring references) were ever
// physically correct for this unit's GNSS header — every prior scan cycle
// came up empty because the real pins simply weren't in the candidate list
// yet. Scanner left enabled (self-locking, stops once real traffic is found)
// as a safety net for other physical units that may be wired differently;
// set GPS_SCAN_MODE to 0 to skip straight to the confirmed pins below.
// ---------------------------------------------------------------------------
// Default pins, per board. GPS_SCAN_MODE 0 = open these directly at boot and
// never sweep; 1 = autoscan the candidate list.
//
// V3: scanning is DISABLED. GPIO 5/6 is the permanent hand-wired GPS pairing,
// verified live on Spencer03 2026-08-02 (ATGM336H: 3,893 bytes in 4 s, 7
// checksum failures, TinyGPS++ parsing cleanly). A fixed install has no reason
// to sweep — scanning tears the UART down and rebuilds it every 4 s, resets the
// byte counter each time (which made diagnostics actively misleading), and on
// this board historically wandered onto flash pins. Opening the known pair once
// at boot removes all of that.
//
// V4: keeps 39/38, which is its integrated JST-SH GNSS connector, plus the
// scan as a fallback for units wired differently.
// ---------------------------------------------------------------------------
#ifdef PDL_BOARD_HELTEC_V3
#  define GPS_SCAN_MODE 0
#  define GPS_RX_PIN_CONFIRMED  5    // GPS module TX -> ESP32 RX
#  define GPS_TX_PIN_CONFIRMED  6    // GPS module RX -> ESP32 TX
#else
#  define GPS_SCAN_MODE 1
#  define GPS_RX_PIN_CONFIRMED  39
#  define GPS_TX_PIN_CONFIRMED  38
#endif

#if GPS_SCAN_MODE
// baud == 0 means "use GPS_BAUD", so every pre-existing entry below keeps its
// original meaning without being touched.
struct GpsPinCandidate { int rx, tx; uint32_t baud; };

#ifdef PDL_BOARD_TDECK
// ---------------------------------------------------------------------------
// LilyGo T-Deck Plus — the Heltec candidate sweep below is ACTIVELY DESTRUCTIVE
// on this board and must never run here.
//
// Observed live on TDeck01 (2026-08-02): the Heltec list drives GPIO34 as
// GPS_EN and then probes 38/39/40/42. On a T-Deck those are:
//     34  -> OCTAL PSRAM data line   (this is the fatal one)
//     38  -> LoRa MISO
//     39  -> microSD CS
//     40  -> LoRa SCK
//     42  -> display backlight
// Driving GPIO34 corrupted PSRAM access and the unit crashed in
// gps_init() with
//     assert failed: spinlock_acquire spinlock.h:122
// then boot-looped continuously — the screen appeared to "flash" because the
// boot splash was being redrawn every cycle.
//
// The T-Deck Plus GPS is on a dedicated UART with no scanning required, and
// it has NO separate enable pin: the module powers from the GPIO10 board rail
// asserted in setup(). So: one known pair, and GPS_EN compiled out entirely.
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// BAUD, NOT WIRING. The T-Deck Plus ships with EITHER of two GNSS modules, and
// they do not run at the same speed:
//
//     Quectel L76K       ->  9600 baud
//     u-blox MIA-M10Q    -> 38400 baud   <-- the common one on current stock
//
// Both sit on the same GPIO44/43 UART (LilyGo repurposed the original T-Deck's
// Grove pins for GPS on the Plus), so the wiring gives no hint which is fitted.
// LilyGo's own reference code probes L76K first and falls back to M10Q at a
// different baud; Meshtastic hit the identical bug and fixed it in PR #4842
// after units reported "No GPS Present" until the probe reached 38400.
//
// This firmware hardcoded 9600 for every board. On an M10Q that yields a
// steady ~200 bytes/s of correctly-clocked-looking garbage: enough to satisfy
// the old byte-count lock, never enough to parse a single satellite. TDeck02
// showed exactly that — live NMEA traffic, 0 satellites, forever — and the
// antenna got pulled apart chasing a fault that was in this file.
//
// M10Q first: it is the variant actually on the current Plus boards.
// ---------------------------------------------------------------------------
static const GpsPinCandidate kGpsPinCandidates[] = {
    {TDECK_GPS_RX, TDECK_GPS_TX, 38400},  // RX=44 TX=43, u-blox MIA-M10Q
    {TDECK_GPS_RX, TDECK_GPS_TX,  9600},  // RX=44 TX=43, Quectel L76K
    {TDECK_GPS_TX, TDECK_GPS_RX, 38400},  // swapped, in case a unit is wired the other way
    {TDECK_GPS_TX, TDECK_GPS_RX,  9600},
};
#else
#ifdef PDL_BOARD_HELTEC_V3
// ---------------------------------------------------------------------------
// Heltec V3 — RESTRICTED, SAFE-PIN LIST.
//
// The V4 list below MUST NOT run on a V3. Per Heltec's official GPIO usage
// guide, on the V3:
//     GPIO 33-38  = SPI Flash / SubSPI      <-- MUST NOT be driven
//     GPIO 0,3,45,46,9,10 = strapping pins
//     GPIO 43,44  = USB serial download
//     GPIO 21     = OLED reset
//     GPIO 39-42  = JTAG
// The V4 list opens {39,38} FIRST and re-tries {38,x}/{34,33} every sweep —
// i.e. it reconfigures SPI-flash pins as a UART every 4 seconds on a board
// where those pins are flash. That is wrong regardless of whether a GPS is
// attached, and it was doing so on Spencer03 continuously.
//
// Heltec explicitly recommends these for external peripherals:
//     GPIO 1, 2, 4, 5, 6, 7, 19, 20, 47, 48
// Only pairs drawn from that set appear below, each in both orientations so a
// swapped TX/RX still self-corrects.
// ---------------------------------------------------------------------------
static const GpsPinCandidate kGpsPinCandidates[] = {
    {5, 6},   {6, 5},
    {4, 5},   {5, 4},
    {6, 7},   {7, 6},
    {2, 1},   {1, 2},
    {19, 20}, {20, 19},
    {47, 48}, {48, 47},
    {4, 7},   {7, 4},
};
#else
// Round 2: every previously-tried pair AND its RX/TX swapped — a swapped
// UART pair is one of the most common wiring mistakes and was never
// actually tested (each pair only tried in one fixed order before).
//
// NOTE: this is the HELTEC V4 list. It includes GPIO 33-42, which are flash
// and JTAG on a V3 — see the V3 block above.
static const GpsPinCandidate kGpsPinCandidates[] = {
    // CONFIRMED — RX=39/TX=38 is the real wiring (see note above). Tried
    // first so a freshly-booted unit locks on within one 4s window instead
    // of waiting through the rest of this historical candidate list.
    {39, 38},
    // Round 3: remaining pins identified by direct multimeter continuity from
    // the JST-SH connector's individual wires to labeled board pads
    // (2026-07-26) — black=GND confirmed, green=38, blue=39, yellow=40,
    // red=42 (white read continuity to BOTH 3V3 and GND, so it's not a clean
    // single-net signal pin — likely RESET/VBAT tied through a pull, left
    // untested here). Kept as a fallback for other physical units.
    {38, 39},
    {38, 40},
    {40, 38},
    {38, 42},
    {42, 38},
    {39, 40},
    {40, 39},
    {39, 42},
    {42, 39},
    {40, 42},
    {42, 40},
    {48, 47},   // sourced Meshtastic/Heltec pinout
    {47, 48},   // ...swapped
    {34, 33},
    {33, 34},
    {3, 4},
    {4, 3},
    {5, 6},
    {6, 5},
    {7, 15},
    {15, 7},
    {16, 45},
    {45, 16},
    {46, 47},
    {47, 46},
    {48, 2},
    {2, 48},
};
#endif // PDL_BOARD_HELTEC_V3 (V3 safe-pin list vs V4 full list)
#endif // PDL_BOARD_TDECK
#define GPS_SCAN_CANDIDATE_COUNT (sizeof(kGpsPinCandidates) / sizeof(kGpsPinCandidates[0]))
#define GPS_SCAN_WINDOW_MS   4000
static int      s_scan_idx = 0;
static uint32_t s_scan_started_ms = 0;
static bool     s_scan_locked = false;
static uint32_t s_scan_valid_base = 0;   // passedChecksum() at candidate start
#endif

// GPS_EN — a SEPARATE power-enable pin, not a UART pin. CONFIRMED 2026-07-26:
// LOW = enabled on this unit (P-channel high-side MOSFET switch, opposite
// sense from the N-channel switches most designs use) — verified live via
// sustained real GPS UART traffic immediately after the auto-polarity logic
// flipped to LOW. Starting on this confirmed polarity so a fresh boot
// doesn't spend 6s on the wrong guess first; auto-flip logic left in place
// as a fallback for other physical units that may wire the MOSFET the
// other way.
// GPS_EN exists ONLY on the Heltec V4, whose integrated GNSS module is behind
// a high-side switch on GPIO34.
//
// GPIO34 MUST NOT BE DRIVEN on the other two boards:
//   * Heltec V3  -> GPIO 33-38 are SPI FLASH / SubSPI (Heltec GPIO guide).
//   * T-Deck     -> GPIO 33-37 are OCTAL PSRAM data lines.
// On both, a hand-wired or on-board GPS is powered from a permanent rail and
// needs no enable line at all.
//
// This was previously gated only on !PDL_BOARD_TDECK, so a V3 build still ran
// pinMode(34, OUTPUT) + digitalWrite(34, ...) at init AND flipped that pin
// every 6 s in gps_tick()'s polarity-search. That is a continuous read/write
// of an output onto the SPI flash bus. It was live on Spencer03 the whole time
// a hand-wired NEO-6M was failing to produce a single byte on correctly wired,
// correctly opened UART pins.
#if !defined(PDL_BOARD_TDECK) && !defined(PDL_BOARD_HELTEC_V3)
#  define GPS_HAS_EN_PIN      1
#  define GPS_EN_PIN          34
#endif
#define GPS_EN_POLARITY_TRY_MS 6000
static bool     s_gps_en_state = false;   // confirmed: LOW = enabled
static uint32_t s_gps_en_last_change_ms = 0;
static bool     s_gps_en_settled = false;

#define GPS_UART_NUM   1
#define GPS_BAUD       9600

// A fix older than this is treated as stale (module lost lock, e.g. moved
// indoors) — falls back to the static NVS position rather than reporting a
// last-known-good position indefinitely.
#define GPS_FIX_STALE_MS   30000

static HardwareSerial s_gps_uart(GPS_UART_NUM);
static TinyGPSPlus     s_gps;
static GpsStats        s_stats{};
static uint32_t        s_last_fix_ms = 0;

// ---------------------------------------------------------------------------
// LIFETIME byte counter — never reset by the scanner.
//
// s_stats.chars_processed is deliberately zeroed on every candidate change
// below, because the scan logic needs "bytes seen on THIS pin pair in THIS
// 4 s window" to decide whether to lock. That is correct for the scanner and
// actively misleading as a diagnostic: polling /status returns whatever the
// current 4 s slice happens to hold, so a working GPS reads 0 roughly 13 times
// out of 14.
//
// On 2026-08-02 that cost hours. A correctly wired NEO-6M (3.32 V measured at
// GPIO 5, 3.25 V at GPIO 6, 3.339 V at the chip) was declared electrically
// dead purely on the strength of this counter reading 0, and the fault was
// hunted through power rails, TX pads and three different pin diagrams.
//
// These two are never cleared by the scanner, so they answer the question the
// scanner's counter cannot: has this unit EVER received anything, and when.
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// Raw NMEA ring — see gps_get_recent_nmea() in gps_module.h.
//
// Sized for GSV specifically: a full multi-constellation GSV set is the
// longest thing the module emits and the only sentence that answers
// "satellites in view", so the ring must be able to hold a whole cycle.
// 12 x 104 = 1248 bytes of static RAM, which is nothing next to the models.
// ---------------------------------------------------------------------------
#define GPS_NMEA_RING_LINES 12
#define GPS_NMEA_LINE_MAX   104

static char    s_nmea_ring[GPS_NMEA_RING_LINES][GPS_NMEA_LINE_MAX];
static uint8_t s_nmea_ring_head  = 0;   // next slot to write
static uint8_t s_nmea_ring_count = 0;   // valid slots, saturating
static char    s_nmea_partial[GPS_NMEA_LINE_MAX];
static uint8_t s_nmea_partial_len = 0;

// Feed one raw byte. Commits a slot on CR/LF; silently drops an over-long
// line rather than splitting it, so a torn sentence never reads as two.
static void nmea_ring_push_char(char c) {
    if (c == '\r' || c == '\n') {
        if (s_nmea_partial_len > 0) {
            s_nmea_partial[s_nmea_partial_len] = '\0';
            memcpy(s_nmea_ring[s_nmea_ring_head], s_nmea_partial,
                   s_nmea_partial_len + 1);
            s_nmea_ring_head = (uint8_t)((s_nmea_ring_head + 1) % GPS_NMEA_RING_LINES);
            if (s_nmea_ring_count < GPS_NMEA_RING_LINES) s_nmea_ring_count++;
            s_nmea_partial_len = 0;
        }
        return;
    }
    if (s_nmea_partial_len < GPS_NMEA_LINE_MAX - 1) {
        s_nmea_partial[s_nmea_partial_len++] = c;
    } else {
        s_nmea_partial_len = 0;   // overflow: discard, resync at next CR/LF
    }
}

static uint32_t s_lifetime_chars    = 0;
static uint32_t s_last_rx_ms        = 0;
static int      s_last_rx_pin       = -1;   // pin that produced the last byte
static int      s_active_rx         = -1;
static int      s_active_tx         = -1;
static uint32_t s_active_baud       = 0;    // what the UART is actually clocked at

#if GPS_SCAN_MODE
static void gps_scan_begin_candidate(int idx) {
    const GpsPinCandidate& c = kGpsPinCandidates[idx];
    const uint32_t baud = c.baud ? c.baud : (uint32_t)GPS_BAUD;
    s_gps_uart.end();
    delay(20);
    s_gps_uart.begin(baud, SERIAL_8N1, c.rx, c.tx);
    s_stats.chars_processed = 0;   // per-candidate: required by the lock logic
    s_active_rx   = c.rx;
    s_active_tx   = c.tx;
    s_active_baud = baud;
    // Baseline the parser's valid-sentence counter so this candidate is judged
    // on what IT delivers, not on anything a previous candidate parsed.
    s_scan_valid_base = s_gps.passedChecksum();
    s_scan_started_ms = millis();
    Serial.printf("[GPS-SCAN] Trying RX=%d TX=%d @ %lu baud (%lu/%lu)...\n",
                  c.rx, c.tx, (unsigned long)baud, (unsigned long)(idx + 1),
                  (unsigned long)GPS_SCAN_CANDIDATE_COUNT);
}
#endif

void gps_init() {
#ifdef GPS_HAS_EN_PIN
    pinMode(GPS_EN_PIN, OUTPUT);
    digitalWrite(GPS_EN_PIN, s_gps_en_state ? HIGH : LOW);
    s_gps_en_last_change_ms = millis();
    Serial.printf("[GPS] GPS_EN=GPIO%d set %s (trying this polarity first)\n",
                  GPS_EN_PIN, s_gps_en_state ? "HIGH" : "LOW");
#else
    // T-Deck: GPS powers with the GPIO10 board rail, nothing to enable here.
    s_gps_en_settled = true;   // skip the polarity-flip state machine entirely
#endif

#if GPS_SCAN_MODE
    gps_scan_begin_candidate(0);
#else
    // Confirmed pins for THIS unit's GNSS header — see note above.
    s_gps_uart.begin(GPS_BAUD, SERIAL_8N1, GPS_RX_PIN_CONFIRMED, GPS_TX_PIN_CONFIRMED);
    s_active_rx   = GPS_RX_PIN_CONFIRMED;
    s_active_tx   = GPS_TX_PIN_CONFIRMED;
    s_active_baud = GPS_BAUD;
    Serial.printf("[GPS] L76K UART init (RX=%d TX=%d @ %d baud)\n",
                  GPS_RX_PIN_CONFIRMED, GPS_TX_PIN_CONFIRMED, GPS_BAUD);
#endif
}

void gps_tick() {
    if (!s_gps_en_settled) {
        if (s_stats.chars_processed > 0) {
            Serial.printf("[GPS] GPS_EN polarity %s confirmed working "
                          "(%lu bytes received) — settled.\n",
                          s_gps_en_state ? "HIGH" : "LOW",
                          (unsigned long)s_stats.chars_processed);
            s_gps_en_settled = true;
        }
#ifdef GPS_HAS_EN_PIN
        else if (millis() - s_gps_en_last_change_ms > GPS_EN_POLARITY_TRY_MS) {
            s_gps_en_state = !s_gps_en_state;
            digitalWrite(GPS_EN_PIN, s_gps_en_state ? HIGH : LOW);
            s_gps_en_last_change_ms = millis();
            Serial.printf("[GPS] No traffic after %d ms — flipping GPS_EN to %s\n",
                          GPS_EN_POLARITY_TRY_MS, s_gps_en_state ? "HIGH" : "LOW");
        }
#endif
    }
#if GPS_SCAN_MODE
    // A single stray byte can be electrical noise on a floating pin, not
    // real GPS data — a genuinely connected module streams NMEA
    // continuously (dozens+ bytes/sec) even with no fix. Require a real
    // sustained rate before calling it a hit; otherwise just report the
    // count and keep cycling so pin pairs can be compared across the run.
    // -----------------------------------------------------------------------
    // LOCK ON PARSED SENTENCES, NOT ON RAW BYTES.
    //
    // This previously locked as soon as 20 raw bytes arrived. Raw bytes prove
    // only that SOMETHING is driving the pin — they say nothing about whether
    // the baud rate is right, because a UART clocked at the wrong speed still
    // produces a steady, plausible-looking byte stream. So on a T-Deck Plus
    // carrying a 38400-baud u-blox M10Q, the very first 9600-baud candidate
    // "succeeded" in under a second, latched, and the scan never reconsidered
    // it. The unit then reported live GPS traffic and zero satellites forever.
    //
    // A checksum-valid NMEA sentence is the weakest signal that actually
    // proves pins AND baud are both correct, so that is now the bar. Raw bytes
    // are still recorded for diagnostics (they distinguish "wrong baud" from
    // "nothing connected"), they just no longer decide the lock.
    // -----------------------------------------------------------------------
    #define GPS_SCAN_VALID_THRESHOLD 3
    if (!s_scan_locked) {
        const uint32_t valid_here = s_gps.passedChecksum() - s_scan_valid_base;
        if (valid_here >= GPS_SCAN_VALID_THRESHOLD) {
            const GpsPinCandidate& c = kGpsPinCandidates[s_scan_idx];
            Serial.printf("[GPS-SCAN] *** VALID NMEA on RX=%d TX=%d @ %lu baud *** "
                          "(%lu good sentences, %lu bytes) — locking.\n",
                          c.rx, c.tx, (unsigned long)s_active_baud,
                          (unsigned long)valid_here,
                          (unsigned long)s_stats.chars_processed);
            s_scan_locked = true;
        } else if (millis() - s_scan_started_ms > GPS_SCAN_WINDOW_MS) {
            const GpsPinCandidate& c = kGpsPinCandidates[s_scan_idx];
            Serial.printf("[GPS-SCAN] RX=%d TX=%d @ %lu baud: %lu bytes but only "
                          "%lu valid sentences in %lu ms (%s) — next candidate.\n",
                          c.rx, c.tx, (unsigned long)s_active_baud,
                          (unsigned long)s_stats.chars_processed,
                          (unsigned long)valid_here,
                          (unsigned long)GPS_SCAN_WINDOW_MS,
                          s_stats.chars_processed > 40
                              ? "traffic present, WRONG BAUD"
                              : "nothing connected");
            s_scan_idx++;
            if ((size_t)s_scan_idx >= GPS_SCAN_CANDIDATE_COUNT) {
                Serial.println("[GPS-SCAN] --- Full cycle complete, no "
                                "sustained hit. Restarting scan. ---");
                s_scan_idx = 0;
            }
            gps_scan_begin_candidate(s_scan_idx);
        }
    }
#endif
    while (s_gps_uart.available() > 0) {
        char c = (char)s_gps_uart.read();
        s_stats.chars_processed++;
        nmea_ring_push_char(c);
        if (s_gps.encode(c)) {
            // A complete, checksum-valid sentence was parsed.
            if (s_gps.location.isValid() && s_gps.location.isUpdated()) {
                s_stats.sentences_with_fix++;
                s_stats.has_fix = true;
                s_last_fix_ms = millis();
            }
            if (s_gps.hdop.isValid()) s_stats.hdop = s_gps.hdop.hdop();
            if (s_gps.satellites.isValid()) {
                s_stats.satellites = (uint8_t)s_gps.satellites.value();
            }
        }
    }
    if (s_gps.failedChecksum() > 0) {
        s_stats.checksum_failures = s_gps.failedChecksum();
    }
}

// ---------------------------------------------------------------------------
// Bench aid — see gps_module.h. Opens an explicit pin pair immediately and
// halts the autoscan so it cannot wander off mid-test. Any raw byte arriving
// proves the electrical path works, even unintelligible garbage.
// ---------------------------------------------------------------------------
bool gps_force_pins(int rx_pin, int tx_pin, uint32_t baud) {
    if (rx_pin < 0 || rx_pin > 48 || tx_pin < 0 || tx_pin > 48) return false;
    if (baud == 0) baud = GPS_BAUD;

    s_gps_uart.end();
    delay(20);
    s_gps_uart.begin(baud, SERIAL_8N1, rx_pin, tx_pin);
    s_stats.chars_processed   = 0;
    s_stats.checksum_failures = 0;
    s_active_rx   = rx_pin;
    s_active_tx   = tx_pin;
    s_active_baud = baud;

#if GPS_SCAN_MODE
    s_scan_locked = true;   // pin this pair; stop the sweep
#endif
    Serial.printf("[GPS] FORCED RX=%d TX=%d @ %lu baud (autoscan halted)\n",
                  rx_pin, tx_pin, (unsigned long)baud);
    return true;
}

// ---------------------------------------------------------------------------
// Loopback self-test — proves whether the ESP32's UART is functional on the
// configured pins, independent of any attached GPS.
//
// Exists because a correctly-wired module produced 0 bytes on correctly-opened
// pins, and every remaining explanation was an assertion about MY code being
// right. With a physical jumper from the TX pin to the RX pin, this writes a
// known pattern and reads it back:
//
//   bytes returned  -> the UART, pin routing and read loop all work; the fault
//                      is the module or its wiring.
//   nothing returned-> the fault is in this firmware's UART setup, and no
//                      amount of module swapping will help.
//
// It is deliberately a hard discriminator: one of those two is true, and this
// says which without further argument.
// ---------------------------------------------------------------------------
bool gps_loopback_test(unsigned long* out_sent, unsigned long* out_received) {
    static const char* kPattern = "PDLLOOPBACK0123456789\r\n";
    const size_t len = strlen(kPattern);

    // Drain anything pending so the count is unambiguous.
    while (s_gps_uart.available() > 0) (void)s_gps_uart.read();

    size_t sent = s_gps_uart.write((const uint8_t*)kPattern, len);
    s_gps_uart.flush();

    // A jumpered byte returns within a character time or two; 300 ms is
    // generous even at 9600 baud.
    uint32_t deadline = millis() + 300;
    uint32_t got = 0;
    char rx[64];
    while ((int32_t)(millis() - deadline) < 0 && got < sizeof(rx) - 1) {
        while (s_gps_uart.available() > 0 && got < sizeof(rx) - 1) {
            rx[got++] = (char)s_gps_uart.read();
        }
    }
    rx[got] = '\0';

    if (out_sent) *out_sent = (uint32_t)sent;
    if (out_received) *out_received = got;

    Serial.printf("[GPS] LOOPBACK sent=%u received=%u data=\"%s\"\n",
                  (unsigned)sent, (unsigned)got, rx);
    return got > 0;
}

void gps_reset_stats() {
    s_stats.chars_processed   = 0;
    s_stats.checksum_failures = 0;
}

uint32_t gps_get_active_baud() {
    return s_active_baud;
}

size_t gps_get_recent_nmea(char* out, size_t out_len) {
    if (!out || out_len == 0) return 0;
    out[0] = '\0';
    if (s_nmea_ring_count == 0) return 0;

    // Oldest-first, so the reader sees the stream in the order it arrived.
    const uint8_t start = (uint8_t)((s_nmea_ring_head + GPS_NMEA_RING_LINES
                                     - s_nmea_ring_count) % GPS_NMEA_RING_LINES);
    size_t written = 0;
    for (uint8_t i = 0; i < s_nmea_ring_count; i++) {
        const char* line = s_nmea_ring[(start + i) % GPS_NMEA_RING_LINES];
        const size_t len = strlen(line);
        if (written + len + 2 > out_len) break;   // +1 newline, +1 NUL
        memcpy(out + written, line, len);
        written += len;
        out[written++] = '\n';
    }
    out[written] = '\0';
    return written;
}

void gps_get_active_pins(int* rx, int* tx, bool* locked, int* candidate_idx,
                         int* candidate_count) {
    if (rx) *rx = s_active_rx;
    if (tx) *tx = s_active_tx;
#if GPS_SCAN_MODE
    if (locked)          *locked = s_scan_locked;
    if (candidate_idx)   *candidate_idx = s_scan_idx;
    if (candidate_count) *candidate_count = (int)GPS_SCAN_CANDIDATE_COUNT;
#else
    if (locked)          *locked = true;
    if (candidate_idx)   *candidate_idx = 0;
    if (candidate_count) *candidate_count = 0;
#endif
}

bool gps_has_fix() {
    if (!s_stats.has_fix) return false;
    if (millis() - s_last_fix_ms > GPS_FIX_STALE_MS) return false;
    return true;
}

void gps_get_location(int32_t* lat_fixed, int32_t* lon_fixed) {
    if (!gps_has_fix()) {
        if (lat_fixed) *lat_fixed = 0;
        if (lon_fixed) *lon_fixed = 0;
        return;
    }
    if (lat_fixed) *lat_fixed = (int32_t)(s_gps.location.lat() * 1e7);
    if (lon_fixed) *lon_fixed = (int32_t)(s_gps.location.lng() * 1e7);
}

uint32_t gps_get_utc_epoch() {
    // Real UTC seconds from the GPS date+time, or 0 if not yet available.
    // On a lone gateway (no mesh time-master) this is the only trustworthy
    // wall-clock source, so detection timestamps can be indexed under the
    // correct day server-side instead of collapsing to 1970.
    if (!s_gps.date.isValid() || !s_gps.time.isValid()) return 0;
    int y = s_gps.date.year();
    if (y < 2020 || y > 2099) return 0;   // GPS date not really locked yet
    int mo = s_gps.date.month();
    int d  = s_gps.date.day();
    int h  = s_gps.time.hour();
    int mi = s_gps.time.minute();
    int se = s_gps.time.second();
    // days-from-civil (Howard Hinnant) — calendar date -> days since 1970-01-01
    int yy = y - (mo <= 2 ? 1 : 0);
    int era = (yy >= 0 ? yy : yy - 399) / 400;
    unsigned yoe = (unsigned)(yy - era * 400);
    unsigned doy = (153u * (unsigned)(mo + (mo > 2 ? -3 : 9)) + 2u) / 5u + (unsigned)d - 1u;
    unsigned doe = yoe * 365u + yoe / 4u - yoe / 100u + doy;
    long days = (long)era * 146097L + (long)doe - 719468L;
    return (uint32_t)((long long)days * 86400LL + h * 3600 + mi * 60 + se);
}

void gps_get_stats(GpsStats* out) {
    if (!out) return;
    *out = s_stats;
    out->has_fix = gps_has_fix();  // reflect staleness, not just "ever had one"
}

#else  // !ARDUINO — native/test stub

void gps_init() {}
void gps_tick() {}
bool gps_has_fix() { return false; }
void gps_get_location(int32_t* lat_fixed, int32_t* lon_fixed) {
    if (lat_fixed) *lat_fixed = 0;
    if (lon_fixed) *lon_fixed = 0;
}
uint32_t gps_get_utc_epoch() { return 0; }
void gps_get_stats(GpsStats* out) {
    if (out) { GpsStats z{}; *out = z; }
}

#endif
