//! TLE parser.
//!
//! Parses three-line TLE sets (name + line 1 + line 2) into the [`Tle`] struct.
//! Handles TLE-specific number formats: implied-decimal eccentricity and
//! scientific notation without the leading "0." or "E".  Validates checksums.
//!
//! Returns `Io<Error, Tle>` for lazy evaluation via comp-cat-rs.

use comp_cat_rs::effect::io::Io;

use crate::error::{Error, TleParseDetail};
use crate::tle::Tle;
use crate::tle::newtypes::{
    ArgPerigee, Bstar, Classification, Eccentricity, ElementSetNumber, EpochDay, EpochYear,
    Inclination, LineNumber, MeanAnomaly, MeanMotion, MeanMotionDdot, MeanMotionDot, Raan,
    RevolutionNumber, SatelliteName, SatelliteNumber,
};

// ---------------------------------------------------------------------------
// Helper: extract a substring slice from a TLE line
// ---------------------------------------------------------------------------

fn slice(line: &str, start: usize, end: usize) -> Result<&str, Error> {
    line.get(start..end).ok_or(
        Error::TleParseFailed(TleParseDetail::LineLengthWrong {
            expected: end,
            actual: line.len(),
        })
    )
}

fn parse_float(s: &str, field: &'static str) -> Result<f64, Error> {
    s.trim()
        .parse::<f64>()
        .map_err(|_| {
            TleParseDetail::MalformedFloat {
                field,
                raw: s.to_string(),
            }
            .into()
        })
}

fn parse_u32(s: &str, field: &'static str) -> Result<u32, Error> {
    s.trim()
        .parse::<u32>()
        .map_err(|_| {
            TleParseDetail::MalformedInt {
                field,
                raw: s.to_string(),
            }
            .into()
        })
}

fn parse_u16(s: &str, field: &'static str) -> Result<u16, Error> {
    s.trim()
        .parse::<u16>()
        .map_err(|_| {
            TleParseDetail::MalformedInt {
                field,
                raw: s.to_string(),
            }
            .into()
        })
}

fn parse_u8(s: &str, field: &'static str) -> Result<u8, Error> {
    s.trim()
        .parse::<u8>()
        .map_err(|_| {
            TleParseDetail::MalformedInt {
                field,
                raw: s.to_string(),
            }
            .into()
        })
}

// ---------------------------------------------------------------------------
// TLE-specific number formats
// ---------------------------------------------------------------------------

/// Parse TLE's implied-decimal eccentricity format.
/// TLE eccentricity is given as digits without a leading "0.", e.g. "0006703" means 0.0006703.
fn parse_eccentricity(s: &str) -> Result<Eccentricity, Error> {
    let trimmed = s.trim();
    let with_decimal = format!("0.{trimmed}");
    parse_float(&with_decimal, "eccentricity").and_then(Eccentricity::new)
}

/// Parse TLE's scientific notation format: "12345-6" means 0.12345e-6.
/// Also handles "+12345-6" and "-12345-6" and " 00000+0".
fn parse_tle_scientific(s: &str, field: &'static str) -> Result<f64, Error> {
    let trimmed = s.trim();

    // Handle the special case of all zeros
    if trimmed.chars().all(|c| c == '0' || c == ' ' || c == '+' || c == '-') {
        // Check if it's truly zero (like " 00000+0" or " 00000-0")
        let has_nonzero_mantissa = trimmed
            .chars()
            .take_while(|c| *c != '+' && *c != '-' || c.is_ascii_whitespace())
            .any(|c| c.is_ascii_digit() && c != '0');
        if !has_nonzero_mantissa {
            return Ok(0.0);
        }
    }

    // Find the exponent sign (the last + or - that isn't at position 0)
    let exp_pos = trimmed
        .char_indices()
        .rev()
        .find(|(i, c)| *i > 0 && (*c == '+' || *c == '-'))
        .map(|(i, _)| i);

    match exp_pos {
        Some(pos) => {
            let mantissa_str = &trimmed[..pos];
            let exp_str = &trimmed[pos..];

            // Determine sign of mantissa
            let (sign, digits) = mantissa_str
                .strip_prefix('-')
                .map_or_else(
                    || {
                        mantissa_str
                            .strip_prefix('+')
                            .map_or((1.0, mantissa_str), |rest| (1.0, rest))
                    },
                    |rest| (-1.0_f64, rest),
                );

            let mantissa = parse_float(&format!("0.{}", digits.trim()), field)?;
            let exponent = parse_float(exp_str, field)?;
            Ok(sign * mantissa * 10.0_f64.powf(exponent))
        }
        None => parse_float(trimmed, field),
    }
}

// ---------------------------------------------------------------------------
// Checksum validation
// ---------------------------------------------------------------------------

