// SPDX-License-Identifier: AGPL-3.0-only
// Copyright (C) 2026 Vallés Puig, Ramon

//! # Azimuth Provider, Trait-Based Dispatch Layer
//!
//! ## Scientific scope
//!
//! Provides a uniform, type‑safe view of azimuth quantities and azimuth
//! periods (intervals during which the body's bearing lies in a given
//! arc) for the Sun, Moon, the seven VSOP87 major planets, fixed stars,
//! and ICRS directions. The azimuth convention is North‑clockwise, with
//! values in `[0°, 360°)`. Accuracy and validity are inherited from the
//! delegated engines (`solar`, `lunar`, `stellar`, VSOP87E pipeline);
//! refraction is not applied.
//!
//! ## Technical scope
//!
//! Defines the [`AzimuthProvider`] trait, default convenience methods
//! ([`in_azimuth_range`](AzimuthProvider::in_azimuth_range),
//! [`outside_azimuth_range`](AzimuthProvider::outside_azimuth_range)), and
//! the free function [`azimuth_periods`]. Each `impl` simply delegates;
//! all astronomical math lives in the per‑body engine modules.
//!
//! | Body type | Engine |
//! |-----------|--------|
//! | [`solar_system::Sun`]  | [`event::solar::sun_azimuth_rad`]            |
//! | [`solar_system::Moon`] | [`event::lunar::moon_azimuth_rad`]           |
//! | [`Star`]               | delegates to [`direction::ICRS`] impl below  |
//! | [`direction::ICRS`]    | [`event::stellar::fixed_star_azimuth_rad`]   |
//! | VSOP87 planets         | local helper + horizontal transform pipeline |
//!
//! ## References
//! None.

use super::events;
use super::types::AzimuthQuery;
use crate::bodies::solar_system;
use crate::bodies::Star;
use crate::coordinates::centers::Geodetic;
use crate::coordinates::frames::ECEF;
use crate::coordinates::spherical::direction;
use crate::event::altitude::SearchOpts;
use crate::qtty::*;
use crate::time::{complement_within, Interval, ModifiedJulianDate};

// Imports for planet azimuth support
use crate::coordinates::transform::Transform;
use crate::coordinates::{cartesian, centers::Geocentric, frames};
use crate::event::horizontal;
use crate::time::JulianDate;

// ---------------------------------------------------------------------------
// Trait Definition
// ---------------------------------------------------------------------------

/// Unified interface for computing azimuth-related quantities and periods
/// for any celestial body.
///
/// ## Time Scale
///
/// All `ModifiedJulianDate` and `Interval<ModifiedJulianDate>` values are on the canonical
/// JD(TT) axis. Convert UTC instants with
/// `tempoch::Time::<tempoch::UTC>::from_chrono(...).to::<tempoch::TT>().into()`
/// into `ModifiedJulianDate` before using this API.
///
/// ## Azimuth convention
///
/// North-clockwise (North = 0°), values in `[0°, 360°)`.
pub trait AzimuthProvider {
    /// Compute the topocentric azimuth of this body at a single instant.
    ///
    /// Returns a value in radians, range `[0, 2π)` (North-clockwise).
    ///
    /// # Arguments
    ///
    /// * `observer`, geodetic site
    /// * `mjd`, instant on the TT axis
    ///
    /// # Returns
    ///
    /// `Radians` in `[0, 2π)`, North‑clockwise.
    fn azimuth_at(&self, observer: &Geodetic<ECEF>, mjd: ModifiedJulianDate) -> Radians;

    /// Compute the topocentric azimuth with an explicit apparent-position
    /// correction policy.
    fn azimuth_at_with_policy(
        &self,
        observer: &Geodetic<ECEF>,
        mjd: ModifiedJulianDate,
        policy: crate::astro::apparent::CorrectionPolicy,
    ) -> Radians {
        let _ = policy;
        self.azimuth_at(observer, mjd)
    }

