use core::fmt;
pub use fpr_cli::*;
use std::{
    env::{args, var},
    ffi::OsStr,
    fs::{self, File},
    io::{BufReader, Read},
    panic::Location,
    path::Path,
};

#[macro_export]
macro_rules! version {
    ($c:ty) => {
        mod build {
            include!(concat!(env!("OUT_DIR"), "/built.rs"));
        }
        #[derive(Args)]
        #[args(f = version, desc = "Print version.")]
        pub struct Version {}
        fn version(_: &$c, _: Version) -> Result<(), String> {
            use build::*;
            println!(
                "{} v{PKG_VERSION}: {TARGET}, {PROFILE}",
                env!("CARGO_PKG_NAME"),
            );
            if !PKG_DESCRIPTION.is_empty() {
                println!("{PKG_DESCRIPTION}");
            }
            if !PKG_HOMEPAGE.is_empty() {
                println!("Homeage: {PKG_HOMEPAGE}");
            }
            if !PKG_LICENSE.is_empty() {
                println!("License: {PKG_LICENSE}");
            }
            if !PKG_AUTHORS.is_empty() {
                println!("Authors: {PKG_AUTHORS}");
            }
            if !PKG_REPOSITORY.is_empty() {
                println!(
                    "Repository: {PKG_REPOSITORY}, commit = {}, dirty = {}",
                    GIT_COMMIT_HASH_SHORT.unwrap_or("none"),
                    GIT_DIRTY
                        .map(|e| e.to_string())
                        .unwrap_or("none".to_string()),
                );
            }
            Ok(())
        }
    };
}

pub trait Ctx
where
    Self: Sized,
{
    fn new(s: &str) -> Result<Self, String>;
}

fn irun<C: Ctx, M: Acts<C>, V: Args<C>, K: AsRef<OsStr>, P: AsRef<str>>(
    config_path_override_env: K,
    config_path_default_home_rel: P,
) -> Result<(), String> {
    let c = C::new(&var(config_path_override_env).unwrap_or_else(|_| {
        let home = var("HOME").expect("Failed to get variable '$HOME'");
        format!("{home}/{}", config_path_default_home_rel.as_ref())
    }))?;
    if args().into_iter().skip(1).any(|e| e == *"--version") {
        V::next_impl(&c, &[]).map_err(|e| format!("{e}"))?;
        return Ok(());
    }
    M::run(&c).map_err(|e| format!("{e}"))?;
    Ok(())
}

pub fn run<C: Ctx, M: Acts<C>, V: Args<C>, K: AsRef<OsStr>, P: AsRef<str>>(
    config_path_override_env: K,
    config_path_default_home_rel: P,
) {
    if let Err(e) = irun::<C, M, V, K, P>(config_path_override_env, config_path_default_home_rel) {
        eprintln!("{e}");
    }
}

fn irund<C: Ctx, M: Args<C>, V: Args<C>, K: AsRef<OsStr>, P: AsRef<str>>(
    config_path_override_env: K,
    config_path_default_home_rel: P,
) -> Result<(), String> {
    let c = C::new(&var(config_path_override_env).unwrap_or_else(|_| {
        let home = var("HOME").expect("Failed to get variable '$HOME'");
        format!("{home}/{}", config_path_default_home_rel.as_ref())
    }))?;
    let args: Vec<_> = args().collect();
    let args: Vec<_> = args.iter().map(|e| e.as_str()).collect();
    if args.iter().any(|e| *e == "--version") {
        V::next_impl(&c, &[]).map_err(|e| format!("{e}"))?;
    } else {
        if let Err(e) = M::next_impl(&c, &args[1..]) {
            match e {
                ErrArgs::Help => {
                    println!("{}", M::full_usage(&c, args[0], &args[1..]));
                }
                e => Err(format!("{e}"))?,
            }
        }
    }
    Ok(())
}
pub fn rund<M: Args<C>, V: Args<C>, C: Ctx, K: AsRef<OsStr>, P: AsRef<str>>(
    config_path_override_env: K,
    config_path_default_home_rel: P,
) {
    if let Err(e) = irund::<C, M, V, K, P>(config_path_override_env, config_path_default_home_rel) {
        eprintln!("{e}");
    }
}

pub fn reader<P: AsRef<Path>>(p: P) -> Result<BufReader<Box<dyn Read>>, String> {
    let f: Box<dyn Read> = Box::new(File::open(p.as_ref()).map_err(|e| {
        format!(
            "Failed to open file '{}' because '{e}'.",
            p.as_ref().to_string_lossy()
        )
    })?);
    Ok(BufReader::new(f))
}
pub fn from_toml<P: AsRef<Path>, O: serde::de::DeserializeOwned>(p: P) -> Result<O, String> {
    let mut buf = String::new();
    reader(p.as_ref())?.read_to_string(&mut buf).map_err(|e| {
        format!(
            "Error while reading '{}' because '{e}'.",
            p.as_ref().to_string_lossy()
        )
    })?;
    Ok(toml::from_str(&buf).map_err(|e| {
        format!(
            "Failed to parse '{}' because '{e}'.",
            p.as_ref().to_string_lossy()
        )
    })?)
}
#[track_caller]
pub fn write_toml<P: AsRef<Path>, O: serde::Serialize + fmt::Debug>(
    o: &O,
    p: P,
) -> Result<(), String> {
    let l = Location::caller();
    let s = toml::to_string_pretty(o)
        .map_err(|e| format!("Failed to serialize '{o:#?}' because '{e}' at {l}"))?;
    fs::write(&p, s).map_err(|e| {
        format!(
            "Failed to write to '{}' because '{e}'",
            p.as_ref().to_string_lossy()
        )
    })?;
    Ok(())
}
#[track_caller]
pub fn write_ctoml<P: AsRef<Path>, O: serde::Serialize + fmt::Debug>(
    o: &O,
    p: P,
) -> Result<(), String> {
    let l = Location::caller();
    let s = toml::to_string(o)
        .map_err(|e| format!("Failed to serialize '{o:#?}' because '{e}' at {l}"))?;
    fs::write(&p, s).map_err(|e| {
        format!(
            "Failed to write to '{}' because '{e}'",
            p.as_ref().to_string_lossy()
        )
    })?;
    Ok(())
}
