//! `pic groups` — manage the org-tree groups (Phase 2).
//!
//! Wraps `/api/v1/admin/groups*`. Structural mutations are gated
//! server-side: create/reparent/delete need group-admin (reparent at both
//! source and destination parent); the slug is frozen at creation.

use std::collections::BTreeMap;

use anyhow::Result;
use picloud_shared::{AppRole, Group};

use crate::client::{Client, CreateGroupBody};
use crate::config;
use crate::output::{KvBlock, OutputMode, Table};

pub async fn ls(mode: OutputMode) -> Result<()> {
    let creds = config::resolve()?;
    let client = Client::from_creds(&creds)?;
    let groups = client.groups_list_with_owner().await?;
    let mut table = Table::new(["slug", "name", "parent", "owner", "created_at"]);
    let by_id: BTreeMap<_, _> = groups
        .iter()
        .map(|g| (g.group.id, g.group.slug.clone()))
        .collect();
    for g in &groups {
        let parent = g
            .group
            .parent_id
            .and_then(|p| by_id.get(&p).cloned())
            .unwrap_or_else(|| "-".into());
        table.row([
            g.group.slug.clone(),
            g.group.name.clone(),
            parent,
            g.owner.clone().unwrap_or_else(|| "—".into()),
            g.group.created_at.to_rfc3339(),
        ]);
    }
    table.print(mode);
    Ok(())
}

/// `pic groups tree` — render the hierarchy as an indented tree (text
/// mode); falls back to the flat list for `--output json`.
pub async fn tree(mode: OutputMode) -> Result<()> {
    let creds = config::resolve()?;
    let client = Client::from_creds(&creds)?;
    let groups = client.groups_list().await?;
    if matches!(mode, OutputMode::Json) {
        // Machine consumers get the flat list; the shape carries parent_id.
        println!("{}", serde_json::to_string_pretty(&groups)?);
        return Ok(());
    }
    // children-by-parent, then DFS from the roots.
    let mut children: BTreeMap<Option<_>, Vec<&Group>> = BTreeMap::new();
    for g in &groups {
        children.entry(g.parent_id).or_default().push(g);
    }
    for kids in children.values_mut() {
        kids.sort_by(|a, b| a.name.cmp(&b.name));
    }
    print_subtree(&children, None, 0);
    Ok(())
}

fn print_subtree(
    children: &BTreeMap<Option<picloud_shared::GroupId>, Vec<&Group>>,
    parent: Option<picloud_shared::GroupId>,
    depth: usize,
) {
    let Some(kids) = children.get(&parent) else {
        return;
    };
    for g in kids {
        println!("{}{} ({})", "  ".repeat(depth), g.name, g.slug);
        print_subtree(children, Some(g.id), depth + 1);
    }
}

pub async fn create(
    slug: &str,
    name: Option<&str>,
    description: Option<&str>,
    parent: Option<&str>,
    mode: OutputMode,
) -> Result<()> {
    let creds = config::resolve()?;
    let client = Client::from_creds(&creds)?;
    let body = CreateGroupBody {
        slug,
        name: name.unwrap_or(slug),
        description,
        parent,
    };
    let group = client.groups_create(&body).await?;
    print_group(&group, mode);
    Ok(())
}

pub async fn show(ident: &str, mode: OutputMode) -> Result<()> {
    let creds = config::resolve()?;
    let client = Client::from_creds(&creds)?;
    let detail = client.groups_get(ident).await?;
    let path = detail
        .path
        .iter()
        .map(|g| g.slug.as_str())
        .collect::<Vec<_>>()
        .join(" / ");
    let mut block = KvBlock::new();
    block
        .field("id", detail.group.id.to_string())
        .field("slug", detail.group.slug.clone())
        .field("name", detail.group.name.clone())
        .field("path", if path.is_empty() { "-".into() } else { path })
        .field(
            "subgroups",
            detail
                .subgroups
                .iter()
                .map(|g| g.slug.as_str())
                .collect::<Vec<_>>()
                .join(", "),
        )
        .field(
            "apps",
            detail
                .apps
                .iter()
                .map(|a| a.slug.as_str())
                .collect::<Vec<_>>()
                .join(", "),
        );
    block.print(mode);
    Ok(())
}

