//! A management tool for the CI broker.
//!
//! This tool lets the node operator query and manage the CI broker's
//! database: the persistent event queue, the list of CI runs, etc.
//!
//! The tool is also used as part of the CI broker's acceptance test
//! suite (see the `ci-broker.subplot` document).

#![allow(clippy::result_large_err)]

use std::{
    error::Error,
    fs::{read, write},
    path::PathBuf,
    process::exit,
    str::FromStr,
};

use clap::Parser;

use radicle::prelude::NodeId;

use radicle_ci_broker::{
    broker::BrokerError,
    ci_event::{CiEvent, CiEventError, CiEventV1},
    db::{Db, DbError, QueueId, QueuedCiEvent},
    ergo::{self, Oid},
    logger,
    msg::{RunId, RunResult},
    notif::{NotificationChannel, NotificationError},
    pages::PageError,
    run::{Run, RunState, Whence},
    util::{self, UtilError},
};

mod cibtoolcmd;

fn main() {
    logger::open(logger::LogLevel::Info);
    if let Err(e) = fallible_main() {
        eprintln!("ERROR: {e}");
        let mut e = e.source();
        while let Some(source) = e {
            eprintln!("caused by: {source}");
            e = source.source();
        }
        exit(1);
    }
}

#[allow(clippy::result_large_err)]
fn fallible_main() -> Result<(), CibToolError> {
    let args = Args::parse();
    args.cmd.run(&args)?;
    Ok(())
}

// A trait for subcommands with subcommands. The implementation of
// this trait would pick the subcommand value and execute its `run`
// method: either via this same trait for the subcommand value, or via
// the [`Leaf`] trait.
//
// Ideally, documentation comments meant to be used as help text by
// `clap` would be added to the enum types that list the subcommands,
// and types that implement `Leaf` trait, but that's only for
// consistency.
trait Subcommand {
    fn run(&self, args: &Args) -> Result<(), CibToolError>;
}

// A trait for subcommands with no subcommands. These do actual work,
// rather than providing a multi-level structure of subcommands. For
// `cibtool`, the leaf commands are in the `cibtoolcmd` module, and
// the higher level subcommands are in `cbitool.rs`.
trait Leaf {
    fn run(&self, args: &Args) -> Result<(), CibToolError>;
}

#[derive(Parser)]
#[command(version = env!("VERSION"))]
struct Args {
    /// Name of the SQLite database file. The file will be created if
    /// it does not already exist. Locking is handled automatically.
    #[clap(long)]
    db: Option<PathBuf>,

    #[clap(subcommand)]
    cmd: Cmd,
}

impl Args {
    fn open_db(&self) -> Result<Db, CibToolError> {
        if let Some(filename) = &self.db {
            Ok(Db::new(filename)?)
        } else {
            Err(CibToolError::NoDb)
        }
    }
}

/// Radicle CI broker management tool for node operators.
///
/// Query and update the CI broker database file: the queue of events
/// waiting to be processed, the list of CI runs. Also, generate HTML
/// report pages from the database.
///
/// This tool can be used whether the CI broker is running or not.
#[derive(Parser)]
enum Cmd {
    #[clap(hide = true)]
    Counter(CounterCmd),
    Event(EventCmd),
    Run(RunCmd),
    Report(cibtoolcmd::ReportCmd),
    Trigger(cibtoolcmd::TriggerCmd),
    #[clap(hide = true)]
    Timeout(cibtoolcmd::TimeoutCmd),
    #[clap(hide = true)]
    Message(cibtoolcmd::MessageCmd),
    Log(cibtoolcmd::LogCmd),
    #[clap(hide = true)]
    Cob(cibtoolcmd::CobCmd),
}

