//! Decoder for the FTMS **Indoor Bike Data** characteristic (`0x2AD2`).
//!
//! The packet is variable length. A leading little-endian 16-bit flags field
//! declares which fields follow, and the fields must be consumed in *strict*
//! specification order — there is no tagging, so a single mis-ordered or
//! mis-sized field turns everything after it into garbage.
//!
//! **The C1 flag (bit 0) is inverted.** It is named "More Data", and
//! Instantaneous Speed is present when the bit is **clear**. Every other bit is
//! a normal "present when set" flag. This trips up nearly every first
//! implementation; see REQUIREMENTS.md §5.2.
//!
//! Field order and units per the Bluetooth SIG FTMS specification v1.0:
//!
//! | # | Field | Flag | Type | Resolution |
//! |---|-------|------|------|------------|
//! | 1 | Instantaneous Speed | bit 0 **clear** | uint16 | 0.01 km/h |
//! | 2 | Average Speed | bit 1 set | uint16 | 0.01 km/h |
//! | 3 | Instantaneous Cadence | bit 2 set | uint16 | 0.5 rpm |
//! | 4 | Average Cadence | bit 3 set | uint16 | 0.5 rpm |
//! | 5 | Total Distance | bit 4 set | uint24 | 1 m |
//! | 6 | Resistance Level | bit 5 set | sint16 | 1 (unitless) |
//! | 7 | Instantaneous Power | bit 6 set | sint16 | 1 W |
//! | 8 | Average Power | bit 7 set | sint16 | 1 W |
//! | 9 | Total Energy / Energy per Hour / Energy per Minute | bit 8 set | uint16, uint16, uint8 | kcal |
//! | 10 | Heart Rate | bit 9 set | uint8 | 1 bpm |
//! | 11 | Metabolic Equivalent | bit 10 set | uint8 | 0.1 |
//! | 12 | Elapsed Time | bit 11 set | uint16 | 1 s |
//! | 13 | Remaining Time | bit 12 set | uint16 | 1 s |
//!
//! Portions of the field ordering and scaling in this module were ported from
//! `obostjancic/smart-trainer-control` (`src/lib/bike/ftms.ts`), MIT licensed,
//! Copyright (c) 2025 Ogi — a working Van Rysel D100 client. See
//! REQUIREMENTS.md §3.2. This Rust version differs in that a truncated packet
//! is a hard error rather than being silently zero-filled (NFR-4).

use bikecontrol_core::types::Telemetry;

/// Bit positions in the Indoor Bike Data flags field.
pub mod flag {
    /// Bit 0 — **inverted**: Instantaneous Speed is present when this is CLEAR.
    pub const MORE_DATA: u16 = 1 << 0;
    pub const AVERAGE_SPEED: u16 = 1 << 1;
    pub const INSTANTANEOUS_CADENCE: u16 = 1 << 2;
    pub const AVERAGE_CADENCE: u16 = 1 << 3;
    pub const TOTAL_DISTANCE: u16 = 1 << 4;
    pub const RESISTANCE_LEVEL: u16 = 1 << 5;
    pub const INSTANTANEOUS_POWER: u16 = 1 << 6;
    pub const AVERAGE_POWER: u16 = 1 << 7;
    pub const EXPENDED_ENERGY: u16 = 1 << 8;
    pub const HEART_RATE: u16 = 1 << 9;
    pub const METABOLIC_EQUIVALENT: u16 = 1 << 10;
    pub const ELAPSED_TIME: u16 = 1 << 11;
    pub const REMAINING_TIME: u16 = 1 << 12;
}

/// Why an Indoor Bike Data packet could not be decoded.
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum DecodeError {
    /// Fewer than the two flag bytes were present.
    #[error("indoor bike data packet is {len} bytes; at least 2 (flags) are required")]
    MissingFlags { len: usize },
    /// The flags promised a field the packet was too short to contain.
    #[error(
        "indoor bike data packet truncated: field {field} needs {need} byte(s) at offset {offset}, \
         but the packet is only {len} bytes"
    )]
    Truncated {
        field: &'static str,
        offset: usize,
        need: usize,
        len: usize,
    },
}

