use std::env;
use std::fs;
use std::io::{Read, Write};
use std::path::{Component, Path, PathBuf};
use std::process::{Command, Stdio};
use std::str::FromStr;

use age::secrecy::SecretString;
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use chacha20poly1305::{
    ChaCha20Poly1305, Nonce,
    aead::{Aead, KeyInit},
};
use hkdf::Hkdf;
use rho_core::{
    IdentityBundle, IdentityBundleManifest, LocalEncryptionKey, LocalIdentityManifest, RhoResult,
    SignatureContext, SignatureManifest, SignatureRecord, arg_value, ensure_parent, file_digest,
    from_yaml, is_rho_encrypted_text as is_rho_encrypted_yaml_text, normalize_actor_id,
    normalize_repo_id, path_matches_pattern, providers, require_arg, to_yaml, uuid_like,
    validate_request_id, yaml_top_level_kind,
};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret};

const SIGNATURE_NAMESPACE: &str = "rho";
const TRANSPARENT_KIND: &str = "rho_transparent_file";
const TRANSPARENT_ALGORITHM: &str = "age-scrypt";
const LEGACY_TRANSPARENT_ALGORITHM: &str = "chacha20poly1305-hkdf-sha256";
const RECIPIENT_KDF: &str = "age";
const LEGACY_RECIPIENT_KDF: &str = "x25519-hkdf-sha256";
const RECIPIENT_CONTENT_ENCRYPTION: &str = "age-encryption.org/v1";
const TRANSPARENT_INFO: &[u8] = b"rho transparent file v1";
const RECIPIENT_INFO: &[u8] = b"rho recipient envelope v1";

fn usage() -> ! {
    eprintln!(
        "usage:\n  rho crypto sign <path> --identity <rho-id> --out <path.rhosig.yaml> [--repo-id <repo-id> (--request-id <req-id>|--message-id <msg-id>) --recipient <rho-id> --purpose <purpose>]\n  rho crypto verify <path> --signature <path.rhosig.yaml> --identity <rho-id> [--repo-root <repo>] [--repo-id <repo-id> (--request-id <req-id>|--message-id <msg-id>) --recipient <rho-id> --purpose <purpose>]\n  rho crypto encrypt <path> --key-file <path> --out <path.rhoenc>\n  rho crypto decrypt <path.rhoenc> --key-file <path> --out <path>\n  rho crypto seal <path> --recipient <rho-id> [--recipient <rho-id>...] --out <path.rhoenc> --repo-root <repo> --path <repo-path> [--purpose <purpose>]\n  rho crypto open <path.rhoenc> --identity <rho-id> --out <path> --repo-root <repo> --path <repo-path> [--purpose <purpose>]\n  rho crypto extract-tar <path.tar> --out-dir <dir>\n  rho crypto view <path> [--root <repo-root>]\n  rho crypto clean --key-file <path>\n  rho crypto smudge --key-file <path>"
    );
    std::process::exit(2);
}

pub fn run(args: &[String]) -> RhoResult<()> {
    let Some(command) = args.first().map(String::as_str) else {
        usage();
    };
    match command {
        "sign" => sign(&args[1..]),
        "verify" => verify(&args[1..]),
        "encrypt" => encrypt(&args[1..]),
        "decrypt" => decrypt(&args[1..]),
        "seal" => seal(&args[1..]),
        "open" => open(&args[1..]),
        "extract-tar" => extract_tar(&args[1..]),
        "view" => view(&args[1..]),
        "clean" => clean(&args[1..]),
        "smudge" => smudge(&args[1..]),
        "--help" | "-h" => usage(),
        _ => usage(),
    }
}