impl Subcommand for Cmd {
    fn run(&self, args: &Args) -> Result<(), CibToolError> {
        match self {
            Self::Counter(x) => x.run(args),
            Self::Event(x) => x.run(args),
            Self::Run(x) => x.run(args),
            Self::Report(x) => x.run(args),
            Self::Trigger(x) => x.run(args),
            Self::Timeout(x) => x.run(args),
            Self::Message(x) => x.run(args),
            Self::Log(x) => x.run(args),
            Self::Cob(x) => x.run(args),
        }
    }
}

#[derive(Parser)]
struct CounterCmd {
    #[clap(subcommand)]
    cmd: CounterSubCmd,
}

impl CounterCmd {
    fn run(&self, args: &Args) -> Result<(), CibToolError> {
        match &self.cmd {
            CounterSubCmd::Show(x) => x.run(args)?,
            CounterSubCmd::Count(x) => x.run(args)?,
        }
        Ok(())
    }
}

/// Manage a counter in the database. This is meant to be used
/// only by the CI broker test suite, not by people.
#[derive(Parser)]
enum CounterSubCmd {
    Show(cibtoolcmd::ShowCounter),
    Count(cibtoolcmd::CountCounter),
}

impl Subcommand for CounterSubCmd {
    fn run(&self, args: &Args) -> Result<(), CibToolError> {
        match self {
            Self::Show(x) => x.run(args),
            Self::Count(x) => x.run(args),
        }
    }
}

#[derive(Parser)]
struct EventCmd {
    #[clap(subcommand)]
    cmd: EventSubCmd,
}

impl Subcommand for EventCmd {
    fn run(&self, args: &Args) -> Result<(), CibToolError> {
        match &self.cmd {
            EventSubCmd::Add(x) => x.run(args)?,
            EventSubCmd::Shutdown(x) => x.run(args)?,
            EventSubCmd::Terminate(x) => x.run(args)?,
            EventSubCmd::List(x) => x.run(args)?,
            EventSubCmd::Count(x) => x.run(args)?,
            EventSubCmd::Show(x) => x.run(args)?,
            EventSubCmd::Remove(x) => x.run(args)?,
            EventSubCmd::Record(x) => x.run(args)?,
            EventSubCmd::Ci(x) => x.run(args)?,
            EventSubCmd::Filter(x) => x.run(args)?,
        }
        Ok(())
    }
}

/// Manage the event queue. The events are for Git refs having
/// changed.
#[derive(Parser)]
enum EventSubCmd {
    List(cibtoolcmd::ListEvents),
    Count(cibtoolcmd::CountEvents),
    Add(cibtoolcmd::AddEvent),
    Shutdown(cibtoolcmd::Shutdown),
    Terminate(cibtoolcmd::Terminate),
    Show(cibtoolcmd::ShowEvent),
    Remove(cibtoolcmd::RemoveEvent),
    Record(cibtoolcmd::RecordEvents),
    Ci(cibtoolcmd::CiEvents),
    Filter(cibtoolcmd::FilterEvents),
}

#[derive(Parser)]
struct RunCmd {
    #[clap(subcommand)]
    cmd: RunSubCmd,
}

impl Subcommand for RunCmd {
    fn run(&self, args: &Args) -> Result<(), CibToolError> {
        match &self.cmd {
            RunSubCmd::Add(x) => x.run(args)?,
            RunSubCmd::List(x) => x.run(args)?,
            RunSubCmd::Update(x) => x.run(args)?,
            RunSubCmd::Remove(x) => x.run(args)?,
            RunSubCmd::Show(x) => x.run(args)?,
        }
        Ok(())
    }
}

/// Manage the list of CI runs.
#[derive(Parser)]
enum RunSubCmd {
    /// Add information about a CI run to the database.
    Add(cibtoolcmd::AddRun),

    /// Update information about a CI run to the database.
    Update(cibtoolcmd::UpdateRun),

    /// Remove information about a CI run to the database.
    Remove(cibtoolcmd::RemoveRun),

    /// Show a CI run as JSON.
    Show(cibtoolcmd::ShowRun),

