9.1 KiB · text History 2803012
use crate::data::{find_person, MotdRemindEnum, PplOps, TierDefaultOps, TierOps};
use crate::entities::{ppl, tier, tier_defaults};
use crate::PplError;
use clap::{Args, Subcommand};
use sea_orm::DatabaseConnection;
use std::io;

#[derive(Args, Debug)]
pub struct TierArgs {
    #[command(subcommand)]
    pub action: Option<TierAction>,
    /// Change one person's settings (name, nick or id) instead of the tier defaults
    #[arg(long, short, global = true)]
    pub person: Option<String>,
}

#[derive(Subcommand, Debug)]
pub enum TierAction {
    /// List tiers and their motd reminder settings
    List,
    /// Remind DAYS days ahead of dates for ppl in this tier
    Set { tier: String, days: u32 },
    /// How often the motd shows ppl in this tier: daily, hourly, random:N, always, never
    Remind { tier: String, mode: MotdRemindEnum },
    /// Remove the reminder window and remind mode for this tier
    Clear { tier: String },
    /// Create a new tier, or with --person put that person in an existing one
    Add {
        tier: String,
        #[arg(long)]
        symbol: Option<String>,
        /// One of: red, gold, amber, green, teal, cyan, blue, violet, pink, slate
        #[arg(long)]
        color: Option<String>,
        /// Remind this many days ahead of dates for ppl in this tier
        #[arg(long)]
        days: Option<u32>,
    },
}

/// Which row gets the change: a tier default, or one person's tier
#[derive(Clone, Copy)]
enum Target {
    Default(i32),
    Person(i32),
}

fn not_found(msg: String) -> PplError {
    io::Error::new(io::ErrorKind::NotFound, msg).into()
}

fn describe(delta: Option<u32>, remind: Option<&str>) -> (String, String) {
    let window = match delta {
        Some(d) => format!("{} days", d),
        None => "-".to_string(),
    };
    let mode = remind
        .and_then(|r| r.parse::<MotdRemindEnum>().ok())
        .map(|r| r.label())
        .unwrap_or_else(|| "-".to_string());
    (window, mode)
}

fn find_default<'a>(
    tiers: &'a [tier_defaults::Model],
    name: &str,
) -> Result<&'a tier_defaults::Model, PplError> {
    tiers
        .iter()
        .find(|t| t.key.eq_ignore_ascii_case(name))
        .ok_or_else(|| {
            let keys = tiers.iter().map(|t| t.key.as_str()).collect::<Vec<_>>();
            not_found(format!(
                "no tier '{}', expected one of: {}",
                name,
                keys.join(", ")
            ))
        })
}

fn find_person_tier<'a>(
    tiers: &'a [tier::Model],
    person: &ppl::Model,
    name: &str,
) -> Result<&'a tier::Model, PplError> {
    let theirs = tiers
        .iter()
        .filter(|t| t.ppl_id == person.id)
        .collect::<Vec<_>>();
    theirs
        .iter()
        .find(|t| t.name.eq_ignore_ascii_case(name))
        .copied()
        .ok_or_else(|| {
            let keys = theirs.iter().map(|t| t.name.as_str()).collect::<Vec<_>>();
            not_found(format!(
                "{} isn't in tier '{}' (their tiers: {})",
                person.name.trim(),
                name,
                if keys.is_empty() {
                    "none".to_string()
                } else {
                    keys.join(", ")
                }
            ))
        })
}

