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

use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use rho_core::{
    IdentityBundleManifest, LocalIdentityManifest, RhoResult, SignatureContext, SignatureManifest,
    Tool, ToolManifest, arg_value, ensure_parent, file_digest, from_json, from_yaml, has_flag,
    normalize_actor_id, normalize_repo_id, providers, to_json_pretty, to_yaml, validate_actor_id,
    validate_relative_safe_path, validate_request_id, yaml_quote,
};
use serde::{Deserialize, Serialize};

fn usage() -> ! {
    eprintln!(
        "usage:\n  rho repo init [--repo-id <rho://repo/...|owner/repo|github-url>] [--root <path>] [--owner <rho-id|github/handle|handle>] [--yes] [--force]\n  rho repo join <rho-id|github/handle|handle> [--root <path>] [--no-branch]\n  rho repo admit <rho-id|github/handle|handle> [--root <path>] [--rho-bin <path>] [--no-branch]\n  rho repo add-user <rho-id|github/handle|handle> [--root <path>]\n  rho repo import-users [--root <path>]\n  rho repo sign-governance [--root <path>] [--identity <rho-id>] [--no-stage]\n  rho repo protect-path <pattern> [--root <path>] [--recipient <rho-id>...] [--key-file <path>] [--rho-bin <path>]\n  rho repo install-filters [--root <path>] [--rho-bin <path>]\n  rho repo refresh-protected [--root <path>] [--rho-bin <path>]\n  rho repo audit-encryption [--root <path>]\n  rho repo status [--root <path>]\n  rho repo doctor [--root <path>] [--flow] [--governance]\n  rho repo check-pr [--root <path>] [--event <github-event.json>] [--actor <login|rho-id>] [--changed-file <path>...] [--base-ref <ref>] [--format json]"
    );
    std::process::exit(2);
}

pub fn run(args: &[String]) -> RhoResult<()> {
    let Some(command) = args.first().map(String::as_str) else {
        usage();
    };
    match command {
        "init" => init(&args[1..]),
        "join" => join(&args[1..]),
        "admit" => admit(&args[1..]),
        "add-user" => add_user(&args[1..]),
        "import-users" => import_users(&args[1..]),
        "sign-governance" => sign_governance(&args[1..]),
        "protect-path" => protect_path(&args[1..]),
        "install-filters" => install_filters(&args[1..]),
        "refresh-protected" => refresh_protected(&args[1..]),
        "audit-encryption" => audit_encryption(&args[1..]),
        "status" => status(&args[1..]),
        "doctor" => doctor(&args[1..]),
        "check-pr" => check_pr(&args[1..]),
        "--help" | "-h" => usage(),
        _ => usage(),
    }
}

fn import_users(args: &[String]) -> RhoResult<()> {
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    ensure_repo(&root)?;
    let participants_dir = root.join("rho/participants");
    let home = rho_home();
    let mut imported = 0usize;
    let mut skipped_local = 0usize;
    if !participants_dir.is_dir() {
        println!("imported users: 0");
        return Ok(());
    }
    for entry in fs::read_dir(&participants_dir)? {
        let path = entry?.path();
        if path.extension().and_then(|value| value.to_str()) != Some("yaml") {
            continue;
        }
        let bundle: IdentityBundleManifest = from_yaml(&fs::read_to_string(&path)?)?;
        validate_actor_id(&bundle.identity.id)?;
        let handle = github_handle_from_identity_id(&bundle.identity.id)?;
        if bundle.identity.kind != "github" || bundle.identity.handle != handle {
            return Err(format!("participant identity mismatch: {}", path.display()).into());
        }
        let local_path = home
            .join("identities")
            .join("github")
            .join(format!("{handle}.yaml"));
        if local_path.is_file() {
            skipped_local += 1;
            println!("local {}", bundle.identity.id);
            continue;
        }
        let peer_path = home
            .join("peers")
            .join("github")
            .join(format!("{handle}.yaml"));
        ensure_parent(&peer_path)?;
        fs::write(&peer_path, to_yaml(&bundle)?)?;
        imported += 1;
        println!("{}", peer_path.display());
    }
    println!("imported users: {imported}");
    if skipped_local > 0 {
        println!("already local identities: {skipped_local}");
    }
    Ok(())
}

fn sign_governance(args: &[String]) -> RhoResult<()> {
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    ensure_repo(&root)?;
    let repo: RepoManifest = from_yaml(&fs::read_to_string(root.join("rho/repo.yaml"))?)?;
    let identity = match arg_value(args, "--identity") {
        Some(identity) => normalize_actor_id(&identity)?,
        None => repo
            .repo
            .owner
            .clone()
            .ok_or("repo owner missing; pass --identity explicitly")?,
    };
    validate_actor_id(&identity)?;

    let files = governance_manifest_files(&root)?;
    if files.is_empty() {
        println!("governance files: none");
        return Ok(());
    }

    for file in files {
        let signed_path = root.join(&file);
        let signature_path = governance_signature_path(&root, &file)?;
        sign_file(&signed_path, &signature_path, &identity)?;
        println!("{}", signature_path.display());
        if !has_flag(args, "--no-stage") {
            git_add_relative(&root, &file)?;
            git_add_path(&root, &signature_path)?;
        }
    }
    if !has_flag(args, "--no-stage") {
        println!("staged governance signatures");
    }
    Ok(())
}

fn status(args: &[String]) -> RhoResult<()> {
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    ensure_repo(&root)?;
    let permissions_path = root.join("rho/policy/permissions.yaml");
    let permissions: PermissionsManifest = from_yaml(&fs::read_to_string(&permissions_path)?)?;
    let files = tracked_and_untracked_files(&root)?;
    let mut protected_count = 0usize;
    let mut warning_count = 0usize;

    println!("rho status");
    println!("root: {}", root.display());

    for file in files {
        let Some(rule) = permissions.permissions.iter().find(|rule| {
            !rule.read.public
                && !rule.read.encrypted_to.is_empty()
                && path_matches_pattern(&file, &rule.pattern)
        }) else {
            continue;
        };
        protected_count += 1;
        println!();
        println!("{file}");
        println!("  policy: {}", rule.pattern);
        println!("  read: encrypted to {}", rule.read.encrypted_to.join(", "));
        match index_blob(&root, &file) {
            Some(content) if content.contains("kind: rho_recipient_envelope") => {
                let missing = missing_recipients(&content, &rule.read.encrypted_to);
                if missing.is_empty() {
                    println!("  staged: encrypted recipient envelope");
                } else {
                    warning_count += 1;
                    println!("  staged: encrypted, but missing {}", missing.join(", "));
                }
            }
            Some(content) if content.contains("kind: rho_transparent_file") => {
                warning_count += 1;
                println!("  staged: encrypted with repo key, not recipient envelope");
            }
            Some(_) => {
                warning_count += 1;
                println!("  staged: NOT encrypted");
            }
            None => {
                println!("  staged: not staged");
                println!("  next add: will use rho-crypt filter from .gitattributes");
            }
        }
    }

    if protected_count == 0 {
        println!("protected files: none");
    } else {
        println!();
        println!("protected files: {protected_count}");
    }
    if warning_count > 0 {
        return Err(format!("{warning_count} protected file warning(s)").into());
    }
    Ok(())
}

#[derive(Debug, Serialize)]
struct CheckPrResult {
    status: String,
    actor: String,
    role: String,
    reason: String,
    close_recommended: bool,
    changed_files: Vec<String>,
    violations: Vec<CheckPrViolation>,
}

#[derive(Debug, Serialize)]
struct CheckPrViolation {
    path: String,
    reason: String,
}

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

#[derive(Debug, Deserialize)]
struct RevocationRecord {
    #[serde(default)]
    identity: String,
    #[serde(default)]
    actor: String,
    #[serde(default)]
    decision: String,
}

fn check_pr(args: &[String]) -> RhoResult<()> {
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    let changed_files = check_pr_changed_files(args, &root)?;
    if !root.join("rho/repo.yaml").is_file() {
        let actor = check_pr_actor(args).unwrap_or_else(|_| "unknown".to_string());
        let result = CheckPrResult {
            status: "pass".to_string(),
            actor,
            role: "unmanaged".to_string(),
            reason: "rho repo not initialized; policy check skipped".to_string(),
            close_recommended: false,
            changed_files,
            violations: Vec::new(),
        };
        print_check_pr_result(args, &result)?;
        return Ok(());
    }
    let actor = check_pr_actor(args)?;
    validate_actor_id(&actor)?;
    let role = check_pr_role(&root, &actor)?;
    let violations = check_pr_violations(&actor, &role, &changed_files);
    let blocked_actor = role == "blocked";
    let status = if blocked_actor || !violations.is_empty() {
        "blocked"
    } else if role == "unknown" {
        "pending"
    } else {
        "pass"
    };
    let reason = check_pr_reason(status, &role, blocked_actor, &violations);
    let result = CheckPrResult {
        status: status.to_string(),
        actor,
        role,
        reason,
        close_recommended: status == "blocked",
        changed_files,
        violations,
    };

    print_check_pr_result(args, &result)?;
    Ok(())
}

fn print_check_pr_result(args: &[String], result: &CheckPrResult) -> RhoResult<()> {
    if arg_value(args, "--format").as_deref() == Some("json") || has_flag(args, "--json") {
        print!("{}", to_json_pretty(&result)?);
    } else {
        println!("status: {}", result.status);
        println!("actor: {}", result.actor);
        println!("role: {}", result.role);
        println!("reason: {}", result.reason);
        for violation in &result.violations {
            println!("blocked: {}: {}", violation.path, violation.reason);
        }
    }
    Ok(())
}

fn check_pr_actor(args: &[String]) -> RhoResult<String> {
    if let Some(actor) = arg_value(args, "--actor") {
        return normalize_actor_id(&actor);
    }
    if let Some(event_path) = arg_value(args, "--event") {
        let text = fs::read_to_string(&event_path)
            .map_err(|error| format!("cannot read GitHub event file {event_path}: {error}"))?;
        let event: serde_json::Value = from_json(&text)?;
        for pointer in [
            "/pull_request/user/login",
            "/sender/login",
            "/issue/user/login",
        ] {
            if let Some(login) = event.pointer(pointer).and_then(|value| value.as_str())
                && !login.trim().is_empty()
            {
                return normalize_actor_id(login);
            }
        }
        return Err(format!("GitHub event has no usable actor login: {event_path}").into());
    }
    Err("check-pr requires --actor or --event".into())
}

fn check_pr_role(root: &Path, actor: &str) -> RhoResult<String> {
    if is_actor_blocked(root, actor)? {
        return Ok("blocked".to_string());
    }
    let membership_path = root.join("rho/membership.yaml");
    let membership: MembershipManifest = from_yaml(&fs::read_to_string(&membership_path)?)?;
    for member in membership.members {
        if member.identity == actor {
            return Ok(match member.role.as_str() {
                "owner" | "admin" => "admin".to_string(),
                "collaborator" | "member" => "member".to_string(),
                other => other.to_string(),
            });
        }
    }
    Ok("unknown".to_string())
}

