use std::{
    collections::BTreeMap,
    ops::{Deref, DerefMut},
};

use async_trait::async_trait;

pub struct CuddleCI {
    pr_action: Vec<Box<dyn PullRequestAction + Send + Sync>>,
    main_action: Vec<Box<dyn MainAction + Send + Sync>>,
    release_action: Vec<Box<dyn ReleaseAction + Send + Sync>>,
}

#[derive(Default, Debug)]
pub struct Context {
    store: BTreeMap<String, String>,
}

impl Deref for Context {
    type Target = BTreeMap<String, String>;

    fn deref(&self) -> &Self::Target {
        &self.store
    }
}

impl DerefMut for Context {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.store
    }
}

impl CuddleCI {
    pub fn new(
        pr: Box<dyn PullRequestAction + Send + Sync>,
        main: Box<dyn MainAction + Send + Sync>,
        release: Box<dyn ReleaseAction + Send + Sync>,
    ) -> Self {
        Self {
            pr_action: vec![pr],
            main_action: vec![main],
            release_action: vec![release],
        }
    }

    pub fn with_pull_request<T>(&mut self, pr: &T) -> &mut Self
    where
        T: PullRequestAction + ToOwned + Send + Sync + 'static,
        T: ToOwned<Owned = T>,
    {
        self.pr_action.push(Box::new(pr.to_owned()));

        self
    }

    pub fn with_main<T>(&mut self, main: &T) -> &mut Self
    where
        T: MainAction + Send + Sync + 'static,
        T: ToOwned<Owned = T>,
    {
        self.main_action.push(Box::new(main.to_owned()));

        self
    }

    pub fn with_release<T>(&mut self, release: &T) -> &mut Self
    where
        T: ReleaseAction + Send + Sync + 'static,
        T: ToOwned<Owned = T>,
    {
        self.release_action.push(Box::new(release.to_owned()));

        self
    }

    pub async fn execute(
        &mut self,
        args: impl IntoIterator<Item = impl Into<String>>,
    ) -> eyre::Result<()> {
        let matches = clap::Command::new("cuddle-ci")
            .about("is a wrapper around common CI actions")
            .subcommand(clap::Command::new("pr"))
            .subcommand(clap::Command::new("main"))
            .subcommand(clap::Command::new("release"))
            .subcommand_required(true)
            .try_get_matches_from(args.into_iter().map(|a| a.into()).collect::<Vec<String>>())?;

        let mut context = Context::default();

        match matches.subcommand() {
            Some((name, args)) => match (name, args) {
                ("pr", _args) => {
                    eprintln!("starting pr validate");
                    for pr_action in self.pr_action.iter() {
                        pr_action.execute_pull_request(&mut context).await?;
                    }
                    eprintln!("finished pr validate");
                }
                ("main", _args) => {
                    eprintln!("starting main validate");
                    for main_action in self.main_action.iter() {
                        main_action.execute_main(&mut context).await?;
                    }
                    eprintln!("finished main validate");
                }
                ("release", _args) => {
                    eprintln!("starting release validate");
                    for release_action in self.release_action.iter() {
                        release_action.execute_release(&mut context).await?;
                    }
                    eprintln!("finished release validate");
                }
                (command_name, _) => {
                    eyre::bail!("command is not recognized: {}", command_name)
                }
            },
            None => eyre::bail!("command required a subcommand [pr, main, release] etc."),
        }

        Ok(())
    }
}

impl Default for CuddleCI {
    fn default() -> Self {
        Self::new(
            Box::new(DefaultPullRequestAction {}),
            Box::new(DefaultMainAction {}),
            Box::new(DefaultReleaseAction {}),
        )
    }
}

#[async_trait]
pub trait PullRequestAction {
    async fn execute_pull_request(&self, _ctx: &mut Context) -> eyre::Result<()> {
        eprintln!("validate pull request: noop");
        Ok(())
    }
}

pub struct DefaultPullRequestAction {}
#[async_trait]
impl PullRequestAction for DefaultPullRequestAction {}

#[async_trait]
pub trait MainAction {
    async fn execute_main(&self, _ctx: &mut Context) -> eyre::Result<()>;
}

pub struct DefaultMainAction {}
#[async_trait]
impl MainAction for DefaultMainAction {
    async fn execute_main(&self, _ctx: &mut Context) -> eyre::Result<()> {
        Ok(())
    }
}

#[async_trait]
pub trait ReleaseAction {
    async fn execute_release(&self, _ctx: &mut Context) -> eyre::Result<()> {
        eprintln!("validate release: noop");

        Ok(())
    }
}

pub struct DefaultReleaseAction {}
#[async_trait]
impl ReleaseAction for DefaultReleaseAction {}

#[cfg(test)]
mod test {
    use super::*;

    #[tokio::test]
    async fn test_can_call_default() -> eyre::Result<()> {
        CuddleCI::default().execute(["cuddle-ci", "pr"]).await?;

        Ok(())
    }

    #[tokio::test]
    async fn test_fails_on_no_command() -> eyre::Result<()> {
        let res = CuddleCI::default().execute(["cuddle-ci"]).await;

        assert!(res.is_err());

        Ok(())
    }

    #[tokio::test]
    async fn test_fails_on_wrong_command() -> eyre::Result<()> {
        let res = CuddleCI::default()
            .execute(["cuddle-ci", "something"])
            .await;

        assert!(res.is_err());

        Ok(())
    }
}
