use std::path::PathBuf;
use std::str;

use async_trait::async_trait;
use directories::UserDirs;
use eyre::{Result, eyre};
use time::macros::format_description;
use time::{OffsetDateTime, PrimitiveDateTime};

use super::{Importer, Loader, get_histfile_path, unix_byte_lines};
use crate::history::History;
use crate::import::read_to_end;

#[derive(Debug)]
pub struct Replxx {
    bytes: Vec<u8>,
}

fn default_histpath() -> Result<PathBuf> {
    let user_dirs = UserDirs::new().ok_or_else(|| eyre!("could not find user directories"))?;
    let home_dir = user_dirs.home_dir();

    // There is no default histfile for replxx.
    // Here we try a couple of common names.
    let mut candidates = ["replxx_history.txt", ".histfile"].iter();
    loop {
        match candidates.next() {
            Some(candidate) => {
                let histpath = home_dir.join(candidate);
                if histpath.exists() {
                    break Ok(histpath);
                }
            }
            None => {
                break Err(eyre!(
                    "Could not find history file. Try setting and exporting $HISTFILE"
                ));
            }
        }
    }
}

#[async_trait]
impl Importer for Replxx {
    const NAME: &'static str = "replxx";

    async fn new() -> Result<Self> {
        let bytes = read_to_end(get_histfile_path(default_histpath)?)?;
        Ok(Self { bytes })
    }

    async fn entries(&mut self) -> Result<usize> {
        Ok(super::count_lines(&self.bytes) / 2)
    }

    async fn load(self, h: &mut impl Loader) -> Result<()> {
        let mut timestamp = OffsetDateTime::UNIX_EPOCH;

        for b in unix_byte_lines(&self.bytes) {
            let Ok(s) = std::str::from_utf8(b) else {
                continue; // we can skip past things like invalid utf8
            };
            match try_parse_line_as_timestamp(s) {
                Some(t) => timestamp = t,
                None => {
                    // replxx uses ETB character (0x17) as line breaker
                    let cmd = s.replace('\u{0017}', "\n");
                    let imported =
                        History::import().shell("replxx").timestamp(timestamp).command(cmd);

                    h.push(imported.build().into()).await?;
                }
            }
        }

        Ok(())
    }
}

fn try_parse_line_as_timestamp(line: &str) -> Option<OffsetDateTime> {
    // replxx history date time format: ### yyyy-mm-dd hh:mm:ss.xxx
    let date_time_str = line.strip_prefix("### ")?;
    let format =
        format_description!("[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:3]");

    let primitive_date_time = PrimitiveDateTime::parse(date_time_str, format).ok()?;
    // There is no safe way to get local time offset.
    // For simplicity let's just assume UTC.
    Some(primitive_date_time.assume_utc())
}

#[cfg(test)]
mod test {

    use super::Replxx;
    use crate::import::Importer;
    use crate::import::tests::TestLoader;

    #[tokio::test]
    async fn parse_complex() {
        let bytes = r"### 2024-02-10 22:16:28.302
select * from remote('127.0.0.1:20222', view(select 1))
### 2024-02-10 22:16:36.919
select * from numbers(10)
### 2024-02-10 22:16:41.710
select * from system.numbers
### 2024-02-10 22:19:28.655
select 1
### 2024-02-22 11:15:33.046
CREATE TABLE test( stamp DateTime('UTC'))ENGINE = MergeTreePARTITION BY toDate(stamp)order by tuple() as select toDateTime('2020-01-01')+number*60 from numbers(80000);
"
        .as_bytes()
        .to_owned();

        let replxx = Replxx { bytes };

        let mut loader = TestLoader::default();
        replxx.load(&mut loader).await.unwrap();
        let mut history = loader.buf.into_iter();

        // simple wrapper for replxx history entry
        macro_rules! history {
            ($timestamp:expr_2021, $command:expr_2021) => {
                let h = history.next().expect("missing entry in history");
                assert_eq!(h.command.as_str(), $command);
                assert_eq!(h.timestamp.unix_timestamp(), $timestamp);
            };
        }

        history!(1_707_603_388, "select * from remote('127.0.0.1:20222', view(select 1))");
        history!(1_707_603_396, "select * from numbers(10)");
        history!(1_707_603_401, "select * from system.numbers");
        history!(1_707_603_568, "select 1");
        history!(
            1_708_600_533,
            "CREATE TABLE test\n( stamp DateTime('UTC'))\nENGINE = MergeTree\nPARTITION BY \
             toDate(stamp)\norder by tuple() as select toDateTime('2020-01-01')+number*60 from \
             numbers(80000);"
        );
    }

    #[tokio::test]
    async fn skips_invalid_utf8_line() {
        let mut bytes = b"### 2024-02-10 22:16:28.302\nselect 1\n".to_vec();
        bytes.extend_from_slice(b"\xff\xfe\n");
        bytes.extend_from_slice(b"### 2024-02-10 22:16:36.919\nselect 2\n");

        let replxx = Replxx { bytes };

        let mut loader = TestLoader::default();
        replxx.load(&mut loader).await.expect("import must not fail");

        let commands: Vec<&str> = loader.buf.iter().map(|h| h.command.as_str()).collect();
        assert_eq!(commands, ["select 1", "select 2"]);
    }
}