fn is_actor_blocked(root: &Path, actor: &str) -> RhoResult<bool> {
    let path = root.join("rho/policy/revocations.yaml");
    if !path.is_file() {
        return Ok(false);
    }
    let manifest: RevocationsManifest = from_yaml(&fs::read_to_string(path)?)?;
    Ok(manifest
        .revocations
        .into_iter()
        .chain(manifest.blocks)
        .any(|record| {
            let identity = if record.identity.is_empty() {
                record.actor
            } else {
                record.identity
            };
            identity == actor
                && (record.decision.is_empty()
                    || matches!(
                        record.decision.as_str(),
                        "block" | "blocked" | "deny" | "denied" | "revoked"
                    ))
        }))
}

fn check_pr_changed_files(args: &[String], root: &Path) -> RhoResult<Vec<String>> {
    let mut files = repeated_arg(args, "--changed-file");
    if files.is_empty()
        && let Some(event_path) = arg_value(args, "--event")
    {
        files = changed_files_from_event(&event_path)?;
    }
    if files.is_empty()
        && let Some(base_ref) = arg_value(args, "--base-ref")
    {
        files = changed_files_from_git(root, &base_ref)?;
    }
    files.sort();
    files.dedup();
    for file in &files {
        validate_relative_safe_path(file)?;
    }
    Ok(files)
}

fn changed_files_from_event(event_path: &str) -> RhoResult<Vec<String>> {
    let text = fs::read_to_string(event_path)
        .map_err(|error| format!("cannot read GitHub event file {event_path}: {error}"))?;
    let event: serde_json::Value = from_json(&text)?;
    let Some(files) = event
        .get("rho_changed_files")
        .and_then(|value| value.as_array())
    else {
        return Ok(Vec::new());
    };
    Ok(files
        .iter()
        .filter_map(|value| value.as_str().map(ToOwned::to_owned))
        .collect())
}

fn changed_files_from_git(root: &Path, base_ref: &str) -> RhoResult<Vec<String>> {
    let output = Command::new("git")
        .arg("-C")
        .arg(root)
        .args(["diff", "--name-only", "-z", &format!("{base_ref}...HEAD")])
        .output()?;
    if !output.status.success() {
        return Err(format!("git diff failed against base ref {base_ref}").into());
    }
    Ok(output
        .stdout
        .split(|byte| *byte == 0)
        .filter(|entry| !entry.is_empty())
        .filter_map(|entry| String::from_utf8(entry.to_vec()).ok())
        .collect())
}

fn check_pr_violations(actor: &str, role: &str, changed_files: &[String]) -> Vec<CheckPrViolation> {
    let mut violations = Vec::new();
    for path in changed_files {
        let reason = if role == "blocked" {
            Some("actor is blocked".to_string())
        } else if role == "admin" || (role == "unknown" && is_unknown_join_path(actor, path)) {
            None
        } else if is_admin_only_path(path) {
            Some("only admins may change Rho policy, membership, participants, tools, actions, or workflows".to_string())
        } else if role == "unknown" && !is_unknown_join_path(actor, path) {
            Some("unknown actors may only submit their own join request files".to_string())
        } else {
            None
        };
        if let Some(reason) = reason {
            violations.push(CheckPrViolation {
                path: path.clone(),
                reason,
            });
        }
    }
    violations
}

fn is_admin_only_path(path: &str) -> bool {
    path == "rho.yaml"
        || path == ".gitmodules"
        || path.starts_with("rho/policy/")
        || path == "rho/membership.yaml"
        || path.starts_with("rho/participants/")
        || path.starts_with("rho/tools/")
        || path.starts_with(".github/workflows/")
        || path.starts_with("action/")
}

fn is_unknown_join_path(actor: &str, path: &str) -> bool {
    let Ok(handle) = github_handle_from_identity_id(actor) else {
        return false;
    };
    path == format!("rho/participants/{handle}.yaml")
        || path.starts_with("rho/requests/join-")
        || path.starts_with(&format!("rho/requests/{handle}/join-"))
}

fn check_pr_reason(
    status: &str,
    role: &str,
    blocked_actor: bool,
    violations: &[CheckPrViolation],
) -> String {
    if blocked_actor {
        return "actor is blocked by Rho policy".to_string();
    }
    if let Some(first) = violations.first() {
        return format!("{}: {}", first.path, first.reason);
    }
    match (status, role) {
        ("pending", "unknown") => "unknown actor requires admin review".to_string(),
        ("pass", "admin") => "admin actor is allowed".to_string(),
        ("pass", "member") => "member actor is allowed".to_string(),
        _ => "Rho PR policy evaluated".to_string(),
    }
}

fn audit_encryption(args: &[String]) -> RhoResult<()> {
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    ensure_repo(&root)?;
    let permissions_path = root.join("rho/policy/permissions.yaml");
    let permissions: PermissionsManifest = from_yaml(&fs::read_to_string(&permissions_path)?)?;
    let files = tracked_files(&root)?;
    let mut protected_count = 0usize;
    let mut checked_blobs = 0usize;
    let mut failures = Vec::new();

    println!("rho encryption audit");
    println!("root: {}", root.display());

    for file in files {
        let Some(rule) = permissions.permissions.iter().find(|rule| {
            !rule.read.public
                && !rule.read.encrypted_to.is_empty()
                && path_matches_pattern(&file, &rule.pattern)
        }) else {
            continue;
        };
        protected_count += 1;
        println!();
        println!("{file}");
        if let Some(content) = index_blob(&root, &file) {
            checked_blobs += 1;
            audit_encrypted_blob(
                "index",
                &file,
                &content,
                &rule.read.encrypted_to,
                &mut failures,
            );
        } else {
            println!("  index: not present");
        }
        if let Some(content) = head_blob(&root, &file) {
            checked_blobs += 1;
            audit_encrypted_blob(
                "HEAD",
                &file,
                &content,
                &rule.read.encrypted_to,
                &mut failures,
            );
        } else {
            println!("  HEAD: not present");
        }
    }

    if protected_count == 0 {
        println!("protected files: none");
    } else {
        println!();
        println!("protected files: {protected_count}");
        println!("checked encrypted blobs: {checked_blobs}");
    }
    if failures.is_empty() {
        println!("rho encryption audit passed");
        return Ok(());
    }

    eprintln!("rho encryption audit failed");
    for failure in &failures {
        eprintln!("- {failure}");
    }
    Err(format!("{} encryption audit failure(s)", failures.len()).into())
}

fn audit_encrypted_blob(
    label: &str,
    relative_path: &str,
    content: &str,
    recipients: &[String],
    failures: &mut Vec<String>,
) {
    if content.contains("kind: rho_recipient_envelope") {
        let missing = missing_recipients(content, recipients);
        if missing.is_empty() {
            println!("  {label}: encrypted recipient envelope");
        } else {
            println!("  {label}: encrypted, but missing {}", missing.join(", "));
            failures.push(format!(
                "{label} blob missing recipient(s) for {relative_path}: {}",
                missing.join(", ")
            ));
        }
        return;
    }
    if content.contains("kind: rho_transparent_file") {
        println!("  {label}: encrypted with repo key, not recipient envelope");
        failures.push(format!(
            "{label} blob uses repo-key encryption where recipients are required: {relative_path}"
        ));
        return;
    }
    println!("  {label}: NOT encrypted");
    failures.push(format!("{label} blob is not encrypted: {relative_path}"));
}

fn init(args: &[String]) -> RhoResult<()> {
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    if root.join("rho/repo.yaml").is_file() && !has_flag(args, "--force") {
        return init_existing_repo(args, &root);
    }
    let repo_id = arg_value(args, "--repo-id")
        .or_else(|| detect_github_remote(&root))
        .map(|value| normalize_repo_id(&value))
        .transpose()?
        .ok_or(
            "could not infer repo id; pass --repo-id owner/repo or https://github.com/owner/repo",
        )?;
    validate_repo_id(&repo_id)?;
    let owner = arg_value(args, "--owner")
        .or_else(active_identity)
        .map(|value| normalize_actor_id(&value))
        .transpose()?
        .unwrap_or_else(|| "repo-owner".to_string());
    validate_actor_id(&owner)?;
    let force = has_flag(args, "--force");
    if !has_flag(args, "--yes") {
        println!("Rho repo init");
        println!("  root: {}", root.display());
        println!("  repo id: {repo_id}");
        println!("  owner: {owner}");
        print!("Use these values? [Y/n] ");
        io::stdout().flush()?;
        let mut answer = String::new();
        io::stdin().read_line(&mut answer)?;
        let answer = answer.trim().to_ascii_lowercase();
        if answer == "n" || answer == "no" {
            return Err("rho repo init cancelled".into());
        }
    }

    fs::create_dir_all(root.join("rho/participants"))?;
    fs::create_dir_all(root.join("rho/policy"))?;
    fs::create_dir_all(root.join("rho/tools"))?;
    fs::create_dir_all(root.join("rho/requests"))?;
    fs::create_dir_all(root.join("rho/control/outbox"))?;
    fs::create_dir_all(root.join("rho/messages/inbox"))?;
    fs::create_dir_all(root.join("workspace"))?;

    write_if_missing_or_forced(
        &root.join("rho.yaml"),
        &format!(
            "rho: 1\nproject:\n  id: {}\n  owner: {}\ntransport:\n  kind: git\nmembers:\n  file: rho/membership.yaml\nparticipants:\n  path: rho/participants/\npermissions:\n  file: rho/policy/permissions.yaml\ncrypto:\n  file: rho/policy/crypto.yaml\nauth:\n  file: rho/policy/auth.yaml\ntools:\n  path: rho/tools/\nresources:\n  file: rho/resources.yaml\n",
            yaml_quote(&repo_id),
            yaml_quote(&owner),
        ),
        force,
    )?;
    write_if_missing_or_forced(
        &root.join("rho/repo.yaml"),
        &format!(
            "version: 1\nrepo:\n  id: {}\n  owner: {}\n  created_at: {}\n",
            yaml_quote(&repo_id),
            yaml_quote(&owner),
            yaml_quote(&rho_core::now_rfc3339()),
        ),
        force,
    )?;
    write_if_missing_or_forced(
        &root.join("rho/membership.yaml"),
        &format!(
            "version: 1\nmembers:\n  - identity: {}\n    role: owner\n",
            yaml_quote(&owner),
        ),
        force,
    )?;
    write_if_missing_or_forced(
        &root.join("rho/policy/paths.yaml"),
        "version: 1\npaths:\n  repo_root: rho://repo/\n  rho_root: rho://repo/rho/\n  workspace: rho://repo/workspace/\n  requests: rho://repo/rho/requests/\n  control_outbox: rho://repo/rho/control/outbox/\n",
        force,
    )?;
    write_if_missing_or_forced(
        &root.join("rho/policy/permissions.yaml"),
        "version: 1\npermissions: []\n",
        force,
    )?;
    write_if_missing_or_forced(
        &root.join("rho/policy/crypto.yaml"),
        "version: 1\ncrypto:\n  signatures: ssh\n  encryption: x25519-recipient-envelope\n  transparent_git_filters: disabled\n",
        force,
    )?;
    write_if_missing_or_forced(
        &root.join("rho/policy/auth.yaml"),
        "version: 1\nauth:\n  identity_proof: github-profile-fragment-claim\n  trust: local\n",
        force,
    )?;
    write_if_missing_or_forced(
        &root.join("rho/policy/revocations.yaml"),
        "version: 1\nrevocations: []\n",
        force,
    )?;
    write_if_missing_or_forced(
        &root.join("rho/resources.yaml"),
        "version: 1\nresources: []\n",
        force,
    )?;
    write_if_missing_or_forced(
        &root.join(".gitignore"),
        "sandbox/\nrho/private/\nrho/local/\n",
        false,
    )?;
    write_if_missing_or_forced(
        &root.join(".gitattributes"),
        "*.rhoenc binary\n*.rhosig.yaml text\nrho/participants/*.yaml text\nrho/requests/*.yaml text\n",
        false,
    )?;
    write_tool(&root, "run_mock", "run_mock_data", &owner, true, force)?;
    write_tool(&root, "run_real", "run_real_data", &owner, true, force)?;
    write_tool(
        &root,
        "publish_results",
        "release_results",
        &owner,
        true,
        force,
    )?;

    println!("{}", root.join("rho.yaml").display());
    println!("{}", root.join("rho/repo.yaml").display());
    println!("{}", root.join("rho/membership.yaml").display());
    println!("{}", root.join("rho/policy/paths.yaml").display());
    println!("{}", root.join("rho/policy/permissions.yaml").display());
    println!("{}", root.join("rho/policy/crypto.yaml").display());
    println!("{}", root.join("rho/policy/auth.yaml").display());
    println!("{}", root.join("rho/policy/revocations.yaml").display());
    println!("{}", root.join("rho/resources.yaml").display());
    println!("{}", root.join("rho/tools/run_mock.yaml").display());
    println!("{}", root.join("rho/tools/run_real.yaml").display());
    println!("{}", root.join("rho/tools/publish_results.yaml").display());
    Ok(())
}

