use radicle::{cob::patch::PatchId, git::BranchName, storage::git::Repository};

use radicle_ci_broker::{
    ergo,
    msg::RequestBuilder,
    refs::{branch_ref, ref_string},
};

use super::*;

/// Trigger a CI run.
#[derive(Debug, Parser)]
pub struct TriggerCmd {
    /// Set the node where the node originated from.
    #[clap(long)]
    node: Option<NodeId>,

    /// Trigger CI to run for all repositories on the local node.
    #[clap(long, required_unless_present_any = ["patch","commit"])]
    all: bool,

    /// Set the repository the event refers to. Can be a RID, or the
    /// repository name.
    #[clap(long, required_unless_present = "all")]
    repo: Option<String>,

    /// Set the name of the ref the event refers to.
    #[clap(long = "ref", aliases = ["name"], default_value = "main")]
    name: Option<String>,

    /// Set the commit the event refers to. Can be the SHA1 commit id,
    /// or a symbolic Git revision, as understood by `git rev-parse`.
    /// For example, `HEAD`.
    #[clap(long, required_unless_present_any = ["patch","all"])]
    commit: Option<String>,

    /// Trigger CI to run on this patch.
    #[clap(long, required_unless_present_any = ["commit","all"])]
    patch: Option<PatchId>,

    /// Write the event ID to this file, after adding the event to the
    /// queue.
    #[clap(long)]
    id_file: Option<PathBuf>,

    /// Output a trigger message for a CI adapter, instead of an event
    /// that `cib` will turn into a trigger message. Output is always to
    /// stdout. There will be one message per line in the output.
    #[clap(long)]
    message: bool,

    /// Output the event to trigger a CI run to the standard output,
    /// instead of adding to the event queue in the database.
    #[clap(long, conflicts_with = "all")]
    stdout: bool,

    /// Output the event to trigger a CI run to a named file, instead
    /// of adding to the event queue in the database.
    #[clap(long, conflicts_with = "all")]
    output: Option<PathBuf>,
}

impl Leaf for TriggerCmd {
    fn run(&self, args: &Args) -> Result<(), CibToolError> {
        let r = ergo::Radicle::new().map_err(TriggerError::Ergonomic)?;
        if self.all {
            for ri in r.repositories().map_err(TriggerError::Ergonomic)? {
                let repo = r.repository(&ri.rid).map_err(TriggerError::Ergonomic)?;
                self.trigger(args, &r, &repo)?;
            }
        } else if let Some(repo) = &self.repo {
            let repo = r
                .repository_by_name(repo)
                .map_err(TriggerError::Ergonomic)?;
            self.trigger(args, &r, &repo)?;
        } else {
            Err(TriggerError::NoRepo)?
        }

        Ok(())
    }
}

impl TriggerCmd {
    fn oid(&self, r: &ergo::Radicle, repo: &Repository) -> Result<Oid, TriggerError> {
        let oid = if let Some(commit) = &self.commit {
            r.resolve_commit(&repo.id, commit)?
        } else if let Some(wanted) = &self.patch {
            *r.patch(&repo.id, wanted)?.head()
        } else {
            r.resolve_commit(&repo.id, "HEAD")?
        };
        Ok(oid)
    }

    fn branch(&self, r: &ergo::Radicle, repo: &Repository) -> Result<BranchName, TriggerError> {
        if let Some(name) = &self.name {
            Ok(branch_ref(&ref_string(name)?)?)
        } else {
            let project = r.project(&repo.id).map_err(TriggerError::Ergonomic)?;
            Ok(project.default_branch().clone())
        }
    }

    fn trigger(
        &self,
        args: &Args,
        r: &ergo::Radicle,
        repo: &Repository,
    ) -> Result<(), TriggerError> {
        let profile = r.profile();
        let nid = self.node.unwrap_or(*profile.id());

        let oid = self.oid(r, repo)?;

        let base = r
            .resolve_commit(&repo.id, &format!("{oid}^"))
            .unwrap_or(oid);

        let branch_name = self.branch(r, repo)?;

        let event = CiEvent::branch_updated(nid, repo.id, &branch_name, oid, base)
            .map_err(TriggerError::CiEvent)?;

        if self.message {
            self.trigger_message(event, r)
        } else {
            self.trigger_event(event, args)
        }
    }

    fn trigger_message(&self, event: CiEvent, r: &ergo::Radicle) -> Result<(), TriggerError> {
        let msg = RequestBuilder::default()
            .profile(r.profile())
            .ci_event(&event)
            .build_trigger_from_ci_event()
            .map_err(TriggerError::TriggerMessage)?;

        let json = serde_json::to_string(&msg).map_err(TriggerError::MessageToJson)?;
        println!("{json}");

        Ok(())
    }

    fn trigger_event(&self, event: CiEvent, args: &Args) -> Result<(), TriggerError> {
        if self.stdout {
            let json = event.to_pretty_json().map_err(TriggerError::EventToJson2)?;
            println!("{json}");
        } else if let Some(filename) = &self.output {
            let json = event.to_pretty_json().map_err(TriggerError::EventToJson2)?;
            std::fs::write(filename, &json)
                .map_err(|err| TriggerError::Write(filename.into(), err))?;
        } else {
            let db = args
                .open_db()
                .map_err(|err| TriggerError::OpenDb(Box::new(err)))?;
            let id = db.push_queued_ci_event(event)?;
            println!("{id}");

            if let Some(filename) = &self.id_file {
                write(filename, id.to_string().as_bytes())
                    .map_err(|e| TriggerError::WriteEventId(filename.into(), e))?;
            }
        }

        Ok(())
    }
}

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

    #[error(transparent)]
    RefError(#[from] radicle_ci_broker::refs::RefError),

    #[error("repository must be given with --repo")]
    NoRepo,

    #[error("failed to construct a CI event")]
    CiEvent(#[source] CiEventError),

    #[error("failed to convert trigger message to JSON")]
    MessageToJson(#[source] serde_json::Error),

    #[error(transparent)]
    EventToJson2(#[from] CiEventError),

    #[error(transparent)]
    Db(#[from] DbError),

    #[error("failed to write file: {0}")]
    Write(PathBuf, #[source] std::io::Error),

    #[error("failed to write event ID to file {0}")]
    WriteEventId(PathBuf, #[source] std::io::Error),

    #[error("failed to get database")]
    OpenDb(#[source] Box<CibToolError>),

    #[error("failed to create a trigger message")]
    TriggerMessage(#[source] radicle_ci_broker::msg::MessageError),
}