fn compute_checksum(line: &str) -> Result<u8, Error> {
    let sum: u32 = line
        .chars()
        .take(68)
        .map(|c| match c {
            '1'..='9' => u32::from(c) - u32::from('0'),
            '-' => 1,
            _ => 0,
        })
        .sum();
    u8::try_from(sum % 10).map_err(|_| {
        TleParseDetail::MalformedInt {
            field: "checksum",
            raw: sum.to_string(),
        }
        .into()
    })
}

fn validate_checksum(line: &str, line_num: u8) -> Result<(), Error> {
    let line_number = LineNumber::new(line_num)?;
    let expected_digit = line
        .chars()
        .nth(68)
        .and_then(|c| c.to_digit(10))
        .ok_or(Error::InvalidLineFormat {
            line_number,
            reason: "missing checksum digit at column 69",
        })?;
    let expected = u8::try_from(expected_digit).map_err(|_| {
        Error::TleParseFailed(TleParseDetail::MalformedInt {
            field: "checksum",
            raw: expected_digit.to_string(),
        })
    })?;
    let actual = compute_checksum(line)?;
    if expected == actual {
        Ok(())
    } else {
        Err(Error::InvalidChecksum { expected, actual })
    }
}

// ---------------------------------------------------------------------------
// Line 1 parsing
// ---------------------------------------------------------------------------

struct Line1 {
    satellite_number: SatelliteNumber,
    classification: Classification,
    epoch_year: EpochYear,
    epoch_day: EpochDay,
    mean_motion_dot: MeanMotionDot,
    mean_motion_ddot: MeanMotionDdot,
    bstar: Bstar,
    element_set_number: ElementSetNumber,
}

fn parse_line1(line: &str) -> Result<Line1, Error> {
    validate_checksum(line, 1)?;

    let sat_num = parse_u32(slice(line, 2, 7)?, "satellite_number")
        .and_then(SatelliteNumber::new)?;

    let classification = slice(line, 7, 8)?
        .chars()
        .next()
        .ok_or(TleParseDetail::MissingField("classification"))
        .and_then(|c| Classification::from_char(c).map_err(|_| {
            TleParseDetail::MalformedInt {
                field: "classification",
                raw: c.to_string(),
            }
        }))?;

    let epoch_year_raw = parse_u8(slice(line, 18, 20)?, "epoch_year")?;
    let epoch_year = EpochYear::from_two_digit(epoch_year_raw);

    let epoch_day = parse_float(slice(line, 20, 32)?, "epoch_day")
        .and_then(EpochDay::new)?;

    let mm_first_deriv = parse_float(slice(line, 33, 43)?, "mean_motion_dot")
        .map(MeanMotionDot::new)?;

    let mm_second_deriv = parse_tle_scientific(slice(line, 44, 52)?, "mean_motion_ddot")
        .map(MeanMotionDdot::new)?;

    let bstar = parse_tle_scientific(slice(line, 53, 61)?, "bstar")
        .map(Bstar::new)?;

    let element_set_number = parse_u16(slice(line, 64, 68)?, "element_set_number")
        .map(ElementSetNumber::new)?;

    Ok(Line1 {
        satellite_number: sat_num,
        classification,
        epoch_year,
        epoch_day,
        mean_motion_dot: mm_first_deriv,
        mean_motion_ddot: mm_second_deriv,
        bstar,
        element_set_number,
    })
}

// ---------------------------------------------------------------------------
// Line 2 parsing
// ---------------------------------------------------------------------------

struct Line2 {
    inclination: Inclination,
    raan: Raan,
    eccentricity: Eccentricity,
    arg_perigee: ArgPerigee,
    mean_anomaly: MeanAnomaly,
    mean_motion: MeanMotion,
    revolution_number: RevolutionNumber,
}

fn parse_line2(line: &str) -> Result<Line2, Error> {
    validate_checksum(line, 2)?;

    let inclination = parse_float(slice(line, 8, 16)?, "inclination")
        .and_then(Inclination::from_degrees)?;

    let raan = parse_float(slice(line, 17, 25)?, "raan")
        .and_then(Raan::from_degrees)?;

    let eccentricity = parse_eccentricity(slice(line, 26, 33)?)?;

    let arg_perigee = parse_float(slice(line, 34, 42)?, "arg_perigee")
        .and_then(ArgPerigee::from_degrees)?;

    let mean_anomaly = parse_float(slice(line, 43, 51)?, "mean_anomaly")
        .and_then(MeanAnomaly::from_degrees)?;

    let mean_motion = parse_float(slice(line, 52, 63)?, "mean_motion")
        .and_then(MeanMotion::new)?;

    let revolution_number = parse_u32(slice(line, 63, 68)?, "revolution_number")
        .map(RevolutionNumber::new)?;

    Ok(Line2 {
        inclination,
        raan,
        eccentricity,
        arg_perigee,
        mean_anomaly,
        mean_motion,
        revolution_number,
    })
}

// ---------------------------------------------------------------------------
// Public API: parse a three-line TLE into Io<Error, Tle>
// ---------------------------------------------------------------------------

