//! Ouput sink, e.g. for INFO and above, which results in a truncating
//! series of files with a common name prefix and date of creation.

use crate::debug;
use fs::File;
use io::BufWriter;
use io::Seek;
use io::SeekFrom;
use io::Write;
use std::fs;
use std::io;
use std::path::Path;
use std::path::PathBuf;
use thiserror::Error;
use time::OffsetDateTime;
use time::PrimitiveDateTime;

#[derive(Debug, Error)]
pub enum SinkError {
    #[error("TextError({0})")]
    Text(String),
    #[error("IOError({0})")]
    IOError(#[from] io::Error),
    #[error("IOError({0})")]
    NextFileExists(PathBuf),
    #[error("Format({0})")]
    Format(#[from] time::error::Format),
}

const FORMAT: &[time::format_description::BorrowedFormatItem<'_>] =
    time::macros::format_description!("[year]-[month]-[day]--[hour]:[minute]:[second]");

struct Fileinfo {
    path: PathBuf,
    name: String,
    len: usize,
}

fn file_list(dir: &Path, prefix: &str) -> Result<Vec<Fileinfo>, SinkError> {
    let prefix2 = format!("{prefix}-20");
    let mut ret = Vec::new();
    let rd = fs::read_dir(&dir)?;
    for e in rd {
        let e = e?;
        let fnos = e.file_name();
        let fns = fnos.to_str().unwrap_or("");
        if fns.starts_with(&prefix2) && fns.ends_with(".log") {
            let meta = e.metadata()?;
            let info = Fileinfo {
                path: e.path(),
                name: fns.into(),
                len: meta.len() as usize,
            };
            ret.push(info);
        }
    }
    ret.sort_by(|a, b| std::cmp::Ord::cmp(&a.name, &b.name));
    Ok(ret)
}

pub enum Filter {
    All,
    Info,
}

pub struct Logsink {
    dir: PathBuf,
    filter: Filter,
    prefix: String,
    fout: BufWriter<File>,
    total_max: usize,
    file_max: usize,
    bytes_written: usize,
}

impl Logsink {
    pub fn new<P: Into<PathBuf>, S: Into<String>>(
        dir: P,
        filter: Filter,
        prefix: S,
        total_max: usize,
        file_max: usize,
    ) -> Result<Self, SinkError> {
        let dir = dir.into();
        let prefix = prefix.into();
        let (fout, len) = Self::open_latest_or_new(&dir, &prefix, file_max)?;
        let ret = Self {
            dir,
            filter,
            prefix,
            fout,
            total_max,
            file_max,
            bytes_written: len,
        };
        ret.prune()?;
        Ok(ret)
    }

    pub fn push_lines(&mut self, lines: &[&[u8]]) -> Result<(), SinkError> {
        for line in lines {
            match self.filter {
                Filter::All => {}
                Filter::Info => {
                    // TODO relax format
                    // 2023-06-07T08:45:40.277Z  INFO msg...
                    if line.len() >= 30 {
                        let s = &line[25..30];
                        if s == b" INFO" || s == b" WARN" || s == b"ERROR" {
                        } else {
                            continue;
                        }
                    } else {
                        continue;
                    }
                }
            }
            self.fout.write_all(line)?;
            self.fout.write_all(b"\n")?;
            self.bytes_written += 1 + line.len();
        }
        Ok(())
    }

    pub fn push_data(&mut self, data: &[u8]) -> Result<(), SinkError> {
        self.fout.write_all(data)?;
        self.bytes_written += 1 + data.len();
        Ok(())
    }

    pub fn flush(&mut self) -> Result<(), SinkError> {
        self.fout.flush()?;
        if self.bytes_written >= self.file_max {
            self.bytes_written = 0;
            self.prune()?;
            self.fout = Self::next_file(&self.dir, &self.prefix)?;
        }
        Ok(())
    }

    fn open_latest_or_new(
        dir: &Path,
        prefix: &str,
        file_mb: usize,
    ) -> Result<(BufWriter<File>, usize), SinkError> {
        let list = file_list(dir, prefix)?;
        if let Some(latest) = list.last() {
            if latest.len < file_mb {
                let mut ret = fs::OpenOptions::new()
                    .write(true)
                    .append(true)
                    .open(&latest.path)?;
                let len = ret.seek(SeekFrom::End(0))? as usize;
                let ret = BufWriter::new(ret);
                Ok((ret, len))
            } else {
                Ok((Self::next_file(dir, prefix)?, 0))
            }
        } else {
            Ok((Self::next_file(dir, prefix)?, 0))
        }
    }

    fn next_file_for_datetime(
        dir: &Path,
        prefix: &str,
        ts: time::PrimitiveDateTime,
    ) -> Result<BufWriter<File>, SinkError> {
        let s = ts.format(FORMAT)?;
        let fname = dir.join(format!("{}-{}.log", prefix, s));
        let ret = fs::OpenOptions::new()
            .write(true)
            .create(true)
            .create_new(false)
            .append(true)
            .open(&fname)?;
        let ret = BufWriter::new(ret);
        Ok(ret)
    }

    fn next_file(dir: &Path, prefix: &str) -> Result<BufWriter<File>, SinkError> {
        let ts1 = OffsetDateTime::now_utc();
        let ts2 = PrimitiveDateTime::new(ts1.date(), ts1.time());
        Self::next_file_for_datetime(dir, prefix, ts2)
    }

    fn prune(&self) -> Result<(), SinkError> {
        let file_list = file_list(&self.dir, &self.prefix)?;
        for e in &file_list {
            debug!("[APPEND-SEES-FILE]  {:?}  {}\n", e.name, e.len);
        }
        let lentot = file_list.iter().map(|x| x.len).fold(0, |a, x| a + x);
        debug!("[APPEND-LENTOT] {}\n", lentot);
        let mut c1 = 0;
        for e in file_list.iter().rev() {
            c1 += e.len;
            if c1 > self.total_max {
                debug!("[APPEND-REMOVE]   {}  {}\n", e.name, e.len);
                fs::remove_file(&e.path)?;
            }
        }
        Ok(())
    }
}