fn init_existing_repo(args: &[String], root: &Path) -> RhoResult<()> {
    ensure_repo(root)?;
    println!("Rho repo already initialized");
    println!("  root: {}", root.display());
    println!("  repo: {}", root.join("rho/repo.yaml").display());
    if !has_flag(args, "--yes") {
        print!("Install filters and import participants? [Y/n] ");
        io::stdout().flush()?;
        let mut answer = String::new();
        io::stdin().read_line(&mut answer)?;
        let answer = answer.trim().to_ascii_lowercase();
        if answer == "n" || answer == "no" {
            return Err("rho repo init maintenance cancelled".into());
        }
    }
    let root_arg = root.display().to_string();
    install_filters(&["--root".to_string(), root_arg.clone()])?;
    import_users(&["--root".to_string(), root_arg])?;
    Ok(())
}

fn add_user(args: &[String]) -> RhoResult<()> {
    let Some(identity_arg) = args.first() else {
        usage();
    };
    let identity_id = normalize_actor_id(identity_arg)?;
    let handle = github_handle_from_identity_id(&identity_id)?;
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    ensure_repo(&root)?;
    let rho_home = rho_home();
    let local_path = rho_home
        .join("identities")
        .join("github")
        .join(format!("{handle}.yaml"));
    let peer_path = rho_home
        .join("peers")
        .join("github")
        .join(format!("{handle}.yaml"));
    let bundle = if local_path.is_file() {
        let local: LocalIdentityManifest = from_yaml(&fs::read_to_string(&local_path)?)?;
        IdentityBundleManifest {
            version: 1,
            identity: local.local_identity.identity,
        }
    } else if peer_path.is_file() {
        from_yaml::<IdentityBundleManifest>(&fs::read_to_string(&peer_path)?)?
    } else {
        return Err(format!(
            "identity not found in local RHO_HOME: {identity_id}; run `rho id init --github {handle}` or `rho id import <file>` first"
        )
        .into());
    };
    if bundle.identity.id != identity_id {
        return Err(format!(
            "identity mismatch: requested {identity_id}, found {}",
            bundle.identity.id
        )
        .into());
    }
    let out = root
        .join("rho")
        .join("participants")
        .join(format!("{handle}.yaml"));
    ensure_parent(&out)?;
    fs::write(&out, to_yaml(&bundle)?)?;
    println!("{}", out.display());
    add_member_if_missing(&root, &identity_id)?;
    Ok(())
}

fn join(args: &[String]) -> RhoResult<()> {
    let Some(identity_arg) = args.first() else {
        usage();
    };
    let identity_id = normalize_actor_id(identity_arg)?;
    let handle = github_handle_from_identity_id(&identity_id)?;
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    ensure_repo(&root)?;
    require_local_or_peer_identity(&identity_id)?;

    if !has_flag(args, "--no-branch") {
        switch_branch(&root, &format!("{handle}/join-rho"))?;
    }
    add_user(&[
        identity_id.clone(),
        "--root".to_string(),
        root.display().to_string(),
    ])?;
    println!("join branch: {handle}/join-rho");
    println!("stage: rho/participants/{handle}.yaml rho/membership.yaml");
    println!("next: rho commit -m \"Request Rho access for {handle}\"");
    Ok(())
}

fn admit(args: &[String]) -> RhoResult<()> {
    let Some(identity_arg) = args.first() else {
        usage();
    };
    let identity_id = normalize_actor_id(identity_arg)?;
    let handle = github_handle_from_identity_id(&identity_id)?;
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    let rho_bin = arg_value(args, "--rho-bin").unwrap_or_else(default_rho_bin);
    ensure_repo(&root)?;
    require_local_or_peer_identity(&identity_id)?;

    if !has_flag(args, "--no-branch") {
        switch_branch(&root, &format!("review/{handle}/admit-rho"))?;
    }
    add_user(&[
        identity_id.clone(),
        "--root".to_string(),
        root.display().to_string(),
    ])?;
    let inbox_pattern = format!("rho/messages/inbox/{handle}/**");
    protect_path(&[
        inbox_pattern.clone(),
        "--root".to_string(),
        root.display().to_string(),
        "--rho-bin".to_string(),
        rho_bin,
        "--recipient".to_string(),
        identity_id.clone(),
    ])?;
    doctor(&["--root".to_string(), root.display().to_string()])?;
    println!("admit branch: review/{handle}/admit-rho");
    println!("protected inbox: {inbox_pattern}");
    println!("stage: .gitattributes rho");
    println!("next: rho commit -m \"Admit {handle} to Rho project\"");
    Ok(())
}

fn require_local_or_peer_identity(identity_id: &str) -> RhoResult<()> {
    let handle = github_handle_from_identity_id(identity_id)?;
    let home = rho_home();
    let local_path = home
        .join("identities")
        .join("github")
        .join(format!("{handle}.yaml"));
    let peer_path = home
        .join("peers")
        .join("github")
        .join(format!("{handle}.yaml"));
    if local_path.is_file() || peer_path.is_file() {
        return Ok(());
    }
    Err(format!(
        "identity not found in local RHO_HOME: {identity_id}; run `rho id init --github {handle}` or `rho id import <file>` first"
    )
    .into())
}

fn switch_branch(root: &Path, branch: &str) -> RhoResult<()> {
    let status = Command::new("git")
        .arg("-C")
        .arg(root)
        .args(["switch", "-C", branch])
        .status()?;
    if !status.success() {
        return Err(format!("git switch failed for branch {branch}").into());
    }
    Ok(())
}

fn add_member_if_missing(root: &Path, identity_id: &str) -> RhoResult<()> {
    let membership_path = root.join("rho/membership.yaml");
    let text = fs::read_to_string(&membership_path)?;
    if let Ok(membership) = from_yaml::<MembershipManifest>(&text)
        && membership
            .members
            .iter()
            .any(|member| member.identity == identity_id)
    {
        return Ok(());
    }

    let repo_path = root.join("rho/repo.yaml");
    let repo: RepoManifest = from_yaml(&fs::read_to_string(&repo_path)?)?;
    let role = if repo.repo.owner.as_deref() == Some(identity_id) {
        "owner"
    } else {
        "collaborator"
    };
    let mut updated = text;
    if !updated.ends_with('\n') {
        updated.push('\n');
    }
    updated.push_str(&format!(
        "  - identity: {}\n    role: {role}\n",
        yaml_quote(identity_id)
    ));
    fs::write(&membership_path, updated)?;
    println!("{}", membership_path.display());
    Ok(())
}

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

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

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

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

#[derive(Debug, Deserialize)]
struct CryptoPolicyManifest {
    crypto: CryptoPolicy,
}

#[derive(Debug, Deserialize)]
struct CryptoPolicy {
    signatures: Option<String>,
    encryption: Option<String>,
    transparent_git_filters: Option<String>,
    filter: Option<String>,
}

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

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

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

#[derive(Debug, Clone, Default, Deserialize)]
struct WritePermission {
    #[serde(default)]
    allowed: Vec<String>,
}

#[derive(Debug, Deserialize)]
struct DatasetManifest {
    version: Option<u32>,
    dataset: Option<DatasetRecord>,
    uuid: Option<String>,
    variants: Option<serde_yaml::Value>,
}

#[derive(Debug, Deserialize)]
struct DatasetRecord {
    uuid: Option<String>,
    name: Option<String>,
    owner: Option<String>,
    variants: Option<DatasetVariants>,
}

#[derive(Debug, Deserialize)]
struct DatasetVariants {
    mock: Option<DatasetVariant>,
    real: Option<DatasetVariant>,
}

#[derive(Debug, Deserialize)]
struct DatasetVariant {
    relative_path: Option<String>,
    share_relative_path: Option<String>,
    sha256: Option<String>,
}

#[derive(Debug, Deserialize)]
struct RunReceiptManifest {
    run_receipt: RunReceiptRecord,
}

#[derive(Debug, Deserialize)]
struct RunReceiptRecord {
    request_id: String,
    run_id: String,
    runner: String,
    status: String,
    tier: String,
    code_path: String,
    code_sha256: String,
    #[serde(default)]
    input_path: Option<String>,
    input_sha256: String,
    output_sha256: String,
    result_path: String,
}

#[derive(Debug, Deserialize)]
struct ResultManifest {
    result: ResultRecord,
}

#[derive(Debug, Deserialize)]
struct ResultRecord {
    request_id: String,
    to: String,
    run_id: String,
}

#[derive(Debug, Deserialize)]
struct ApprovalGrantManifest {
    approval_grant: ApprovalGrantRecord,
}

#[derive(Debug, Deserialize)]
struct ApprovalGrantRecord {
    action_id: String,
    request_id: String,
    tool_id: String,
    action_type: String,
    decision: String,
    granted_by: String,
    action: ApprovalGrantFile,
    local_grant: ApprovalGrantFile,
    code: ApprovalGrantInput,
    private_input: ApprovalGrantPrivateInput,
}

#[derive(Debug, Deserialize)]
struct ApprovalGrantFile {
    path: String,
    sha256: String,
}

#[derive(Debug, Deserialize)]
struct ApprovalGrantInput {
    kind: String,
    path: String,
    sha256: String,
}

#[derive(Debug, Deserialize)]
struct ApprovalGrantPrivateInput {
    dataset_uuid: String,
    tier: String,
    sha256: String,
}