/// Parse a three-line TLE set (name line + line 1 + line 2) into a `Tle`.
///
/// Returns an `Io<Error, Tle>` that executes lazily.
#[must_use]
pub fn parse_tle(line0: &str, line1: &str, line2: &str) -> Io<Error, Tle> {
    let line0 = line0.to_string();
    let line1 = line1.to_string();
    let line2 = line2.to_string();

    Io::suspend(move || {
        let name = SatelliteName::new(&line0);
        let l1 = parse_line1(&line1)?;
        let l2 = parse_line2(&line2)?;

        Ok(Tle::new(
            name,
            l1.satellite_number,
            l1.classification,
            l1.epoch_year,
            l1.epoch_day,
            l1.mean_motion_dot,
            l1.mean_motion_ddot,
            l1.bstar,
            l1.element_set_number,
            l2.inclination,
            l2.raan,
            l2.eccentricity,
            l2.arg_perigee,
            l2.mean_anomaly,
            l2.mean_motion,
            l2.revolution_number,
        ))
    })
}

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

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

    // ISS TLE (a well-known test case)
    const ISS_LINE0: &str = "ISS (ZARYA)";
    const ISS_LINE1: &str = "1 25544U 98067A   24045.54339622  .00016717  00000-0  10270-3 0  9118";
    const ISS_LINE2: &str = "2 25544  51.6400 208.2546 0006703  44.2624  67.4637 15.50056823 23455";

    fn parse_iss() -> Result<Tle, Error> {
        parse_tle(ISS_LINE0, ISS_LINE1, ISS_LINE2).run()
    }

    #[test]
    fn parse_iss_tle_succeeds() -> Result<(), Error> {
        parse_iss().map(|_| ())
    }

    #[test]
    fn parsed_iss_satellite_number() -> Result<(), Error> {
        let tle = parse_iss()?;
        assert_eq!(tle.satellite_number().value(), 25544);
        Ok(())
    }

    #[test]
    fn parsed_iss_name() -> Result<(), Error> {
        let tle = parse_iss()?;
        assert_eq!(tle.name().value(), "ISS (ZARYA)");
        Ok(())
    }

    #[test]
    fn parsed_iss_epoch_year() -> Result<(), Error> {
        let tle = parse_iss()?;
        assert_eq!(tle.epoch_year().value(), 2024);
        Ok(())
    }

    #[test]
    fn parsed_iss_inclination_degrees() -> Result<(), Error> {
        let tle = parse_iss()?;
        assert!((tle.inclination().degrees() - 51.64).abs() < 0.01);
        Ok(())
    }

    #[test]
    fn parsed_iss_eccentricity() -> Result<(), Error> {
        let tle = parse_iss()?;
        assert!((tle.eccentricity().value() - 0.0006703).abs() < 1e-8);
        Ok(())
    }

    #[test]
    fn parsed_iss_mean_motion() -> Result<(), Error> {
        let tle = parse_iss()?;
        assert!((tle.mean_motion().revs_per_day() - 15.50056823).abs() < 1e-6);
        Ok(())
    }

    #[test]
    fn checksum_validation_catches_bad_line() {
        let bad_line1 = "1 25544U 98067A   24045.54339622  .00016717  00000-0  10270-3 0  9119";
        let result = parse_tle(ISS_LINE0, bad_line1, ISS_LINE2).run();
        assert!(result.is_err());
    }

    #[test]
    fn short_line_produces_error() {
        let result = parse_tle(ISS_LINE0, "1 25544", ISS_LINE2).run();
        assert!(result.is_err());
    }

    #[test]
    fn tle_scientific_notation_parses_correctly() -> Result<(), Error> {
        let val = parse_tle_scientific(" 10270-3", "test")?;
        assert!((val - 0.00010270).abs() < 1e-10);
        Ok(())
    }

    #[test]
    fn tle_scientific_notation_zero() -> Result<(), Error> {
        let val = parse_tle_scientific(" 00000+0", "test")?;
        assert!((val - 0.0).abs() < 1e-15);
        Ok(())
    }

    #[test]
    fn parse_tiangong_tle_succeeds() -> Result<(), Error> {
        parse_tle(
            "TIANGONG",
            "1 48274U 21035A   24045.38741100  .00015720  00000-0  18614-3 0  9993",
            "2 48274  41.4720 157.3210 0005390 282.4110  77.6356 15.61052866157635",
        )
        .run()
        .map(|_| ())
    }

    #[test]
    fn parse_hst_tle_succeeds() -> Result<(), Error> {
        parse_tle(
            "HST",
            "1 20580U 90037B   24044.81517361  .00001264  00000-0  64072-4 0  9996",
            "2 20580  28.4688 138.4362 0002585 120.2784 239.8455 15.09759513422261",
        )
        .run()
        .map(|_| ())
    }
}