    /// Returns all contiguous intervals inside `query.window` where the body's
    /// azimuth is within `[query.min_azimuth, query.max_azimuth]`.
    ///
    /// If `min_azimuth > max_azimuth` the query is a **wrap-around** (North-
    /// crossing) range and the result covers the combined arc.
    ///
    /// The returned vector is sorted chronologically.  An empty vector means
    /// the body never enters the requested band during the window.
    ///
    /// # Arguments
    ///
    /// * `query`, observer/window/azimuth‑band specification
    ///
    /// # Returns
    ///
    /// Sorted, non‑overlapping `Vec<Interval<ModifiedJulianDate>>`.
    fn azimuth_periods(&self, query: &AzimuthQuery) -> Vec<Interval<ModifiedJulianDate>>;

    /// Convenience: intervals where azimuth is within `[min_az, max_az]`.
    ///
    /// Equivalent to calling [`azimuth_periods`](Self::azimuth_periods)
    /// directly.
    ///
    /// # Arguments
    ///
    /// * `observer`, geodetic site
    /// * `window`, MJD/TT search interval
    /// * `min_az`, lower (or wrap‑start) bearing
    /// * `max_az`, upper (or wrap‑end) bearing
    /// * `opts`, search precision options (scan step, tolerance)
    ///
    /// # Returns
    ///
    /// Sorted `Vec<Interval<ModifiedJulianDate>>`.
    fn in_azimuth_range(
        &self,
        observer: Geodetic<ECEF>,
        window: Interval<ModifiedJulianDate>,
        min_az: Degrees,
        max_az: Degrees,
        opts: SearchOpts,
    ) -> Vec<Interval<ModifiedJulianDate>> {
        self.azimuth_periods(&AzimuthQuery {
            observer,
            window,
            min_azimuth: min_az,
            max_azimuth: max_az,
            opts,
            correction_policy: crate::astro::apparent::CorrectionPolicy::APPARENT,
        })
    }

    /// Convenience: intervals where azimuth is **outside** `[min_az, max_az]`.
    ///
    /// Returns the complement of [`in_azimuth_range`](Self::in_azimuth_range)
    /// within `window`.
    ///
    /// # Arguments
    ///
    /// * `observer`, geodetic site
    /// * `window`, MJD/TT search interval
    /// * `min_az`, lower (or wrap‑start) bearing
    /// * `max_az`, upper (or wrap‑end) bearing
    /// * `opts`, search precision options (scan step, tolerance)
    ///
    /// # Returns
    ///
    /// Sorted `Vec<Interval<ModifiedJulianDate>>`.
    fn outside_azimuth_range(
        &self,
        observer: Geodetic<ECEF>,
        window: Interval<ModifiedJulianDate>,
        min_az: Degrees,
        max_az: Degrees,
        opts: SearchOpts,
    ) -> Vec<Interval<ModifiedJulianDate>> {
        let inside = self.in_azimuth_range(observer, window, min_az, max_az, opts);
        complement_within(window, &inside)
    }

    /// Hint for the scan step to use when searching for azimuth events.
    ///
    /// Returns `None` to use the default (10 minutes). Bodies with slower
    /// apparent motion (like the Moon) can return a larger step.
    ///
    /// # Returns
    ///
    /// `Some(step)` to override the default scan step, or `None`.
    fn scan_step_hint(&self) -> Option<Days> {
        None
    }
}

// ---------------------------------------------------------------------------
// Free Function Entry Point
// ---------------------------------------------------------------------------

