// SPDX-License-Identifier: GPL-3.0-or-later

/// Sunrise/sunset calculation using simplified solar equations.
/// No external dependencies — pure Rust implementation.
///
/// Based on the NOAA Solar Calculator algorithm:
/// https://www.esrl.noaa.gov/gmd/grad/solcalc/calcdetails.html
use chrono::{Datelike, NaiveDate, NaiveTime};

/// Result of sunrise/sunset calculation for a given day.
#[derive(Debug, Clone, Copy)]
pub struct SolarTimes {
    pub sunrise: NaiveTime,
    pub sunset: NaiveTime,
}

/// Calculate sunrise and sunset for a given date and location.
///
/// # Arguments
/// * `date` — The date to calculate for
/// * `lat` — Latitude in degrees (positive = North)
/// * `lon` — Longitude in degrees (positive = East)
///
/// # Returns
/// Solar times in the local timezone, or None if the sun doesn't rise/set
/// on that date (extreme latitudes during polar day/night).
pub fn calculate(date: NaiveDate, lat: f64, lon: f64) -> Option<SolarTimes> {
    let jd = julian_day(date);
    let n = jd - 2451545.0; // days since J2000.0

    // Mean anomaly of the Sun
    let ma = (357.5291 + 0.98560028 * n) % 360.0;
    let ma_rad = ma.to_radians();

    // Equation of center
    let eq_center = 1.9148 * ma_rad.sin()
        + 0.0200 * (2.0 * ma_rad).sin()
        + 0.0003 * (3.0 * ma_rad).sin();

    // Ecliptic longitude
    let sun_lon = ma + eq_center + 180.0 + 102.9372;

    // Solar declination
    let sun_lon_rad = sun_lon.to_radians();
    let declination = (sun_lon_rad.sin() * 0.397793).asin(); // sin(23.44°) ≈ 0.397793

    // Solar noon (in days)
    let solar_noon = jd_from_j2000(date) - lon / 360.0;

    // Hour angle for sunrise/sunset (with atmospheric refraction correction)
    let lat_rad = lat.to_radians();
    let cos_ha = (0.833_f64.to_radians().sin() - lat_rad.sin() * declination.sin())
        / (lat_rad.cos() * declination.cos());

    // Check for polar day/night
    if !(-1.0..=1.0).contains(&cos_ha) {
        return None;
    }

    let ha = cos_ha.acos().to_degrees();

    // Sunrise and sunset times in UTC hours (approximate)
    let sunrise_utc_hours = (solar_noon % 1.0) * 24.0 - ha / 15.0;
    let sunset_utc_hours = (solar_noon % 1.0) * 24.0 + ha / 15.0;

    // Adjust to handle the fractional day wrapping
    let sunrise_utc = normalize_hours(sunrise_utc_hours);
    let sunset_utc = normalize_hours(sunset_utc_hours);

    // Convert UTC to local solar time using longitude-based offset
    // (15° per hour, independent of machine timezone)
    let offset_seconds = (lon * 3600.0 / 15.0) as i64;
    let sunrise_local = utc_to_local_hours(sunrise_utc, offset_seconds);
    let sunset_local = utc_to_local_hours(sunset_utc, offset_seconds);

    let sunrise = hours_to_naive_time(sunrise_local)?;
    let sunset = hours_to_naive_time(sunset_local)?;

    Some(SolarTimes { sunrise, sunset })
}

/// Julian Day Number from a naive date.
fn julian_day(date: NaiveDate) -> f64 {
    let y = date.year() as f64;
    let m = date.month() as f64;
    let d = date.day() as f64;

    let (y, m) = if m <= 2.0 {
        (y - 1.0, m + 12.0)
    } else {
        (y, m)
    };

    let a = (y / 100.0).floor();
    let b = 2.0 - a + (a / 4.0).floor();

    (365.25 * (y + 4716.0)).floor()
        + (30.6001 * (m + 1.0)).floor()
        + d
        + b
        - 1524.5
}

/// Days since J2000.0 from a naive date.
fn jd_from_j2000(date: NaiveDate) -> f64 {
    julian_day(date) - 2451545.0
}

fn normalize_hours(h: f64) -> f64 {
    let mut h = h % 24.0;
    if h < 0.0 {
        h += 24.0;
    }
    h
}

fn utc_to_local_hours(utc_hours: f64, offset_seconds: i64) -> f64 {
    normalize_hours(utc_hours + offset_seconds as f64 / 3600.0)
}

