//! Shared types. This module is the contract between the BLE layer, the ride
//! engine, the recorder and the UI. Change it deliberately.

use serde::{Deserialize, Serialize};

/// One telemetry sample decoded from the trainer's Indoor Bike Data
/// characteristic. Every field is optional because FTMS packets are
/// variable-length — presence is driven by the leading flags bitfield, and a
/// given trainer may never send some of them.
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
pub struct Telemetry {
    /// Milliseconds since the ride started.
    pub elapsed_ms: u64,
    pub power_w: Option<i16>,
    pub cadence_rpm: Option<f32>,
    /// Trainer-reported speed. Diagnostic only — the ride engine computes its
    /// own virtual speed from power (FR-7.1, FR-7.5).
    pub speed_kph: Option<f32>,
    pub resistance_level: Option<i16>,
    pub heart_rate_bpm: Option<u8>,
    pub total_distance_m: Option<u32>,
    pub total_energy_kcal: Option<u16>,
}

/// A command to the trainer. Which variant is used depends on the active
/// [`ControlMode`] and on what the trainer actually supports (FR-2.6).
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum ControlTarget {
    /// Simulated gradient, in percent. Positive is uphill.
    Gradient { percent: f32 },
    /// Raw trainer resistance level, in the trainer's own units.
    Resistance { level: i16 },
    /// Target power in watts (ERG-style).
    Power { watts: u16 },
}

/// Hard limits applied at the point of transmission, regardless of where the
/// target came from (SAF-3, SAF-6). A profile with absurd parameters must not
/// be able to command an unsafe target.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct SafetyLimits {
    pub min_gradient_pct: f32,
    pub max_gradient_pct: f32,
    pub min_resistance: i16,
    pub max_resistance: i16,
    pub min_power_w: u16,
    pub max_power_w: u16,
}

impl Default for SafetyLimits {
    fn default() -> Self {
        // Gradient range per FR-5.3; power range per the D100 reference
        // implementation (§3.2 of REQUIREMENTS.md).
        Self {
            min_gradient_pct: -10.0,
            max_gradient_pct: 15.0,
            min_resistance: 0,
            max_resistance: 100,
            min_power_w: 50,
            max_power_w: 600,
        }
    }
}

impl SafetyLimits {
    /// Clamp a target into the safe range. Every path to the trainer must go
    /// through this.
    pub fn clamp(&self, target: ControlTarget) -> ControlTarget {
        match target {
            ControlTarget::Gradient { percent } => ControlTarget::Gradient {
                percent: percent.clamp(self.min_gradient_pct, self.max_gradient_pct),
            },
            ControlTarget::Resistance { level } => ControlTarget::Resistance {
                level: level.clamp(self.min_resistance, self.max_resistance),
            },
            ControlTarget::Power { watts } => ControlTarget::Power {
                watts: watts.clamp(self.min_power_w, self.max_power_w),
            },
        }
    }
}

/// Where the base target comes from (§5.4). Note that in the full design
/// gearing and gradient are simultaneously active; mode selects the *source* of
/// the base gradient, not whether shifting works.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum ControlMode {
    /// Rider sets gradient directly; no profile running. The default: it is the
    /// mode a session with nothing loaded is already in.
    #[default]
    ManualGrade,
    /// Rider sets raw resistance; physics ignored.
    Resistance,
    /// Gradient driven by a loaded profile at the current distance/time.
    Profile,
    /// Fixed target power.
    Erg,
}