/// Generic entry point: compute azimuth periods for any body implementing
/// [`AzimuthProvider`].
///
/// This is a thin wrapper around the trait method for callers preferring a
/// function-style API.
///
/// # Example
/// ```rust
/// use siderust::event::azimuth::{azimuth_periods, AzimuthQuery};
/// use siderust::bodies::Sun;
/// use siderust::coordinates::centers::Geodetic;
/// use siderust::coordinates::frames::ECEF;
/// use siderust::time::{ModifiedJulianDate, MJD, Interval};
/// use siderust::qtty::*;
///
/// let site = Geodetic::<ECEF>::new(Degrees::new(0.0), Degrees::new(51.48), Meters::new(0.0));
/// let window = Interval::new(
///     siderust::ModifiedJulianDate::new(60000.0),
///     siderust::ModifiedJulianDate::new(60001.0),
/// );
/// let query = AzimuthQuery {
///     observer: site,
///     window,
///     min_azimuth: Degrees::new(90.0),
///     max_azimuth: Degrees::new(270.0),
///     opts: siderust::event::azimuth::SearchOpts::default(),
///     correction_policy: siderust::astro::apparent::CorrectionPolicy::APPARENT,
/// };
/// let periods = azimuth_periods(&Sun, &query);
/// ```
///
/// # Arguments
///
/// * `body`, any value implementing [`AzimuthProvider`]
/// * `query`, observer/window/azimuth‑band specification
///
/// # Returns
///
/// Sorted, non‑overlapping `Vec<Interval<ModifiedJulianDate>>` produced by
/// `body.azimuth_periods(query)`.
#[inline]
pub fn azimuth_periods<B: AzimuthProvider>(
    body: &B,
    query: &AzimuthQuery,
) -> Vec<Interval<ModifiedJulianDate>> {
    body.azimuth_periods(query)
}

// ---------------------------------------------------------------------------
// Implementations
// ---------------------------------------------------------------------------

/// **Sun**, delegates to `event::solar::sun_azimuth_rad`.
impl AzimuthProvider for solar_system::Sun {
    fn azimuth_at(&self, observer: &Geodetic<ECEF>, mjd: ModifiedJulianDate) -> Radians {
        crate::event::solar::sun_azimuth_rad(mjd, observer)
    }

    fn azimuth_periods(&self, query: &AzimuthQuery) -> Vec<Interval<ModifiedJulianDate>> {
        if (query.window.end.raw() - query.window.start.raw()) <= Days::zero() {
            return Vec::new();
        }
        events::azimuth_range_periods(self, query)
    }

    fn scan_step_hint(&self) -> Option<Days> {
        Some(Hours::new(2.0).to::<Day>())
    }
}

/// **Moon**, delegates to `event::lunar::moon_azimuth_rad`.
impl AzimuthProvider for solar_system::Moon {
    fn azimuth_at(&self, observer: &Geodetic<ECEF>, mjd: ModifiedJulianDate) -> Radians {
        crate::event::lunar::moon_azimuth_rad(mjd, observer)
    }

    fn azimuth_periods(&self, query: &AzimuthQuery) -> Vec<Interval<ModifiedJulianDate>> {
        if (query.window.end.raw() - query.window.start.raw()) <= Days::zero() {
            return Vec::new();
        }
        events::azimuth_range_periods(self, query)
    }

    fn scan_step_hint(&self) -> Option<Days> {
        // Moon moves slower; 2-hour steps are sufficient.
        Some(Hours::new(2.0).to::<Day>())
    }
}

/// **Star**, extracts RA/Dec, delegates to [`direction::ICRS`].
impl AzimuthProvider for Star<'_> {
    fn azimuth_at(&self, observer: &Geodetic<ECEF>, mjd: ModifiedJulianDate) -> Radians {
        let dir = direction::ICRS::from(self);
        dir.azimuth_at(observer, mjd)
    }

    fn azimuth_periods(&self, query: &AzimuthQuery) -> Vec<Interval<ModifiedJulianDate>> {
        let dir = direction::ICRS::from(self);
        dir.azimuth_periods(query)
    }
}

/// **direction::ICRS**, raw RA/Dec → stellar engine.
impl AzimuthProvider for direction::ICRS {
    fn azimuth_at(&self, observer: &Geodetic<ECEF>, mjd: ModifiedJulianDate) -> Radians {
        crate::event::stellar::fixed_star_azimuth_rad(mjd, observer, self.ra(), self.dec())
    }

    fn azimuth_at_with_policy(
        &self,
        observer: &Geodetic<ECEF>,
        mjd: ModifiedJulianDate,
        policy: crate::astro::apparent::CorrectionPolicy,
    ) -> Radians {
        crate::event::stellar::fixed_star_azimuth_rad_with_policy(
            mjd,
            observer,
            self.ra(),
            self.dec(),
            policy,
        )
    }