/// Every field FTMS can put in an Indoor Bike Data packet, already scaled into
/// physical units. `None` means the trainer did not send the field.
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct IndoorBikeData {
    /// Raw flags field, retained for diagnostics (NFR-8).
    pub flags: u16,
    /// km/h.
    pub instant_speed_kph: Option<f32>,
    /// km/h.
    pub average_speed_kph: Option<f32>,
    /// rpm.
    pub instant_cadence_rpm: Option<f32>,
    /// rpm.
    pub average_cadence_rpm: Option<f32>,
    /// metres.
    pub total_distance_m: Option<u32>,
    /// Trainer-specific resistance units.
    pub resistance_level: Option<i16>,
    /// watts.
    pub instant_power_w: Option<i16>,
    /// watts.
    pub average_power_w: Option<i16>,
    /// kcal.
    pub total_energy_kcal: Option<u16>,
    /// kcal/h.
    pub energy_per_hour_kcal: Option<u16>,
    /// kcal/min.
    pub energy_per_minute_kcal: Option<u8>,
    /// bpm.
    pub heart_rate_bpm: Option<u8>,
    pub metabolic_equivalent: Option<f32>,
    /// seconds.
    pub elapsed_time_s: Option<u16>,
    /// seconds.
    pub remaining_time_s: Option<u16>,
    /// Number of bytes consumed. If this is less than the packet length the
    /// trainer appended data we do not understand — worth logging, not an error.
    pub consumed: usize,
}

impl IndoorBikeData {
    /// Project onto the shared [`Telemetry`] contract. `elapsed_ms` is the
    /// ride-clock timestamp; the packet's own Elapsed Time field is the
    /// *machine's* session clock and is deliberately not used for it.
    pub fn to_telemetry(&self, elapsed_ms: u64) -> Telemetry {
        Telemetry {
            elapsed_ms,
            power_w: self.instant_power_w,
            cadence_rpm: self.instant_cadence_rpm,
            speed_kph: self.instant_speed_kph,
            resistance_level: self.resistance_level,
            heart_rate_bpm: self.heart_rate_bpm,
            total_distance_m: self.total_distance_m,
            total_energy_kcal: self.total_energy_kcal,
        }
    }
}

/// A bounds-checked little-endian cursor. Every read names the field it is
/// reading so a truncated packet produces a diagnosable error.
struct Cursor<'a> {
    data: &'a [u8],
    offset: usize,
}

impl<'a> Cursor<'a> {
    fn new(data: &'a [u8]) -> Self {
        Self { data, offset: 0 }
    }

    fn take(&mut self, field: &'static str, n: usize) -> Result<&'a [u8], DecodeError> {
        let end = self.offset.checked_add(n).ok_or(DecodeError::Truncated {
            field,
            offset: self.offset,
            need: n,
            len: self.data.len(),
        })?;
        if end > self.data.len() {
            return Err(DecodeError::Truncated {
                field,
                offset: self.offset,
                need: n,
                len: self.data.len(),
            });
        }
        let out = &self.data[self.offset..end];
        self.offset = end;
        Ok(out)
    }

    fn u8(&mut self, field: &'static str) -> Result<u8, DecodeError> {
        Ok(self.take(field, 1)?[0])
    }

    fn u16(&mut self, field: &'static str) -> Result<u16, DecodeError> {
        let b = self.take(field, 2)?;
        Ok(u16::from_le_bytes([b[0], b[1]]))
    }

    fn i16(&mut self, field: &'static str) -> Result<i16, DecodeError> {
        Ok(self.u16(field)? as i16)
    }

    /// uint24, little-endian.
    fn u24(&mut self, field: &'static str) -> Result<u32, DecodeError> {
        let b = self.take(field, 3)?;
        Ok(u32::from_le_bytes([b[0], b[1], b[2], 0]))
    }
}

/// Decode one Indoor Bike Data notification.
///
/// This is a pure function over bytes — no I/O, no state — so it is fully
/// testable without hardware.
pub fn decode(data: &[u8]) -> Result<IndoorBikeData, DecodeError> {
    if data.len() < 2 {
        return Err(DecodeError::MissingFlags { len: data.len() });
    }

    let mut cur = Cursor::new(data);
    let flags = cur.u16("flags")?;
    let present = |bit: u16| flags & bit != 0;

    let mut out = IndoorBikeData {
        flags,
        ..Default::default()
    };

    // 1. Instantaneous Speed — INVERTED FLAG: present when bit 0 is CLEAR.
    if !present(flag::MORE_DATA) {
        out.instant_speed_kph = Some(cur.u16("instantaneous speed")? as f32 / 100.0);
    }
    // 2. Average Speed.
    if present(flag::AVERAGE_SPEED) {
        out.average_speed_kph = Some(cur.u16("average speed")? as f32 / 100.0);
    }
    // 3. Instantaneous Cadence.
    if present(flag::INSTANTANEOUS_CADENCE) {
        out.instant_cadence_rpm = Some(cur.u16("instantaneous cadence")? as f32 / 2.0);
    }
    // 4. Average Cadence.
    if present(flag::AVERAGE_CADENCE) {
        out.average_cadence_rpm = Some(cur.u16("average cadence")? as f32 / 2.0);
    }
    // 5. Total Distance — uint24.
    if present(flag::TOTAL_DISTANCE) {
        out.total_distance_m = Some(cur.u24("total distance")?);
    }
    // 6. Resistance Level — sint16.
    if present(flag::RESISTANCE_LEVEL) {
        out.resistance_level = Some(cur.i16("resistance level")?);
    }
    // 7. Instantaneous Power — sint16 watts.
    if present(flag::INSTANTANEOUS_POWER) {
        out.instant_power_w = Some(cur.i16("instantaneous power")?);
    }
    // 8. Average Power — sint16 watts.
    if present(flag::AVERAGE_POWER) {
        out.average_power_w = Some(cur.i16("average power")?);
    }
    // 9. Expended Energy — three fields under one flag.
    if present(flag::EXPENDED_ENERGY) {
        out.total_energy_kcal = Some(cur.u16("total energy")?);
        out.energy_per_hour_kcal = Some(cur.u16("energy per hour")?);
        out.energy_per_minute_kcal = Some(cur.u8("energy per minute")?);
    }
    // 10. Heart Rate.
    if present(flag::HEART_RATE) {
        out.heart_rate_bpm = Some(cur.u8("heart rate")?);
    }
    // 11. Metabolic Equivalent — uint8, 0.1 resolution.
    if present(flag::METABOLIC_EQUIVALENT) {
        out.metabolic_equivalent = Some(cur.u8("metabolic equivalent")? as f32 / 10.0);
    }
    // 12. Elapsed Time.
    if present(flag::ELAPSED_TIME) {
        out.elapsed_time_s = Some(cur.u16("elapsed time")?);
    }
    // 13. Remaining Time.
    if present(flag::REMAINING_TIME) {
        out.remaining_time_s = Some(cur.u16("remaining time")?);
    }

    out.consumed = cur.offset;
    Ok(out)
}

/// Format a byte slice as lowercase hex, for `NFR-8` style raw logging.
pub fn hex(data: &[u8]) -> String {
    use std::fmt::Write as _;
    let mut s = String::with_capacity(data.len() * 2);
    for b in data {
        let _ = write!(s, "{b:02x}");
    }
    s
}

#[cfg(test)]
mod tests {
    use super::flag::*;
    use super::*;

