atlassian-cli 0.7.2

Unified CLI for Atlassian Cloud products
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use url::form_urlencoded;

use super::utils::BitbucketContext;
use crate::commands::common::{render_success, MutationResult};

#[derive(Deserialize)]
struct RepoList {
    values: Vec<Repo>,
}

#[derive(Deserialize)]
struct Repo {
    slug: String,
    name: Option<String>,
    #[serde(default)]
    is_private: bool,
    #[serde(default)]
    mainbranch: Option<BranchRef>,
    #[serde(rename = "full_name", default)]
    full_name: Option<String>,
    #[serde(default)]
    description: Option<String>,
    #[serde(default)]
    language: Option<String>,
    #[serde(default)]
    size: Option<i64>,
}

#[derive(Deserialize)]
struct BranchRef {
    name: String,
}

pub async fn list_repos(ctx: &BitbucketContext<'_>, workspace: &str, limit: usize) -> Result<()> {
    let query = form_urlencoded::Serializer::new(String::new())
        .append_pair("pagelen", &limit.min(100).to_string())
        .finish();
    let path = format!("/2.0/repositories/{workspace}?{query}");

    let response: RepoList = ctx
        .client
        .get(&path)
        .await
        .with_context(|| format!("Failed to list repositories for workspace {workspace}"))?;

    #[derive(Serialize)]
    struct Row<'a> {
        slug: &'a str,
        name: &'a str,
        main_branch: &'a str,
        visibility: &'a str,
        language: &'a str,
    }

    let rows: Vec<Row<'_>> = response
        .values
        .iter()
        .map(|repo| Row {
            slug: repo.slug.as_str(),
            name: repo.name.as_deref().unwrap_or(""),
            main_branch: repo
                .mainbranch
                .as_ref()
                .map(|b| b.name.as_str())
                .unwrap_or(""),
            visibility: if repo.is_private { "private" } else { "public" },
            language: repo.language.as_deref().unwrap_or(""),
        })
        .collect();

    if rows.is_empty() {
        ctx.verify_auth().await?;
        tracing::info!(workspace, "No repositories found");
    }

    ctx.renderer
        .render_list_or_empty(&rows, "No repositories found")
}

pub async fn get_repo(ctx: &BitbucketContext<'_>, workspace: &str, slug: &str) -> Result<()> {
    let path = format!("/2.0/repositories/{workspace}/{slug}");
    let repo: Repo = ctx
        .client
        .get(&path)
        .await
        .with_context(|| format!("Failed to fetch repository {workspace}/{slug}"))?;

    #[derive(Serialize)]
    struct View<'a> {
        slug: &'a str,
        name: &'a str,
        full_name: &'a str,
        description: &'a str,
        main_branch: &'a str,
        visibility: &'a str,
        language: &'a str,
        size_bytes: String,
    }

    let view = View {
        slug: repo.slug.as_str(),
        name: repo.name.as_deref().unwrap_or(""),
        full_name: repo.full_name.as_deref().unwrap_or(""),
        description: repo.description.as_deref().unwrap_or(""),
        main_branch: repo
            .mainbranch
            .as_ref()
            .map(|b| b.name.as_str())
            .unwrap_or(""),
        visibility: if repo.is_private { "private" } else { "public" },
        language: repo.language.as_deref().unwrap_or(""),
        size_bytes: repo.size.map(|s| s.to_string()).unwrap_or_default(),
    };

    ctx.renderer.render(&view)
}

pub async fn create_repo(
    ctx: &BitbucketContext<'_>,
    workspace: &str,
    slug: &str,
    name: Option<&str>,
    description: Option<&str>,
    is_private: bool,
    project_key: Option<&str>,
) -> Result<()> {
    let mut payload = serde_json::json!({
        "scm": "git",
        "is_private": is_private
    });

    if let Some(n) = name {
        payload["name"] = serde_json::json!(n);
    }

    if let Some(d) = description {
        payload["description"] = serde_json::json!(d);
    }

    if let Some(pk) = project_key {
        payload["project"] = serde_json::json!({"key": pk});
    }

    let path = format!("/2.0/repositories/{workspace}/{slug}");
    let repo: Repo = ctx
        .client
        .post(&path, &payload)
        .await
        .with_context(|| format!("Failed to create repository {workspace}/{slug}"))?;

    tracing::info!(
        slug = repo.slug.as_str(),
        workspace,
        "Repository created successfully"
    );

    #[derive(Serialize)]
    struct Created<'a> {
        slug: &'a str,
        name: &'a str,
        full_name: &'a str,
        visibility: &'a str,
    }

    let created = Created {
        slug: repo.slug.as_str(),
        name: repo.name.as_deref().unwrap_or(""),
        full_name: repo.full_name.as_deref().unwrap_or(""),
        visibility: if repo.is_private { "private" } else { "public" },
    };

    ctx.renderer.render(&created)
}

pub async fn update_repo(
    ctx: &BitbucketContext<'_>,
    workspace: &str,
    slug: &str,
    name: Option<&str>,
    description: Option<&str>,
    language: Option<&str>,
) -> Result<()> {
    let mut payload = serde_json::json!({});

    if let Some(n) = name {
        payload["name"] = serde_json::json!(n);
    }

    if let Some(d) = description {
        payload["description"] = serde_json::json!(d);
    }

    if let Some(l) = language {
        payload["language"] = serde_json::json!(l);
    }

    let path = format!("/2.0/repositories/{workspace}/{slug}");
    let repo: Repo = ctx
        .client
        .put(&path, &payload)
        .await
        .with_context(|| format!("Failed to update repository {workspace}/{slug}"))?;

    tracing::info!(
        slug = repo.slug.as_str(),
        workspace,
        "Repository updated successfully"
    );

    #[derive(Serialize)]
    struct Updated<'a> {
        slug: &'a str,
        name: &'a str,
        description: &'a str,
        language: &'a str,
    }

    let updated = Updated {
        slug: repo.slug.as_str(),
        name: repo.name.as_deref().unwrap_or(""),
        description: repo.description.as_deref().unwrap_or(""),
        language: repo.language.as_deref().unwrap_or(""),
    };

    ctx.renderer.render(&updated)
}

pub async fn delete_repo(
    ctx: &BitbucketContext<'_>,
    workspace: &str,
    slug: &str,
    force: bool,
) -> Result<()> {
    if !force {
        use std::io::{self, Write};
        print!("Are you sure you want to delete repository {workspace}/{slug}? [y/N]: ");
        io::stdout().flush()?;
        let mut input = String::new();
        io::stdin().read_line(&mut input)?;
        if !input.trim().eq_ignore_ascii_case("y") {
            tracing::info!("Repository deletion cancelled");
            return Ok(());
        }
    }

    let path = format!("/2.0/repositories/{workspace}/{slug}");
    let _: serde_json::Value = ctx
        .client
        .delete(&path)
        .await
        .with_context(|| format!("Failed to delete repository {workspace}/{slug}"))?;

    tracing::info!(slug, workspace, "Repository deleted successfully");
    render_success(
        ctx.renderer,
        &format!("✅ Repository {workspace}/{slug} deleted"),
        &MutationResult::with_id(format!("Repository {workspace}/{slug} deleted"), slug),
    )
}