fn view(args: &[String]) -> RhoResult<()> {
    let Some(input_path) = args.first().map(PathBuf::from) else {
        usage();
    };
    let repo_root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    let repo_path = repo_relative_path(&repo_root, &input_path)?;
    if let Some(content) = index_blob(&repo_root, &repo_path)? {
        std::io::stdout().write_all(&content)?;
        return Ok(());
    }
    let plaintext = fs::read(repo_root.join(&repo_path))
        .map_err(|_| format!("file not found: {}", input_path.display()))?;
    if is_rho_encrypted_text(&plaintext) {
        std::io::stdout().write_all(&plaintext)?;
        return Ok(());
    }
    if let Some(recipients) = policy_recipients_for_path(&repo_root, &repo_path)? {
        let context = recipient_envelope_context_for(&repo_root, &repo_path, "rho.git_filter")?;
        let envelope = seal_bytes_for_recipients_with_repo(
            &plaintext,
            &recipients,
            Some(&repo_root),
            context,
        )?;
        std::io::stdout().write_all(to_yaml(&envelope)?.as_bytes())?;
        return Ok(());
    }
    Err(format!("no staged blob and no rho encryption policy matches: {repo_path}").into())
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
struct EncryptedFileManifest {
    version: u32,
    kind: String,
    crypto: EncryptedFileCrypto,
    payload: EncryptedFilePayload,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
struct EncryptedFileCrypto {
    algorithm: String,
    iterations: u32,
    key_source: String,
    created_at: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
struct EncryptedFilePayload {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    nonce_base64: Option<String>,
    ciphertext_base64: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
struct RecipientEnvelopeManifest {
    version: u32,
    kind: String,
    crypto: RecipientEnvelopeCrypto,
    recipients: Vec<RecipientEnvelopeRecipient>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    payload: Option<RecipientEnvelopePayload>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
struct RecipientEnvelopeCrypto {
    algorithm: String,
    content_encryption: String,
    created_at: String,
    context: RecipientEnvelopeContext,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
struct RecipientEnvelopeContext {
    repo_id: String,
    path: String,
    purpose: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
struct RecipientEnvelopeRecipient {
    identity_id: String,
    key_id: String,
    algorithm: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    ephemeral_public_key: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    nonce_base64: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    ciphertext_base64: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
struct RecipientEnvelopePayload {
    ciphertext_base64: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
struct AgeRecipientPlaintext {
    version: u32,
    context: RecipientEnvelopeContext,
    plaintext_base64: String,
}

fn encrypt(args: &[String]) -> RhoResult<()> {
    let Some(input_path) = args.first().map(PathBuf::from) else {
        usage();
    };
    let key_file = PathBuf::from(require_arg(args, "--key-file").unwrap_or_else(|_| usage()));
    let out = PathBuf::from(require_arg(args, "--out").unwrap_or_else(|_| usage()));
    let plaintext = fs::read(&input_path)
        .map_err(|_| format!("file to encrypt not found: {}", input_path.display()))?;
    let envelope = encrypt_bytes(&plaintext, &key_file)?;
    ensure_parent(&out)?;
    fs::write(&out, to_yaml(&envelope)?)?;
    println!("{}", out.display());
    Ok(())
}

fn seal(args: &[String]) -> RhoResult<()> {
    let Some(input_path) = args.first().map(PathBuf::from) else {
        usage();
    };
    let recipients = repeated_arg(args, "--recipient")
        .into_iter()
        .map(|recipient| normalize_actor_id(&recipient))
        .collect::<RhoResult<Vec<_>>>()?;
    if recipients.is_empty() {
        return Err("missing required argument: --recipient".into());
    }
    let out = PathBuf::from(require_arg(args, "--out").unwrap_or_else(|_| usage()));
    let plaintext = fs::read(&input_path)
        .map_err(|_| format!("file to seal not found: {}", input_path.display()))?;
    let (repo_root, context) = recipient_envelope_context_arg(args, "rho.bundle")?;
    let expanded_recipients = expand_policy_recipients(&repo_root, recipients)?;
    let envelope = seal_bytes_for_recipients_with_repo(
        &plaintext,
        &expanded_recipients,
        Some(&repo_root),
        context,
    )?;
    ensure_parent(&out)?;
    fs::write(&out, to_yaml(&envelope)?)?;
    println!("{}", out.display());
    Ok(())
}

fn seal_bytes_for_recipients_with_repo(
    plaintext: &[u8],
    recipients: &[String],
    repo_root: Option<&Path>,
    context: RecipientEnvelopeContext,
) -> RhoResult<RecipientEnvelopeManifest> {
    let rho_home = rho_home()?;
    let mut records = Vec::new();
    let mut age_recipients: Vec<Box<dyn age::Recipient + Send>> = Vec::new();
    for recipient_id in unique_recipients(recipients) {
        let handle = github_handle_from_identity_id(&recipient_id)?;
        let identity = read_identity_bundle_for_recipient(repo_root, &rho_home, &handle)?;
        let key = age_encryption_public_key(&identity)?;
        let recipient = age::x25519::Recipient::from_str(key.public_key.trim())
            .map_err(|error| format!("invalid age recipient key for {recipient_id}: {error}"))?;
        age_recipients.push(Box::new(recipient));
        records.push(RecipientEnvelopeRecipient {
            identity_id: recipient_id,
            key_id: key.id.clone(),
            algorithm: "age-x25519".to_string(),
            ephemeral_public_key: None,
            nonce_base64: None,
            ciphertext_base64: None,
        });
    }
    let payload = AgeRecipientPlaintext {
        version: 1,
        context: context.clone(),
        plaintext_base64: BASE64.encode(plaintext),
    };
    let ciphertext = age_encrypt(&to_yaml(&payload)?.into_bytes(), age_recipients)?;
    Ok(RecipientEnvelopeManifest {
        version: 1,
        kind: "rho_recipient_envelope".to_string(),
        crypto: RecipientEnvelopeCrypto {
            algorithm: RECIPIENT_KDF.to_string(),
            content_encryption: RECIPIENT_CONTENT_ENCRYPTION.to_string(),
            created_at: rho_core::now_rfc3339(),
            context,
        },
        recipients: records,
        payload: Some(RecipientEnvelopePayload {
            ciphertext_base64: BASE64.encode(ciphertext),
        }),
    })
}

fn open(args: &[String]) -> RhoResult<()> {
    let Some(input_path) = args.first().map(PathBuf::from) else {
        usage();
    };
    let identity_id = identity_arg_or_active(args)?;
    let out = PathBuf::from(require_arg(args, "--out").unwrap_or_else(|_| usage()));
    let text = fs::read_to_string(&input_path)
        .map_err(|_| format!("recipient envelope not found: {}", input_path.display()))?;
    let (_, expected_context) = recipient_envelope_context_arg(args, "rho.bundle")?;
    check_recipient_envelope_context(&text, &expected_context)?;
    let plaintext = open_text_for_identity(&text, &identity_id)?;
    ensure_parent(&out)?;
    fs::write(&out, plaintext)?;
    println!("{}", out.display());
    Ok(())
}

fn extract_tar(args: &[String]) -> RhoResult<()> {
    let Some(input_path) = args.first().map(PathBuf::from) else {
        usage();
    };
    let out_dir = PathBuf::from(require_arg(args, "--out-dir").unwrap_or_else(|_| usage()));
    let bytes = fs::read(&input_path)
        .map_err(|_| format!("tar file not found: {}", input_path.display()))?;
    safe_extract_tar_bytes(&bytes, &out_dir)?;
    println!("{}", out_dir.display());
    Ok(())
}

fn decrypt(args: &[String]) -> RhoResult<()> {
    let Some(input_path) = args.first().map(PathBuf::from) else {
        usage();
    };
    let key_file = PathBuf::from(require_arg(args, "--key-file").unwrap_or_else(|_| usage()));
    let out = PathBuf::from(require_arg(args, "--out").unwrap_or_else(|_| usage()));
    let text = fs::read_to_string(&input_path)
        .map_err(|_| format!("encrypted file not found: {}", input_path.display()))?;
    let plaintext = decrypt_text(&text, &key_file)?;
    ensure_parent(&out)?;
    fs::write(&out, plaintext)?;
    println!("{}", out.display());
    Ok(())
}

fn clean(args: &[String]) -> RhoResult<()> {
    let key_file = arg_value(args, "--key-file").map(PathBuf::from);
    let repo_root = arg_value(args, "--repo-root").map(PathBuf::from);
    let repo_path = arg_value(args, "--path");
    let mut input = Vec::new();
    std::io::stdin().read_to_end(&mut input)?;
    if is_rho_encrypted_text(&input) {
        std::io::stdout().write_all(&input)?;
        return Ok(());
    }
    if let (Some(repo_root), Some(repo_path)) = (repo_root, repo_path)
        && let Some(recipients) = policy_recipients_for_path(&repo_root, &repo_path)?
    {
        let context = recipient_envelope_context_for(&repo_root, &repo_path, "rho.git_filter")?;
        if let Some(existing) = reusable_index_envelope(&repo_root, &repo_path, &input, &context)? {
            std::io::stdout().write_all(&existing)?;
            return Ok(());
        }
        let envelope =
            seal_bytes_for_recipients_with_repo(&input, &recipients, Some(&repo_root), context)?;
        std::io::stdout().write_all(to_yaml(&envelope)?.as_bytes())?;
        return Ok(());
    }
    let Some(key_file) = key_file else {
        return Err("missing required argument: --key-file or --repo-root/--path".into());
    };
    let envelope = encrypt_bytes(&input, &key_file)?;
    std::io::stdout().write_all(to_yaml(&envelope)?.as_bytes())?;
    Ok(())
}

fn smudge(args: &[String]) -> RhoResult<()> {
    let key_file = arg_value(args, "--key-file").map(PathBuf::from);
    let expected_context = match (
        arg_value(args, "--repo-root").map(PathBuf::from),
        arg_value(args, "--path"),
    ) {
        (Some(repo_root), Some(repo_path)) => Some(recipient_envelope_context_for(
            &repo_root,
            &repo_path,
            "rho.git_filter",
        )?),
        _ => None,
    };
    let mut input = Vec::new();
    std::io::stdin().read_to_end(&mut input)?;
    if !is_rho_encrypted_text(&input) {
        std::io::stdout().write_all(&input)?;
        return Ok(());
    }
    let text = String::from_utf8(input)?;
    if yaml_top_level_kind(&text).as_deref() == Some("rho_recipient_envelope") {
        let envelope: RecipientEnvelopeManifest = from_yaml(&text)?;
        let Some(expected_context) = expected_context.as_ref() else {
            return Err("recipient envelope smudge requires --repo-root and --path".into());
        };
        check_recipient_envelope_context_manifest(&envelope, expected_context)?;
        if let Ok(identity_id) = active_identity() {
            match open_envelope_for_identity(&envelope, &identity_id) {
                Ok(plaintext) => std::io::stdout().write_all(&plaintext)?,
                Err(error)
                    if recipient_envelope_manifest_has_recipient(&envelope, &identity_id) =>
                {
                    return Err(format!(
                        "recipient envelope decrypt failed for {identity_id}: {error}"
                    )
                    .into());
                }
                Err(_) => std::io::stdout().write_all(text.as_bytes())?,
            }
        } else {
            match open_envelope_with_any_local_identity(&envelope) {
                Ok(plaintext) => std::io::stdout().write_all(&plaintext)?,
                Err(_) => std::io::stdout().write_all(text.as_bytes())?,
            }
        }
        return Ok(());
    }
    let Some(key_file) = key_file else {
        std::io::stdout().write_all(text.as_bytes())?;
        return Ok(());
    };
    let plaintext = decrypt_text(&text, &key_file)?;
    std::io::stdout().write_all(&plaintext)?;
    Ok(())
}

fn encrypt_bytes(plaintext: &[u8], key_file: &Path) -> RhoResult<EncryptedFileManifest> {
    if !key_file.is_file() {
        return Err(format!("rho crypto key file not found: {}", key_file.display()).into());
    }
    let key_material = fs::read(key_file)?;
    let ciphertext = age_scrypt_encrypt(plaintext, &key_material)?;
    Ok(EncryptedFileManifest {
        version: 1,
        kind: TRANSPARENT_KIND.to_string(),
        crypto: EncryptedFileCrypto {
            algorithm: TRANSPARENT_ALGORITHM.to_string(),
            iterations: 0,
            key_source: "repo-local-key-file".to_string(),
            created_at: rho_core::now_rfc3339(),
        },
        payload: EncryptedFilePayload {
            nonce_base64: None,
            ciphertext_base64: BASE64.encode(ciphertext),
        },
    })
}

fn decrypt_text(text: &str, key_file: &Path) -> RhoResult<Vec<u8>> {
    if !key_file.is_file() {
        return Err(format!("rho crypto key file not found: {}", key_file.display()).into());
    }
    let envelope: EncryptedFileManifest = from_yaml(text)?;
    if envelope.kind != TRANSPARENT_KIND {
        return Err(format!("unsupported encrypted file kind: {}", envelope.kind).into());
    }
    let key_material = fs::read(key_file)?;
    let ciphertext = BASE64.decode(&envelope.payload.ciphertext_base64)?;
    match envelope.crypto.algorithm.as_str() {
        TRANSPARENT_ALGORITHM => age_scrypt_decrypt(&ciphertext, &key_material),
        LEGACY_TRANSPARENT_ALGORITHM => {
            let nonce_base64 = envelope
                .payload
                .nonce_base64
                .as_ref()
                .ok_or("legacy transparent envelope missing nonce_base64")?;
            let nonce = decode_fixed::<12>(nonce_base64, "nonce")?;
            decrypt_aead(
                &ciphertext,
                &derive_key(&key_material, TRANSPARENT_INFO)?,
                &nonce,
            )
        }
        _ => Err(format!(
            "unsupported encrypted file algorithm: {}",
            envelope.crypto.algorithm
        )
        .into()),
    }
}

fn is_rho_encrypted_text(input: &[u8]) -> bool {
    std::str::from_utf8(input)
        .map(is_rho_encrypted_yaml_text)
        .unwrap_or(false)
}

fn reusable_index_envelope(
    repo_root: &Path,
    repo_path: &str,
    plaintext: &[u8],
    expected_context: &RecipientEnvelopeContext,
) -> RhoResult<Option<Vec<u8>>> {
    let Some(blob) = index_blob(repo_root, repo_path)? else {
        return Ok(None);
    };
    let Ok(text) = std::str::from_utf8(&blob) else {
        return Ok(None);
    };
    if yaml_top_level_kind(text).as_deref() != Some("rho_recipient_envelope") {
        return Ok(None);
    };
    let envelope: RecipientEnvelopeManifest = from_yaml(text)?;
    if envelope.crypto.context != *expected_context {
        return Ok(None);
    }
    let opened = active_identity()
        .ok()
        .and_then(|identity_id| open_envelope_for_identity(&envelope, &identity_id).ok())
        .or_else(|| open_envelope_with_any_local_identity(&envelope).ok());
    if opened.as_deref() == Some(plaintext) {
        return Ok(Some(blob));
    }
    Ok(None)
}

fn open_for_recipient(
    record: &RecipientEnvelopeRecipient,
    private_key_path: &str,
    context: &RecipientEnvelopeContext,
) -> RhoResult<Vec<u8>> {
    if record.algorithm != "x25519" {
        return Err(format!("unsupported recipient algorithm: {}", record.algorithm).into());
    }
    let private_key = PathBuf::from(private_key_path);
    if !private_key.is_file() {
        return Err(format!(
            "private encryption key not found: {}",
            private_key.display()
        )
        .into());
    }
    let private = StaticSecret::from(decode_fixed::<32>(
        fs::read_to_string(&private_key)?.trim(),
        "local private encryption key",
    )?);
    let ephemeral_public_key = record
        .ephemeral_public_key
        .as_ref()
        .ok_or("legacy recipient record missing ephemeral_public_key")?;
    let ephemeral_public = X25519PublicKey::from(decode_fixed::<32>(
        ephemeral_public_key,
        "ephemeral public key",
    )?);
    let shared = private.diffie_hellman(&ephemeral_public);
    let nonce_base64 = record
        .nonce_base64
        .as_ref()
        .ok_or("legacy recipient record missing nonce_base64")?;
    let ciphertext_base64 = record
        .ciphertext_base64
        .as_ref()
        .ok_or("legacy recipient record missing ciphertext_base64")?;
    let nonce = decode_fixed::<12>(nonce_base64, "nonce")?;
    let ciphertext = BASE64.decode(ciphertext_base64)?;
    decrypt_aead(
        &ciphertext,
        &derive_key(
            shared.as_bytes(),
            recipient_envelope_info(context).as_bytes(),
        )?,
        &nonce,
    )
}

fn open_envelope_with_any_local_identity(
    envelope: &RecipientEnvelopeManifest,
) -> RhoResult<Vec<u8>> {
    let rho_home = rho_home()?;
    let identities_dir = rho_home.join("identities/github");
    if !identities_dir.is_dir() {
        return Err("no local identities available".into());
    }
    let mut last_error = None;
    for entry in fs::read_dir(identities_dir)? {
        let path = entry?.path();
        if path.extension().and_then(|value| value.to_str()) != Some("yaml") {
            continue;
        }
        let text = fs::read_to_string(&path)?;
        let local: LocalIdentityManifest = from_yaml(&text)?;
        let identity_id = local.local_identity.identity.id.clone();
        let Some(record) = envelope
            .recipients
            .iter()
            .find(|recipient| recipient.identity_id == identity_id)
        else {
            continue;
        };
        let Some(encryption_key) = local.local_identity.encryption_key else {
            continue;
        };
        match if envelope.crypto.algorithm == RECIPIENT_KDF {
            open_age_envelope_with_local_key(envelope, &encryption_key)
        } else {
            open_with_local_encryption_key(record, &encryption_key, &envelope.crypto.context)
        } {
            Ok(plaintext) => return Ok(plaintext),
            Err(error) => last_error = Some(error.to_string()),
        }
    }
    Err(last_error
        .unwrap_or_else(|| "no local identity can open recipient envelope".to_string())
        .into())
}

fn open_text_for_identity(text: &str, identity_id: &str) -> RhoResult<Vec<u8>> {
    let envelope: RecipientEnvelopeManifest = from_yaml(text)?;
    if envelope.kind != "rho_recipient_envelope" {
        return Err(format!("unsupported envelope kind: {}", envelope.kind).into());
    }
    open_envelope_for_identity(&envelope, identity_id)
}

fn open_envelope_for_identity(
    envelope: &RecipientEnvelopeManifest,
    identity_id: &str,
) -> RhoResult<Vec<u8>> {
    let _record = envelope
        .recipients
        .iter()
        .find(|recipient| recipient.identity_id == identity_id)
        .ok_or_else(|| format!("envelope has no recipient entry for {identity_id}"))?;
    let rho_home = rho_home()?;
    let handle = github_handle_from_identity_id(identity_id)?;
    let local = read_local_identity(&rho_home, &handle)?;
    let encryption_key = local
        .local_identity
        .encryption_key
        .ok_or("local identity has no encryption key")?;
    if envelope.crypto.algorithm == RECIPIENT_KDF {
        return open_age_envelope_with_local_key(envelope, &encryption_key);
    }
    if envelope.crypto.algorithm == LEGACY_RECIPIENT_KDF {
        return open_legacy_envelope_for_identity(envelope, identity_id, &encryption_key);
    }
    Err(format!(
        "unsupported recipient envelope algorithm: {}",
        envelope.crypto.algorithm
    )
    .into())
}

fn recipient_envelope_manifest_has_recipient(
    envelope: &RecipientEnvelopeManifest,
    identity_id: &str,
) -> bool {
    envelope
        .recipients
        .iter()
        .any(|recipient| recipient.identity_id == identity_id)
}

fn open_with_local_encryption_key(
    record: &RecipientEnvelopeRecipient,
    encryption_key: &LocalEncryptionKey,
    context: &RecipientEnvelopeContext,
) -> RhoResult<Vec<u8>> {
    if encryption_key.algorithm != "x25519" {
        return Err(format!(
            "unsupported local encryption key algorithm: {}",
            encryption_key.algorithm
        )
        .into());
    }
    open_for_recipient(record, &encryption_key.private_key_ref.path, context)
}

fn open_age_envelope_with_local_key(
    envelope: &RecipientEnvelopeManifest,
    encryption_key: &LocalEncryptionKey,
) -> RhoResult<Vec<u8>> {
    if encryption_key.algorithm != "age-x25519" {
        return Err(format!(
            "unsupported local encryption key algorithm for age envelope: {}",
            encryption_key.algorithm
        )
        .into());
    }
    let private_key = PathBuf::from(&encryption_key.private_key_ref.path);
    let identity_text = fs::read_to_string(&private_key)
        .map_err(|_| format!("private age key not found: {}", private_key.display()))?;
    let identity = age::x25519::Identity::from_str(identity_text.trim())
        .map_err(|error| format!("invalid private age key: {error}"))?;
    let payload = envelope
        .payload
        .as_ref()
        .ok_or("age recipient envelope missing payload")?;
    let ciphertext = BASE64.decode(&payload.ciphertext_base64)?;
    let decrypted = age_decrypt(&ciphertext, &identity)?;
    let inner: AgeRecipientPlaintext = from_yaml(&String::from_utf8(decrypted)?)?;
    if inner.context != envelope.crypto.context {
        return Err("recipient envelope inner context mismatch".into());
    }
    Ok(BASE64.decode(inner.plaintext_base64)?)
}

fn open_legacy_envelope_for_identity(
    envelope: &RecipientEnvelopeManifest,
    identity_id: &str,
    encryption_key: &LocalEncryptionKey,
) -> RhoResult<Vec<u8>> {
    let record = envelope
        .recipients
        .iter()
        .find(|recipient| recipient.identity_id == identity_id)
        .ok_or_else(|| format!("envelope has no recipient entry for {identity_id}"))?;
    open_with_local_encryption_key(record, encryption_key, &envelope.crypto.context)
}

fn recipient_envelope_context_arg(
    args: &[String],
    default_purpose: &str,
) -> RhoResult<(PathBuf, RecipientEnvelopeContext)> {
    match (
        arg_value(args, "--repo-root").map(PathBuf::from),
        arg_value(args, "--path"),
    ) {
        (Some(repo_root), Some(repo_path)) => {
            let purpose =
                arg_value(args, "--purpose").unwrap_or_else(|| default_purpose.to_string());
            let context = recipient_envelope_context_for(&repo_root, &repo_path, &purpose)?;
            Ok((repo_root, context))
        }
        _ => Err("recipient envelopes require --repo-root and --path".into()),
    }
}

fn recipient_envelope_context_for(
    repo_root: &Path,
    repo_path: &str,
    purpose: &str,
) -> RhoResult<RecipientEnvelopeContext> {
    Ok(RecipientEnvelopeContext {
        repo_id: read_repo_id(repo_root)?,
        path: normalize_repo_path_context(repo_path)?,
        purpose: purpose.to_string(),
    })
}

fn check_recipient_envelope_context(
    text: &str,
    expected: &RecipientEnvelopeContext,
) -> RhoResult<()> {
    let envelope: RecipientEnvelopeManifest = from_yaml(text)?;
    check_recipient_envelope_context_manifest(&envelope, expected)
}

fn check_recipient_envelope_context_manifest(
    envelope: &RecipientEnvelopeManifest,
    expected: &RecipientEnvelopeContext,
) -> RhoResult<()> {
    let actual = &envelope.crypto.context;
    if actual != expected {
        return Err(format!(
            "recipient envelope context mismatch: expected repo_id={} path={} purpose={}, got repo_id={} path={} purpose={}",
            expected.repo_id,
            expected.path,
            expected.purpose,
            actual.repo_id,
            actual.path,
            actual.purpose
        )
        .into());
    }
    Ok(())
}

fn recipient_envelope_info(context: &RecipientEnvelopeContext) -> String {
    format!(
        "{}\nrepo_id: {}\npath: {}\npurpose: {}\n",
        String::from_utf8_lossy(RECIPIENT_INFO),
        context.repo_id,
        context.path,
        context.purpose
    )
}

fn normalize_repo_path_context(repo_path: &str) -> RhoResult<String> {
    let path = Path::new(repo_path);
    if path.is_absolute() {
        return Err(format!("recipient envelope path must be repo-relative: {repo_path}").into());
    }
    for component in path.components() {
        match component {
            Component::Normal(_) | Component::CurDir => {}
            Component::ParentDir | Component::RootDir | Component::Prefix(_) => {
                return Err(format!("unsafe recipient envelope path: {repo_path}").into());
            }
        }
    }
    Ok(repo_path.replace('\\', "/"))
}

fn age_encryption_public_key(identity: &IdentityBundle) -> RhoResult<&rho_core::IdentityPublicKey> {
    identity
        .public_keys
        .iter()
        .find(|key| key.kind == "age-encryption" && key.algorithm == "age-x25519")
        .ok_or_else(|| format!("identity has no age encryption key: {}", identity.id).into())
}

fn derive_key(input: &[u8], info: &[u8]) -> RhoResult<[u8; 32]> {
    let hk = Hkdf::<Sha256>::new(None, input);
    let mut output = [0u8; 32];
    hk.expand(info, &mut output)
        .map_err(|_| "HKDF output length was invalid")?;
    Ok(output)
}

fn decrypt_aead(ciphertext: &[u8], key: &[u8; 32], nonce: &[u8; 12]) -> RhoResult<Vec<u8>> {
    let cipher = ChaCha20Poly1305::new(key.into());
    cipher
        .decrypt(Nonce::from_slice(nonce), ciphertext)
        .map_err(|_| "AEAD decryption failed".into())
}

fn age_encrypt(
    plaintext: &[u8],
    recipients: Vec<Box<dyn age::Recipient + Send>>,
) -> RhoResult<Vec<u8>> {
    let encryptor = age::Encryptor::with_recipients(
        recipients.iter().map(|recipient| recipient.as_ref() as _),
    )?;
    let mut ciphertext = Vec::new();
    let mut writer = encryptor.wrap_output(&mut ciphertext)?;
    writer.write_all(plaintext)?;
    writer.finish()?;
    Ok(ciphertext)
}

fn age_decrypt(ciphertext: &[u8], identity: &age::x25519::Identity) -> RhoResult<Vec<u8>> {
    Ok(age::decrypt(identity, ciphertext)?)
}

fn age_scrypt_encrypt(plaintext: &[u8], key_material: &[u8]) -> RhoResult<Vec<u8>> {
    let passphrase = transparent_age_passphrase(key_material);
    let encryptor = age::Encryptor::with_user_passphrase(passphrase);
    let mut ciphertext = Vec::new();
    let mut writer = encryptor.wrap_output(&mut ciphertext)?;
    writer.write_all(plaintext)?;
    writer.finish()?;
    Ok(ciphertext)
}

fn age_scrypt_decrypt(ciphertext: &[u8], key_material: &[u8]) -> RhoResult<Vec<u8>> {
    let passphrase = transparent_age_passphrase(key_material);
    let decryptor = age::Decryptor::new_buffered(ciphertext)?;
    let mut reader =
        decryptor.decrypt(std::iter::once(&age::scrypt::Identity::new(passphrase) as _))?;
    let mut plaintext = Vec::new();
    reader.read_to_end(&mut plaintext)?;
    Ok(plaintext)
}

fn transparent_age_passphrase(key_material: &[u8]) -> SecretString {
    SecretString::from(BASE64.encode(key_material))
}

fn safe_extract_tar_bytes(bytes: &[u8], out_dir: &Path) -> RhoResult<()> {
    fs::create_dir_all(out_dir)?;
    let mut offset = 0usize;
    while offset + 512 <= bytes.len() {
        let header = &bytes[offset..offset + 512];
        offset += 512;
        if header.iter().all(|byte| *byte == 0) {
            break;
        }

        let path = tar_entry_path(header)?;
        validate_tar_path(&path)?;
        let entry_type = header[156];
        let size = tar_octal(&header[124..136])?;
        let data_end = offset.checked_add(size).ok_or("tar entry size overflow")?;
        if data_end > bytes.len() {
            return Err(
                format!("tar entry extends past end of archive: {}", path.display()).into(),
            );
        }

        match entry_type {
            0 | b'0' => {
                let target = out_dir.join(&path);
                ensure_parent(&target)?;
                fs::write(&target, &bytes[offset..data_end])?;
            }
            b'5' => {
                fs::create_dir_all(out_dir.join(&path))?;
            }
            b'1' => return Err(format!("tar hardlink rejected: {}", path.display()).into()),
            b'2' => return Err(format!("tar symlink rejected: {}", path.display()).into()),
            b'3' | b'4' => {
                return Err(format!("tar device file rejected: {}", path.display()).into());
            }
            b'6' => return Err(format!("tar fifo rejected: {}", path.display()).into()),
            _ => {
                return Err(format!(
                    "unsupported tar entry type {} for {}",
                    entry_type,
                    path.display()
                )
                .into());
            }
        }

        offset = data_end + padding_len(size);
    }
    Ok(())
}

fn tar_entry_path(header: &[u8]) -> RhoResult<PathBuf> {
    let name = tar_string(&header[0..100]);
    let prefix = tar_string(&header[345..500]);
    let combined = if prefix.is_empty() {
        name
    } else {
        format!("{prefix}/{name}")
    };
    if combined.is_empty() {
        return Err("tar entry has empty path".into());
    }
    Ok(PathBuf::from(combined))
}

fn tar_string(bytes: &[u8]) -> String {
    let end = bytes
        .iter()
        .position(|byte| *byte == 0)
        .unwrap_or(bytes.len());
    String::from_utf8_lossy(&bytes[..end]).trim().to_string()
}

fn tar_octal(bytes: &[u8]) -> RhoResult<usize> {
    let text = tar_string(bytes);
    let trimmed = text.trim_matches(char::from(0)).trim();
    if trimmed.is_empty() {
        return Ok(0);
    }
    usize::from_str_radix(trimmed, 8)
        .map_err(|error| format!("invalid tar octal size {trimmed}: {error}").into())
}

fn padding_len(size: usize) -> usize {
    let remainder = size % 512;
    if remainder == 0 { 0 } else { 512 - remainder }
}

fn validate_tar_path(path: &Path) -> RhoResult<()> {
    if path.is_absolute() {
        return Err(format!("tar absolute path rejected: {}", path.display()).into());
    }
    let mut has_component = false;
    for component in path.components() {
        match component {
            Component::Normal(_) => has_component = true,
            Component::CurDir => {}
            Component::ParentDir => {
                return Err(format!("tar parent path rejected: {}", path.display()).into());
            }
            Component::RootDir | Component::Prefix(_) => {
                return Err(format!("tar absolute path rejected: {}", path.display()).into());
            }
        }
    }
    if !has_component {
        return Err(format!("tar empty path rejected: {}", path.display()).into());
    }
    Ok(())
}

fn decode_fixed<const N: usize>(value: &str, label: &str) -> RhoResult<[u8; N]> {
    let bytes = BASE64.decode(value.trim())?;
    bytes
        .try_into()
        .map_err(|bytes: Vec<u8>| format!("{label} must be {N} bytes, got {}", bytes.len()).into())
}

#[derive(Debug, Deserialize)]
struct PermissionsManifest {
    #[serde(default)]
    permissions: Vec<PermissionRule>,
}

#[derive(Debug, Deserialize)]
struct RepoManifest {
    repo: RepoRecord,
}

#[derive(Debug, Deserialize)]
struct RepoRecord {
    id: String,
}

#[derive(Debug, Deserialize)]
struct PermissionRule {
    pattern: String,
    #[serde(default)]
    read: ReadPermission,
}

#[derive(Debug, Default, Deserialize)]
struct ReadPermission {
    #[serde(default)]
    public: bool,
    #[serde(default)]
    encrypted_to: Vec<String>,
}

#[derive(Debug, Deserialize)]
struct MembershipManifest {
    #[serde(default)]
    members: Vec<MemberRecord>,
}

#[derive(Debug, Deserialize)]
struct MemberRecord {
    identity: String,
    role: String,
}

#[derive(Debug, Deserialize)]
struct RevocationsManifest {
    #[serde(default)]
    revocations: Vec<RevocationRecord>,
}

#[derive(Debug, Deserialize)]
struct RevocationRecord {
    identity: String,
    key_id: String,
    reason: Option<String>,
    revoked_at: Option<String>,
    revoked_by: Option<String>,
}

fn policy_recipients_for_path(repo_root: &Path, repo_path: &str) -> RhoResult<Option<Vec<String>>> {
    let path = repo_root.join("rho/policy/permissions.yaml");
    let text = match fs::read_to_string(&path) {
        Ok(text) => text,
        Err(_) => return Ok(None),
    };
    let manifest: PermissionsManifest = from_yaml(&text)?;
    manifest
        .permissions
        .into_iter()
        .find(|rule| !rule.read.public && path_matches_pattern(repo_path, &rule.pattern))
        .map(|rule| expand_policy_recipients(repo_root, rule.read.encrypted_to))
        .transpose()
}

fn read_repo_id(repo_root: &Path) -> RhoResult<String> {
    let path = repo_root.join("rho/repo.yaml");
    let text = fs::read_to_string(&path)
        .map_err(|_| format!("repo manifest not found: {}", path.display()))?;
    let manifest: RepoManifest = from_yaml(&text)?;
    normalize_repo_id(&manifest.repo.id)
}

fn expand_policy_recipients(
    repo_root: &Path,
    mut recipients: Vec<String>,
) -> RhoResult<Vec<String>> {
    recipients.extend(repo_admin_recipients(repo_root)?);
    if let Some(identity) = local_sender_identity()? {
        recipients.push(identity);
    }
    Ok(unique_recipients(&recipients))
}

fn repo_admin_recipients(repo_root: &Path) -> RhoResult<Vec<String>> {
    let path = repo_root.join("rho/membership.yaml");
    let text = match fs::read_to_string(path) {
        Ok(text) => text,
        Err(_) => return Ok(Vec::new()),
    };
    let manifest: MembershipManifest = from_yaml(&text)?;
    Ok(manifest
        .members
        .into_iter()
        .filter(|member| matches!(member.role.as_str(), "owner" | "admin"))
        .map(|member| member.identity)
        .collect())
}

fn local_sender_identity() -> RhoResult<Option<String>> {
    if let Ok(identity) = active_identity() {
        return Ok(Some(identity));
    }
    let rho_home = rho_home()?;
    let identity_dir = rho_home.join("identities").join("github");
    let entries = match fs::read_dir(identity_dir) {
        Ok(entries) => entries,
        Err(_) => return Ok(None),
    };
    let mut identities = Vec::new();
    for entry in entries {
        let path = entry?.path();
        if path.extension().and_then(|value| value.to_str()) != Some("yaml") {
            continue;
        }
        let text = fs::read_to_string(path)?;
        let manifest: LocalIdentityManifest = from_yaml(&text)?;
        identities.push(manifest.local_identity.identity.id);
    }
    identities.sort();
    identities.dedup();
    Ok(if identities.len() == 1 {
        identities.pop()
    } else {
        None
    })
}

fn unique_recipients(recipients: &[String]) -> Vec<String> {
    let mut unique = Vec::new();
    for recipient in recipients {
        if !unique.iter().any(|seen| seen == recipient) {
            unique.push(recipient.clone());
        }
    }
    unique
}

fn repo_relative_path(repo_root: &Path, input_path: &Path) -> RhoResult<String> {
    let path = if input_path.is_absolute() {
        let root = repo_root
            .canonicalize()
            .unwrap_or_else(|_| repo_root.to_path_buf());
        let canonical = input_path
            .canonicalize()
            .map_err(|_| format!("file not found: {}", input_path.display()))?;
        canonical
            .strip_prefix(&root)
            .map_err(|_| {
                format!(
                    "path is outside repo root: {} under {}",
                    input_path.display(),
                    repo_root.display()
                )
            })?
            .to_path_buf()
    } else {
        input_path.to_path_buf()
    };
    let text = path
        .to_str()
        .ok_or_else(|| -> Box<dyn std::error::Error> { "path is not valid UTF-8".into() })?
        .trim_start_matches("./")
        .to_string();
    if text.is_empty() || text.starts_with("../") || text.contains("/../") {
        return Err(format!("unsafe repo path: {}", input_path.display()).into());
    }
    Ok(text)
}

fn index_blob(repo_root: &Path, repo_path: &str) -> RhoResult<Option<Vec<u8>>> {
    let output = Command::new("git")
        .arg("-C")
        .arg(repo_root)
        .args(["show", &format!(":{repo_path}")])
        .output()?;
    if output.status.success() {
        return Ok(Some(output.stdout));
    }
    Ok(None)
}

fn repeated_arg(args: &[String], flag: &str) -> Vec<String> {
    args.windows(2)
        .filter(|window| window[0] == flag)
        .map(|window| window[1].clone())
        .collect()
}

fn sign(args: &[String]) -> RhoResult<()> {
    let Some(signed_path) = args.first().map(PathBuf::from) else {
        usage();
    };
    let identity_id = identity_arg_or_active(args)?;
    let out = PathBuf::from(require_arg(args, "--out").unwrap_or_else(|_| usage()));
    if !signed_path.is_file() {
        return Err(format!("file to sign not found: {}", signed_path.display()).into());
    }

    let rho_home = rho_home()?;
    let handle = github_handle_from_identity_id(&identity_id)?;
    let local = read_local_identity(&rho_home, &handle)?;
    let identity = &local.local_identity.identity;
    if identity.id != identity_id {
        return Err(format!(
            "identity mismatch: requested {identity_id}, found {}",
            identity.id
        )
        .into());
    }
    let key = identity
        .public_keys
        .first()
        .ok_or("local identity has no public keys")?;
    let private_key = PathBuf::from(&local.local_identity.signing_key.private_key_ref.path);
    if !private_key.is_file() {
        return Err(format!("private signing key not found: {}", private_key.display()).into());
    }

    let context = signature_context_from_args(args)?;
    let signed_sha256 = file_digest(&signed_path)?;
    let raw_signature = if let Some(context) = context.as_ref() {
        let payload = signature_context_payload(&signed_sha256, context);
        ssh_sign_bytes(payload.as_bytes(), &private_key, out.parent())?
    } else {
        ssh_sign_file(&signed_path, &private_key, out.parent())?
    };
    ensure_parent(&out)?;
    let manifest = SignatureManifest {
        version: 1,
        signature: SignatureRecord {
            signed_path: signed_path.display().to_string(),
            signed_sha256,
            signer: identity.id.clone(),
            key_id: key.id.clone(),
            algorithm: key.algorithm.clone(),
            namespace: SIGNATURE_NAMESPACE.to_string(),
            context,
            signature: raw_signature,
            created_at: rho_core::now_rfc3339(),
        },
    };
    fs::write(&out, to_yaml(&manifest)?)?;
    println!("{}", out.display());
    Ok(())
}

fn verify(args: &[String]) -> RhoResult<()> {
    let Some(signed_path) = args.first().map(PathBuf::from) else {
        usage();
    };
    let signature_path =
        PathBuf::from(require_arg(args, "--signature").unwrap_or_else(|_| usage()));
    let identity_id = identity_arg_or_active(args)?;
    let repo_root = arg_value(args, "--repo-root").map(PathBuf::from);
    let expected_context = signature_context_from_args(args)?;
    if !signed_path.is_file() {
        return Err(format!("signed file not found: {}", signed_path.display()).into());
    }

    let text = fs::read_to_string(&signature_path)
        .map_err(|_| format!("signature not found: {}", signature_path.display()))?;
    let manifest: SignatureManifest = from_yaml(&text)?;
    let record = manifest.signature;
    if record.signer != identity_id {
        return Err(format!(
            "signature signer mismatch: expected {identity_id}, got {}",
            record.signer
        )
        .into());
    }
    if record.namespace != SIGNATURE_NAMESPACE {
        return Err(format!("unsupported signature namespace: {}", record.namespace).into());
    }
    check_revocation(repo_root.as_deref(), &record)?;
    let actual_digest = file_digest(&signed_path)?;
    if record.signed_sha256 != actual_digest {
        return Err(format!(
            "signed file digest mismatch: expected {}, got {actual_digest}",
            record.signed_sha256
        )
        .into());
    }
    if let Some(expected_context) = expected_context.as_ref() {
        check_signature_context(record.context.as_ref(), expected_context)?;
    }

    let rho_home = rho_home()?;
    let handle = github_handle_from_identity_id(&identity_id)?;
    let identity = read_identity_bundle_for_recipient(repo_root.as_deref(), &rho_home, &handle)?;
    let key = identity
        .public_keys
        .iter()
        .find(|key| key.id == record.key_id)
        .ok_or_else(|| format!("identity does not contain key {}", record.key_id))?;
    if key.algorithm != record.algorithm {
        return Err(format!(
            "signature algorithm mismatch: key has {}, signature has {}",
            key.algorithm, record.algorithm
        )
        .into());
    }
    if let Some(context) = record.context.as_ref() {
        let payload = signature_context_payload(&record.signed_sha256, context);
        ssh_verify_bytes(
            payload.as_bytes(),
            &record.signature,
            &identity.id,
            &key.public_key,
        )?;
    } else {
        ssh_verify_file(
            &signed_path,
            &record.signature,
            &identity.id,
            &key.public_key,
        )?;
    }
    println!("verified {}", signature_path.display());
    Ok(())
}

fn signature_context_from_args(args: &[String]) -> RhoResult<Option<SignatureContext>> {
    let repo_id = arg_value(args, "--repo-id")
        .map(|value| normalize_repo_id(&value))
        .transpose()?;
    let request_id = arg_value(args, "--request-id")
        .map(|value| {
            validate_request_id(&value)?;
            Ok::<String, Box<dyn std::error::Error>>(value)
        })
        .transpose()?;
    let message_id = arg_value(args, "--message-id")
        .map(|value| normalize_message_id(&value))
        .transpose()?;
    let recipient_id = arg_value(args, "--recipient")
        .map(|value| normalize_actor_id(&value))
        .transpose()?;
    let purpose = arg_value(args, "--purpose")
        .map(|value| normalize_signature_purpose(&value))
        .transpose()?;

    let any_context = repo_id.is_some()
        || request_id.is_some()
        || message_id.is_some()
        || recipient_id.is_some()
        || purpose.is_some();
    if !any_context {
        return Ok(None);
    }
    if request_id.is_some() && message_id.is_some() {
        return Err("signature context cannot include both --request-id and --message-id".into());
    }
    if repo_id.is_none()
        || (request_id.is_none() && message_id.is_none())
        || recipient_id.is_none()
        || purpose.is_none()
    {
        return Err(
            "signature context requires --repo-id, one of --request-id or --message-id, --recipient, and --purpose".into(),
        );
    }
    Ok(Some(SignatureContext {
        repo_id,
        request_id,
        message_id,
        recipient_id,
        purpose,
    }))
}

fn normalize_message_id(value: &str) -> RhoResult<String> {
    let value = value.trim();
    if value.is_empty() {
        return Err("message id must not be empty".into());
    }
    if value.len() > 128 {
        return Err("message id must be 128 characters or fewer".into());
    }
    if !value
        .chars()
        .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '.' | '_' | '-'))
    {
        return Err(format!(
            "message id must contain only ASCII letters, digits, '.', '_', or '-': {value}"
        )
        .into());
    }
    Ok(value.to_string())
}

fn normalize_signature_purpose(value: &str) -> RhoResult<String> {
    let value = value.trim();
    if value.is_empty() {
        return Err("signature purpose must not be empty".into());
    }
    if !value
        .chars()
        .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '.' | '_' | '-' | ':'))
    {
        return Err(format!(
            "signature purpose must contain only ASCII letters, digits, '.', '_', '-', or ':': {value}"
        )
        .into());
    }
    Ok(value.to_string())
}

fn check_signature_context(
    actual: Option<&SignatureContext>,
    expected: &SignatureContext,
) -> RhoResult<()> {
    let Some(actual) = actual else {
        return Err("signature missing required context".into());
    };
    check_context_field(
        "repo_id",
        actual.repo_id.as_ref(),
        expected.repo_id.as_ref(),
    )?;
    check_context_field(
        "request_id",
        actual.request_id.as_ref(),
        expected.request_id.as_ref(),
    )?;
    check_context_field(
        "message_id",
        actual.message_id.as_ref(),
        expected.message_id.as_ref(),
    )?;
    check_context_field(
        "recipient_id",
        actual.recipient_id.as_ref(),
        expected.recipient_id.as_ref(),
    )?;
    check_context_field(
        "purpose",
        actual.purpose.as_ref(),
        expected.purpose.as_ref(),
    )?;
    Ok(())
}

fn check_context_field(
    field: &str,
    actual: Option<&String>,
    expected: Option<&String>,
) -> RhoResult<()> {
    if actual != expected {
        return Err(format!(
            "signature context mismatch for {field}: expected {}, got {}",
            expected.map(String::as_str).unwrap_or("<missing>"),
            actual.map(String::as_str).unwrap_or("<missing>")
        )
        .into());
    }
    Ok(())
}

fn signature_context_payload(signed_sha256: &str, context: &SignatureContext) -> String {
    if context.message_id.is_none() {
        return format!(
            "rho signature payload v1\nsigned_sha256: {signed_sha256}\nrepo_id: {}\nrequest_id: {}\nrecipient_id: {}\npurpose: {}\n",
            context.repo_id.as_deref().unwrap_or(""),
            context.request_id.as_deref().unwrap_or(""),
            context.recipient_id.as_deref().unwrap_or(""),
            context.purpose.as_deref().unwrap_or("")
        );
    }
    format!(
        "rho signature payload v2\nsigned_sha256: {signed_sha256}\nrepo_id: {}\nrequest_id: {}\nmessage_id: {}\nrecipient_id: {}\npurpose: {}\n",
        context.repo_id.as_deref().unwrap_or(""),
        context.request_id.as_deref().unwrap_or(""),
        context.message_id.as_deref().unwrap_or(""),
        context.recipient_id.as_deref().unwrap_or(""),
        context.purpose.as_deref().unwrap_or("")
    )
}

fn check_revocation(repo_root: Option<&Path>, record: &SignatureRecord) -> RhoResult<()> {
    let Some(repo_root) = repo_root else {
        return Ok(());
    };
    let path = repo_root.join("rho/policy/revocations.yaml");
    let text = match fs::read_to_string(&path) {
        Ok(text) => text,
        Err(_) => return Ok(()),
    };
    let manifest: RevocationsManifest = from_yaml(&text)?;
    if let Some(revocation) = manifest.revocations.into_iter().find(|revocation| {
        revocation.identity == record.signer && revocation.key_id == record.key_id
    }) {
        let mut message = format!(
            "signature key revoked: signer {} key {}",
            record.signer, record.key_id
        );
        if let Some(reason) = revocation.reason.as_deref()
            && !reason.trim().is_empty()
        {
            message.push_str(&format!(" ({})", reason.trim()));
        }
        if let Some(revoked_at) = revocation.revoked_at.as_deref()
            && !revoked_at.trim().is_empty()
        {
            message.push_str(&format!(" revoked_at={}", revoked_at.trim()));
        }
        if let Some(revoked_by) = revocation.revoked_by.as_deref()
            && !revoked_by.trim().is_empty()
        {
            message.push_str(&format!(" revoked_by={}", revoked_by.trim()));
        }
        return Err(message.into());
    }
    Ok(())
}

fn ssh_sign_file(
    signed_path: &Path,
    private_key: &Path,
    temp_dir: Option<&Path>,
) -> RhoResult<String> {
    ssh_sign_bytes(&fs::read(signed_path)?, private_key, temp_dir)
}

fn ssh_sign_bytes(
    message_bytes: &[u8],
    private_key: &Path,
    temp_dir: Option<&Path>,
) -> RhoResult<String> {
    let work_dir = temp_dir
        .map(PathBuf::from)
        .unwrap_or_else(env::temp_dir)
        .join(format!("rho-sign-{}", uuid_like()));
    fs::create_dir_all(&work_dir)?;
    let message = work_dir.join("message");
    fs::write(&message, message_bytes)?;
    let output = Command::new("ssh-keygen")
        .args(["-Y", "sign", "-f"])
        .arg(private_key)
        .args(["-n", SIGNATURE_NAMESPACE])
        .arg(&message)
        .output()?;
    if !output.status.success() {
        let _ = fs::remove_dir_all(&work_dir);
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(format!("ssh-keygen signing failed: {}", stderr.trim()).into());
    }
    let signature_path = message.with_extension("sig");
    let signature = fs::read_to_string(&signature_path)?;
    fs::remove_dir_all(&work_dir)?;
    Ok(signature)
}

fn ssh_verify_file(
    signed_path: &Path,
    signature: &str,
    identity_id: &str,
    public_key: &str,
) -> RhoResult<()> {
    ssh_verify_bytes(&fs::read(signed_path)?, signature, identity_id, public_key)
}

fn ssh_verify_bytes(
    message_bytes: &[u8],
    signature: &str,
    identity_id: &str,
    public_key: &str,
) -> RhoResult<()> {
    let work_dir = env::temp_dir().join(format!("rho-verify-{}", uuid_like()));
    fs::create_dir_all(&work_dir)?;
    let signature_path = work_dir.join("signature.sig");
    let allowed_signers_path = work_dir.join("allowed_signers");
    fs::write(&signature_path, signature)?;
    fs::write(
        &allowed_signers_path,
        format!("{identity_id} namespaces=\"{SIGNATURE_NAMESPACE}\" {public_key}\n"),
    )?;

    let mut child = Command::new("ssh-keygen")
        .args(["-Y", "verify", "-f"])
        .arg(&allowed_signers_path)
        .args(["-I", identity_id, "-n", SIGNATURE_NAMESPACE, "-s"])
        .arg(&signature_path)
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()?;
    {
        let stdin = child
            .stdin
            .as_mut()
            .ok_or("failed to open ssh-keygen stdin")?;
        stdin.write_all(message_bytes)?;
    }
    let output = child.wait_with_output()?;
    fs::remove_dir_all(&work_dir)?;
    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(format!("ssh signature verification failed: {}", stderr.trim()).into());
    }
    Ok(())
}

fn rho_home() -> RhoResult<PathBuf> {
    if let Ok(value) = env::var("RHO_HOME")
        && !value.is_empty()
    {
        return Ok(PathBuf::from(value));
    }
    let home = env::var("HOME").map_err(|_| "HOME is not set and RHO_HOME was not provided")?;
    Ok(PathBuf::from(home).join(".rho"))
}

fn active_identity() -> RhoResult<String> {
    let identity = env::var("RHO_IDENTITY")
        .map(|value| value.trim().to_string())
        .ok()
        .filter(|value| !value.is_empty())
        .ok_or_else(|| -> Box<dyn std::error::Error> { "RHO_IDENTITY is not set".into() })?;
    normalize_actor_id(&identity)
}

fn identity_arg_or_active(args: &[String]) -> RhoResult<String> {
    match arg_value(args, "--identity") {
        Some(identity) => normalize_actor_id(&identity),
        None => active_identity().map_err(|_| {
            "missing required argument: --identity, and RHO_IDENTITY is not set".into()
        }),
    }
}

fn read_local_identity(rho_home: &Path, handle: &str) -> RhoResult<LocalIdentityManifest> {
    let path = rho_home
        .join("identities")
        .join("github")
        .join(format!("{handle}.yaml"));
    let text = fs::read_to_string(&path)
        .map_err(|_| format!("local identity not found: {}", path.display()))?;
    from_yaml(&text)
}

fn read_identity_bundle(rho_home: &Path, handle: &str) -> RhoResult<IdentityBundle> {
    let local_path = rho_home
        .join("identities")
        .join("github")
        .join(format!("{handle}.yaml"));
    if local_path.is_file() {
        let text = fs::read_to_string(local_path)?;
        let manifest: LocalIdentityManifest = from_yaml(&text)?;
        return Ok(manifest.local_identity.identity);
    }
    let peer_path = rho_home
        .join("peers")
        .join("github")
        .join(format!("{handle}.yaml"));
    let text = fs::read_to_string(&peer_path)
        .map_err(|_| format!("identity not found: {}", peer_path.display()))?;
    let manifest: IdentityBundleManifest = from_yaml(&text)?;
    Ok(manifest.identity)
}

fn read_identity_bundle_for_recipient(
    repo_root: Option<&Path>,
    rho_home: &Path,
    handle: &str,
) -> RhoResult<IdentityBundle> {
    if let Some(repo_root) = repo_root {
        let participant_path = repo_root
            .join("rho")
            .join("participants")
            .join(format!("{handle}.yaml"));
        if participant_path.is_file() {
            let text = fs::read_to_string(participant_path)?;
            let manifest: IdentityBundleManifest = from_yaml(&text)?;
            return Ok(manifest.identity);
        }
    }
    read_identity_bundle(rho_home, handle)
}

fn github_handle_from_identity_id(identity_id: &str) -> RhoResult<String> {
    providers::github::handle_from_identity_id(identity_id)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_github_identity_id() {
        assert_eq!(
            github_handle_from_identity_id("rho://id/github/user1").unwrap(),
            "user1"
        );
        assert!(github_handle_from_identity_id("rho://id/email/user1").is_err());
    }

    #[test]
    fn detects_transparent_envelope_text() {
        let text = b"version: 1\nkind: rho_transparent_file\n";
        assert!(is_rho_encrypted_text(text));
        assert!(is_rho_encrypted_text(
            b"version: 1\nkind: rho_recipient_envelope\n"
        ));
        assert!(!is_rho_encrypted_text(b"the private value is 1113.03\n"));
    }

    #[test]
    fn safe_tar_extracts_regular_files() {
        let out_dir = env::temp_dir().join(format!("rho-safe-tar-test-{}", uuid_like()));
        let tar = test_tar(vec![
            test_tar_entry("rho/requests/req.yaml", b"version: 1\n", b'0'),
            test_tar_entry("rho/requests", b"", b'5'),
        ]);
        safe_extract_tar_bytes(&tar, &out_dir).unwrap();
        assert_eq!(
            fs::read_to_string(out_dir.join("rho/requests/req.yaml")).unwrap(),
            "version: 1\n"
        );
        fs::remove_dir_all(out_dir).unwrap();
    }

    #[test]
    fn safe_tar_rejects_hostile_paths_and_links() {
        let out_dir = env::temp_dir().join(format!("rho-hostile-tar-test-{}", uuid_like()));
        for (path, entry_type, expected) in [
            ("../escape.txt", b'0', "parent path"),
            ("/tmp/escape.txt", b'0', "absolute path"),
            ("link", b'2', "symlink"),
            ("hardlink", b'1', "hardlink"),
        ] {
            let tar = test_tar(vec![test_tar_entry(path, b"", entry_type)]);
            let error = safe_extract_tar_bytes(&tar, &out_dir)
                .unwrap_err()
                .to_string();
            assert!(
                error.contains(expected),
                "expected {expected:?} in {error:?}"
            );
        }
        let _ = fs::remove_dir_all(out_dir);
    }

    fn test_tar(entries: Vec<Vec<u8>>) -> Vec<u8> {
        let mut tar = Vec::new();
        for entry in entries {
            tar.extend(entry);
        }
        tar.extend([0u8; 1024]);
        tar
    }

    fn test_tar_entry(path: &str, data: &[u8], entry_type: u8) -> Vec<u8> {
        let mut header = [0u8; 512];
        let path_bytes = path.as_bytes();
        assert!(path_bytes.len() <= 100);
        header[..path_bytes.len()].copy_from_slice(path_bytes);
        let size = if entry_type == b'5' { 0 } else { data.len() };
        let size_text = format!("{size:011o}\0");
        header[124..124 + size_text.len()].copy_from_slice(size_text.as_bytes());
        header[156] = entry_type;
        header[257..263].copy_from_slice(b"ustar\0");
        let mut out = header.to_vec();
        if size > 0 {
            out.extend(data);
            out.extend(vec![0u8; padding_len(size)]);
        }
        out
    }
}