    fn azimuth_periods(&self, query: &AzimuthQuery) -> Vec<Interval<ModifiedJulianDate>> {
        if (query.window.end.raw() - query.window.start.raw()) <= Days::zero() {
            return Vec::new();
        }
        events::azimuth_range_periods(self, query)
    }
}

// ---------------------------------------------------------------------------
// Implementations: VSOP87 Planets (Mercury–Neptune)
// ---------------------------------------------------------------------------

/// Computes the topocentric azimuth (in radians, North-clockwise) of a
/// VSOP87 planet at a given instant using the full transform pipeline.
fn vsop87_planet_azimuth_rad<F>(
    vsop87e_fn: F,
    mjd: ModifiedJulianDate,
    site: &Geodetic<ECEF>,
) -> Radians
where
    F: Fn(
        JulianDate,
    ) -> cartesian::Position<
        crate::coordinates::centers::Barycentric,
        frames::EclipticMeanJ2000,
        AstronomicalUnit,
    >,
{
    let jd: JulianDate = mjd.to::<crate::JD>();
    let bary_ecl = vsop87e_fn(jd);
    let geo_equ: cartesian::Position<Geocentric, frames::EquatorialMeanJ2000, AstronomicalUnit> =
        bary_ecl.transform(jd);
    let topo = horizontal::geocentric_j2000_to_apparent_topocentric(&geo_equ, *site, jd);
    let horiz = horizontal::equatorial_to_horizontal(&topo, *site, jd);
    horiz.az().to::<Radian>()
}

/// Helper macro: implement [`AzimuthProvider`] for a VSOP87-backed planet.
macro_rules! impl_azimuth_provider_vsop87 {
    ($($Planet:ident),+ $(,)?) => {
        $(
            impl AzimuthProvider for solar_system::$Planet {
                fn azimuth_at(
                    &self,
                    observer: &Geodetic<ECEF>,
                    mjd: ModifiedJulianDate,
                ) -> Radians {
                    vsop87_planet_azimuth_rad(
                        solar_system::$Planet::vsop87e, mjd, observer,
                    )
                }

                fn azimuth_periods(&self, query: &AzimuthQuery) -> Vec<Interval<ModifiedJulianDate>> {
                    if (query.window.end.raw() - query.window.start.raw()) <= Days::zero() {
                        return Vec::new();
                    }
                    events::azimuth_range_periods(self, query)
                }

                fn scan_step_hint(&self) -> Option<Days> {
                    Some(Hours::new(2.0).to::<Day>())
                }
            }
        )+
    };
}