fn doctor(args: &[String]) -> RhoResult<()> {
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    ensure_repo(&root)?;
    let mut failures = Vec::new();

    let repo_path = root.join("rho/repo.yaml");
    let repo: Option<RepoManifest> = read_yaml_check(&repo_path, &mut failures);
    if let Some(repo) = &repo {
        if let Err(error) = validate_repo_id(&repo.repo.id) {
            failures.push(format!(
                "invalid repo id in {}: {error}",
                repo_path.display()
            ));
        }
        match repo.repo.owner.as_deref() {
            Some(owner) => {
                if let Err(error) = validate_actor_id(owner) {
                    failures.push(format!(
                        "invalid repo owner in {}: {error}",
                        repo_path.display()
                    ));
                }
            }
            None => failures.push(format!("missing repo.owner in {}", repo_path.display())),
        }
    }

    require_file(&root.join("rho.yaml"), &mut failures);
    require_file(&root.join("rho/policy/auth.yaml"), &mut failures);
    require_file(&root.join("rho/policy/permissions.yaml"), &mut failures);
    require_file(&root.join("rho/policy/crypto.yaml"), &mut failures);
    require_file(&root.join("rho/resources.yaml"), &mut failures);
    require_file(&root.join("rho/policy/revocations.yaml"), &mut failures);
    require_file(&root.join(".gitattributes"), &mut failures);

    let membership_path = root.join("rho/membership.yaml");
    let membership: Option<MembershipManifest> = read_yaml_check(&membership_path, &mut failures);
    let mut member_ids = Vec::new();
    if let Some(membership) = &membership {
        if membership.members.is_empty() {
            failures.push(format!(
                "membership has no members: {}",
                membership_path.display()
            ));
        }
        for member in &membership.members {
            if let Err(error) = validate_actor_id(&member.identity) {
                failures.push(format!(
                    "invalid member identity {}: {error}",
                    member.identity
                ));
            }
            if member.role != "owner" && member.role != "collaborator" {
                failures.push(format!(
                    "unsupported role for {}: {}",
                    member.identity, member.role
                ));
            }
            member_ids.push(member.identity.clone());
            check_participant(&root, &member.identity, &mut failures);
        }
        if let Some(owner) = repo.as_ref().and_then(|repo| repo.repo.owner.as_ref())
            && !membership
                .members
                .iter()
                .any(|member| member.identity == *owner && member.role == "owner")
        {
            failures.push(format!(
                "repo owner is not listed as owner in membership: {owner}"
            ));
        }
    }

    let crypto_path = root.join("rho/policy/crypto.yaml");
    let crypto: Option<CryptoPolicyManifest> = read_yaml_check(&crypto_path, &mut failures);
    if let Some(crypto) = &crypto {
        check_crypto_policy(&crypto.crypto, &root, &mut failures);
    }

    let permissions_path = root.join("rho/policy/permissions.yaml");
    let permissions: Option<PermissionsManifest> =
        read_yaml_check(&permissions_path, &mut failures);
    if let Some(permissions) = permissions {
        check_permissions_policy(&root, &permissions, &member_ids, &mut failures)?;
    }
    check_dataset_manifests(&root, &mut failures)?;
    if has_flag(args, "--flow") {
        if let Some(repo) = repo.as_ref() {
            if let Some(owner) = repo.repo.owner.as_deref() {
                check_flow_receipts(&root, &repo.repo.id, owner, &mut failures)?;
            } else {
                failures.push("flow audit requires repo.owner".to_string());
            }
        } else {
            failures.push("flow audit requires repo manifest".to_string());
        }
    }
    if has_flag(args, "--governance") {
        if let Some(owner) = repo.as_ref().and_then(|repo| repo.repo.owner.as_deref()) {
            check_governance_signatures(&root, owner, &mut failures)?;
        } else {
            failures.push("governance signatures require repo.owner".to_string());
        }
    }

    if failures.is_empty() {
        println!("rho repo doctor passed");
        println!("{}", root.join("rho/repo.yaml").display());
        println!("{}", root.join("rho/membership.yaml").display());
        println!("{}", root.join("rho/policy/permissions.yaml").display());
        println!("{}", root.join("rho/policy/crypto.yaml").display());
        if has_flag(args, "--flow") {
            println!("flow: passed");
        }
        if has_flag(args, "--governance") {
            println!("governance: passed");
        }
        return Ok(());
    }

    eprintln!("rho repo doctor failed");
    for failure in &failures {
        eprintln!("- {failure}");
    }
    Err(format!("{} repo health check(s) failed", failures.len()).into())
}

fn read_yaml_check<T: for<'de> Deserialize<'de>>(
    path: &Path,
    failures: &mut Vec<String>,
) -> Option<T> {
    match fs::read_to_string(path) {
        Ok(text) => match from_yaml(&text) {
            Ok(value) => Some(value),
            Err(error) => {
                failures.push(format!("invalid YAML in {}: {error}", path.display()));
                None
            }
        },
        Err(_) => {
            failures.push(format!("missing required file: {}", path.display()));
            None
        }
    }
}

fn require_file(path: &Path, failures: &mut Vec<String>) {
    if !path.is_file() {
        failures.push(format!("missing required file: {}", path.display()));
    }
}

fn check_participant(root: &Path, identity_id: &str, failures: &mut Vec<String>) {
    let Ok(handle) = github_handle_from_identity_id(identity_id) else {
        return;
    };
    let path = root.join("rho/participants").join(format!("{handle}.yaml"));
    let Some(bundle): Option<IdentityBundleManifest> = read_yaml_check(&path, failures) else {
        return;
    };
    if bundle.identity.id != identity_id {
        failures.push(format!(
            "participant id mismatch in {}: expected {}, got {}",
            path.display(),
            identity_id,
            bundle.identity.id
        ));
    }
    if !bundle
        .identity
        .public_keys
        .iter()
        .any(|key| key.kind == "ssh-signing")
    {
        failures.push(format!(
            "participant has no signing key: {}",
            path.display()
        ));
    }
    if !bundle
        .identity
        .public_keys
        .iter()
        .any(|key| key.kind == "x25519-encryption" && key.algorithm == "x25519")
    {
        failures.push(format!(
            "participant has no X25519 encryption key: {}",
            path.display()
        ));
    }
}

fn check_crypto_policy(crypto: &CryptoPolicy, root: &Path, failures: &mut Vec<String>) {
    if crypto.signatures.as_deref() != Some("ssh") {
        failures.push("crypto.signatures must be ssh".to_string());
    }
    if crypto.encryption.as_deref() != Some("x25519-recipient-envelope") {
        failures.push("crypto.encryption must be x25519-recipient-envelope".to_string());
    }
    let attributes = fs::read_to_string(root.join(".gitattributes")).unwrap_or_default();
    let has_rho_filter = attributes
        .lines()
        .any(|line| line.contains("filter=rho-crypt"));
    if has_rho_filter && crypto.transparent_git_filters.as_deref() == Some("disabled") {
        failures.push(
            "crypto.transparent_git_filters is disabled but .gitattributes uses filter=rho-crypt"
                .to_string(),
        );
    }
    if has_rho_filter && crypto.filter.as_deref() != Some("rho-crypt") {
        failures
            .push("crypto.filter must be rho-crypt when protected filters are used".to_string());
    }
}

fn check_permissions_policy(
    root: &Path,
    permissions: &PermissionsManifest,
    member_ids: &[String],
    failures: &mut Vec<String>,
) -> RhoResult<()> {
    let attributes = fs::read_to_string(root.join(".gitattributes")).unwrap_or_default();
    for rule in &permissions.permissions {
        if rule.read.public && !rule.read.encrypted_to.is_empty() {
            failures.push(format!(
                "permission cannot be both public and encrypted: {}",
                rule.pattern
            ));
        }
        for recipient in &rule.read.encrypted_to {
            if !member_ids.is_empty() && !member_ids.iter().any(|member| member == recipient) {
                failures.push(format!(
                    "permission encrypted reader is not a member: {} -> {}",
                    rule.pattern, recipient
                ));
            }
            if let Err(error) = validate_actor_id(recipient) {
                failures.push(format!(
                    "invalid permission encrypted reader {recipient}: {error}"
                ));
            }
        }
        if !rule.read.encrypted_to.is_empty() {
            let expected_attr = format!("{} filter=rho-crypt", rule.pattern);
            if !attributes
                .lines()
                .any(|line| line.starts_with(&expected_attr))
            {
                failures.push(format!(
                    ".gitattributes is missing rho-crypt filter for {}",
                    rule.pattern
                ));
            }
        }
    }

    let files = tracked_and_untracked_files(root)?;
    for file in files {
        for rule in &permissions.permissions {
            if path_matches_pattern(&file, &rule.pattern) {
                check_permission_file(root, &file, rule, failures);
            }
        }
    }
    Ok(())
}

fn check_dataset_manifests(root: &Path, failures: &mut Vec<String>) -> RhoResult<()> {
    for file in tracked_and_untracked_files(root)? {
        if file == "dataset.yaml" || file.ends_with("/dataset.yaml") {
            check_dataset_manifest(root, &file, failures);
        }
    }
    Ok(())
}

fn check_dataset_manifest(root: &Path, relative_path: &str, failures: &mut Vec<String>) {
    let path = root.join(relative_path);
    let text = match fs::read_to_string(&path) {
        Ok(text) => text,
        Err(error) => {
            failures.push(format!(
                "cannot read dataset manifest {relative_path}: {error}"
            ));
            return;
        }
    };
    let manifest: DatasetManifest = match from_yaml(&text) {
        Ok(manifest) => manifest,
        Err(error) => {
            failures.push(format!("invalid dataset manifest {relative_path}: {error}"));
            return;
        }
    };
    if manifest.version != Some(1) {
        failures.push(format!(
            "dataset manifest version must be 1: {relative_path}"
        ));
    }
    if manifest.uuid.is_some() || manifest.variants.is_some() {
        failures.push(format!(
            "dataset manifest uses deprecated top-level fields; expected dataset.uuid and dataset.variants: {relative_path}"
        ));
    }
    let Some(dataset) = manifest.dataset else {
        failures.push(format!("missing dataset section in {relative_path}"));
        return;
    };
    let Some(uuid) = non_empty(dataset.uuid.as_deref()) else {
        failures.push(format!("missing dataset.uuid in {relative_path}"));
        return;
    };
    if non_empty(dataset.name.as_deref()).is_none() {
        failures.push(format!("missing dataset.name in {relative_path}"));
    }
    if let Some(owner) = non_empty(dataset.owner.as_deref()) {
        if let Err(error) = validate_actor_id(owner) {
            failures.push(format!(
                "invalid dataset.owner in {relative_path}: {owner}: {error}"
            ));
        }
    } else {
        failures.push(format!("missing dataset.owner in {relative_path}"));
    }
    let Some(variants) = dataset.variants else {
        failures.push(format!("missing dataset.variants in {relative_path}"));
        return;
    };
    let Some(mock) = variants.mock else {
        failures.push(format!("missing dataset.variants.mock in {relative_path}"));
        return;
    };
    if non_empty(mock.sha256.as_deref()).is_none() {
        failures.push(format!(
            "missing dataset.variants.mock.sha256 in {relative_path}"
        ));
    }
    match non_empty(
        mock.relative_path
            .as_deref()
            .or(mock.share_relative_path.as_deref()),
    ) {
        Some(mock_path) => check_dataset_relative_path(root, relative_path, mock_path, failures),
        None => failures.push(format!(
            "missing dataset.variants.mock.relative_path in {relative_path}"
        )),
    }
    let Some(real) = variants.real else {
        failures.push(format!("missing dataset.variants.real in {relative_path}"));
        return;
    };
    if let Some(real_path) = non_empty(real.relative_path.as_deref()) {
        check_dataset_relative_path(root, relative_path, real_path, failures);
        if non_empty(real.sha256.as_deref()).is_none() {
            failures.push(format!(
                "missing dataset.variants.real.sha256 in {relative_path}"
            ));
        }
    }
    if !relative_path.contains(uuid) {
        failures.push(format!(
            "dataset.uuid {uuid} is not reflected in manifest path {relative_path}"
        ));
    }
}