fn hours_to_naive_time(hours: f64) -> Option<NaiveTime> {
    if !(0.0..24.0).contains(&hours) {
        return None;
    }
    let total_secs = (hours * 3600.0).round() as u32;
    let h = total_secs / 3600;
    let m = (total_secs % 3600) / 60;
    let s = total_secs % 60;
    NaiveTime::from_hms_opt(h, m, s)
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::Timelike;

    #[test]
    fn test_sunrise_sunset_returns_some_for_mid_latitudes() {
        // Chicago: ~41.88°N, 87.63°W
        let date = NaiveDate::from_ymd_opt(2026, 6, 15).unwrap();
        let result = calculate(date, 41.88, -87.63);
        assert!(result.is_some());

        let solar = result.unwrap();
        // June 15 in Chicago: sunrise ~5:15 AM, sunset ~8:30 PM CDT
        let sunrise_h = solar.sunrise.hour() as f64 + solar.sunrise.minute() as f64 / 60.0;
        let sunset_h = solar.sunset.hour() as f64 + solar.sunset.minute() as f64 / 60.0;

        // Allow ±1 hour tolerance for simplified algorithm
        assert!(
            sunrise_h > 4.0 && sunrise_h < 7.0,
            "Sunrise {} should be between 4:00-7:00",
            solar.sunrise
        );
        assert!(
            sunset_h > 19.0 && sunset_h < 22.0,
            "Sunset {} should be between 19:00-22:00",
            solar.sunset
        );
    }

    #[test]
    fn test_sunrise_before_sunset() {
        let date = NaiveDate::from_ymd_opt(2026, 3, 20).unwrap();
        let result = calculate(date, 41.88, -87.63);
        assert!(result.is_some());
        let solar = result.unwrap();
        assert!(solar.sunrise < solar.sunset);
    }

    #[test]
    fn test_polar_regions_dont_panic() {
        // North pole in summer — should be polar day (no sunset)
        let date = NaiveDate::from_ymd_opt(2026, 6, 21).unwrap();
        let _result = calculate(date, 80.0, 0.0);
        // Just verify it doesn't panic
    }

    #[test]
    fn test_equator_has_12h_days() {
        // Near equator, sunrise ~6am, sunset ~6pm year-round
        let date = NaiveDate::from_ymd_opt(2026, 6, 15).unwrap();
        let result = calculate(date, 0.0, 0.0);
        assert!(result.is_some());
        let solar = result.unwrap();
        let sunrise_h = solar.sunrise.hour() as f64 + solar.sunrise.minute() as f64 / 60.0;
        let sunset_h = solar.sunset.hour() as f64 + solar.sunset.minute() as f64 / 60.0;
        // Equator: ~12h daylight
        let daylight = sunset_h - sunrise_h;
        assert!(
            daylight > 11.0 && daylight < 13.0,
            "Equator should have ~12h daylight, got {:.1}h",
            daylight
        );
    }

    #[test]
    fn test_winter_shorter_days_northern_hemisphere() {
        // Chicago winter solstice — short days
        let summer = NaiveDate::from_ymd_opt(2026, 6, 21).unwrap();
        let winter = NaiveDate::from_ymd_opt(2026, 12, 21).unwrap();

        let summer_solar = calculate(summer, 41.88, -87.63).unwrap();
        let winter_solar = calculate(winter, 41.88, -87.63).unwrap();

        let summer_daylight = time_to_hours(summer_solar.sunset) - time_to_hours(summer_solar.sunrise);
        let winter_daylight = time_to_hours(winter_solar.sunset) - time_to_hours(winter_solar.sunrise);

        assert!(
            summer_daylight > winter_daylight,
            "Summer ({:.1}h) should have more daylight than winter ({:.1}h)",
            summer_daylight, winter_daylight
        );
    }

    #[test]
    fn test_different_longitudes_different_times() {
        // Same latitude, different longitude → different local times
        let date = NaiveDate::from_ymd_opt(2026, 6, 15).unwrap();
        // Note: this test depends on local timezone being consistent
        let _east = calculate(date, 41.88, -80.0);
        let _west = calculate(date, 41.88, -100.0);
        // Both should return Some, exact times depend on local TZ offset
        assert!(_east.is_some());
        assert!(_west.is_some());
    }

    fn time_to_hours(t: NaiveTime) -> f64 {
        t.hour() as f64 + t.minute() as f64 / 60.0 + t.second() as f64 / 3600.0
    }
}
