use sqlite::{Connection, State};

use crate::BString;
use crate::date::DateTime;
use std::env::current_dir;
use std::path::PathBuf;

fn db_file() -> PathBuf {
    crate::basedir::data_dir().join("history.db")
}

#[derive(Clone)]
pub struct HistoryEntry {
    /// time of execution
    pub time: DateTime,

    /// absolute path where the command was executed
    pub loc: BString,

    /// the command
    pub cmd: BString,
}

pub fn canonical_path(mut path: BString) -> BString {
    while let Some(b'/') = path.last() {
        path.pop();
    }
    if path.is_empty() {
        path.push(b'/');
    }
    path
}

impl HistoryEntry {
    pub fn new(cmd: BString) -> Self {
        Self {
            time: DateTime::now(),
            loc: canonical_path(
                current_dir()
                    .unwrap()
                    .as_os_str()
                    .as_encoded_bytes()
                    .to_vec(),
            ),
            cmd,
        }
    }
}

fn try_db() -> sqlite::Result<Connection> {
    sqlite::open(db_file())
}

fn db() -> Connection {
    try_db().unwrap()
}

pub fn setup() {
    let db = db();

    db.execute(
        "
    CREATE TABLE IF NOT EXISTS history (
        id  INTEGER PRIMARY KEY AUTOINCREMENT,
        ts  INTEGER NOT NULL,
        loc BLOB NOT NULL,
        cmd BLOB NOT NULL
    )
    ",
    )
    .unwrap();

    db.execute("CREATE INDEX IF NOT EXISTS idx_history_ts ON history(ts)")
        .unwrap();
}

fn try_persist(entry: &HistoryEntry) -> sqlite::Result<()> {
    let db = try_db()?;
    let mut s = db.prepare("INSERT INTO history (ts, loc, cmd) VALUES (?, ?, ?)")?;
    s.bind((1, entry.time.unix() as i64))?;
    s.bind((2, entry.loc.as_slice()))?;
    s.bind((3, entry.cmd.as_slice()))?;
    s.next()?;
    Ok(())
}

pub fn persist(entry: &HistoryEntry) {
    // keep quiet in case db fails
    // TODO maybe better behavior?
    let _ = try_persist(entry);
}

pub struct HistoryQueryer {
    db: Connection,
}

impl HistoryQueryer {
    pub fn new() -> sqlite::Result<Self> {
        Ok(Self { db: try_db()? })
    }

    pub fn query(
        &self,
        min_time: Option<DateTime>,
        max_time: Option<DateTime>,
        path_prefix: Option<&[u8]>,
        path_strict: bool,
    ) -> sqlite::Result<impl Iterator<Item = HistoryEntry>> {
        let mut query = String::from("SELECT id, ts, loc, cmd FROM history\n");
        let mut has_cond = false;
        let mut cond = |c| {
            if has_cond {
                query += "AND ";
            } else {
                query += "WHERE ";
            }
            query += c;
            query += "\n";
            has_cond = true;
        };

        let path_prefix = path_prefix.map(|p| canonical_path(p.to_vec()));
        let mut upper = Vec::new();

        if let Some(path_prefix) = &path_prefix {
            if path_strict {
                cond("loc = ?");
            } else {
                upper = path_prefix.to_vec();
                if let Some(last) = upper.last_mut() {
                    *last = last.saturating_add(1);
                } else {
                    upper.push(0xFF);
                }
                cond("loc >= ? AND loc < ?");
            }
        }

        if min_time.is_some() {
            cond("ts >= ?");
        }

        if max_time.is_some() {
            cond("ts <= ?");
        }

        query += "ORDER BY id asc";
        let mut stmt = self.db.prepare(&query)?;

        let mut i = 1;
        if let Some(path_prefix) = &path_prefix {
            stmt.bind((i, path_prefix.as_slice()))?;
            i += 1;
            if !path_strict {
                stmt.bind((i, upper.as_slice()))?;
                i += 1;
            }
        }

        if let Some(t) = min_time {
            stmt.bind((i, t.unix() as i64))?;
            i += 1;
        }

        if let Some(t) = max_time {
            stmt.bind((i, t.unix() as i64))?;
            i += 1;
        }

        let _ = i;

        Ok(std::iter::from_fn(move || match stmt.next() {
            Ok(State::Row) => {
                let ts: i64 = stmt.read::<i64, _>(1).ok()?;
                let loc: Vec<u8> = stmt.read::<Vec<u8>, _>(2).ok()?;
                let cmd: Vec<u8> = stmt.read::<Vec<u8>, _>(3).ok()?;
                let time = DateTime::from_unix(ts as u64);
                Some(HistoryEntry { time, loc, cmd })
            }
            _ => None,
        }))
    }
}

pub fn local_history_filter(
    hist: Vec<HistoryEntry>,
    min_time: Option<DateTime>,
    max_time: Option<DateTime>,
    path_prefix: Option<&[u8]>,
    path_strict: bool,
) -> impl Iterator<Item = HistoryEntry> {
    let path_prefix = path_prefix.map(|p| canonical_path(p.to_vec()));

    hist.into_iter().filter(move |entry| {
        if let Some(t) = &min_time
            && t.unix() > entry.time.unix()
        {
            return false;
        }

        if let Some(t) = &max_time
            && t.unix() < entry.time.unix()
        {
            return false;
        }

        if let Some(path_prefix) = &path_prefix {
            if path_strict {
                let canon_path = canonical_path(entry.loc.clone());
                return &canon_path == path_prefix;
            } else if !entry.loc.starts_with(path_prefix) {
                return false;
            }
        }

        true
    })
}