fn check_dataset_relative_path(
    root: &Path,
    manifest_path: &str,
    relative_payload: &str,
    failures: &mut Vec<String>,
) {
    let payload = Path::new(relative_payload);
    if payload.is_absolute()
        || payload.components().any(|component| {
            matches!(
                component,
                Component::ParentDir | Component::RootDir | Component::Prefix(_)
            )
        })
    {
        failures.push(format!(
            "unsafe dataset payload path in {manifest_path}: {relative_payload}"
        ));
        return;
    }
    let manifest_dir = root
        .join(manifest_path)
        .parent()
        .map(Path::to_path_buf)
        .unwrap_or_else(|| root.to_path_buf());
    if !manifest_dir.join(payload).is_file() {
        failures.push(format!(
            "dataset payload missing for {manifest_path}: {relative_payload}"
        ));
    }
}

fn non_empty(value: Option<&str>) -> Option<&str> {
    value.map(str::trim).filter(|value| !value.is_empty())
}

fn check_flow_receipts(
    root: &Path,
    repo_id: &str,
    owner: &str,
    failures: &mut Vec<String>,
) -> RhoResult<()> {
    let receipt_files: Vec<String> = tracked_and_untracked_files(root)?
        .into_iter()
        .filter(|file| {
            file.starts_with("rho/run-receipts/")
                && file.ends_with(".yaml")
                && !file.ends_with(".rhosig.yaml")
        })
        .collect();
    if receipt_files.is_empty() {
        failures.push("flow has no committed run receipts under rho/run-receipts".to_string());
        return Ok(());
    }
    for receipt_file in receipt_files {
        check_flow_receipt(root, repo_id, owner, &receipt_file, failures);
    }
    Ok(())
}

fn check_flow_receipt(
    root: &Path,
    repo_id: &str,
    owner: &str,
    receipt_file: &str,
    failures: &mut Vec<String>,
) {
    let receipt_path = root.join(receipt_file);
    let receipt: RunReceiptManifest = match read_yaml_check(&receipt_path, failures) {
        Some(receipt) => receipt,
        None => return,
    };
    let record = receipt.run_receipt;
    if let Err(error) = validate_request_id(&record.request_id) {
        failures.push(format!(
            "invalid run receipt request_id in {receipt_file}: {error}"
        ));
    }
    for (label, value) in [
        ("run_id", record.run_id.as_str()),
        ("runner", record.runner.as_str()),
        ("status", record.status.as_str()),
        ("tier", record.tier.as_str()),
        ("code_path", record.code_path.as_str()),
        ("code_sha256", record.code_sha256.as_str()),
        ("output_sha256", record.output_sha256.as_str()),
        ("result_path", record.result_path.as_str()),
    ] {
        if value.trim().is_empty() {
            failures.push(format!("missing run receipt {label} in {receipt_file}"));
        }
    }
    if record.tier == "real" && record.input_sha256.trim().is_empty() {
        failures.push(format!(
            "missing real input commitment in run receipt {receipt_file}"
        ));
    }
    if let Some(input_path) = non_empty(record.input_path.as_deref())
        && Path::new(input_path).is_absolute()
    {
        failures.push(format!(
            "run receipt leaks absolute input_path in {receipt_file}: {input_path}"
        ));
    }
    check_flow_approval_grant(root, owner, &record, receipt_file, failures);
    if !repo_request_exists(root, &record.request_id) {
        failures.push(format!(
            "run receipt references missing request {}: {receipt_file}",
            record.request_id
        ));
    }
    let Some(result_path) = safe_repo_path(&record.result_path) else {
        failures.push(format!(
            "unsafe run receipt result_path in {receipt_file}: {}",
            record.result_path
        ));
        return;
    };
    let result_path = root.join(result_path);
    if !result_path.is_file() {
        failures.push(format!(
            "run receipt result_path does not exist in {receipt_file}: {}",
            record.result_path
        ));
    }
    let result = if result_path.is_file() {
        read_result_for_flow(&result_path, &record, failures)
    } else {
        None
    };
    let result_signature = result_path.with_file_name("result.rhosig.yaml");
    if !result_signature.is_file() {
        failures.push(format!(
            "run receipt result signature missing for {}",
            record.result_path
        ));
    }
    let receipt_signature = receipt_path.with_file_name(format!("{}.rhosig.yaml", record.run_id));
    if !receipt_signature.is_file() {
        failures.push(format!("run receipt signature missing for {receipt_file}"));
    }
    if receipt_signature.is_file() {
        if let Some(result) = &result {
            let context =
                flow_signature_context(repo_id, &record.request_id, &result.to, "rho.run_receipt");
            verify_repo_signature_with_context(
                root,
                &receipt_path,
                &receipt_signature,
                Some(&context),
                failures,
            );
        } else {
            verify_repo_signature(root, &receipt_path, &receipt_signature, failures);
        }
    }
    if result_path.is_file() && result_signature.is_file() {
        if let Some(result) = &result {
            let context =
                flow_signature_context(repo_id, &record.request_id, &result.to, "rho.result");
            verify_repo_signature_with_context(
                root,
                &result_path,
                &result_signature,
                Some(&context),
                failures,
            );
        } else {
            verify_repo_signature(root, &result_path, &result_signature, failures);
        }
    }
}

fn read_result_for_flow(
    result_path: &Path,
    receipt: &RunReceiptRecord,
    failures: &mut Vec<String>,
) -> Option<ResultRecord> {
    let result: ResultManifest = read_yaml_check(result_path, failures)?;
    let result = result.result;
    if result.request_id != receipt.request_id {
        failures.push(format!(
            "result request_id mismatch for {}: expected {}, got {}",
            result_path.display(),
            receipt.request_id,
            result.request_id
        ));
    }
    if result.run_id != receipt.run_id {
        failures.push(format!(
            "result run_id mismatch for {}: expected {}, got {}",
            result_path.display(),
            receipt.run_id,
            result.run_id
        ));
    }
    if let Err(error) = validate_actor_id(&result.to) {
        failures.push(format!(
            "invalid result recipient in {}: {error}",
            result_path.display()
        ));
    }
    Some(result)
}

fn flow_signature_context(
    repo_id: &str,
    request_id: &str,
    recipient_id: &str,
    purpose: &str,
) -> SignatureContext {
    SignatureContext {
        repo_id: Some(repo_id.to_string()),
        request_id: Some(request_id.to_string()),
        recipient_id: Some(recipient_id.to_string()),
        purpose: Some(purpose.to_string()),
    }
}

fn check_flow_approval_grant(
    root: &Path,
    owner: &str,
    receipt: &RunReceiptRecord,
    receipt_file: &str,
    failures: &mut Vec<String>,
) {
    let Some(action_id) = action_id_for_run_id(&receipt.run_id) else {
        failures.push(format!(
            "run receipt has non-conventional run_id with no approval grant mapping in {receipt_file}: {}",
            receipt.run_id
        ));
        return;
    };
    let relative_grant = format!(
        "rho/approval-grants/{}/{}.yaml",
        receipt.request_id, action_id
    );
    let grant_path = root.join(&relative_grant);
    if !grant_path.is_file() {
        failures.push(format!(
            "approval grant missing for run receipt {receipt_file}: {relative_grant}"
        ));
        return;
    }
    let signature_path = grant_path.with_file_name(format!("{action_id}.rhosig.yaml"));
    if !signature_path.is_file() {
        failures.push(format!(
            "approval grant signature missing for {relative_grant}"
        ));
    } else {
        match signature_signer(&signature_path) {
            Ok(signer) if signer == owner => {}
            Ok(signer) => failures.push(format!(
                "approval grant signer mismatch for {relative_grant}: expected {owner}, got {signer}"
            )),
            Err(error) => failures.push(format!(
                "invalid approval grant signature {}: {error}",
                signature_path.display()
            )),
        }
        verify_repo_signature(root, &grant_path, &signature_path, failures);
    }

    let grant: ApprovalGrantManifest = match read_yaml_check(&grant_path, failures) {
        Some(grant) => grant,
        None => return,
    };
    let grant = grant.approval_grant;
    if grant.action_id != action_id {
        failures.push(format!(
            "approval grant action_id mismatch for {relative_grant}: expected {action_id}, got {}",
            grant.action_id
        ));
    }
    if grant.request_id != receipt.request_id {
        failures.push(format!(
            "approval grant request_id mismatch for {relative_grant}: expected {}, got {}",
            receipt.request_id, grant.request_id
        ));
    }
    if grant.decision != "approved" {
        failures.push(format!(
            "approval grant decision is not approved for {relative_grant}: {}",
            grant.decision
        ));
    }
    if grant.granted_by != owner {
        failures.push(format!(
            "approval grant granted_by mismatch for {relative_grant}: expected {owner}, got {}",
            grant.granted_by
        ));
    }
    for (label, value) in [
        ("tool_id", grant.tool_id.as_str()),
        ("action_type", grant.action_type.as_str()),
        ("action.sha256", grant.action.sha256.as_str()),
        ("local_grant.sha256", grant.local_grant.sha256.as_str()),
        ("code.path", grant.code.path.as_str()),
        (
            "private_input.dataset_uuid",
            grant.private_input.dataset_uuid.as_str(),
        ),
    ] {
        if value.trim().is_empty() {
            failures.push(format!(
                "missing approval grant {label} in {relative_grant}"
            ));
        }
    }
    let expected_local_grant = format!(".rho/action-grants/{action_id}.yaml");
    if grant.local_grant.path != expected_local_grant {
        failures.push(format!(
            "approval grant local_grant path mismatch for {relative_grant}: expected {expected_local_grant}, got {}",
            grant.local_grant.path
        ));
    }
    if grant.code.kind != "code" {
        failures.push(format!(
            "approval grant code input kind mismatch for {relative_grant}: {}",
            grant.code.kind
        ));
    }
    if grant.code.sha256 != receipt.code_sha256 {
        failures.push(format!(
            "approval grant code digest mismatch for {relative_grant}: expected {}, got {}",
            receipt.code_sha256, grant.code.sha256
        ));
    }
    if grant.private_input.tier != receipt.tier {
        failures.push(format!(
            "approval grant private input tier mismatch for {relative_grant}: expected {}, got {}",
            receipt.tier, grant.private_input.tier
        ));
    }
    if grant.private_input.sha256 != receipt.input_sha256 {
        failures.push(format!(
            "approval grant private input digest mismatch for {relative_grant}: expected {}, got {}",
            receipt.input_sha256, grant.private_input.sha256
        ));
    }
    if let Some(action_path) = safe_repo_path(&grant.action.path)
        && root.join(&action_path).is_file()
        && let Ok(digest) = file_digest(&root.join(action_path))
        && digest != grant.action.sha256
    {
        failures.push(format!(
            "approval grant action digest mismatch for {relative_grant}: expected {}, got {digest}",
            grant.action.sha256
        ));
    }
}

