//! The real backend: `bikecontrol_core::RideSession` driven by trainer
//! telemetry.
//!
//! This is the app's only data source. It holds the latest decoded Indoor
//! Bike Data sample — published by [`crate::trainer`] from `bikecontrol_ble`'s
//! telemetry stream — and feeds it to the ride engine once per tick.
//!
//! The engine, not this module, owns the physics: FR-7.1/7.5 say virtual speed
//! and distance are computed from *power*, not from the trainer's own speed
//! reading. That is what makes the ride behave correctly when the trainer's
//! speed is wrong, or absent, or (as on the D100) reported in units nobody has
//! confirmed.
//!
//! When no trainer is attached the watch channel carries `Telemetry::default()`
//! — zero power — so the ride coasts to a stop instead of freezing on the last
//! real sample.

use bikecontrol_core::session::{RideSession, SessionEvent};
use bikecontrol_core::types::{RideSnapshot, Telemetry};
use bikecontrol_core::{Gearing, VirtualCassette};
use tokio::sync::watch;

use crate::backend::{RideBackend, RideInputs, Tick};
use crate::events::RideStatus;

pub struct SessionBackend {
    session: RideSession,
    /// Latest decoded Indoor Bike Data, published by [`crate::trainer`].
    telemetry: watch::Receiver<Telemetry>,
    /// Latest reading from a dedicated heart rate monitor, published by
    /// [`crate::heart_rate`]. Stamped onto the trainer's telemetry each tick —
    /// but never over a heart rate FTMS itself reported, on the same principle
    /// as the Zwift cadence merge: a trainer that declares a field is the
    /// better authority on it.
    heart_rate: watch::Receiver<Option<u8>>,
    last_snapshot: Option<RideSnapshot>,
    /// Set once a profile has been handed to the session, so a profile swap is
    /// noticed but the same profile is not reloaded every tick.
    loaded_profile: Option<usize>,
    /// The cassette currently installed in the session, for the same reason.
    loaded_cassette: VirtualCassette,
}

impl SessionBackend {
    pub fn new(
        inputs: &RideInputs,
        telemetry: watch::Receiver<Telemetry>,
        heart_rate: watch::Receiver<Option<u8>>,
    ) -> Self {
        let mut session = RideSession::new(inputs.rider, inputs.limits);
        session.gearing = Gearing::new(inputs.cassette.clone());
        Self {
            session,
            telemetry,
            heart_rate,
            last_snapshot: None,
            loaded_profile: None,
            loaded_cassette: inputs.cassette.clone(),
        }
    }
}

impl RideBackend for SessionBackend {
    fn reset(&mut self) {
        self.session = RideSession::new(self.session.config, self.session.limits);
        self.session.gearing = Gearing::new(self.loaded_cassette.clone());
        self.last_snapshot = None;
        self.loaded_profile = None;
    }