/// Rider and bike parameters feeding the physics model (FR-7.4).
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct RiderConfig {
    pub rider_kg: f32,
    pub bike_kg: f32,
    /// Coefficient of rolling resistance.
    pub crr: f32,
    /// A fixed power loss that never goes away, watts.
    ///
    /// Everything a trainer costs you that does not scale the way road forces
    /// do: belt and bearing drag, the chain, the flywheel's own bearings. On
    /// this hardware that is about 12 W.
    ///
    /// Held as a *power* rather than a force because that is how it presents —
    /// a constant tax on what you put in. Converted where it is used by
    /// dividing by speed, so it is a small force when you are moving fast and a
    /// large one as you slow down. That asymmetry is the point: it barely
    /// touches a fast descent and it is what brings a coast to an actual halt
    /// rather than a long asymptotic drift.
    #[serde(default = "default_rolling_loss_w")]
    pub rolling_loss_w: f32,
    /// Drag coefficient × frontal area, m².
    pub cda: f32,
    /// Fraction of measured power reaching the wheel.
    pub drivetrain_efficiency: f32,
    /// Air density, kg/m³.
    pub air_density: f32,
    pub wheel_circumference_m: f32,
    /// Crank length — the radius of the circle the pedal travels, metres.
    ///
    /// The last lever in the chain. Work is force × distance either side of it:
    /// per crank revolution the pedal travels `2πr` while the bike travels
    /// `development`, so the force the rider's leg feels is
    ///
    /// ```text
    /// F_pedal = F_road × development / (2πr)
    /// ```
    ///
    /// It changes nothing about what is commanded — the trainer is asked for a
    /// force at the wheel, and leverage past the wheel is the rider's own
    /// business — but it is the only way to say what a gear will actually feel
    /// like, which is what makes a gear ladder sane or absurd.
    ///
    /// Road bikes are 170–175 mm; nothing sold is near 300 mm.
    #[serde(default = "default_crank_length")]
    pub crank_length_m: f32,
    /// The steepest descent the trainer is ever *asked* to simulate.
    ///
    /// On a real descent a trainer unloads almost completely, and on a
    /// single-cog drivetrain the rider then spins out against nothing and can
    /// produce no watts at all — so their effort stops mattering exactly when
    /// they can see the speed climbing. Flooring the *commanded* gradient keeps
    /// some load under the pedals while the *simulated* gradient stays true to
    /// the route, so the descent is still fast but the rider can contribute to
    /// it. Set to a large negative number to disable.
    #[serde(default = "default_descent_load_floor")]
    pub descent_load_floor_pct: f32,
    /// Development of the bike's *real* gear — metres travelled per crank
    /// revolution through the Zwift Cog. Virtual gears are expressed relative
    /// to this, so it sets the leverage between the two.
    ///
    /// A 34T chainring on a 14T cog with a 2.1 m wheel is 5.1 m.
    #[serde(default = "default_physical_development")]
    pub physical_development_m: f32,
    /// Which FTMS channel the computed load is sent on. See [`LoadChannel`].
    #[serde(default)]
    pub load_channel: LoadChannel,
}

fn default_physical_development() -> f32 {
    5.1
}

fn default_rolling_loss_w() -> f32 {
    12.0
}

fn default_crank_length() -> f32 {
    0.1725
}

fn default_descent_load_floor() -> f32 {
    -1.0
}

impl Default for RiderConfig {
    fn default() -> Self {
        Self {
            rider_kg: 105.0,
            bike_kg: 8.0,
            crr: 0.004,
            rolling_loss_w: default_rolling_loss_w(),
            cda: 0.32,
            drivetrain_efficiency: 0.97,
            air_density: 1.225,
            wheel_circumference_m: 2.1,
            crank_length_m: default_crank_length(),
            descent_load_floor_pct: default_descent_load_floor(),
            physical_development_m: default_physical_development(),
            load_channel: LoadChannel::default(),
        }
    }
}

impl RiderConfig {
    pub fn total_mass_kg(&self) -> f32 {
        self.rider_kg + self.bike_kg
    }
}

