zitadel-tui 0.1.2

A terminal UI for managing Zitadel resources
use anyhow::{Context, Result};
use serde_json::Value;

use crate::{
    client::ZitadelClient,
    config::UserTemplate,
    tui::{CanvasMode, FieldKind, FormField, FormState, MessageState, Record},
};

pub(super) fn map_app_record(app: Value) -> Record {
    let kind = app
        .get("oidcConfig")
        .and_then(|oidc| oidc.get("authMethodType"))
        .and_then(|value| value.as_str())
        .map(|value| {
            if value == "OIDC_AUTH_METHOD_TYPE_NONE" {
                "public".to_string()
            } else {
                "confidential".to_string()
            }
        })
        .unwrap_or_else(|| "unknown".to_string());
    let client_id = app
        .get("oidcConfig")
        .and_then(|oidc| oidc.get("clientId"))
        .and_then(|value| value.as_str())
        .unwrap_or("missing-client-id")
        .to_string();
    let redirect_count = app
        .get("oidcConfig")
        .and_then(|oidc| oidc.get("redirectUris"))
        .and_then(|value| value.as_array())
        .map(|uris| uris.len())
        .unwrap_or(0);
    Record {
        id: string_field(&app, "id", "missing-id"),
        name: string_field(&app, "name", "unnamed"),
        kind,
        summary: format!("{redirect_count} redirects"),
        detail: client_id,
        changed_at: string_field(&app, "state", "unknown"),
    }
}

pub(super) fn map_user_record(user: Value) -> Record {
    Record {
        id: string_field(&user, "id", "missing-id"),
        name: string_field(&user, "userName", "unknown-user"),
        kind: string_field(&user, "state", "unknown"),
        summary: user
            .get("human")
            .and_then(|human| human.get("email"))
            .and_then(|email| email.get("email"))
            .and_then(|email| email.as_str())
            .unwrap_or("no email")
            .to_string(),
        detail: user
            .get("human")
            .and_then(|human| human.get("profile"))
            .and_then(|profile| profile.get("displayName"))
            .and_then(|display_name| display_name.as_str())
            .unwrap_or("human user")
            .to_string(),
        changed_at: "loaded".to_string(),
    }
}

pub(super) fn map_idp_record(idp: Value) -> Record {
    Record {
        id: string_field(&idp, "id", "missing-id"),
        name: string_field(&idp, "name", "unnamed-idp"),
        kind: string_field(&idp, "state", "unknown"),
        summary: string_field(&idp, "type", "provider"),
        detail: "manual credentials".to_string(),
        changed_at: "configured".to_string(),
    }
}

pub(super) async fn resolve_project_id(
    client: &ZitadelClient,
    cli_project: Option<String>,
    config_project: Option<String>,
) -> Result<String> {
    if let Some(project) = cli_project.or(config_project) {
        return Ok(project);
    }

    client
        .get_default_project()
        .await?
        .get("id")
        .and_then(|value| value.as_str())
        .map(ToOwned::to_owned)
        .context("failed to determine default project id")
}

pub(super) fn error_mode(title: &str, message: &str) -> CanvasMode {
    CanvasMode::Error(MessageState {
        title: title.to_string(),
        lines: vec![message.to_string()],
    })
}

pub(super) fn text_field(key: &'static str, label: &str, value: &str, help: &str) -> FormField {
    FormField {
        key,
        label: label.to_string(),
        value: value.to_string(),
        kind: FieldKind::Text,
        help: help.to_string(),
    }
}

pub(super) fn secret_field(key: &'static str, label: &str, value: &str, help: &str) -> FormField {
    FormField {
        key,
        label: label.to_string(),
        value: value.to_string(),
        kind: FieldKind::Secret,
        help: help.to_string(),
    }
}

pub(super) fn toggle_field(key: &'static str, label: &str, value: bool, help: &str) -> FormField {
    FormField {
        key,
        label: label.to_string(),
        value: if value { "true" } else { "false" }.to_string(),
        kind: FieldKind::Toggle,
        help: help.to_string(),
    }
}

pub(super) fn choice_field(
    key: &'static str,
    label: &str,
    value: &str,
    options: Vec<String>,
    help: &str,
) -> FormField {
    FormField {
        key,
        label: label.to_string(),
        value: value.to_string(),
        kind: FieldKind::Choice(options),
        help: help.to_string(),
    }
}

pub(super) fn checkbox_field(key: &'static str, label: &str, value: bool, help: &str) -> FormField {
    FormField {
        key,
        label: label.to_string(),
        value: if value { "true" } else { "false" }.to_string(),
        kind: FieldKind::Checkbox,
        help: help.to_string(),
    }
}

pub(super) fn form_value(form: &FormState, key: &str) -> String {
    form.fields
        .iter()
        .find(|field| field.key == key)
        .map(|field| field.value.clone())
        .unwrap_or_default()
}

pub(super) fn optional_value(form: &FormState, key: &str) -> Option<String> {
    let value = form_value(form, key);
    if value.trim().is_empty() {
        None
    } else {
        Some(value)
    }
}

pub(super) fn bool_value(form: &FormState, key: &str) -> bool {
    matches!(form_value(form, key).as_str(), "true" | "1" | "yes" | "on")
}

pub(super) fn split_csv(input: &str) -> Vec<String> {
    input
        .split(',')
        .map(str::trim)
        .filter(|item| !item.is_empty())
        .map(ToOwned::to_owned)
        .collect()
}

pub(super) fn app_creation_summary(name: &str, value: &Value) -> String {
    format!(
        "{} -> {}",
        name,
        value
            .get("clientId")
            .and_then(|v| v.as_str())
            .unwrap_or("created")
    )
}

pub(super) async fn create_quick_user(
    client: &ZitadelClient,
    user: UserTemplate,
) -> Result<Vec<String>> {
    let mut lines = Vec::new();
    let result = client
        .create_human_user(&user.email, &user.first_name, &user.last_name, None)
        .await?;
    lines.push(format!("Created {}", user.email));
    if user.admin {
        if let Some(user_id) = result.get("userId").and_then(|v| v.as_str()) {
            client.grant_iam_owner(user_id).await?;
            lines.push(format!("Granted IAM_OWNER to {}", user.email));
        }
    }
    Ok(lines)
}

pub(super) fn checkbox_enabled(value: &str) -> bool {
    matches!(value, "true" | "1" | "yes" | "on")
}

pub(super) fn string_field(value: &Value, key: &str, fallback: &str) -> String {
    value
        .get(key)
        .and_then(|field| field.as_str())
        .unwrap_or(fallback)
        .to_string()
}