    fn tick(&mut self, dt_s: f32, inputs: &RideInputs) -> Tick {
        // Rider intent lives in `RideInputs` (the Tauri commands mutate it);
        // the session is told about it each tick rather than being driven
        // directly, so there is one source of truth for what the rider asked
        // for regardless of which backend is running.
        self.session.config = inputs.rider;
        self.session.limits = inputs.limits;
        self.session.mode = inputs.mode;
        self.session.set_resistance(inputs.resistance_level);
        self.session.set_erg_power(inputs.power_target_w);

        match inputs.profile.as_deref() {
            Some(profile) => {
                // `Arc` identity, not contents: reloading resets the session's
                // position, which must not happen every tick.
                let id = inputs
                    .profile
                    .as_ref()
                    .map(|p| std::sync::Arc::as_ptr(p) as usize);
                if self.loaded_profile != id {
                    self.session.load_profile(profile.clone());
                    self.session.mode = inputs.mode;
                    self.loaded_profile = id;
                }
            }
            None => self.loaded_profile = None,
        }

        // FR-4.2: in ManualGrade the rider's absolute setting *is* the gradient;
        // elsewhere the nudge is a trim on top of the profile.
        let offset = match inputs.mode {
            bikecontrol_core::types::ControlMode::ManualGrade => {
                inputs.manual_gradient_pct + inputs.gradient_offset_pct
            }
            _ => inputs.gradient_offset_pct,
        };
        self.session.reset_gradient_offset();
        self.session.nudge_gradient(offset);

        // The cassette is the rider's, not the session's default. Rebuilding
        // the gearing is only correct when it actually changed — doing it every
        // tick would reset the cadence readout to zero forever.
        if self.loaded_cassette != inputs.cassette {
            self.session.gearing = Gearing::new(inputs.cassette.clone());
            self.loaded_cassette = inputs.cassette.clone();
        }
        self.session.gearing.set_gear(inputs.gear);

        match inputs.status {
            RideStatus::Running => self.session.start(),
            RideStatus::Paused => self.session.pause(),
            _ => {}
        }

        let mut telemetry = *self.telemetry.borrow();
        if telemetry.heart_rate_bpm.is_none() {
            telemetry.heart_rate_bpm = *self.heart_rate.borrow();
        }
        let mut command = None;
        let mut snapshot = None;
        for event in self.session.tick(telemetry, dt_s) {
            match event {
                SessionEvent::Command(target) => command = Some(target),
                SessionEvent::Snapshot(s) => snapshot = Some(s),
                SessionEvent::ProfileFinished | SessionEvent::Lap { .. } => {}
            }
        }
        let mut snapshot = snapshot
            .or(self.last_snapshot)
            .unwrap_or_else(|| self.session.snapshot(telemetry));

        // A ride that is not running is a rider who is not moving, and the
        // screen must say so. The engine deliberately keeps the physics state
        // across a pause so the ride resumes where it stopped — but reporting
        // that held velocity reads as "you are doing 38 km/h" to someone
        // standing still, which is the one thing a readout must never do.
        // Distance and elapsed already freeze; speed has to go to zero.
        if inputs.status != RideStatus::Running {
            snapshot.virtual_speed_kph = 0.0;
        }
        self.last_snapshot = Some(snapshot);

        Tick { snapshot, command }
    }
}

/// A heart rate channel with nothing on it, for tests about everything else.
#[cfg(test)]
fn no_hr() -> watch::Receiver<Option<u8>> {
    watch::channel(None).1
}

#[cfg(test)]
mod tests {
    use super::*;
    use bikecontrol_core::types::{ControlMode, ControlTarget};

    fn running(mode: ControlMode) -> RideInputs {
        RideInputs {
            status: RideStatus::Running,
            mode,
            ..RideInputs::default()
        }
    }

    #[test]
    fn real_power_drives_the_ride_forward() {
        let (tx, rx) = watch::channel(Telemetry::default());
        let inputs = running(ControlMode::ManualGrade);
        let mut backend = SessionBackend::new(&inputs, rx, no_hr());

        // No power: nothing moves.
        for _ in 0..8 {
            backend.tick(0.25, &inputs);
        }
        assert_eq!(backend.tick(0.25, &inputs).snapshot.virtual_distance_m, 0.0);

        // 200 W and turning the cranks: the engine moves. Cadence is not
        // garnish — speed is cadence × the selected gear, so a sample with
        // power and no cadence is a bike going nowhere.
        let _ = tx.send(Telemetry {
            power_w: Some(200),
            cadence_rpm: Some(85.0),
            ..Telemetry::default()
        });
        for _ in 0..40 {
            backend.tick(0.25, &inputs);
        }
        let snapshot = backend.tick(0.25, &inputs).snapshot;
        assert!(snapshot.virtual_speed_kph > 5.0, "{snapshot:?}");
        assert!(snapshot.virtual_distance_m > 10.0, "{snapshot:?}");
        assert_eq!(snapshot.telemetry.power_w, Some(200));
    }