fn action_id_for_run_id(run_id: &str) -> Option<String> {
    run_id
        .strip_prefix("run-")
        .map(|suffix| format!("act-{suffix}"))
}

fn verify_repo_signature(
    root: &Path,
    signed_path: &Path,
    signature_path: &Path,
    failures: &mut Vec<String>,
) {
    verify_repo_signature_with_context(root, signed_path, signature_path, None, failures);
}

fn verify_repo_signature_with_context(
    root: &Path,
    signed_path: &Path,
    signature_path: &Path,
    context: Option<&SignatureContext>,
    failures: &mut Vec<String>,
) {
    let text = match fs::read_to_string(signature_path) {
        Ok(text) => text,
        Err(error) => {
            failures.push(format!(
                "cannot read signature {}: {error}",
                signature_path.display()
            ));
            return;
        }
    };
    let manifest: SignatureManifest = match from_yaml(&text) {
        Ok(manifest) => manifest,
        Err(error) => {
            failures.push(format!(
                "invalid signature manifest {}: {error}",
                signature_path.display()
            ));
            return;
        }
    };
    let mut rho_crypto = rho_crypto_command();
    rho_crypto
        .arg("verify")
        .arg(signed_path)
        .arg("--signature")
        .arg(signature_path)
        .arg("--identity")
        .arg(&manifest.signature.signer)
        .arg("--repo-root")
        .arg(root);
    if let Some(context) = context {
        rho_crypto
            .arg("--repo-id")
            .arg(context.repo_id.as_deref().unwrap_or(""))
            .arg("--request-id")
            .arg(context.request_id.as_deref().unwrap_or(""))
            .arg("--recipient")
            .arg(context.recipient_id.as_deref().unwrap_or(""))
            .arg("--purpose")
            .arg(context.purpose.as_deref().unwrap_or(""));
    }
    let output = rho_crypto.output();
    match output {
        Ok(output) if output.status.success() => {}
        Ok(output) => {
            let stderr = String::from_utf8_lossy(&output.stderr);
            failures.push(format!(
                "signature verification failed for {}: {}",
                signed_path.display(),
                stderr.trim()
            ));
        }
        Err(error) => failures.push(format!(
            "failed to run rho crypto verify for {}: {error}",
            signed_path.display()
        )),
    }
}

fn check_governance_signatures(
    root: &Path,
    owner: &str,
    failures: &mut Vec<String>,
) -> RhoResult<()> {
    let files = governance_manifest_files(root)?;
    if files.is_empty() {
        failures.push("governance has no manifest files to verify".to_string());
        return Ok(());
    }
    for file in files {
        let signed_path = root.join(&file);
        let signature_path = governance_signature_path(root, &file)?;
        if !signature_path.is_file() {
            failures.push(format!(
                "governance signature missing for {file}: {}",
                repo_relative_display(root, &signature_path)
            ));
            continue;
        }
        match signature_signer(&signature_path) {
            Ok(signer) if signer == owner => {}
            Ok(signer) => failures.push(format!(
                "governance signature signer mismatch for {file}: expected {owner}, got {signer}"
            )),
            Err(error) => failures.push(format!(
                "invalid governance signature {}: {error}",
                signature_path.display()
            )),
        }
        verify_repo_signature(root, &signed_path, &signature_path, failures);
    }
    Ok(())
}

fn governance_manifest_files(root: &Path) -> RhoResult<Vec<String>> {
    let mut files: Vec<String> = tracked_and_untracked_files(root)?
        .into_iter()
        .filter(|file| is_governance_manifest(file))
        .collect();
    files.sort();
    files.dedup();
    Ok(files)
}

fn is_governance_manifest(file: &str) -> bool {
    if !file.ends_with(".yaml") || file.ends_with(".rhosig.yaml") {
        return false;
    }
    matches!(
        file,
        "rho.yaml" | "rho/repo.yaml" | "rho/membership.yaml" | "rho/resources.yaml"
    ) || (file.starts_with("rho/policy/") && !file["rho/policy/".len()..].contains('/'))
        || (file.starts_with("rho/participants/")
            && !file["rho/participants/".len()..].contains('/'))
        || (file.starts_with("rho/tools/") && !file["rho/tools/".len()..].contains('/'))
        || file == "dataset.yaml"
        || file.ends_with("/dataset.yaml")
}

fn governance_signature_path(root: &Path, relative_file: &str) -> RhoResult<PathBuf> {
    let path = root.join(relative_file);
    let Some(file_name) = path.file_name().and_then(|value| value.to_str()) else {
        return Err(format!("invalid governance path: {relative_file}").into());
    };
    let signature_name = if let Some(stem) = file_name.strip_suffix(".yaml") {
        format!("{stem}.rhosig.yaml")
    } else {
        format!("{file_name}.rhosig.yaml")
    };
    Ok(path.with_file_name(signature_name))
}

fn signature_signer(signature_path: &Path) -> RhoResult<String> {
    let text = fs::read_to_string(signature_path)?;
    let manifest: SignatureManifest = from_yaml(&text)?;
    Ok(manifest.signature.signer)
}

fn sign_file(signed_path: &Path, signature_path: &Path, identity: &str) -> RhoResult<()> {
    let output = rho_crypto_command()
        .arg("sign")
        .arg(signed_path)
        .arg("--identity")
        .arg(identity)
        .arg("--out")
        .arg(signature_path)
        .output()?;
    if output.status.success() {
        return Ok(());
    }
    let stderr = String::from_utf8_lossy(&output.stderr);
    Err(format!(
        "failed to sign {}: {}",
        signed_path.display(),
        stderr.trim()
    )
    .into())
}

fn repo_relative_display(root: &Path, path: &Path) -> String {
    path.strip_prefix(root)
        .map(|path| path.display().to_string())
        .unwrap_or_else(|_| path.display().to_string())
}

fn rho_crypto_command() -> Command {
    let rho = env::current_exe()
        .ok()
        .map(|path| path.with_file_name("rho"))
        .unwrap_or_else(|| PathBuf::from("rho"));
    let mut command = if rho.is_file() {
        Command::new(rho)
    } else {
        let mut command = Command::new("cargo");
        command.args(["run", "--quiet", "--bin", "rho", "--"]);
        command
    };
    command.arg("crypto");
    command
}

fn repo_request_exists(root: &Path, request_id: &str) -> bool {
    let inbox = root.join("rho/messages/inbox");
    request_exists_under(&inbox, request_id)
}

fn request_exists_under(dir: &Path, request_id: &str) -> bool {
    let Ok(entries) = fs::read_dir(dir) else {
        return false;
    };
    for entry in entries.flatten() {
        let path = entry.path();
        if path.is_dir() {
            if path.file_name().and_then(|value| value.to_str()) == Some(request_id)
                && path.join("request.yaml").is_file()
            {
                return true;
            }
            if request_exists_under(&path, request_id) {
                return true;
            }
        }
    }
    false
}

fn safe_repo_path(path: &str) -> Option<PathBuf> {
    let path = Path::new(path);
    if path.is_absolute() {
        return None;
    }
    let mut has_component = false;
    for component in path.components() {
        match component {
            Component::Normal(_) => has_component = true,
            Component::CurDir => {}
            Component::ParentDir | Component::RootDir | Component::Prefix(_) => return None,
        }
    }
    has_component.then(|| path.to_path_buf())
}

fn tracked_and_untracked_files(root: &Path) -> RhoResult<Vec<String>> {
    let output = Command::new("git")
        .arg("-C")
        .arg(root)
        .args([
            "ls-files",
            "--cached",
            "--others",
            "--exclude-standard",
            "-z",
        ])
        .output()?;
    if !output.status.success() {
        return Err("git ls-files failed".into());
    }
    Ok(output
        .stdout
        .split(|byte| *byte == 0)
        .filter(|entry| !entry.is_empty())
        .filter_map(|entry| String::from_utf8(entry.to_vec()).ok())
        .collect())
}

fn check_permission_file(
    root: &Path,
    relative_path: &str,
    rule: &PermissionRule,
    failures: &mut Vec<String>,
) {
    if rule.read.public {
        return;
    }
    let recipients = &rule.read.encrypted_to;
    let content = index_blob(root, relative_path)
        .or_else(|| fs::read_to_string(root.join(relative_path)).ok())
        .unwrap_or_default();
    if !content.contains("kind: rho_recipient_envelope")
        && !content.contains("kind: rho_transparent_file")
    {
        failures.push(format!(
            "private-read file is not encrypted: {relative_path}"
        ));
        return;
    }
    if !recipients.is_empty() && content.contains("kind: rho_transparent_file") {
        failures.push(format!(
            "protected file uses repo-key encryption but policy requires recipients: {relative_path}"
        ));
        return;
    }
    if content.contains("kind: rho_recipient_envelope") {
        for recipient in recipients {
            if !content.contains(&format!("identity_id: {recipient}"))
                && !content.contains(&format!("identity_id: \"{recipient}\""))
            {
                failures.push(format!(
                    "protected file is not encrypted to expected recipient {recipient}: {relative_path}"
                ));
            }
        }
    }
}

fn missing_recipients(content: &str, recipients: &[String]) -> Vec<String> {
    recipients
        .iter()
        .filter(|recipient| {
            !content.contains(&format!("identity_id: {recipient}"))
                && !content.contains(&format!("identity_id: \"{recipient}\""))
        })
        .cloned()
        .collect()
}

fn index_blob(root: &Path, relative_path: &str) -> Option<String> {
    let output = Command::new("git")
        .arg("-C")
        .arg(root)
        .args(["show", &format!(":{relative_path}")])
        .output()
        .ok()?;
    if !output.status.success() {
        return None;
    }
    String::from_utf8(output.stdout).ok()
}

fn head_blob(root: &Path, relative_path: &str) -> Option<String> {
    let output = Command::new("git")
        .arg("-C")
        .arg(root)
        .args(["show", &format!("HEAD:{relative_path}")])
        .output()
        .ok()?;
    if !output.status.success() {
        return None;
    }
    String::from_utf8(output.stdout).ok()
}

fn path_matches_pattern(path: &str, pattern: &str) -> bool {
    if let Some(prefix) = pattern.strip_suffix("/**") {
        return path == prefix || path.starts_with(&format!("{prefix}/"));
    }
    if let Some(prefix) = pattern.strip_suffix('*') {
        return path.starts_with(prefix);
    }
    path == pattern
}

fn github_handle_from_identity_id(identity_id: &str) -> RhoResult<String> {
    let Some(handle) = identity_id.strip_prefix("rho://id/github/") else {
        return Err(format!("unsupported identity id: {identity_id}").into());
    };
    if handle.is_empty() || handle.contains('/') {
        return Err(format!("unsupported identity id: {identity_id}").into());
    }
    Ok(handle.to_string())
}