impl_azimuth_provider_vsop87!(Mercury, Venus, Mars, Jupiter, Saturn, Uranus, Neptune);

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::bodies::catalog;
    use crate::bodies::solar_system::Moon;

    fn greenwich() -> Geodetic<ECEF> {
        Geodetic::<ECEF>::new(Degrees::new(0.0), Degrees::new(51.4769), Meters::new(0.0))
    }

    fn one_day_window() -> Interval<ModifiedJulianDate> {
        Interval::new(
            crate::time::ModifiedJulianDate::new(60000.0),
            crate::time::ModifiedJulianDate::new(60001.0),
        )
    }

    #[test]
    fn sun_azimuth_at_returns_valid_range() {
        let az = solar_system::Sun.azimuth_at(
            &greenwich(),
            crate::time::ModifiedJulianDate::new(60000.5), // noon-ish
        );
        assert!(az.value() >= 0.0, "azimuth must be ≥ 0");
        assert!(az.value() < std::f64::consts::TAU, "azimuth must be < 2π");
    }

    #[test]
    fn moon_azimuth_at_returns_valid_range() {
        let az = Moon.azimuth_at(&greenwich(), crate::time::ModifiedJulianDate::new(60000.5));
        assert!(az.value() >= 0.0);
        assert!(az.value() < std::f64::consts::TAU);
    }

    #[test]
    fn star_azimuth_at_returns_valid_range() {
        let sirius = &catalog::SIRIUS;
        let az = sirius.azimuth_at(&greenwich(), crate::time::ModifiedJulianDate::new(60000.5));
        assert!(az.value() >= 0.0);
        assert!(az.value() < std::f64::consts::TAU);
    }

    #[test]
    fn star_and_icrs_agree() {
        let sirius = &catalog::SIRIUS;
        let dir = direction::ICRS::from(sirius);
        let mjd = crate::time::ModifiedJulianDate::new(60000.5);
        let az_star = sirius.azimuth_at(&greenwich(), mjd);
        let az_dir = dir.azimuth_at(&greenwich(), mjd);
        assert!(
            (az_star.value() - az_dir.value()).abs() < 1e-10,
            "Star and direction::ICRS must agree"
        );
    }

    #[test]
    fn sun_azimuth_periods_eastern_half() {
        let query = AzimuthQuery {
            observer: greenwich(),
            window: one_day_window(),
            min_azimuth: Degrees::new(90.0),
            max_azimuth: Degrees::new(270.0),
            opts: SearchOpts::default(),
            correction_policy: crate::astro::apparent::CorrectionPolicy::APPARENT,
        };
        let periods = solar_system::Sun.azimuth_periods(&query);
        // Sun is on the eastern half (az 90-270) for roughly half a day
        assert!(
            !periods.is_empty(),
            "Sun should cross the eastern half in 24h"
        );
    }

    #[test]
    fn outside_azimuth_range_is_complement() {
        let observer = greenwich();
        let window = one_day_window();
        let inside = solar_system::Sun.in_azimuth_range(
            observer,
            window,
            Degrees::new(90.0),
            Degrees::new(270.0),
            SearchOpts::default(),
        );
        let outside = solar_system::Sun.outside_azimuth_range(
            observer,
            window,
            Degrees::new(90.0),
            Degrees::new(270.0),
            SearchOpts::default(),
        );

        // Total covered should be the full window
        let total_inside: f64 = inside.iter().map(|p| p.length().value()).sum();
        let total_outside: f64 = outside.iter().map(|p| p.length().value()).sum();
        let window_len = (window.end.raw() - window.start.raw()).value();
        assert!(
            (total_inside + total_outside - window_len).abs() < 1e-6,
            "inside + outside should equal the full window"
        );
    }

    // --- Planet azimuth ---

    #[test]
    fn mars_azimuth_at_returns_valid_range() {
        let az = solar_system::Mars
            .azimuth_at(&greenwich(), crate::time::ModifiedJulianDate::new(60000.5));
        assert!(az.value() >= 0.0, "azimuth must be ≥ 0, got {}", az);
        assert!(
            az.value() < std::f64::consts::TAU,
            "azimuth must be < 2π, got {}",
            az
        );
    }

    #[test]
    fn all_planets_azimuth_valid() {
        let observer = greenwich();
        let mjd = crate::time::ModifiedJulianDate::new(60000.5);
        let mercury_az = solar_system::Mercury.azimuth_at(&observer, mjd);
        let venus_az = solar_system::Venus.azimuth_at(&observer, mjd);
        let mars_az = solar_system::Mars.azimuth_at(&observer, mjd);
        let jupiter_az = solar_system::Jupiter.azimuth_at(&observer, mjd);
        let saturn_az = solar_system::Saturn.azimuth_at(&observer, mjd);
        let uranus_az = solar_system::Uranus.azimuth_at(&observer, mjd);
        let neptune_az = solar_system::Neptune.azimuth_at(&observer, mjd);

        for (name, az) in [
            ("Mercury", mercury_az),
            ("Venus", venus_az),
            ("Mars", mars_az),
            ("Jupiter", jupiter_az),
            ("Saturn", saturn_az),
            ("Uranus", uranus_az),
            ("Neptune", neptune_az),
        ] {
            assert!(
                az.value() >= 0.0 && az.value() < std::f64::consts::TAU,
                "{name} azimuth out of range: {az}"
            );
        }
    }
}