    #[test]
    fn losing_the_trainer_coasts_to_a_stop_rather_than_freezing() {
        let (tx, rx) = watch::channel(Telemetry {
            power_w: Some(250),
            cadence_rpm: Some(85.0),
            ..Telemetry::default()
        });
        let inputs = running(ControlMode::ManualGrade);
        let mut backend = SessionBackend::new(&inputs, rx, no_hr());
        for _ in 0..60 {
            backend.tick(0.25, &inputs);
        }
        let moving = backend.tick(0.25, &inputs).snapshot.virtual_speed_kph;
        assert!(moving > 5.0);

        // The supervisor zeroes telemetry when the link drops.
        let _ = tx.send(Telemetry::default());
        for _ in 0..400 {
            backend.tick(0.25, &inputs);
        }
        let stopped = backend.tick(0.25, &inputs).snapshot;
        assert!(
            stopped.virtual_speed_kph < moving,
            "speed must decay, not hold"
        );
        assert_eq!(stopped.telemetry.power_w, None);
    }

    #[test]
    fn the_manual_gradient_reaches_the_trainer() {
        let (_tx, rx) = watch::channel(Telemetry::default());
        let mut inputs = running(ControlMode::ManualGrade);
        // The gradient channel specifically; the power channel expresses the
        // same load in watts and is covered in the engine's own tests.
        inputs.rider.load_channel = bikecontrol_core::types::LoadChannel::Gradient;
        inputs.manual_gradient_pct = 5.0;
        let mut backend = SessionBackend::new(&inputs, rx, no_hr());
        let tick = backend.tick(0.25, &inputs);
        // The road is the rider's setting exactly; what the trainer is asked
        // for is the load that road implies through the selected gear, which is
        // a different number (see `bikecontrol_core::gearing`).
        assert_eq!(tick.snapshot.gradient_pct, 5.0);
        let commanded = match tick.command {
            Some(ControlTarget::Gradient { percent }) => percent,
            other => panic!("expected a gradient command, got {other:?}"),
        };
        assert!(
            commanded > 0.0,
            "a 5% road must put load on the pedals: {commanded}"
        );
    }

    #[test]
    fn a_trim_stacks_on_the_manual_gradient_and_is_not_applied_twice() {
        let (_tx, rx) = watch::channel(Telemetry::default());
        let mut inputs = running(ControlMode::ManualGrade);
        inputs.manual_gradient_pct = 3.0;
        inputs.gradient_offset_pct = 1.5;
        let mut backend = SessionBackend::new(&inputs, rx, no_hr());
        for _ in 0..10 {
            backend.tick(0.25, &inputs);
        }
        // Would be 3 + 1.5 * 10 if the nudge accumulated across ticks.
        assert_eq!(backend.tick(0.25, &inputs).snapshot.gradient_pct, 4.5);
    }

    #[test]
    fn erg_and_resistance_targets_come_from_rider_intent() {
        let (_tx, rx) = watch::channel(Telemetry::default());
        let mut inputs = running(ControlMode::Erg);
        inputs.power_target_w = 275;
        let mut backend = SessionBackend::new(&inputs, rx, no_hr());
        assert_eq!(
            backend.tick(0.25, &inputs).command,
            Some(ControlTarget::Power { watts: 275 })
        );

        let mut inputs = running(ControlMode::Resistance);
        inputs.resistance_level = 42;
        assert_eq!(
            backend.tick(0.25, &inputs).command,
            Some(ControlTarget::Resistance { level: 42 })
        );
    }

    #[test]
    fn targets_are_clamped_before_they_leave() {
        // SAF-3 is enforced by the engine; assert the backend does not bypass it.
        let (_tx, rx) = watch::channel(Telemetry::default());
        let mut inputs = running(ControlMode::ManualGrade);
        inputs.rider.load_channel = bikecontrol_core::types::LoadChannel::Gradient;
        inputs.manual_gradient_pct = 400.0;
        let mut backend = SessionBackend::new(&inputs, rx, no_hr());
        let tick = backend.tick(0.25, &inputs);
        assert_eq!(
            tick.command,
            Some(ControlTarget::Gradient {
                percent: inputs.limits.max_gradient_pct
            })
        );
    }