fn active_identity() -> Option<String> {
    env::var("RHO_IDENTITY")
        .ok()
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
}

fn detect_github_remote(root: &Path) -> Option<String> {
    let output = Command::new("git")
        .arg("-C")
        .arg(root)
        .args(["remote", "get-url", "origin"])
        .output()
        .ok()?;
    if !output.status.success() {
        return None;
    }
    let remote = String::from_utf8_lossy(&output.stdout).trim().to_string();
    if remote.is_empty() {
        None
    } else {
        Some(providers::github::repo_candidate_from_remote(&remote).unwrap_or(remote))
    }
}

fn protect_path(args: &[String]) -> RhoResult<()> {
    let Some(pattern) = args.first().map(String::as_str) else {
        usage();
    };
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    let key_file = arg_value(args, "--key-file")
        .map(PathBuf::from)
        .unwrap_or_else(|| default_repo_key_file(&root));
    let rho_bin = arg_value(args, "--rho-bin").unwrap_or_else(default_rho_bin);
    let recipients = repeated_arg(args, "--recipient")
        .into_iter()
        .map(|recipient| normalize_actor_id(&recipient))
        .collect::<RhoResult<Vec<_>>>()?;

    ensure_repo(&root)?;
    if recipients.is_empty() {
        ensure_repo_key(&key_file)?;
    } else {
        for recipient in &recipients {
            validate_actor_id(recipient)?;
        }
    }
    if recipients.is_empty() {
        if let Some(base) = pattern.strip_suffix("/**") {
            remove_line_if_present(
                &root.join(".gitattributes"),
                &format!("{base} filter=rho-crypt diff=rho-crypt"),
            )?;
        }
        append_line_if_missing(
            &root.join(".gitattributes"),
            &format!("{pattern} filter=rho-crypt diff=rho-crypt"),
        )?;
        configure_git_filter(&root, &rho_bin, &key_file)?;
        write_if_missing_or_forced(
            &root.join("rho/policy/crypto.yaml"),
            "version: 1\ncrypto:\n  signatures: ssh\n  encrypted_envelopes: explicit\n  transparent_git_filters: enabled\n  filter: rho-crypt\n",
            true,
        )?;
    } else {
        configure_git_filter_for_policy(&root, &rho_bin)?;
        write_recipient_crypto_policy(&root)?;
        write_permissions_policy_rule(&root, pattern, &recipients)?;
        sync_gitattributes_from_permissions(&root)?;
    }
    install_pre_commit_hook(&root)?;

    if recipients.is_empty() {
        println!("{}", key_file.display());
    }
    println!("{}", root.join(".gitattributes").display());
    println!("{}", root.join("rho/policy/permissions.yaml").display());
    println!("{}", root.join(".git/hooks/pre-commit").display());
    Ok(())
}

fn install_filters(args: &[String]) -> RhoResult<()> {
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    let rho_bin = arg_value(args, "--rho-bin").unwrap_or_else(default_rho_bin);

    ensure_repo(&root)?;
    let permissions_path = root.join("rho/policy/permissions.yaml");
    let permissions: PermissionsManifest = from_yaml(&fs::read_to_string(&permissions_path)?)?;
    let installed = encrypted_permission_rules(&permissions).count();
    write_generated_gitattributes_block(&root, &permissions)?;
    configure_git_filter_for_policy(&root, &rho_bin)?;
    write_recipient_crypto_policy(&root)?;
    install_pre_commit_hook(&root)?;

    println!("{}", root.join(".gitattributes").display());
    println!("{}", root.join(".git/hooks/pre-commit").display());
    println!("installed encrypted path filters: {installed}");
    Ok(())
}

fn refresh_protected(args: &[String]) -> RhoResult<()> {
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    let rho_bin = arg_value(args, "--rho-bin").unwrap_or_else(default_rho_bin);

    ensure_repo(&root)?;
    let root_arg = root.display().to_string();
    install_filters(&[
        "--root".to_string(),
        root_arg,
        "--rho-bin".to_string(),
        rho_bin.clone(),
    ])?;

    let permissions_path = root.join("rho/policy/permissions.yaml");
    let permissions: PermissionsManifest = from_yaml(&fs::read_to_string(&permissions_path)?)?;
    let tracked = tracked_files(&root)?;
    let mut refreshed = 0usize;
    let mut skipped_dirty = 0usize;
    let mut unchanged = 0usize;

    for file in tracked {
        let Some(_rule) = permissions.permissions.iter().find(|rule| {
            !rule.read.public
                && !rule.read.encrypted_to.is_empty()
                && path_matches_pattern(&file, &rule.pattern)
        }) else {
            continue;
        };
        let path = root.join(&file);
        let Ok(text) = fs::read_to_string(&path) else {
            continue;
        };
        if !is_rho_encrypted_text(&text) {
            continue;
        }
        if path_has_worktree_changes(&root, &file)? {
            skipped_dirty += 1;
            println!("skipped dirty protected file: {file}");
            continue;
        }
        let plaintext = smudge_file_text(&root, &rho_bin, &file, &text)?;
        if plaintext == text || is_rho_encrypted_text(&plaintext) {
            unchanged += 1;
            continue;
        }
        fs::write(&path, plaintext)?;
        refreshed += 1;
        println!("refreshed protected file: {file}");
    }

    println!("refreshed protected files: {refreshed}");
    if skipped_dirty > 0 {
        println!("skipped dirty protected files: {skipped_dirty}");
    }
    if unchanged > 0 {
        println!("unchanged protected envelopes: {unchanged}");
    }
    Ok(())
}

fn tracked_files(root: &Path) -> RhoResult<Vec<String>> {
    let output = Command::new("git")
        .arg("-C")
        .arg(root)
        .args(["ls-files", "-z"])
        .output()?;
    if !output.status.success() {
        return Err("git ls-files failed".into());
    }
    Ok(output
        .stdout
        .split(|byte| *byte == 0)
        .filter(|entry| !entry.is_empty())
        .filter_map(|entry| String::from_utf8(entry.to_vec()).ok())
        .collect())
}

fn path_has_worktree_changes(root: &Path, relative_path: &str) -> RhoResult<bool> {
    let output = Command::new("git")
        .arg("-C")
        .arg(root)
        .args(["status", "--porcelain", "--"])
        .arg(relative_path)
        .output()?;
    if !output.status.success() {
        return Err(format!("git status failed for {relative_path}").into());
    }
    Ok(!output.stdout.is_empty())
}

fn smudge_file_text(
    root: &Path,
    rho_bin: &str,
    relative_path: &str,
    text: &str,
) -> RhoResult<String> {
    let mut child = Command::new(rho_bin)
        .args(["crypto", "smudge", "--repo-root"])
        .arg(root)
        .args(["--path", relative_path])
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()?;
    {
        let Some(stdin) = child.stdin.as_mut() else {
            return Err("failed to open rho crypto smudge stdin".into());
        };
        stdin.write_all(text.as_bytes())?;
    }
    let output = child.wait_with_output()?;
    if !output.status.success() {
        return Err(format!("rho crypto smudge failed for {relative_path}").into());
    }
    Ok(String::from_utf8(output.stdout)?)
}

fn is_rho_encrypted_text(text: &str) -> bool {
    text.contains("kind: rho_recipient_envelope") || text.contains("kind: rho_transparent_file")
}

fn ensure_repo(root: &Path) -> RhoResult<()> {
    if !root.join(".git").exists() {
        return Err(format!("not a Git working tree root: {}", root.display()).into());
    }
    if !root.join("rho/repo.yaml").is_file() {
        return Err(format!(
            "rho repo not initialized: {}",
            root.join("rho/repo.yaml").display()
        )
        .into());
    }
    Ok(())
}

fn default_repo_key_file(root: &Path) -> PathBuf {
    let repo_slug = fs::read_to_string(root.join("rho/repo.yaml"))
        .ok()
        .and_then(|text| {
            text.lines().find_map(|line| {
                line.trim()
                    .strip_prefix("id:")
                    .map(str::trim)
                    .map(ToOwned::to_owned)
            })
        })
        .unwrap_or_else(|| "rho-repo".to_string())
        .trim_matches('"')
        .replace("rho://repo/", "")
        .replace(['/', ':'], "-");
    rho_home()
        .join("repo-keys")
        .join(format!("{repo_slug}.key"))
}

fn default_rho_bin() -> String {
    if let Ok(exe) = env::current_exe()
        && let Some(parent) = exe.parent()
    {
        let sibling = parent.join("rho");
        if sibling.is_file() {
            return sibling.display().to_string();
        }
    }
    "rho".to_string()
}

fn rho_home() -> PathBuf {
    if let Ok(value) = env::var("RHO_HOME")
        && !value.is_empty()
    {
        return PathBuf::from(value);
    }
    env::var("HOME")
        .map(PathBuf::from)
        .unwrap_or_else(|_| PathBuf::from("."))
        .join(".rho")
}

fn ensure_repo_key(path: &Path) -> RhoResult<()> {
    if path.is_file() {
        return Ok(());
    }
    ensure_parent(path)?;
    let mut key = [0u8; 32];
    getrandom::getrandom(&mut key).map_err(|error| error.to_string())?;
    fs::write(path, format!("{}\n", BASE64.encode(key)))?;
    Ok(())
}

fn append_line_if_missing(path: &Path, line: &str) -> RhoResult<()> {
    let existing = fs::read_to_string(path).unwrap_or_default();
    if existing.lines().any(|existing_line| existing_line == line) {
        return Ok(());
    }
    ensure_parent(path)?;
    let mut file = fs::OpenOptions::new()
        .create(true)
        .append(true)
        .open(path)?;
    if !existing.is_empty() && !existing.ends_with('\n') {
        writeln!(file)?;
    }
    writeln!(file, "{line}")?;
    Ok(())
}

fn remove_line_if_present(path: &Path, line: &str) -> RhoResult<()> {
    let existing = fs::read_to_string(path).unwrap_or_default();
    if existing.is_empty() || !existing.lines().any(|existing_line| existing_line == line) {
        return Ok(());
    }
    let mut updated = String::new();
    for existing_line in existing.lines() {
        if existing_line != line {
            updated.push_str(existing_line);
            updated.push('\n');
        }
    }
    fs::write(path, updated)?;
    Ok(())
}

fn configure_git_filter(root: &Path, rho_bin: &str, key_file: &Path) -> RhoResult<()> {
    let clean = format!(
        "{} crypto clean --key-file {}",
        shell_quote(rho_bin),
        shell_quote(&key_file.display().to_string())
    );
    let smudge = format!(
        "{} crypto smudge --key-file {}",
        shell_quote(rho_bin),
        shell_quote(&key_file.display().to_string())
    );
    git_config(root, "filter.rho-crypt.clean", &clean)?;
    git_config(root, "filter.rho-crypt.smudge", &smudge)?;
    git_config(root, "filter.rho-crypt.required", "true")?;
    git_config(root, "diff.rho-crypt.textconv", &smudge)?;
    Ok(())
}

