pub(crate) use crate::import::atuin::Atuin;
pub(crate) use crate::import::autojump::Autojump;
pub(crate) use crate::import::fasd::Fasd;
pub(crate) use crate::import::z::Z;
pub(crate) use crate::import::z_lua::ZLua;
pub(crate) use crate::import::zsh_z::ZshZ;

mod atuin;
mod autojump;
mod fasd;
mod z;
mod z_lua;
mod zsh_z;

use std::io::{self, Write};
use std::path::PathBuf;

use anyhow::Result;

use crate::config;
use crate::db::{Database, Dir};

pub(crate) trait Importer {
    /// Yields directory entries to be imported.
    ///
    /// The outer `Result` reports failure to fetch the input (e.g. missing
    /// file, subprocess errored). The per-item `Result` reports a malformed
    /// row, which doesn't necessarily abort the whole import.
    fn dirs(&self) -> Result<impl Iterator<Item = Result<Dir<'static>, ImportError>>>;
}

/// A single record that failed to import.
#[derive(Debug)]
pub(crate) struct ImportError {
    /// Path of the source file containing the offending record. `None` if the
    /// importer is not file-based (e.g. atuin streams from a subprocess).
    pub path: Option<PathBuf>,

    /// 1-indexed line number of the offending input.
    pub line_num: usize,

    /// Underlying reason the record could not be imported.
    pub source: anyhow::Error,
}

/// Drives a single importer end-to-end: writes each `Ok` dir into the
/// database and prints each `Err` to stderr in `<path>:<line>: <reason>`
/// format. Doesn't abort on per-record errors — bad rows are skipped, the
/// rest of the import continues. After the iteration completes successfully,
/// the database is deduplicated and aged.
pub(crate) fn run(importer: &impl Importer, db: &mut Database) -> Result<()> {
    let exclude_dirs = config::exclude_dirs()?;

    let stderr = io::stderr();
    let mut stderr = stderr.lock();

    for entry in importer.dirs()? {
        match entry {
            Ok(dir) => {
                if exclude_dirs.iter().any(|glob| glob.matches(&dir.path)) {
                    continue;
                }
                db.add_unchecked(dir.path, dir.rank, dir.last_accessed);
            }
            Err(e) => {
                let location = match &e.path {
                    Some(path) => format!("{}:{}", path.display(), e.line_num),
                    None => format!("line {}", e.line_num),
                };
                _ = writeln!(stderr, "{location}: {:#}", e.source);
            }
        }
    }

    if db.dirty() {
        db.dedup();
        let max_age = config::maxage()?;
        db.age(max_age);
    }

    Ok(())
}
