use std::collections::HashSet;

use radicle_ci_broker::{ergo, run::RunBuilder};
use radicle_job::JobId;

use super::*;

#[derive(Parser)]
pub struct AddRun {
    /// Set the adapter run ID.
    #[clap(long)]
    id: Option<RunId>,

    /// Set optional URL to information about the CI run.
    #[clap(long)]
    url: Option<String>,

    /// Set the repository the event refers to. Can be a RID, or the
    /// repository name.
    #[clap(long)]
    repo: String,

    /// Set timestamp of the CI run.
    #[clap(long)]
    timestamp: Option<String>,

    /// Set the Git branch used by the CI run.
    #[clap(long)]
    branch: String,

    /// Set the commit SHA ID used by the CI run.
    #[clap(long)]
    commit: String,

    /// Set the author of the commit used by the CI run.
    #[clap(long)]
    who: Option<String>,

    /// Set the state of the CI run to "triggered".
    #[clap(long, required_unless_present_any = ["running", "success", "failure"])]
    triggered: bool,

    /// Set the state of the CI run to "running".
    #[clap(long)]
    #[clap(long, required_unless_present_any = ["triggered", "success", "failure"])]
    running: bool,

    /// Mark the CI run as finished and successful.
    #[clap(long, required_unless_present_any = ["triggered", "running", "failure"])]
    success: bool,

    /// Mark the CI run as finished and failed.
    #[clap(long, required_unless_present_any = ["triggered", "running", "success"])]
    failure: bool,

    /// Record job COB ID created for this run.
    #[clap(long)]
    job: Option<JobId>,
}

impl Leaf for AddRun {
    fn run(&self, args: &Args) -> Result<(), CibToolError> {
        let db = args.open_db()?;

        let r = ergo::Radicle::new().map_err(RunError::Ergonomic)?;
        let repo = r
            .repository_by_name(&self.repo)
            .map_err(RunError::Ergonomic)?;
        let repo_name = r
            .project(&repo.id)
            .map_err(RunError::Ergonomic)?
            .name()
            .to_string();
        let oid = r
            .resolve_commit(&repo.id, &self.commit)
            .map_err(RunError::Ergonomic)?;
        let ts = self.timestamp.clone().unwrap_or(util::now()?);

        let whence = Whence::Branch {
            name: self.branch.clone(),
            commit: oid,
            who: self.who.clone(),
        };
        let mut run = RunBuilder::default()
            .broker_run_id(RunId::default())
            .repo_id(repo.id)
            .repo_name(&repo_name)
            .whence(whence)
            .timestamp(ts)
            .build();

        let id = self.id.clone().unwrap_or_default();
        run.set_adapter_run_id(id);

        if let Some(url) = &self.url {
            run.set_adapter_info_url(url);
        }

        if let Some(job) = &self.job {
            run.set_job_id(*job);
        }

        if self.triggered {
            run.set_state(RunState::Triggered);
            run.unset_result();
        } else if self.running {
            run.set_state(RunState::Running);
            run.unset_result();
        } else if self.success {
            run.set_result(RunResult::Success);
            run.set_state(RunState::Finished);
        } else if self.failure {
            run.set_result(RunResult::Failure);
            run.set_state(RunState::Finished);
        } else {
            return Err(CibToolError::AddRunConfusion);
        }

        db.push_run(&run)?;

        Ok(())
    }
}

#[derive(Parser)]
pub struct UpdateRun {
    /// ID of run to update.
    #[clap(long)]
    id: RunId,

    /// Set the state of the CI run to "triggered".
    #[clap(long, required_unless_present_any = ["running", "success", "failure"])]
    triggered: bool,

    /// Set the state of the CI run to "running".
    #[clap(long)]
    #[clap(long, required_unless_present_any = ["triggered", "success", "failure"])]
    running: bool,

    /// Mark the CI run as finished and successful.
    #[clap(long, required_unless_present_any = ["triggered", "running", "failure"])]
    success: bool,

    /// Mark the CI run as finished and failed.
    #[clap(long, required_unless_present_any = ["triggered", "running", "success"])]
    failure: bool,
}

impl Leaf for UpdateRun {
    fn run(&self, args: &Args) -> Result<(), CibToolError> {
        let db = args.open_db()?;

        let runs = db.find_runs(&self.id)?;
        if runs.is_empty() {
            Err(CibToolError::RunNotFound(self.id.clone()))
        } else {
            let mut run = runs.first().unwrap().clone(); // this is safe: runs is not empty

            run.unset_result();
            if self.triggered {
                run.set_state(RunState::Triggered);
            } else if self.running {
                run.set_state(RunState::Running);
            } else if self.success {
                run.set_state(RunState::Finished);
                run.set_result(RunResult::Success);
            } else if self.failure {
                run.set_state(RunState::Finished);
                run.set_result(RunResult::Failure);
            }

            db.update_run(&run)?;
            Ok(())
        }
    }
}

#[derive(Parser)]
pub struct ShowRun {
    /// Broker or adapter run ID.
    id: RunId,
}

impl Leaf for ShowRun {
    fn run(&self, args: &Args) -> Result<(), CibToolError> {
        let db = args.open_db()?;
        let runs = db.find_runs(&self.id)?;
        let json = serde_json::to_string_pretty(&runs).map_err(CibToolError::RunToJson)?;
        println!("{json}");

        Ok(())
    }
}

#[derive(Parser)]
pub struct RemoveRun {
    /// Broker or adapter run IDs of runs to remove.
    ids: Vec<RunId>,

    /// The run ids are from the adapter, not the CI broker.
    #[clap(long)]
    adapter: bool,
}

impl Leaf for RemoveRun {
    fn run(&self, args: &Args) -> Result<(), CibToolError> {
        let db = args.open_db()?;

        if self.adapter {
            let unwanted: HashSet<RunId> = HashSet::from_iter(self.ids.iter().cloned());
            let remove: Vec<Run> = db
                .get_all_runs()?
                .iter()
                .filter(|run| {
                    if let Some(id) = run.adapter_run_id() {
                        unwanted.contains(id)
                    } else {
                        unwanted.contains(run.broker_run_id())
                    }
                })
                .cloned()
                .collect();

            for run in remove.iter() {
                db.remove_run(run.broker_run_id())?;
            }
        } else {
            for run_id in self.ids.iter() {
                db.remove_run(run_id)?;
                println!("removed run {run_id}");
            }
        }
        Ok(())
    }
}

#[derive(Parser)]
pub struct ListRuns {
    #[clap(long)]
    json: bool,

    #[clap(long)]
    adapter_run_id: Option<RunId>,
}

impl Leaf for ListRuns {
    fn run(&self, args: &Args) -> Result<(), CibToolError> {
        let db = args.open_db()?;

        let runs = if let Some(wanted) = &self.adapter_run_id {
            db.find_runs(wanted)?
        } else {
            db.get_all_runs()?
        };

        if self.json {
            println!(
                "{}",
                serde_json::to_string_pretty(&runs).map_err(CibToolError::RunToJson)?
            );
        } else {
            for run in runs {
                println!(
                    "{} {}",
                    run.broker_run_id(),
                    run.adapter_run_id()
                        .map(|id| id.to_string())
                        .unwrap_or("unknown".into())
                );
            }
        }

        Ok(())
    }
}

#[derive(Debug, thiserror::Error)]
pub enum RunError {
    #[error(transparent)]
    Ergonomic(#[from] radicle_ci_broker::ergo::ErgoError),
}
