use std::{
    fmt::{self, Display},
    fs,
    io::Error,
    path::Path,
    vec::Vec,
};

use chrono::{NaiveDate, NaiveTime};
use serde::Serialize;
use thiserror::Error;
#[cfg(feature = "bindings")]
use ts_rs::TS;

use crate::igc::{parse_b_record, parse_date, IgcError, IgcFix};

#[derive(Debug, Error)]
#[error("(line {line_number}): {source}")]
pub struct IgcErrorWithLine {
    source: IgcError,
    line_number: usize,
}

#[derive(Error, Debug)]
pub enum FlightError {
    #[error("Invalid IGC file {0}")]
    InvalidFile(#[from] IgcErrorWithLine),

    #[error("Could not read IGC file: {0}")]
    Io(#[from] Error),

    #[error("Invalid IGC file: Missing date")]
    MissingDate,
}

#[derive(Default, Debug, Serialize, Clone)]
#[cfg_attr(feature = "bindings", derive(TS))]
#[cfg_attr(feature = "bindings", ts(export))]
pub struct Flight {
    #[cfg_attr(feature = "bindings", ts(as = "str"))]
    pub date: NaiveDate,
    pub positions: Vec<IgcFix>,
}

impl Display for Flight {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let first_fix = match self.positions.first() {
            Some(fix) => fix.time.to_rfc3339(),
            None => "?".into(),
        };
        write!(
            f,
            "first_fix: {} fixes: {}",
            first_fix,
            self.positions.len()
        )
    }
}

impl Flight {
    pub fn from_igc_file(file: &Path) -> Result<Self, FlightError> {
        let contents = fs::read_to_string(file)?;
        Self::from_igc_contents(&contents)
    }

    pub fn from_igc_contents(contents: &str) -> Result<Self, FlightError> {
        let (date_line_number, date_line) = contents
            .lines()
            .enumerate()
            .find(|(_, line)| line.starts_with("HFDTE"))
            .ok_or(FlightError::MissingDate)?;

        let date = parse_date(date_line).map_err(|source| IgcErrorWithLine {
            source,
            line_number: date_line_number + 1,
        })?;

        let mut current_fix_date = date;
        let mut last_fix_naive_time: Option<NaiveTime> = None;
        let mut positions = vec![];

        for (i, line) in contents.lines().enumerate() {
            if let Some('B') = line.chars().next() {
                let record = parse_b_record(line).map_err(|source| IgcErrorWithLine {
                    source,
                    line_number: i + 1,
                })?;

                if let Some(last_time) = last_fix_naive_time {
                    if record.time < last_time {
                        current_fix_date = current_fix_date
                            .succ_opt()
                            .ok_or(IgcError::DateOverflow {})
                            .map_err(|source| IgcErrorWithLine {
                                source,
                                line_number: i + 1,
                            })?;
                    }
                }

                positions.push(IgcFix {
                    time: current_fix_date.and_time(record.time).and_utc(),
                    lat: record.lat,
                    lon: record.lon,
                    alt: record.alt,
                });
                last_fix_naive_time = Some(record.time);
            }
        }

        Ok(Self { date, positions })
    }
}

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

    #[test]
    fn test_missing_date() {
        let contents = "";
        let result = Flight::from_igc_contents(contents);
        assert!(result.is_err());
        assert_eq!(
            result.unwrap_err().to_string(),
            "Invalid IGC file: Missing date"
        );
    }

    #[test]
    fn test_truncated_date() {
        let contents = "HFDTE";
        let result = Flight::from_igc_contents(contents);
        assert!(result.is_err());
        assert_eq!(
            result.unwrap_err().to_string(),
            "Invalid IGC file (line 1): Incomplete record"
        );
    }

    #[test]
    fn test_invalid_date() {
        let contents = "HFDTE123456";
        let result = Flight::from_igc_contents(contents);
        assert!(result.is_err());
        assert!(result
            .unwrap_err()
            .to_string()
            .starts_with("Invalid IGC file (line 1): Invalid date"));
    }

    #[test]
    fn test_first_fix_timestamp() {
        let contents = vec!["HFDTE120924", "B1912123838057N11203636WA0260302664"].join("\n");
        let flight = Flight::from_igc_contents(&contents).unwrap();
        let first_fix = flight.positions.first().unwrap();
        assert_eq!(first_fix.time.to_rfc3339(), "2024-09-12T19:12:12+00:00");
    }

    #[test]
    fn test_read_igc() {
        let contents = include_str!("../data/2024-pwca-monroe-t3-33.igc");
        let flight = Flight::from_igc_contents(contents).unwrap();
        assert_eq!(flight.positions.len(), 8398);
        assert_eq!(flight.date, NaiveDate::from_ymd_opt(2024, 9, 12).unwrap());
    }

    #[test]
    fn test_read_igc_date_wraparound() {
        let contents = include_str!("../data/utc-date-wraparound.igc");
        let flight = Flight::from_igc_contents(contents).unwrap();
        assert_eq!(
            flight.positions.first().unwrap().time.to_rfc3339(),
            "2024-06-05T23:55:00+00:00"
        );
        assert_eq!(
            flight.positions.last().unwrap().time.to_rfc3339(),
            "2024-06-06T00:05:00+00:00"
        );
    }
}