/// How the computed road load is expressed to the trainer.
///
/// The load itself is the same physics either way; this only chooses the FTMS
/// channel it is sent on, and the right answer is whichever one the hardware
/// actually acts on.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum LoadChannel {
    /// `SetIndoorBikeSimulationParameters` (0x11) — tell the trainer the slope
    /// and let its own model produce the resistance.
    Gradient,
    /// `SetTargetPower` (0x05) — command the watts the road demands at the
    /// current speed, recomputed every tick.
    ///
    /// The default, because on the D100 it is the channel with room to work.
    /// Its declared inclination range is 0–6% in 0.1% steps and refuses
    /// negatives outright, so a gearing difference that should be dramatic
    /// arrives as a fraction of a percent; its power range is 50–600 W in 1 W
    /// steps. Whether 0x11 does anything at all on this trainer is still
    /// unconfirmed (TASK-2), whereas power is declared in its feature bits and
    /// is what the physics computes natively.
    ///
    /// This is not ERG. On the D100 the target is a **ceiling**, not a
    /// setpoint: the brake absorbs up to that many watts and no more, so
    /// anything the rider produces beyond it becomes speed instead of being
    /// resisted away.
    ///
    /// That is very nearly what a road is. Command the watts the road demands
    /// at the current speed and the rest follows on its own — push harder than
    /// the road asks and you accelerate, ease off and you slow, and because the
    /// demand is recomputed from the new speed each tick the whole thing
    /// settles where the physics says it should. A true ERG setpoint would
    /// fight the rider instead, pressing harder the slower they went.
    #[default]
    Power,
}

/// Which rule decided the virtual speed on a given tick.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum SpeedSource {
    /// The rider is driving: speed is cadence × the selected gear.
    Drivetrain,
    /// The rider has stopped putting power in: speed runs down to whatever the
    /// gradient sustains on none.
    Coasting,
    /// No cadence reaching the engine, so there is no drivetrain to read and
    /// the ride is held at a stop. A fault, not a mode: cadence comes from the
    /// trainer's Zwift channel, and this says it is not arriving.
    NoCadence,
    /// The ride is not running. The default, because a snapshot that has not
    /// been through a tick describes a bike nobody is on.
    #[default]
    Stopped,
}

/// A snapshot of the ride, pushed to the UI each tick. This is what the
/// frontend renders; it should contain everything the ride screen needs and
/// nothing it does not.
/// `Default` is the zeroed ride — nothing elapsed, nothing moving, no gear
/// engaged. It exists for test fixtures and for the first frame before a tick
/// has run, so that adding a field here does not force every construction site
/// to be edited. Note that it is NOT a valid ride state: `gear_count` of zero
/// means no cassette, which the UI renders as a dash rather than "gear 0 of 0".
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
pub struct RideSnapshot {
    pub elapsed_ms: u64,
    pub telemetry: Telemetry,
    /// Virtual speed computed by the physics engine, km/h.
    pub virtual_speed_kph: f32,
    /// Virtual distance travelled, metres.
    pub virtual_distance_m: f64,
    /// The gradient of the *road*, percent — the profile's slope plus the
    /// rider's trim. Not what the trainer was asked for: gearing sits between
    /// the two, so see `target` for that.
    pub gradient_pct: f32,
    /// Cumulative elevation gained, metres.
    pub elevation_gain_m: f32,
    /// Selected virtual gear, one-based (FR-4.1).
    pub gear: usize,
    /// How many gears there are to choose from.
    pub gear_count: usize,
    /// Metres travelled per crank revolution in the selected gear.
    pub development_m: f32,
    /// Cadence the selected gear implies at the current speed, rpm — what the
    /// rider would be turning if they were riding this gear honestly.
    pub target_cadence_rpm: f32,
    /// Force the rider's leg is pushing against at the pedal, newtons. The
    /// commanded gradient in the units the legs actually work in — see
    /// [`crate::gearing::Gearing::pedal_force_n`].
    pub pedal_force_n: f32,
    /// Which rule produced `virtual_speed_kph` this tick. Diagnostic: the three
    /// behave very differently, and "the speed is wrong" is not answerable
    /// without knowing which one was in charge. See `RideSession::tick`.
    pub speed_source: SpeedSource,
    pub mode: ControlMode,
    /// The target most recently sent to the trainer, post-clamp.
    pub target: Option<ControlTarget>,
    /// Fractional progress through the loaded profile, 0.0–1.0, if one is
    /// loaded and has finite length.
    pub profile_progress: Option<f32>,
}

/// Connection state for a single BLE peripheral (FR-1.7).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConnectionState {
    Idle,
    Scanning,
    Connecting,
    /// Connected at the BLE level but control not yet acquired. For a trainer,
    /// connected ≠ controllable (FR-9.3).
    Connected,
    /// FTMS control point acquired; commands will be accepted.
    Controlling,
    Reconnecting,
    Lost { reason: String },
}