fn configure_git_filter_for_policy(root: &Path, rho_bin: &str) -> RhoResult<()> {
    let env_prefix = env::var("RHO_HOME")
        .ok()
        .filter(|value| !value.is_empty())
        .map(|value| format!("RHO_HOME={} ", shell_quote(&value)))
        .unwrap_or_default();
    let clean = format!(
        "{}{} crypto clean --repo-root {} --path %f",
        env_prefix,
        shell_quote(rho_bin),
        shell_quote(&root.display().to_string())
    );
    let smudge = format!(
        "{}{} crypto smudge --repo-root {} --path %f",
        env_prefix,
        shell_quote(rho_bin),
        shell_quote(&root.display().to_string())
    );
    git_config(root, "filter.rho-crypt.clean", &clean)?;
    git_config(root, "filter.rho-crypt.smudge", &smudge)?;
    git_config(root, "filter.rho-crypt.required", "true")?;
    git_config(root, "diff.rho-crypt.textconv", &smudge)?;
    Ok(())
}

fn write_recipient_crypto_policy(root: &Path) -> RhoResult<()> {
    write_if_missing_or_forced(
        &root.join("rho/policy/crypto.yaml"),
        "version: 1\ncrypto:\n  signatures: ssh\n  encryption: x25519-recipient-envelope\n  transparent_git_filters: enabled\n  filter: rho-crypt\n",
        true,
    )
}

const GENERATED_ATTRIBUTES_START: &str = "# >>> rho generated encrypted path filters >>>";
const GENERATED_ATTRIBUTES_END: &str = "# <<< rho generated encrypted path filters <<<";

fn encrypted_permission_rules(
    permissions: &PermissionsManifest,
) -> impl Iterator<Item = &PermissionRule> {
    permissions
        .permissions
        .iter()
        .filter(|rule| !rule.read.encrypted_to.is_empty())
}

fn generated_gitattributes_block(permissions: &PermissionsManifest) -> String {
    let mut rules = encrypted_permission_rules(permissions).collect::<Vec<_>>();
    rules.sort_by(|left, right| left.pattern.cmp(&right.pattern));
    rules.dedup_by(|left, right| left.pattern == right.pattern);
    if rules.is_empty() {
        return String::new();
    }

    let mut text = String::new();
    text.push_str(GENERATED_ATTRIBUTES_START);
    text.push('\n');
    text.push_str("# Generated from rho/policy/permissions.yaml; run `rho repo install-filters` after policy edits.\n");
    for rule in rules {
        text.push_str(&format!(
            "{} filter=rho-crypt diff=rho-crypt\n",
            rule.pattern
        ));
    }
    text.push_str(GENERATED_ATTRIBUTES_END);
    text.push('\n');
    text
}

fn is_legacy_generated_attribute(line: &str, patterns: &[&str]) -> bool {
    if !line.contains("filter=rho-crypt") {
        return false;
    }
    let Some(pattern) = line.split_whitespace().next() else {
        return false;
    };
    patterns.contains(&pattern)
}

fn write_generated_gitattributes_block(
    root: &Path,
    permissions: &PermissionsManifest,
) -> RhoResult<()> {
    let attributes_path = root.join(".gitattributes");
    let existing = fs::read_to_string(&attributes_path).unwrap_or_default();
    let patterns = encrypted_permission_rules(permissions)
        .map(|rule| rule.pattern.as_str())
        .collect::<Vec<_>>();
    let mut lines = Vec::new();
    let mut in_generated_block = false;
    for line in existing.lines() {
        let trimmed = line.trim();
        if trimmed == GENERATED_ATTRIBUTES_START {
            in_generated_block = true;
            continue;
        }
        if in_generated_block {
            if trimmed == GENERATED_ATTRIBUTES_END {
                in_generated_block = false;
            }
            continue;
        }
        if is_legacy_generated_attribute(line, &patterns) {
            continue;
        }
        lines.push(line.to_string());
    }
    while lines.last().is_some_and(|line| line.trim().is_empty()) {
        lines.pop();
    }

    let mut updated = lines.join("\n");
    if !updated.is_empty() {
        updated.push('\n');
    }
    let block = generated_gitattributes_block(permissions);
    if !block.is_empty() {
        if !updated.is_empty() {
            updated.push('\n');
        }
        updated.push_str(&block);
    }
    fs::write(attributes_path, updated)?;
    Ok(())
}

fn sync_gitattributes_from_permissions(root: &Path) -> RhoResult<()> {
    let permissions_path = root.join("rho/policy/permissions.yaml");
    let permissions: PermissionsManifest = from_yaml(&fs::read_to_string(&permissions_path)?)?;
    write_generated_gitattributes_block(root, &permissions)
}

fn write_permissions_policy_rule(
    root: &Path,
    pattern: &str,
    recipients: &[String],
) -> RhoResult<()> {
    let permissions_path = root.join("rho/policy/permissions.yaml");
    let existing: Option<PermissionsManifest> = read_yaml_check(&permissions_path, &mut Vec::new());
    let mut rules = existing
        .map(|manifest| manifest.permissions)
        .unwrap_or_default();
    if let Some(base) = pattern.strip_suffix("/**") {
        rules.retain(|rule| rule.pattern != base);
    }
    if let Some(rule) = rules.iter_mut().find(|rule| rule.pattern == pattern) {
        rule.read.public = false;
        rule.read.encrypted_to = recipients.to_vec();
        if rule.write.allowed.is_empty() {
            rule.write.allowed = vec!["members".to_string()];
        }
    } else {
        let id = unique_permission_id(pattern, &rules);
        rules.push(PermissionRule {
            id,
            pattern: pattern.to_string(),
            read: ReadPermission {
                public: false,
                encrypted_to: recipients.to_vec(),
            },
            write: WritePermission {
                allowed: vec!["members".to_string()],
            },
        });
    }
    let mut text = String::from("version: 1\npermissions:\n");
    for rule in rules {
        text.push_str(&format!("  - id: {}\n", yaml_quote(&rule.id)));
        text.push_str(&format!("    pattern: {}\n", yaml_quote(&rule.pattern)));
        if !rule.write.allowed.is_empty() {
            text.push_str("    write:\n      allowed:\n");
            for allowed in rule.write.allowed {
                text.push_str(&format!("        - {}\n", yaml_quote(&allowed)));
            }
        }
        text.push_str("    read:\n");
        if rule.read.public {
            text.push_str("      public: true\n");
        }
        if !rule.read.encrypted_to.is_empty() {
            text.push_str("      encrypted_to:\n");
            for recipient in rule.read.encrypted_to {
                text.push_str(&format!("        - {}\n", yaml_quote(&recipient)));
            }
        }
    }
    write_if_missing_or_forced(&permissions_path, &text, true)
}

fn unique_permission_id(pattern: &str, rules: &[PermissionRule]) -> String {
    let base = permission_id_from_pattern(pattern);
    if !rules.iter().any(|rule| rule.id == base) {
        return base;
    }
    let mut index = 2usize;
    loop {
        let candidate = format!("{base}_{index}");
        if !rules.iter().any(|rule| rule.id == candidate) {
            return candidate;
        }
        index += 1;
    }
}

fn permission_id_from_pattern(pattern: &str) -> String {
    let mut id = String::from("perm");
    for ch in pattern.chars() {
        if ch.is_ascii_alphanumeric() {
            id.push(ch.to_ascii_lowercase());
        } else if !id.ends_with('_') {
            id.push('_');
        }
    }
    id.trim_end_matches('_').to_string()
}

fn git_config(root: &Path, key: &str, value: &str) -> RhoResult<()> {
    let status = Command::new("git")
        .arg("-C")
        .arg(root)
        .args(["config", "--local", key, value])
        .status()?;
    if !status.success() {
        return Err(format!("git config failed for {key}").into());
    }
    Ok(())
}

fn git_add_relative(root: &Path, relative_path: &str) -> RhoResult<()> {
    let status = Command::new("git")
        .arg("-C")
        .arg(root)
        .arg("add")
        .arg("--")
        .arg(relative_path)
        .status()?;
    if !status.success() {
        return Err(format!("git add failed for {relative_path}").into());
    }
    Ok(())
}

fn git_add_path(root: &Path, path: &Path) -> RhoResult<()> {
    let relative = path
        .strip_prefix(root)
        .map(|path| path.display().to_string())
        .unwrap_or_else(|_| path.display().to_string());
    git_add_relative(root, &relative)
}

fn install_pre_commit_hook(root: &Path) -> RhoResult<()> {
    let hook = root.join(".git/hooks/pre-commit");
    ensure_parent(&hook)?;
    let content = r#"#!/usr/bin/env bash
set -euo pipefail

failed=0
while IFS= read -r -d '' path; do
  attr="$(git check-attr filter -- "$path" | sed 's/^.*: filter: //')"
  if [[ "$attr" == "rho-crypt" ]]; then
    if ! git show ":$path" | grep -Eq "kind: rho_(transparent_file|recipient_envelope)"; then
      echo "rho: protected path is staged without rho encryption: $path" >&2
      failed=1
    fi
  fi
done < <(git diff --cached --name-only -z)

if [[ "$failed" -ne 0 ]]; then
  echo "rho: run git add again after configuring rho repo protect-path, or use rho crypto encrypt for explicit envelopes." >&2
  exit 1
fi
"#;
    fs::write(&hook, content)?;
    let status = Command::new("chmod").arg("+x").arg(&hook).status()?;
    if !status.success() {
        return Err(format!("failed to chmod hook: {}", hook.display()).into());
    }
    Ok(())
}

fn shell_quote(value: &str) -> String {
    format!("'{}'", value.replace('\'', "'\\''"))
}

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

fn write_tool(
    root: &Path,
    id: &str,
    action_type: &str,
    owner: &str,
    approval_required: bool,
    force: bool,
) -> RhoResult<()> {
    let path = root.join("rho/tools").join(format!("{id}.yaml"));
    let manifest = ToolManifest {
        version: 1,
        tool: Tool {
            id: id.to_string(),
            action_type: action_type.to_string(),
            owner: owner.to_string(),
            approval_required,
        },
    };
    write_if_missing_or_forced(&path, &to_yaml(&manifest)?, force)
}

fn write_if_missing_or_forced(path: &Path, content: &str, force: bool) -> RhoResult<()> {
    if path.exists() && !force {
        return Ok(());
    }
    ensure_parent(path)?;
    fs::write(path, content)?;
    Ok(())
}

fn validate_repo_id(value: &str) -> RhoResult<()> {
    if !value.starts_with("rho://repo/") {
        return Err(format!("repo id must start with rho://repo/: {value}").into());
    }
    if value.contains('\0') || value.contains("..") {
        return Err(format!("invalid repo id: {value}").into());
    }
    Ok(())
}

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

    #[test]
    fn validates_repo_ids() {
        assert!(validate_repo_id("rho://repo/local/example").is_ok());
        assert!(validate_repo_id("rho://id/github/user").is_err());
        assert!(validate_repo_id("rho://repo/../bad").is_err());
    }

    #[test]
    fn shell_quotes_filter_commands() {
        assert_eq!(shell_quote("/tmp/rho"), "'/tmp/rho'");
        assert_eq!(shell_quote("/tmp/rho's bin"), "'/tmp/rho'\\''s bin'");
    }
}
