pub use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
use std::fmt;
use std::path::PathBuf;

pub type FixpNum = fixed::types::U54F10;

pub struct Handle {
    base: PathBuf,
    name: String,
}

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("CSV error: {0:?}")]
    Csv(#[from] csv::Error),

    #[error("CSV into_inner/flush error: {}", .0.error())]
    CsvIntoInner(#[from] csv::IntoInnerError<csv::Writer<tempfile::NamedTempFile>>),

    #[error("persist error: {}", .0.error)]
    Persist(#[from] tempfile::PersistError),
}

#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct LogEntry {
    /// we don't expect that we would need to convert this data-set to a
    /// different time zone
    pub date: NaiveDate,

    pub cmd: LogCommand,

    pub value: Option<FixpNum>,

    /// may be empty
    pub comment: String,
}

#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum LogCommand {
    /// meter broke, value approximated
    #[serde(rename = "brk")]
    Broken,
    /// indicate that the meter/counter value was reset
    #[serde(rename = "rst")]
    Reset,
    /// this assumes a monotonic increase
    #[serde(rename = "inc")]
    Increase,
    /// this assumes a monotonic decrease, e.g. for (accidentially) reversed counters
    #[serde(rename = "dec")]
    Decrease,
}

impl Handle {
    pub fn new(base: PathBuf, name: String) -> Self {
        Self { base, name }
    }

    pub fn name(&self) -> &str {
        &self.name
    }

    pub fn log_path(&self) -> PathBuf {
        let mut path = self.base.join(&self.name);
        path.set_extension("metercsv");
        path
    }

    pub fn read_logdata(&self) -> Result<BTreeSet<LogEntry>, Error> {
        csv::ReaderBuilder::new()
            .delimiter(0x1f)
            .from_path(self.log_path())?
            .deserialize()
            .map(|i| i.map_err(Into::into))
            .collect()
    }

    pub fn write_logdata(&self, data: &BTreeSet<LogEntry>) -> Result<(), Error> {
        use std::io::Write;
        let f = tempfile::NamedTempFile::new_in(&self.base).map_err(csv::Error::from)?;
        let mut f = csv::WriterBuilder::new().delimiter(0x1f).from_writer(f);
        for i in data {
            f.serialize(i)?;
        }
        // get the NamedTempFile back
        let mut f = f.into_inner()?;
        f.flush().map_err(csv::Error::from)?;
        f.persist(self.log_path())?;
        Ok(())
    }
}

#[derive(Clone, Copy)]
pub enum ValidateErrorKind {
    InvalidValueDir,
}

pub struct ValidateError<'e> {
    a: &'e LogEntry,
    b: &'e LogEntry,
    kind: ValidateErrorKind,
}

impl fmt::Display for ValidateError<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use ValidateErrorKind as K;
        writeln!(
            f,
            "log error: {}\n  a = {:?}\n  b = {:?}",
            match self.kind {
                K::InvalidValueDir => "invalid value direction",
            },
            self.a,
            self.b
        )
    }
}

pub fn validate_logdata<'a>(
    logdata: &'a BTreeSet<LogEntry>,
    limit_date: Option<&NaiveDate>,
) -> Vec<ValidateError<'a>> {
    let ldv: Vec<&_> = logdata.iter().collect();
    ldv.windows(2)
        .map(|i| match i {
            [a, b] => (a, b),
            _ => unreachable!(),
        })
        .filter(|(a, b)| {
            if let Some(x) = limit_date {
                if !(&a.date == x || &b.date == x) {
                    return false;
                }
            }
            true
        })
        .filter_map(|(a, b)| {
            use LogCommand as C;
            use ValidateErrorKind as K;
            Some(ValidateError {
                a,
                b,
                kind: match (a.cmd, b.cmd) {
                    (C::Increase, C::Increase) if a.value >= b.value => K::InvalidValueDir,
                    (C::Decrease, C::Decrease) if a.value <= b.value => K::InvalidValueDir,
                    _ => return None,
                },
            })
        })
        .collect()
}

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

    #[test]
    fn paths() {
        assert_eq!(
            Handle::new(PathBuf::from("../meters"), "whubbs".to_string()).log_path(),
            PathBuf::from("../meters/whubbs.metercsv")
        );
    }

    #[test]
    fn basic() {
        let dir = tempfile::tempdir().expect("unable to create temporary directory");
        let handle = Handle::new(
            dir.path().to_path_buf(),
            "basic-countermeter-test".to_string(),
        );

        let data = vec![
            LogEntry {
                date: NaiveDate::from_ymd(2021, 10, 10),
                cmd: LogCommand::Reset,
                value: None,
                comment: "it starts happening\"!!".to_string(),
            },
            LogEntry {
                date: NaiveDate::from_ymd(2021, 10, 11),
                cmd: LogCommand::Increase,
                value: Some(fixed_macro::fixed!(0.010: U54F10)),
                comment: String::new(),
            },
            LogEntry {
                date: NaiveDate::from_ymd(2021, 10, 12),
                cmd: LogCommand::Increase,
                value: Some(fixed_macro::fixed!(0.011: U54F10)),
                comment: String::new(),
            },
            LogEntry {
                date: NaiveDate::from_ymd(2021, 10, 13),
                cmd: LogCommand::Broken,
                value: Some(fixed_macro::fixed!(0.012: U54F10)),
                comment: "water damage, ouch...".to_string(),
            },
            LogEntry {
                date: NaiveDate::from_ymd(2021, 10, 14),
                cmd: LogCommand::Decrease,
                value: Some(fixed_macro::fixed!(0.010: U54F10)),
                comment: String::new(),
            },
        ]
        .into_iter()
        .collect::<BTreeSet<_>>();

        handle.write_logdata(&data).expect("unable to write data");
        {
            // compare serialization output directly
            let datv = std::fs::read(handle.log_path()).expect("unable to read data (1)");
            assert_eq!(
                std::str::from_utf8(&datv[..]).expect("utf8?"),
                r#"datecmdvaluecomment
2021-10-10rst"it starts happening""!!"
2021-10-11inc0.01
2021-10-12inc0.011
2021-10-13brk0.012water damage, ouch...
2021-10-14dec0.01
"#
            );
        }
        assert_eq!(
            data,
            handle.read_logdata().expect("unable to read data (2)")
        );
    }
}