clauth 0.1.4

A simple Claude Code account switcher - swap OAuth and API profiles in a second
use anyhow::{Context, Result};
use std::path::PathBuf;

use crate::profile::{AppConfig, ClaudeCredentials, home_dir, profile_dir, save_profile};

fn claude_credentials_path() -> Result<PathBuf> {
    Ok(home_dir()?.join(".claude").join(".credentials.json"))
}

fn claude_settings_path() -> Result<PathBuf> {
    Ok(home_dir()?.join(".claude").join("settings.json"))
}

pub(crate) fn read_claude_credentials() -> Result<Option<ClaudeCredentials>> {
    let path = claude_credentials_path()?;
    if !path.exists() {
        return Ok(None);
    }
    let content = std::fs::read_to_string(&path).context("Failed to read .credentials.json")?;
    serde_json::from_str(&content)
        .context("Failed to parse .credentials.json")
        .map(Some)
}

#[cfg(unix)]
fn create_symlink(target: &std::path::Path, link: &std::path::Path) -> Result<()> {
    std::os::unix::fs::symlink(target, link).context("Failed to create credential symlink")
}

#[cfg(windows)]
fn create_symlink(target: &std::path::Path, link: &std::path::Path) -> Result<()> {
    match std::os::windows::fs::symlink_file(target, link) {
        Ok(()) => Ok(()),
        Err(_) => std::fs::copy(target, link)
            .map(|_| ())
            .context("Failed to copy credentials"),
    }
}

#[cfg(not(any(unix, windows)))]
fn create_symlink(target: &std::path::Path, link: &std::path::Path) -> Result<()> {
    std::fs::copy(target, link)
        .map(|_| ())
        .context("Failed to copy credentials")
}

/// Symlinks `~/.claude/.credentials.json` → profile's `credentials.json`; copies on Windows without symlink privilege.
pub(crate) fn link_profile_credentials(name: &str) -> Result<()> {
    let link = claude_credentials_path()?;

    if link.symlink_metadata().is_ok() {
        std::fs::remove_file(&link).context("Failed to remove old .credentials.json")?;
    }

    let target = profile_dir(name)?.join("credentials.json");
    if target.exists() {
        if let Some(parent) = link.parent() {
            std::fs::create_dir_all(parent)?;
        }
        create_symlink(&target, &link)?;
    }

    Ok(())
}

pub(crate) fn clear_claude_credentials() -> Result<()> {
    let link = claude_credentials_path()?;
    if link.symlink_metadata().is_ok() {
        std::fs::remove_file(&link).context("Failed to remove .credentials.json")?;
    }
    Ok(())
}

pub(crate) struct ClaudeEndpoint {
    pub(crate) base_url: Option<String>,
    pub(crate) api_key: Option<String>,
}

pub(crate) fn read_claude_endpoint_config() -> Result<ClaudeEndpoint> {
    let path = claude_settings_path()?;
    if !path.exists() {
        return Ok(ClaudeEndpoint {
            base_url: None,
            api_key: None,
        });
    }
    let content = std::fs::read_to_string(&path).context("Failed to read settings.json")?;
    let settings: serde_json::Value =
        serde_json::from_str(&content).context("Failed to parse settings.json")?;
    Ok(ClaudeEndpoint {
        base_url: settings["env"]["ANTHROPIC_BASE_URL"]
            .as_str()
            .map(str::to_owned),
        api_key: settings["env"]["ANTHROPIC_AUTH_TOKEN"]
            .as_str()
            .map(str::to_owned),
    })
}

/// Patches only ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN inside the `env`
/// object of settings.json. Every other key and field is left untouched.
pub(crate) fn apply_endpoint_to_claude_settings(
    base_url: Option<&str>,
    api_key: Option<&str>,
) -> Result<()> {
    let path = claude_settings_path()?;

    if base_url.is_none() && api_key.is_none() && !path.exists() {
        return Ok(());
    }

    let mut settings: serde_json::Value = if path.exists() {
        let content = std::fs::read_to_string(&path).context("Failed to read settings.json")?;
        serde_json::from_str(&content).context("Failed to parse settings.json")?
    } else {
        serde_json::json!({})
    };

    if settings.get("env").is_none() {
        settings["env"] = serde_json::json!({});
    }

    let env = settings["env"]
        .as_object_mut()
        .context("settings.json `env` is not an object")?;

    match base_url {
        Some(url) => {
            env.insert("ANTHROPIC_BASE_URL".into(), url.into());
        }
        None => {
            env.remove("ANTHROPIC_BASE_URL");
        }
    }
    match api_key {
        Some(key) => {
            env.insert("ANTHROPIC_AUTH_TOKEN".into(), key.into());
        }
        None => {
            env.remove("ANTHROPIC_AUTH_TOKEN");
        }
    }

    std::fs::write(&path, serde_json::to_string_pretty(&settings)?)
        .context("Failed to write settings.json")
}

/// Reads the live .credentials.json and saves it to the active profile.
pub(crate) fn snapshot_active_credentials(config: &mut AppConfig) -> Result<()> {
    let Some(active) = config.state.active_profile.clone() else {
        return Ok(());
    };
    let credentials = read_claude_credentials()?;
    if let Some(profile) = config.find_mut(&active) {
        profile.credentials = credentials;
        save_profile(profile)?;
    }
    Ok(())
}