    #[test]
    fn a_paused_ride_commands_nothing() {
        // SAF-1: the last target stands; a pause must not push a new load.
        let (_tx, rx) = watch::channel(Telemetry {
            power_w: Some(200),
            ..Telemetry::default()
        });
        let mut inputs = running(ControlMode::ManualGrade);
        let mut backend = SessionBackend::new(&inputs, rx, no_hr());
        backend.tick(0.25, &inputs);
        inputs.status = RideStatus::Paused;
        inputs.manual_gradient_pct = 9.0;
        assert_eq!(backend.tick(0.25, &inputs).command, None);
    }

    #[test]
    fn a_ride_that_is_not_running_reports_no_speed() {
        // Regression: elapsed and distance froze on pause but speed held its
        // last value, so a stationary rider was shown 38 km/h indefinitely.
        let (tx, rx) = watch::channel(Telemetry::default());
        let mut inputs = running(ControlMode::ManualGrade);
        let mut backend = SessionBackend::new(&inputs, rx, no_hr());
        let _ = tx.send(Telemetry {
            power_w: Some(250),
            cadence_rpm: Some(85.0),
            ..Telemetry::default()
        });
        for _ in 0..60 {
            backend.tick(0.25, &inputs);
        }
        let moving = backend.tick(0.25, &inputs).snapshot;
        assert!(moving.virtual_speed_kph > 10.0, "{moving:?}");

        for status in [RideStatus::Paused, RideStatus::Finished, RideStatus::Idle] {
            inputs.status = status;
            let stopped = backend.tick(0.25, &inputs).snapshot;
            assert_eq!(
                stopped.virtual_speed_kph, 0.0,
                "{status:?} still showed speed"
            );
            // Distance must not be thrown away — the ride resumes where it was.
            assert!(stopped.virtual_distance_m >= moving.virtual_distance_m);
        }

        // And resuming picks the ride back up rather than starting from rest.
        inputs.status = RideStatus::Running;
        let resumed = backend.tick(0.25, &inputs).snapshot;
        assert!(resumed.virtual_speed_kph > 10.0, "{resumed:?}");
    }

    #[test]
    fn a_monitors_heart_rate_reaches_the_snapshot_but_never_overrides_ftms() {
        let (telemetry_tx, telemetry_rx) = watch::channel(Telemetry::default());
        let (hr_tx, hr_rx) = watch::channel(None);
        let inputs = running(ControlMode::ManualGrade);
        let mut backend = SessionBackend::new(&inputs, telemetry_rx, hr_rx);

        // No monitor, no trainer HR: the field stays absent, so a ride without
        // a strap carries no heart rate rather than a zero (see fit::builder).
        let snap = backend.tick(0.25, &inputs).snapshot;
        assert_eq!(snap.telemetry.heart_rate_bpm, None);

        // The monitor's reading fills the gap FTMS leaves.
        let _ = hr_tx.send(Some(147));
        let snap = backend.tick(0.25, &inputs).snapshot;
        assert_eq!(snap.telemetry.heart_rate_bpm, Some(147));

        // A trainer that reports its own heart rate is the better authority.
        let _ = telemetry_tx.send(Telemetry {
            heart_rate_bpm: Some(151),
            ..Telemetry::default()
        });
        let snap = backend.tick(0.25, &inputs).snapshot;
        assert_eq!(snap.telemetry.heart_rate_bpm, Some(151));

        // The monitor going quiet clears the reading, not freezes it.
        let _ = telemetry_tx.send(Telemetry::default());
        let _ = hr_tx.send(None);
        let snap = backend.tick(0.25, &inputs).snapshot;
        assert_eq!(snap.telemetry.heart_rate_bpm, None);
    }

    #[test]
    fn reset_returns_to_the_start_line() {
        let (tx, rx) = watch::channel(Telemetry::default());
        let inputs = running(ControlMode::ManualGrade);
        let mut backend = SessionBackend::new(&inputs, rx, no_hr());
        let _ = tx.send(Telemetry {
            power_w: Some(300),
            cadence_rpm: Some(90.0),
            ..Telemetry::default()
        });
        for _ in 0..40 {
            backend.tick(0.25, &inputs);
        }
        assert!(backend.tick(0.25, &inputs).snapshot.virtual_distance_m > 0.0);
        backend.reset();
        let snapshot = backend.tick(0.25, &inputs).snapshot;
        // One tick from standstill covers centimetres, not the metres just ridden.
        assert!(snapshot.virtual_distance_m < 1.0, "{snapshot:?}");
        assert_eq!(snapshot.elapsed_ms, 250);
    }
}