    /// Build a packet from a flags value and a payload.
    fn packet(flags: u16, payload: &[u8]) -> Vec<u8> {
        let mut v = flags.to_le_bytes().to_vec();
        v.extend_from_slice(payload);
        v
    }

    #[test]
    fn speed_present_when_bit0_is_clear() {
        // flags = 0x0000: no bits set at all -> speed present, nothing else.
        // 3000 * 0.01 = 30.00 km/h
        let pkt = packet(0x0000, &3000u16.to_le_bytes());
        let d = decode(&pkt).unwrap();
        assert_eq!(d.instant_speed_kph, Some(30.0));
        assert_eq!(d.consumed, 4);
        assert_eq!(d.instant_power_w, None);
    }

    #[test]
    fn speed_absent_when_bit0_is_set() {
        // MORE_DATA set -> NO speed field. Power follows the flags directly.
        let pkt = packet(MORE_DATA | INSTANTANEOUS_POWER, &200i16.to_le_bytes());
        let d = decode(&pkt).unwrap();
        assert_eq!(d.instant_speed_kph, None);
        assert_eq!(d.instant_power_w, Some(200));
        assert_eq!(d.consumed, 4);
    }

    /// The regression this whole module exists to prevent: if bit 0 were
    /// treated as a normal present-when-set flag, the two power bytes would be
    /// eaten by "speed" and power would decode as garbage (or fail).
    #[test]
    fn inverted_bit0_does_not_shift_later_fields() {
        let pkt = packet(MORE_DATA | INSTANTANEOUS_POWER, &250i16.to_le_bytes());
        let d = decode(&pkt).unwrap();
        assert_eq!(d.instant_power_w, Some(250));

        // And the mirrored case: bit 0 clear means speed IS there and power
        // starts two bytes later.
        let mut payload = 1234u16.to_le_bytes().to_vec(); // 12.34 km/h
        payload.extend_from_slice(&250i16.to_le_bytes());
        let pkt = packet(INSTANTANEOUS_POWER, &payload);
        let d = decode(&pkt).unwrap();
        assert_eq!(d.instant_speed_kph, Some(12.34));
        assert_eq!(d.instant_power_w, Some(250));
    }

