//! The seam between the Tauri shell and what is actually riding:
//! [`crate::session_backend::SessionBackend`] — `bikecontrol_core::RideSession`
//! fed by real FTMS telemetry from `bikecontrol_ble`. Rider intent in, a
//! `RideSnapshot` out, and a `ControlTarget` to push to the trainer.
//!
//! There is no synthetic alternative: with no trainer attached the ride reads
//! zero, which is the truth (§4.3 — the control loop lives in Rust; the
//! frontend only ever sees snapshots).

use std::sync::Arc;

use bikecontrol_core::profile::Profile;
use bikecontrol_core::types::{
    ControlMode, ControlTarget, RideSnapshot, RiderConfig, SafetyLimits,
};
use bikecontrol_core::VirtualCassette;

use crate::events::RideStatus;

/// Rider intent, owned by the Tauri layer and read by the backend each tick.
#[derive(Debug, Clone)]
pub struct RideInputs {
    pub status: RideStatus,
    pub mode: ControlMode,
    /// Base gradient in `ManualGrade` mode.
    pub manual_gradient_pct: f32,
    /// Trim applied on top of whatever the base gradient is (FR-4.2).
    pub gradient_offset_pct: f32,
    /// Selected virtual gear, one-based (FR-4.1).
    pub gear: usize,
    /// The ladder `gear` indexes into. Held here rather than left to the
    /// session's own default so that the gear and the cassette it is counted
    /// against can never disagree — a "gear 12 of 12" that is really gear 12 of
    /// 8 is worse than no readout at all.
    pub cassette: VirtualCassette,
    pub resistance_level: i16,
    pub power_target_w: u16,
    pub profile: Option<Arc<Profile>>,
    pub rider: RiderConfig,
    pub limits: SafetyLimits,
}

impl Default for RideInputs {
    fn default() -> Self {
        Self {
            status: RideStatus::Idle,
            mode: ControlMode::ManualGrade,
            gear: bikecontrol_core::Gearing::default().gear(),
            cassette: VirtualCassette::default(),
            manual_gradient_pct: 0.0,
            gradient_offset_pct: 0.0,
            resistance_level: 20,
            power_target_w: 200,
            profile: None,
            rider: RiderConfig::default(),
            limits: SafetyLimits::default(),
        }
    }
}

impl RideInputs {
    /// How many gears the selected cassette offers.
    pub fn gear_count(&self) -> usize {
        self.cassette.len().max(1)
    }

    /// Select a gear, one-based. Out-of-range values clamp to the ends.
    pub fn set_gear(&mut self, gear: usize) {
        self.gear = gear.clamp(1, self.gear_count());
    }

    /// Shift by `delta` gears, clamping at both ends (FR-4.1.3 — never wraps).
    pub fn shift_gear(&mut self, delta: i32) {
        let next = self.gear as i64 + delta as i64;
        self.set_gear(next.clamp(1, self.gear_count() as i64) as usize);
    }
}

/// What a tick produced.
pub struct Tick {
    pub snapshot: RideSnapshot,
    /// Post-clamp target to transmit (SAF-3). `None` when the ride is not
    /// running, so a paused ride never pushes a new load.
    pub command: Option<ControlTarget>,
}

pub trait RideBackend: Send + 'static {
    /// Advance the ride by `dt_s`.
    fn tick(&mut self, dt_s: f32, inputs: &RideInputs) -> Tick;
    /// Return to a fresh ride: zero elapsed, distance and speed.
    fn reset(&mut self);
}
