use frunk::hlist;
use frunk::Func;
use frunk::HCons;
use frunk::HNil;

use frunk::Poly;
use std::env;
use std::fmt::Display;
use std::future::Future;
use std::sync::Arc;
use std::time::Duration;
use tokio::task::JoinError;

use crate::providers::basic::device_name;
use crate::providers::basic::distro;
use crate::providers::basic::full_pwd;
use crate::providers::basic::hostname;
use crate::providers::basic::pwd;
use crate::providers::basic::realname;
use crate::providers::basic::user;
use crate::providers::duration::show as duration_show;
use crate::providers::exit_code::show as exit_code_show;

use crate::chunk::Chunk;
use crate::providers::huge_pages::show as huge_pages_show;
use crate::providers::manifest::show as manifest_show;
use crate::providers::memory::show as memory_show;
use crate::providers::netif::show as netif_show;
use crate::providers::netns::show as net_namespace;
use crate::providers::os::show as os_show;
use crate::providers::ssh::show as ssh_show;
use crate::providers::vcs::{infer_vcs, Vcs, VcsTrait};
use crate::providers::virt::show as virt_show;

use crate::style::build_color_style;

use crate::Options;
use owo_colors::Style;

macro_rules! item {
    ($provider:expr, $opt:expr, $style:expr) => {{
        let cloned_opts = Arc::clone(&$opt);
        let style = $style;
        tokio::spawn(async move {
            let begin = std::time::Instant::now();
            let res = $provider(&cloned_opts)
                .await
                .map(|c| c.with_style(style.0, style.1));
            (provider_name(&$provider), begin.elapsed(), res)
        })
    }};
}

macro_rules! item_vcs {
    ($vcs:expr, $provider:expr, $opt:expr, $style:expr) => {{
        let cloned_opts = Arc::clone(&$opt);
        let vcs = $vcs.clone();
        let style = $style;
        tokio::spawn(async move {
            let begin = std::time::Instant::now();

            if let Some((vcs, path)) = vcs {
                let res = $provider(&vcs, &cloned_opts, &path)
                    .await
                    .map(|c| c.with_style(style.0, style.1));
                return (provider_name(&$provider), begin.elapsed(), res);
            }

            (provider_name(&$provider), begin.elapsed(), None)
        })
    }};
}

pub async fn print_prompt(opts: Options) -> anyhow::Result<()> {
    let start = if opts.timings {
        Some(std::time::Instant::now())
    } else {
        None
    };

    let opts = Arc::new(opts);
    let cwd = env::current_dir()?;
    let vcs = infer_vcs(cwd, &opts).await;

    let (color, bold, def) = (
        build_color_style(opts.theme.as_deref()),
        Style::new().bold(),
        Style::default(),
    );

    let async_prompt = hlist![
        item![user, opts, (color, bold)],
        item![realname, opts, (color, bold)],
        item![hostname, opts, (color, bold.dimmed())],
        item![device_name, opts, (color, bold.dimmed())],
        item![distro, opts, (color, bold.dimmed())],
        item![pwd, opts, (color, bold)],
        item![full_pwd, opts, (color, bold)],
        item![os_show, opts, (color, bold)],
        item![virt_show, opts, (bold, bold)],
        item![memory_show, opts, (bold, bold)],
        item![huge_pages_show, opts, (bold, bold)],
        item![ssh_show, opts, (bold, def)],
        item![netif_show, opts, (bold.dimmed(), def.dimmed())],
        item![net_namespace, opts, (bold, bold)],
        item![manifest_show, opts, (color, bold)],
        item_vcs![vcs, <Vcs as VcsTrait>::branch, opts, (bold, color.bold())],
        item_vcs![vcs, <Vcs as VcsTrait>::status, opts, (bold, color)],
        item_vcs![vcs, <Vcs as VcsTrait>::stash, opts, (bold, def)],
        item_vcs![
            vcs,
            <Vcs as VcsTrait>::worktree,
            opts,
            (bold, bold.dimmed())
        ],
        item_vcs![vcs, <Vcs as VcsTrait>::commit, opts, (bold, bold)],
        item_vcs![vcs, <Vcs as VcsTrait>::divergence, opts, (bold, def)],
        item![duration_show, opts, (def, def.dimmed())],
        item![exit_code_show, opts, (bold.red(), bold)],
    ];

    let prompt = async_prompt.hjoin().await;

    if let Some(start) = start {
        prompt.map(Poly(TimingMapper));
        println!("{:<40} -> {:>15?}", "total time", start.elapsed());
    } else {
        prompt.map(Poly(PrintMapper));
    }

    Ok(())
}

trait HListJoin {
    type Output;
    async fn hjoin(self) -> Self::Output;
}

impl HListJoin for HNil {
    type Output = HNil;
    async fn hjoin(self) -> Self::Output {
        HNil
    }
}

impl<H, T> HListJoin for HCons<H, T>
where
    H: Future + Send,
    T: HListJoin + Send,
    T::Output: Send,
{
    type Output = HCons<H::Output, T::Output>;
    async fn hjoin(self) -> Self::Output {
        let (head_res, tail_res) = tokio::join!(self.head, self.tail.hjoin());
        HCons {
            head: head_res,
            tail: tail_res,
        }
    }
}

struct PrintMapper;
impl<T> Func<Result<(&'static str, Duration, Option<Chunk<T>>), JoinError>> for PrintMapper
where
    T: Display,
{
    type Output = ();

    fn call(input: Result<(&'static str, Duration, Option<Chunk<T>>), JoinError>) -> Self::Output {
        if let (_, _, Some(c)) = input.expect("Task panicked") {
            print!("{c} ")
        }
    }
}

struct TimingMapper;
impl<T> Func<Result<(&'static str, Duration, Option<Chunk<T>>), JoinError>> for TimingMapper
where
    T: Display,
{
    type Output = ();

    fn call(input: Result<(&'static str, Duration, Option<Chunk<T>>), JoinError>) -> Self::Output {
        let (f, dur, c) = input.expect("Task panicked");
        let f = f.replace("auraline::providers::", "");
        if let Some(chunk) = c {
            println!("{f:<40} -> {dur:>15?} : ({chunk})");
        } else {
            println!("{f:<40} -> {dur:>15?} : (_)");
        }
    }
}
#[inline]
fn provider_name<T>(_: &T) -> &'static str {
    std::any::type_name::<T>()
}