    #[test]
    fn typical_trainer_packet_speed_cadence_power() {
        // A very common D100-class combination: speed (implicit), cadence,
        // power. flags = cadence | power, bit 0 clear.
        let flags = INSTANTANEOUS_CADENCE | INSTANTANEOUS_POWER;
        let mut p = Vec::new();
        p.extend_from_slice(&2550u16.to_le_bytes()); // 25.50 km/h
        p.extend_from_slice(&180u16.to_le_bytes()); // 90.0 rpm (0.5 resolution)
        p.extend_from_slice(&213i16.to_le_bytes()); // 213 W
        let d = decode(&packet(flags, &p)).unwrap();
        assert_eq!(d.instant_speed_kph, Some(25.5));
        assert_eq!(d.instant_cadence_rpm, Some(90.0));
        assert_eq!(d.instant_power_w, Some(213));
        assert_eq!(d.consumed, 8);
        assert_eq!(d.consumed, packet(flags, &p).len());
    }

    #[test]
    fn all_fields_present() {
        let flags = AVERAGE_SPEED
            | INSTANTANEOUS_CADENCE
            | AVERAGE_CADENCE
            | TOTAL_DISTANCE
            | RESISTANCE_LEVEL
            | INSTANTANEOUS_POWER
            | AVERAGE_POWER
            | EXPENDED_ENERGY
            | HEART_RATE
            | METABOLIC_EQUIVALENT
            | ELAPSED_TIME
            | REMAINING_TIME; // bit 0 clear -> instantaneous speed also present

        let mut p = Vec::new();
        p.extend_from_slice(&3512u16.to_le_bytes()); // instant speed 35.12
        p.extend_from_slice(&3000u16.to_le_bytes()); // average speed 30.00
        p.extend_from_slice(&191u16.to_le_bytes()); // cadence 95.5
        p.extend_from_slice(&180u16.to_le_bytes()); // avg cadence 90.0
        p.extend_from_slice(&[0x40, 0x0d, 0x03]); // distance uint24 LE = 0x030d40 = 200000
        p.extend_from_slice(&12i16.to_le_bytes()); // resistance 12
        p.extend_from_slice(&245i16.to_le_bytes()); // power 245
        p.extend_from_slice(&230i16.to_le_bytes()); // avg power 230
        p.extend_from_slice(&150u16.to_le_bytes()); // total energy 150 kcal
        p.extend_from_slice(&600u16.to_le_bytes()); // energy/hour
        p.push(10); // energy/minute
        p.push(142); // heart rate
        p.push(85); // MET 8.5
        p.extend_from_slice(&3600u16.to_le_bytes()); // elapsed 3600 s
        p.extend_from_slice(&1800u16.to_le_bytes()); // remaining 1800 s

        let pkt = packet(flags, &p);
        let d = decode(&pkt).unwrap();

        assert_eq!(d.instant_speed_kph, Some(35.12));
        assert_eq!(d.average_speed_kph, Some(30.0));
        assert_eq!(d.instant_cadence_rpm, Some(95.5));
        assert_eq!(d.average_cadence_rpm, Some(90.0));
        assert_eq!(d.total_distance_m, Some(200_000));
        assert_eq!(d.resistance_level, Some(12));
        assert_eq!(d.instant_power_w, Some(245));
        assert_eq!(d.average_power_w, Some(230));
        assert_eq!(d.total_energy_kcal, Some(150));
        assert_eq!(d.energy_per_hour_kcal, Some(600));
        assert_eq!(d.energy_per_minute_kcal, Some(10));
        assert_eq!(d.heart_rate_bpm, Some(142));
        assert_eq!(d.metabolic_equivalent, Some(8.5));
        assert_eq!(d.elapsed_time_s, Some(3600));
        assert_eq!(d.remaining_time_s, Some(1800));
        // Every byte consumed: proof the field order and widths line up.
        assert_eq!(d.consumed, pkt.len());
    }

    #[test]
    fn total_distance_is_uint24_little_endian() {
        // 0xAABBCC as uint24 LE is bytes CC BB AA.
        let pkt = packet(MORE_DATA | TOTAL_DISTANCE, &[0xcc, 0xbb, 0xaa]);
        let d = decode(&pkt).unwrap();
        assert_eq!(d.total_distance_m, Some(0x00AA_BBCC));
        assert_eq!(d.consumed, 5);
    }

