///|
/// A geometric illumination classification for a satellite state.
///
/// `Penumbra` means the satellite is inside the Earth's partial shadow. The
/// model is intentionally geometric and is suitable for visibility analysis,
/// not for spacecraft power or mission-safety decisions.
pub(all) enum EclipseState {
  Sunlit
  Penumbra
  Umbra
} derive(Eq, Debug)

///|
/// Low-precision geocentric Sun position for visibility and eclipse analysis.
///
/// The position is expressed in the same J2000-style inertial frame used by
/// the library's ECI helpers. Distance is in kilometres, right ascension and
/// declination are in degrees, and distance_au is retained for diagnostics.
pub(all) struct SunPosition {
  position_km : Vector3
  distance_km : Double
  distance_au : Double
  right_ascension_deg : Double
  declination_deg : Double
} derive(Eq, Debug)

///|
pub fn EclipseState::to_string(self : EclipseState) -> String {
  match self {
    Sunlit => "sunlit"
    Penumbra => "penumbra"
    Umbra => "umbra"
  }
}

///|
/// Mean astronomical-unit length used by the low-precision solar model.
const ASTRONOMICAL_UNIT_KM : Double = 149597870.7

///|
/// Approximate solar radius used for the umbra and penumbra cones.
const SUN_RADIUS_KM : Double = 695700.0

///|
fn normalize_degrees(value : Double) -> Double {
  let wrapped = value % 360.0
  if wrapped < 0.0 {
    wrapped + 360.0
  } else {
    wrapped
  }
}

///|
/// Compute a low-precision geocentric Sun position.
///
/// This follows the compact NOAA-style solar position equations. It is
/// accurate enough for eclipse classification and educational visualisation;
/// high-precision ephemerides remain outside the current scope.
pub fn sun_position_eci(instant : UtcDateTime) -> SunPosition {
  let days_since_j2000 = julian_day(instant) - 2451545.0
  let mean_longitude_deg = normalize_degrees(
    280.460 + 0.9856474 * days_since_j2000,
  )
  let mean_anomaly_rad = normalize_degrees(
      357.528 + 0.9856003 * days_since_j2000,
    ) *
    @math.PI /
    180.0
  let ecliptic_longitude_rad = normalize_degrees(
      mean_longitude_deg +
      1.915 * @math.sin(mean_anomaly_rad) +
      0.020 * @math.sin(2.0 * mean_anomaly_rad),
    ) *
    @math.PI /
    180.0
  let obliquity_rad = (23.439 - 0.0000004 * days_since_j2000) * @math.PI / 180.0
  let distance_au = 1.00014 -
    0.01671 * @math.cos(mean_anomaly_rad) -
    0.00014 * @math.cos(2.0 * mean_anomaly_rad)
  let distance_km = distance_au * ASTRONOMICAL_UNIT_KM
  let cosine_longitude = @math.cos(ecliptic_longitude_rad)
  let sine_longitude = @math.sin(ecliptic_longitude_rad)
  let position = {
    x: distance_km * cosine_longitude,
    y: distance_km * @math.cos(obliquity_rad) * sine_longitude,
    z: distance_km * @math.sin(obliquity_rad) * sine_longitude,
  }
  let right_ascension = @math.atan2(position.y, position.x) * 180.0 / @math.PI
  let declination = @math.atan2(
      position.z,
      (position.x * position.x + position.y * position.y).sqrt(),
    ) *
    180.0 /
    @math.PI
  {
    position_km: position,
    distance_km,
    distance_au,
    right_ascension_deg: normalize_degrees(right_ascension),
    declination_deg: declination,
  }
}

///|
/// Classify whether a satellite is sunlit, in the penumbra, or in the umbra.
///
/// The Earth is modelled as a sphere with WGS-84's equatorial radius. The
/// solar umbra and penumbra are treated as cones whose apexes are determined
/// by the apparent Sun and Earth radii.
pub fn eclipse_state(state : OrbitState) -> EclipseState {
  let sun = sun_position_eci(state.epoch)
  let sun_direction = sun.position_km.scale(1.0 / sun.distance_km)
  let projection = state.position_km.dot(sun_direction)
  if projection >= 0.0 {
    return Sunlit
  }
  let behind_earth_km = -projection
  let perpendicular_vector = state.position_km.sub(
    sun_direction.scale(projection),
  )
  let perpendicular_distance_km = perpendicular_vector
    .dot(perpendicular_vector)
    .sqrt()
  let earth_radius_km = 6378.137
  let umbra_radius_km = earth_radius_km -
    behind_earth_km * (SUN_RADIUS_KM - earth_radius_km) / sun.distance_km
  let penumbra_radius_km = earth_radius_km +
    behind_earth_km * (SUN_RADIUS_KM + earth_radius_km) / sun.distance_km
  if umbra_radius_km > 0.0 && perpendicular_distance_km <= umbra_radius_km {
    Umbra
  } else if perpendicular_distance_km <= penumbra_radius_km {
    Penumbra
  } else {
    Sunlit
  }
}

///|
/// Propagate a TLE and classify its illumination at the requested instant.
pub fn eclipse_state_sgp4(
  tle : Tle,
  instant : UtcDateTime,
) -> Result[EclipseState, SatError] {
  match propagate_sgp4(tle, instant) {
    Ok(state) => Ok(eclipse_state(state))
    Err(error) => Err(error)
  }
}