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

use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use rho_core::{
    IdentityBundleManifest, LocalIdentityManifest, RhoResult, Tool, ToolManifest, arg_value,
    ensure_parent, from_yaml, has_flag, normalize_actor_id, normalize_repo_id, to_yaml,
    validate_actor_id, yaml_quote,
};
use serde::Deserialize;

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 add-user <rho-id|github/handle|handle> [--root <path>]\n  rho-repo import-users [--root <path>]\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 status [--root <path>]\n  rho-repo doctor [--root <path>]"
    );
    std::process::exit(2);
}

fn main() {
    if let Err(error) = run() {
        eprintln!("{error}");
        std::process::exit(1);
    }
}

fn run() -> RhoResult<()> {
    let args: Vec<String> = env::args().skip(1).collect();
    let Some(command) = args.first().map(String::as_str) else {
        usage();
    };
    match command {
        "init" => init(&args[1..]),
        "add-user" => add_user(&args[1..]),
        "import-users" => import_users(&args[1..]),
        "protect-path" => protect_path(&args[1..]),
        "install-filters" => install_filters(&args[1..]),
        "status" => status(&args[1..]),
        "doctor" => doctor(&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 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(())
}

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(".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/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 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>,
}

#[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>,
}

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(".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, &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)?;
    }

    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());
        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, 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());
    }
}

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 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 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(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 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"),
    )?;
    if recipients.is_empty() {
        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_permissions_policy_rule(&root, pattern, &recipients)?;
    }
    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 mut installed = 0usize;
    for rule in &permissions.permissions {
        if rule.read.encrypted_to.is_empty() {
            continue;
        }
        append_line_if_missing(
            &root.join(".gitattributes"),
            &format!("{} filter=rho-crypt diff=rho-crypt", rule.pattern),
        )?;
        installed += 1;
    }
    configure_git_filter_for_policy(&root, &rho_bin)?;
    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 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_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 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'");
    }
}