    #[test]
    fn negative_power_and_resistance_are_signed() {
        let flags = MORE_DATA | RESISTANCE_LEVEL | INSTANTANEOUS_POWER;
        let mut p = Vec::new();
        p.extend_from_slice(&(-5i16).to_le_bytes());
        p.extend_from_slice(&(-30i16).to_le_bytes());
        let d = decode(&packet(flags, &p)).unwrap();
        assert_eq!(d.resistance_level, Some(-5));
        assert_eq!(d.instant_power_w, Some(-30));
    }

    #[test]
    fn energy_block_is_three_fields_under_one_flag() {
        // If EXPENDED_ENERGY were treated as a single uint16 the heart rate
        // would come out wrong. This pins all five bytes.
        let flags = MORE_DATA | EXPENDED_ENERGY | HEART_RATE;
        let mut p = Vec::new();
        p.extend_from_slice(&321u16.to_le_bytes());
        p.extend_from_slice(&654u16.to_le_bytes());
        p.push(9);
        p.push(155);
        let d = decode(&packet(flags, &p)).unwrap();
        assert_eq!(d.total_energy_kcal, Some(321));
        assert_eq!(d.energy_per_hour_kcal, Some(654));
        assert_eq!(d.energy_per_minute_kcal, Some(9));
        assert_eq!(d.heart_rate_bpm, Some(155));
    }

    #[test]
    fn flags_only_packet_with_more_data_set_is_valid_and_empty() {
        let d = decode(&packet(MORE_DATA, &[])).unwrap();
        assert_eq!(d, IndoorBikeData {
            flags: MORE_DATA,
            consumed: 2,
            ..Default::default()
        });
    }

    #[test]
    fn unknown_high_bits_are_ignored_not_fatal() {
        // Bits 13-15 are RFU. A trainer setting them must not break decoding of
        // the fields we do understand.
        let flags = 0xE000 | MORE_DATA | INSTANTANEOUS_POWER;
        let d = decode(&packet(flags, &100i16.to_le_bytes())).unwrap();
        assert_eq!(d.instant_power_w, Some(100));
    }

    #[test]
    fn trailing_unknown_bytes_are_reported_not_fatal() {
        let mut pkt = packet(MORE_DATA | INSTANTANEOUS_POWER, &100i16.to_le_bytes());
        pkt.extend_from_slice(&[0xde, 0xad]);
        let d = decode(&pkt).unwrap();
        assert_eq!(d.instant_power_w, Some(100));
        assert_eq!(d.consumed, 4);
        assert!(d.consumed < pkt.len());
    }

    #[test]
    fn truncated_packet_is_an_error_not_a_panic() {
        // Flags promise power but only one byte follows.
        let err = decode(&packet(MORE_DATA | INSTANTANEOUS_POWER, &[0x01])).unwrap_err();
        assert_eq!(
            err,
            DecodeError::Truncated {
                field: "instantaneous power",
                offset: 2,
                need: 2,
                len: 3,
            }
        );
    }

    #[test]
    fn short_packets_are_errors() {
        assert_eq!(decode(&[]).unwrap_err(), DecodeError::MissingFlags { len: 0 });
        assert_eq!(
            decode(&[0x00]).unwrap_err(),
            DecodeError::MissingFlags { len: 1 }
        );
    }

    #[test]
    fn no_input_panics_across_every_flag_combination_and_length() {
        // Exhaustive robustness sweep (NFR-4): every meaningful flags value
        // against every payload length up to a full packet must either decode
        // or return an error, never panic.
        for flags in 0u16..=0x1FFF {
            for len in 0..40usize {
                let payload: Vec<u8> = (0..len).map(|i| i as u8).collect();
                let _ = decode(&packet(flags, &payload));
            }
        }
    }

    #[test]
    fn to_telemetry_maps_the_core_contract() {
        let flags = INSTANTANEOUS_CADENCE | INSTANTANEOUS_POWER | HEART_RATE;
        let mut p = Vec::new();
        p.extend_from_slice(&2000u16.to_le_bytes());
        p.extend_from_slice(&170u16.to_le_bytes());
        p.extend_from_slice(&199i16.to_le_bytes());
        p.push(130);
        let t = decode(&packet(flags, &p)).unwrap().to_telemetry(1234);
        assert_eq!(t.elapsed_ms, 1234);
        assert_eq!(t.speed_kph, Some(20.0));
        assert_eq!(t.cadence_rpm, Some(85.0));
        assert_eq!(t.power_w, Some(199));
        assert_eq!(t.heart_rate_bpm, Some(130));
        assert_eq!(t.total_distance_m, None);
    }

    #[test]
    fn hex_formats_lowercase_fixed_width() {
        assert_eq!(hex(&[0x00, 0x0f, 0xff]), "000fff");
    }
}