pub async fn do_tiers(args: &TierArgs, db: &DatabaseConnection) -> Result<(), PplError> {
    let defaults = TierDefaultOps::list(db).await?;
    let person = match &args.person {
        Some(q) => {
            let ppl = PplOps::list(db).await?;
            Some(find_person(&ppl, q)?.clone())
        }
        None => None,
    };
    let person_tiers = TierOps::list(db).await?;

    let action = match &args.action {
        None | Some(TierAction::List) => {
            list(&defaults, &person_tiers, person.as_ref());
            return Ok(());
        }
        Some(TierAction::Add {
            tier,
            symbol,
            color,
            days,
        }) => {
            return add(
                db,
                &defaults,
                &person_tiers,
                person.as_ref(),
                tier,
                symbol,
                color,
                *days,
            )
            .await;
        }
        Some(a) => a,
    };
    let tier_name = match action {
        TierAction::Set { tier, .. }
        | TierAction::Remind { tier, .. }
        | TierAction::Clear { tier } => tier,
        TierAction::List | TierAction::Add { .. } => unreachable!(),
    };

    // Who we're changing, for the confirmation message
    let (target, label) = match &person {
        Some(p) => {
            let t = find_person_tier(&person_tiers, p, tier_name)?;
            (
                Target::Person(t.id),
                format!("{} ({})", t.name, p.name.trim()),
            )
        }
        None => {
            let t = find_default(&defaults, tier_name)?;
            (Target::Default(t.id), t.key.clone())
        }
    };

    let set_delta = |delta| async move {
        match target {
            Target::Default(id) => TierDefaultOps::set_sig_date_delta(db, id, delta).await,
            Target::Person(id) => TierOps::set_sig_date_delta(db, id, delta).await,
        }
    };
    let set_remind = |remind| async move {
        match target {
            Target::Default(id) => TierDefaultOps::set_sig_remind_enum(db, id, remind).await,
            Target::Person(id) => TierOps::set_sig_remind_enum(db, id, remind).await,
        }
    };

    match action {
        TierAction::Set { days, .. } => {
            set_delta(Some(*days)).await?;
            println!("{}: remind {} days ahead", label, days);
        }
        TierAction::Remind { mode, .. } => {
            set_remind(Some(*mode)).await?;
            println!("{}: remind {}", label, mode.label());
        }
        TierAction::Clear { .. } => {
            set_delta(None).await?;
            set_remind(None).await?;
            match person {
                Some(_) => println!("{}: using the tier defaults", label),
                None => println!("{}: reminder settings cleared", label),
            }
        }
        TierAction::List | TierAction::Add { .. } => unreachable!(),
    }
    Ok(())
}

async fn add(
    db: &DatabaseConnection,
    defaults: &[tier_defaults::Model],
    person_tiers: &[tier::Model],
    person: Option<&ppl::Model>,
    tier: &str,
    symbol: &Option<String>,
    color: &Option<String>,
    days: Option<u32>,
) -> Result<(), PplError> {
    let Some(p) = person else {
        let key = TierDefaultOps::add(
            db,
            tier,
            symbol.as_deref().unwrap_or(""),
            color.as_deref().unwrap_or(""),
            days,
        )
        .await?;
        println!("added tier {}", key);
        return Ok(());
    };
    if symbol.is_some() || color.is_some() || days.is_some() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            "--symbol, --color and --days describe a new tier; leave out --person to create one",
        )
        .into());
    }
    let d = find_default(defaults, tier)?;
    if person_tiers
        .iter()
        .any(|t| t.ppl_id == p.id && t.name == d.key)
    {
        println!("{} is already in {}", p.name.trim(), d.key);
        return Ok(());
    }
    TierOps::create(db, p.id, d.key.clone(), None).await?;
    println!("{} added to {}", p.name.trim(), d.key);
    Ok(())
}

fn list(
    defaults: &[tier_defaults::Model],
    person_tiers: &[tier::Model],
    person: Option<&ppl::Model>,
) {
    match person {
        None => {
            for t in defaults {
                let (window, mode) = describe(t.sig_date_delta, t.sig_remind_enum.as_deref());
                println!(
                    "{} {:<16} {:<10} {:<12} {}",
                    t.symbol.as_deref().unwrap_or(" "),
                    t.key,
                    window,
                    mode,
                    if t.enabled { "" } else { "(disabled)" }
                );
            }
        }
        Some(p) => {
            let theirs = person_tiers
                .iter()
                .filter(|t| t.ppl_id == p.id)
                .collect::<Vec<_>>();
            if theirs.is_empty() {
                println!("{} isn't in any tiers", p.name.trim());
            }
            // Show the person's own value, or the tier default it falls back to
            for t in theirs {
                let d = defaults.iter().find(|d| d.key == t.name);
                let (window, mode) = describe(
                    t.sig_date_delta.or(d.and_then(|d| d.sig_date_delta)),
                    t.sig_remind_enum
                        .as_deref()
                        .or(d.and_then(|d| d.sig_remind_enum.as_deref())),
                );
                let mark = |own: bool| if own { "" } else { " (default)" };
                let window = format!("{}{}", window, mark(t.sig_date_delta.is_some()));
                println!(
                    "{} {:<16} {:<20} {}{}",
                    t.symbol
                        .as_deref()
                        .or(d.and_then(|d| d.symbol.as_deref()))
                        .unwrap_or(" "),
                    t.name,
                    window,
                    mode,
                    mark(t.sig_remind_enum.is_some()),
                );
            }
        }
    }
}