#[cfg(test)]
mod drag_race_tests {
    use super::*;
    use bikecontrol_core::types::ControlMode;

    /// Telemetry shaped exactly like the D100's: power and wheel speed, and
    /// **no cadence** — the firmware does not send it (qdomyos-zwift#3282).
    fn d100(power_w: i16, speed_kph: f32) -> Telemetry {
        Telemetry {
            power_w: Some(power_w),
            cadence_rpm: None,
            speed_kph: Some(speed_kph),
            ..Telemetry::default()
        }
    }

    #[test]
    fn a_drag_race_on_d100_telemetry_actually_moves() {
        // End to end on the real shipped profile: if this passes and the app
        // still shows zero, the fault is above the engine — in what is being
        // fed to it, or in the ride never having been started.
        let yaml = crate::samples::all()
            .into_iter()
            .find(|s| s.name == "Drag race")
            .expect("drag race must be shipped")
            .text;
        let profile = bikecontrol_core::profile::Profile::from_yaml(&yaml).unwrap();

        let (tx, rx) = watch::channel(Telemetry::default());
        let mut inputs = RideInputs {
            status: RideStatus::Running,
            mode: ControlMode::Profile,
            profile: Some(std::sync::Arc::new(profile)),
            ..RideInputs::default()
        };
        inputs.set_gear(3);
        let mut backend = SessionBackend::new(&inputs, rx, no_hr());

        // Rolling: 180 W at 22 km/h on the flywheel.
        let _ = tx.send(d100(180, 22.0));
        for _ in 0..40 {
            backend.tick(0.25, &inputs);
        }
        let snap = backend.tick(0.25, &inputs).snapshot;
        assert_eq!(
            snap.speed_source,
            bikecontrol_core::types::SpeedSource::Drivetrain,
            "cadence must be inferred from wheel speed: {snap:?}"
        );
        assert!(snap.virtual_speed_kph > 5.0, "the bike must move: {snap:?}");
        assert!(snap.virtual_distance_m > 0.0, "the race must progress: {snap:?}");
    }

    #[test]
    fn a_drag_race_that_was_never_started_reports_zero() {
        // The other explanation for a stationary drag race, and it is not a
        // bug: loading a profile does not start the ride. Pinned so the two
        // causes stay distinguishable.
        let (tx, rx) = watch::channel(Telemetry::default());
        let inputs = RideInputs {
            status: RideStatus::Idle,
            mode: ControlMode::Profile,
            ..RideInputs::default()
        };
        let mut backend = SessionBackend::new(&inputs, rx, no_hr());
        let _ = tx.send(d100(180, 22.0));
        for _ in 0..40 {
            backend.tick(0.25, &inputs);
        }
        let snap = backend.tick(0.25, &inputs).snapshot;
        assert_eq!(snap.virtual_speed_kph, 0.0);
        assert_eq!(snap.elapsed_ms, 0, "an unstarted ride has no clock");
    }

    #[test]
    fn shifting_up_mid_race_speeds_the_rider_up_for_the_same_flywheel() {
        // The gear doing its job on inferred cadence: same trainer speed, more
        // ground covered.
        let ride = |gear: usize| {
            let (tx, rx) = watch::channel(Telemetry::default());
            let mut inputs = RideInputs {
                status: RideStatus::Running,
                mode: ControlMode::ManualGrade,
                ..RideInputs::default()
            };
            inputs.set_gear(gear);
            let mut backend = SessionBackend::new(&inputs, rx, no_hr());
            let _ = tx.send(d100(180, 22.0));
            for _ in 0..60 {
                backend.tick(0.25, &inputs);
            }
            backend.tick(0.25, &inputs).snapshot.virtual_speed_kph
        };
        let low = ride(2);
        let high = ride(11);
        assert!(high > low * 2.0, "a longer gear must cover more ground: {high} vs {low}");
    }
}