    /// List known CI runs on this node to the database.
    List(cibtoolcmd::ListRuns),
}

#[derive(Debug, thiserror::Error)]
enum CibToolError {
    #[error("failed to load ergonomic Radicle wrapper")]
    Ergo(#[source] ergo::ErgoError),

    #[error("failed to create cache of job COBs")]
    KnownJobCobs(#[source] radicle_ci_broker::cob::JobError),

    #[error("cannot find CI run with id {0}")]
    RunNotFound(RunId),

    #[error(transparent)]
    Broker(#[from] BrokerError),

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

    #[error("failed to serialize CI event to JSON: {0:#?}")]
    EventToJson(CiEvent, #[source] serde_json::Error),

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

    #[error("failed to serialize node event to JSON: {0:#?}")]
    NodeEevnetToJson(radicle::node::Event, #[source] serde_json::Error),

    #[error("failed to serialize list of stored broker event to JSON")]
    EventListToJson(#[source] serde_json::Error),

    #[error("failed to serialize CI run information to JSON")]
    RunToJson(#[source] serde_json::Error),

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

    #[error(transparent)]
    Page(#[from] PageError),

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

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

    #[error("one of --id or --id-file or --all is required")]
    MissingId,

    #[error("programming error: confused about state and result of run")]
    AddRunConfusion,

    #[error(transparent)]
    Util(#[from] UtilError),

    #[error("no database file specified with the --db option")]
    NoDb,

    #[error("failed to subscribe to node events")]
    EventSubscribe(#[source] radicle_ci_broker::node_event_source::NodeEventError),

    #[error("failed to get next node event")]
    GetNodeEvent(#[source] radicle_ci_broker::node_event_source::NodeEventError),

    #[error("failed to create file for node events: {0}")]
    CreateEventsFile(PathBuf, #[source] std::io::Error),

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

    #[error("failed to read node events from file {0}")]
    ReadEvents(PathBuf, #[source] std::io::Error),

    #[error("failed to read broker events from file {0}")]
    ReadBrokerEvents(PathBuf, #[source] std::io::Error),

    #[error("failed to read node events as UTF8 from file {0}")]
    NodeEventNotUtf8(PathBuf, #[source] std::string::FromUtf8Error),

    #[error("failed to read broker events as UTF8 from file {0}")]
    BrokerEventNotUtf8(PathBuf, #[source] std::string::FromUtf8Error),

    #[error("failed to read node events as JSON from file {0}")]
    JsonToNodeEvent(PathBuf, #[source] serde_json::Error),

    #[error("failed to create file for broker events: {0}")]
    CreateBrokerEventsFile(PathBuf, #[source] std::io::Error),

    #[error("failed to read filters from YAML file {0}")]
    ReadFilters(PathBuf, #[source] radicle_ci_broker::filter::FilterError),

    #[error("when adding a branch-create event, the --base option is not allowed")]
    NoBaseAllowed,

    #[error("when adding a branch-update event, the --base option is required")]
    BaseRequired,

    #[error("when adding a patch-create or patch--update event, the --patch-id option is required")]
    PatchIdRequired,

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

    #[error("programming error: failed to set up inter-thread notification channel")]
    Notification(#[source] NotificationError),

    #[error(transparent)]
    Timeout(#[from] radicle_ci_broker::timeoutcmd::TimeoutError),

    #[error(transparent)]
    Message(#[from] cibtoolcmd::MessageError),

    #[error(transparent)]
    Log(#[from] cibtoolcmd::LogError),

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

    #[error(transparent)]
    Trigger(#[from] cibtoolcmd::TriggerError),

    #[error(transparent)]
    Event(#[from] cibtoolcmd::EventError),

    #[error(transparent)]
    Run(#[from] cibtoolcmd::RunError),

    #[error(transparent)]
    Cob(#[from] radicle_ci_broker::cob::JobError),
}