pub async fn rename(
    ident: &str,
    name: Option<&str>,
    description: Option<&str>,
    mode: OutputMode,
) -> Result<()> {
    let creds = config::resolve()?;
    let client = Client::from_creds(&creds)?;
    let group = client.groups_rename(ident, name, description).await?;
    print_group(&group, mode);
    Ok(())
}

pub async fn reparent(ident: &str, to: Option<&str>, mode: OutputMode) -> Result<()> {
    let creds = config::resolve()?;
    let client = Client::from_creds(&creds)?;
    let group = client.groups_reparent(ident, to).await?;
    print_group(&group, mode);
    Ok(())
}

/// `pic groups rm <slug>`. The server enforces delete=RESTRICT (409 on a
/// non-empty group); `--recursive` expands the delete into ordered,
/// leaf-first child deletions (groups + apps) the operator opted into.
pub async fn rm(ident: &str, recursive: bool) -> Result<()> {
    let creds = config::resolve()?;
    let client = Client::from_creds(&creds)?;
    if !recursive {
        client.groups_delete(ident).await?;
        println!("Deleted group {ident}");
        return Ok(());
    }
    // Recursive: delete the subtree leaf-first so each DB delete sees an
    // empty node (the FK stays RESTRICT — we never cascade implicitly).
    let detail = client.groups_get(ident).await?;
    if let Some(app) = detail.apps.first() {
        anyhow::bail!(
            "group {ident} contains app {:?}; move or delete apps before a recursive group delete \
             (apps are never auto-deleted)",
            app.slug
        );
    }
    for sub in &detail.subgroups {
        Box::pin(rm(&sub.slug, true)).await?;
    }
    client.groups_delete(ident).await?;
    println!("Deleted group {ident}");
    Ok(())
}

// --- members ---------------------------------------------------------------

pub async fn members_ls(group: &str, mode: OutputMode) -> Result<()> {
    let creds = config::resolve()?;
    let client = Client::from_creds(&creds)?;
    let members = client.group_members_list(group).await?;
    let mut table = Table::new(["user_id", "username", "role", "instance_role", "active"]);
    for m in members {
        table.row([
            m.user_id.to_string(),
            m.username,
            m.role.as_str().to_string(),
            format!("{:?}", m.instance_role).to_lowercase(),
            m.is_active.to_string(),
        ]);
    }
    table.print(mode);
    Ok(())
}

pub async fn members_add(group: &str, user_id: &str, role: &str, mode: OutputMode) -> Result<()> {
    let creds = config::resolve()?;
    let client = Client::from_creds(&creds)?;
    let m = client
        .group_members_grant(group, user_id, parse_role(role)?)
        .await?;
    print_member(&m, mode);
    Ok(())
}

pub async fn members_set(group: &str, user_id: &str, role: &str, mode: OutputMode) -> Result<()> {
    let creds = config::resolve()?;
    let client = Client::from_creds(&creds)?;
    let m = client
        .group_members_set_role(group, user_id, parse_role(role)?)
        .await?;
    print_member(&m, mode);
    Ok(())
}

pub async fn members_rm(group: &str, user_id: &str) -> Result<()> {
    let creds = config::resolve()?;
    let client = Client::from_creds(&creds)?;
    client.group_members_remove(group, user_id).await?;
    println!("Removed {user_id} from {group}");
    Ok(())
}

fn print_group(g: &Group, mode: OutputMode) {
    let mut block = KvBlock::new();
    block
        .field("id", g.id.to_string())
        .field("slug", g.slug.clone())
        .field("name", g.name.clone())
        .field(
            "parent_id",
            g.parent_id.map_or_else(|| "-".into(), |p| p.to_string()),
        )
        .field("created_at", g.created_at.to_rfc3339());
    block.print(mode);
}

fn print_member(m: &crate::client::AppMemberDto, mode: OutputMode) {
    let mut block = KvBlock::new();
    block
        .field("user_id", m.user_id.to_string())
        .field("username", m.username.clone())
        .field("role", m.role.as_str().to_string());
    block.print(mode);
}

fn parse_role(role: &str) -> Result<AppRole> {
    AppRole::from_db_str(role)
        .ok_or_else(|| anyhow::anyhow!("invalid role {role:?}; want app_admin | editor | viewer"))
}
