use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};

use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use rho_core::{
    IdentityBundle, IdentityBundleManifest, IdentityProof, IdentityPublicKey, LocalEncryptionKey,
    LocalGitIdentity, LocalIdentity, LocalIdentityManifest, LocalSigningKey, PrivateKeyRef,
    RhoResult, TrustManifest, TrustRecord, arg_value, ensure_parent, file_digest, from_yaml,
    has_flag, normalize_actor_id, require_arg, to_yaml,
};
use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret};

fn usage() -> ! {
    eprintln!(
        "usage:\n  rho id init <github/handle|handle> [--display-name <name>] [--yes]\n  rho id init --github <handle> (--ssh-key <path.pub>|--generate-ssh-key) [--display-name <name>]\n  rho id rotate-key <rho-id|github/handle|handle> [--yes]\n  rho id export --identity <rho-id> --out <path>\n  rho id import <identity.yaml>\n  rho id verify-github --identity <rho-id> [--provider-file <path>]\n  rho id trust <rho-id>\n  rho id list\n  rho id show <rho-id>\n  rho id status <rho-id>"
    );
    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..]),
        "rotate-key" => rotate_key(&args[1..]),
        "export" => export(&args[1..]),
        "import" => import(&args[1..]),
        "verify-github" => verify_github(&args[1..]),
        "trust" => trust(&args[1..]),
        "list" => list(),
        "show" => show(&args[1..]),
        "status" => status(&args[1..]),
        "--help" | "-h" => usage(),
        _ => usage(),
    }
}

fn init(args: &[String]) -> RhoResult<()> {
    let shorthand = !args.is_empty() && !args[0].starts_with('-');
    let handle = if shorthand {
        let identity_id = normalize_actor_id(&args[0])?;
        github_handle_from_identity_id(&identity_id)?
    } else {
        require_arg(args, "--github").unwrap_or_else(|_| usage())
    };
    validate_github_handle(&handle)?;
    let mut display_name = arg_value(args, "--display-name").unwrap_or_else(|| handle.clone());
    let generate = has_flag(args, "--generate-ssh-key");
    let ssh_key = arg_value(args, "--ssh-key");
    if shorthand && (generate || ssh_key.is_some()) {
        return Err(
            "shorthand init uses default key management; omit --ssh-key and --generate-ssh-key"
                .into(),
        );
    }
    if !shorthand && generate == ssh_key.is_some() {
        return Err("choose exactly one of --ssh-key or --generate-ssh-key".into());
    }

    let rho_home = rho_home()?;
    let path = local_identity_path(&rho_home, &handle);
    if path.is_file() {
        let mut local: LocalIdentityManifest = from_yaml(&fs::read_to_string(&path)?)?;
        if shorthand && !has_flag(args, "--yes") {
            maybe_bind_github_commit_identity(&mut local)?;
            fs::write(&path, to_yaml(&local)?)?;
        }
        print_existing_identity(&rho_home, &path, &local);
        return Ok(());
    }
    if shorthand && !has_flag(args, "--yes") {
        println!("Rho identity init");
        println!("  identity: {}", github_identity_id(&handle));
        println!("  rho home: {}", rho_home.display());
        println!(
            "  signing key: {}",
            default_ssh_private_key(&rho_home, &handle).display()
        );
        println!(
            "  encryption key: {}",
            default_x25519_private_key(&rho_home, &handle).display()
        );
        let mut answer = String::new();
        println!("Display name [{display_name}]: ");
        std::io::stdin().read_line(&mut answer)?;
        let display_name_answer = answer.trim();
        if !display_name_answer.is_empty() {
            display_name = display_name_answer.to_string();
        }
        answer.clear();
        println!("Create this identity? [Y/n] ");
        std::io::stdin().read_line(&mut answer)?;
        let answer = answer.trim().to_ascii_lowercase();
        if answer == "n" || answer == "no" {
            return Err("rho id init cancelled".into());
        }
    }

    let (public_key_path, private_key_path, signing_key_source) = if shorthand {
        let source = if default_ssh_key_pair_exists(&rho_home, &handle) {
            "reused default"
        } else {
            "generated default"
        };
        let (public, private) = ensure_default_ssh_key(&rho_home, &handle)?;
        (public, private, source)
    } else if generate {
        let (public, private) = generate_ssh_key(&rho_home, &handle)?;
        (public, private, "generated")
    } else {
        let (public, private) = selected_ssh_key(ssh_key.as_deref().unwrap())?;
        (public, private, "provided")
    };
    let public_key = fs::read_to_string(&public_key_path)
        .map_err(|_| format!("ssh public key not found: {}", public_key_path.display()))?;
    let algorithm = public_key_algorithm(&public_key)?;
    let fingerprint = key_fingerprint(&public_key_path)?;
    let (encryption_public_key_path, encryption_private_key_path, encryption_key_source) =
        if shorthand {
            let source = if default_x25519_key_pair_exists(&rho_home, &handle) {
                "reused default"
            } else {
                "generated default"
            };
            let (public, private) = ensure_default_x25519_key(&rho_home, &handle)?;
            (public, private, source)
        } else {
            let (public, private) = generate_x25519_key(&rho_home, &handle)?;
            (public, private, "generated")
        };
    let encryption_public_key = fs::read_to_string(&encryption_public_key_path)?;
    let encryption_fingerprint = file_digest(&encryption_public_key_path)?;
    let created_at = rho_core::now_rfc3339();
    let identity_id = github_identity_id(&handle);
    let key_id = format!("{identity_id}/{algorithm}-1").replace("rho://id/", "rho://key/");
    let encryption_key_id = format!("{identity_id}/x25519-1").replace("rho://id/", "rho://key/");
    let claim = proof_claim(&handle, &algorithm, &fingerprint);
    let provider_url = github_provider_url(&handle);
    let proof_url = proof_url(&provider_url, &claim);
    let identity = IdentityBundle {
        id: identity_id.clone(),
        kind: "github".to_string(),
        handle: handle.clone(),
        display_name,
        public_keys: vec![
            IdentityPublicKey {
                id: key_id,
                kind: "ssh-signing".to_string(),
                algorithm,
                public_key: public_key.trim().to_string(),
                fingerprint: fingerprint.clone(),
                created_at: created_at.clone(),
            },
            IdentityPublicKey {
                id: encryption_key_id,
                kind: "x25519-encryption".to_string(),
                algorithm: "x25519".to_string(),
                public_key: encryption_public_key.trim().to_string(),
                fingerprint: encryption_fingerprint,
                created_at: created_at.clone(),
            },
        ],
        proofs: vec![IdentityProof {
            kind: "github-profile-fragment-claim".to_string(),
            provider_url: Some(provider_url),
            claim: Some(claim.clone()),
            proof_url: proof_url.clone(),
            verified_at: None,
        }],
        created_at,
    };
    let mut local = LocalIdentityManifest {
        version: 1,
        local_identity: LocalIdentity {
            identity,
            signing_key: LocalSigningKey {
                kind: "ssh-signing".to_string(),
                algorithm: "ssh-ed25519".to_string(),
                public_key_path: public_key_path.display().to_string(),
                private_key_ref: PrivateKeyRef {
                    backend: "ssh-file".to_string(),
                    path: private_key_path.display().to_string(),
                },
            },
            encryption_key: Some(LocalEncryptionKey {
                kind: "x25519-encryption".to_string(),
                algorithm: "x25519".to_string(),
                public_key_path: encryption_public_key_path.display().to_string(),
                private_key_ref: PrivateKeyRef {
                    backend: "rho-file".to_string(),
                    path: encryption_private_key_path.display().to_string(),
                },
            }),
            git: None,
        },
    };
    if shorthand && !has_flag(args, "--yes") {
        maybe_bind_github_commit_identity(&mut local)?;
    }
    ensure_parent(&path)?;
    fs::write(&path, to_yaml(&local)?)?;

    println!("created identity");
    println!("identity: {identity_id}");
    println!("rho home: {}", rho_home.display());
    println!("local manifest: {}", path.display());
    println!("public key: {}", public_key_path.display());
    println!("signing key source: {signing_key_source}");
    println!(
        "encryption public key: {}",
        encryption_public_key_path.display()
    );
    println!("encryption key source: {encryption_key_source}");
    println!("github profile proof URL:");
    println!("{proof_url}");
    println!();
    println!("GitHub verification:");
    println!("1. Copy this full proof URL into your public GitHub profile:");
    println!("{proof_url}");
    println!("   If GitHub only shows the link target partially, make sure this claim text");
    println!("   is visible somewhere on the fetched profile page:");
    println!("{claim}");
    println!("2. Verify it with:");
    println!("rho id verify-github --identity {identity_id}");
    Ok(())
}

fn print_existing_identity(rho_home: &Path, path: &Path, local: &LocalIdentityManifest) {
    let identity = &local.local_identity.identity;
    println!("identity already exists");
    println!("identity: {}", identity.id);
    println!("rho home: {}", rho_home.display());
    println!("local manifest: {}", path.display());
    println!(
        "public key: {}",
        local.local_identity.signing_key.public_key_path
    );
    if let Some(encryption_key) = &local.local_identity.encryption_key {
        println!("encryption public key: {}", encryption_key.public_key_path);
    }
    if let Some(git) = &local.local_identity.git {
        println!("github commit login: {}", git.github_login);
        println!(
            "git commit author: {} <{}>",
            git.commit_name, git.commit_email
        );
    }
    if let Ok(proof) = github_profile_proof(identity) {
        if !proof.proof_url.is_empty() {
            println!("github profile proof URL:");
            println!("{}", proof.proof_url);
            println!();
            println!("GitHub verification:");
            println!("1. Copy this full proof URL into your public GitHub profile:");
            println!("{}", proof.proof_url);
        }
        if let Some(claim) = &proof.claim {
            println!(
                "   If GitHub only shows the link target partially, make sure this claim text"
            );
            println!("   is visible somewhere on the fetched profile page:");
            println!("{claim}");
        }
        println!("2. Verify it with:");
        println!("rho id verify-github --identity {}", identity.id);
    }
}

fn rotate_key(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 rho_home = rho_home()?;
    let path = local_identity_path(&rho_home, &handle);
    let mut local = read_local_identity(&rho_home, &handle)?;
    if local.local_identity.identity.id != identity_id {
        return Err(format!(
            "identity mismatch: requested {identity_id}, found {}",
            local.local_identity.identity.id
        )
        .into());
    }
    if !has_flag(args, "--yes") {
        println!("Rotate Rho signing key");
        println!("  identity: {identity_id}");
        println!("  local manifest: {}", path.display());
        println!(
            "  current public key: {}",
            local.local_identity.signing_key.public_key_path
        );
        println!("This creates a new GitHub profile proof claim. Continue? [y/N] ");
        let mut answer = String::new();
        std::io::stdin().read_line(&mut answer)?;
        let answer = answer.trim().to_ascii_lowercase();
        if answer != "y" && answer != "yes" {
            return Err("rho id rotate-key cancelled".into());
        }
    }

    let next_index = next_signing_key_index(&local.local_identity.identity);
    let (public_key_path, private_key_path) =
        generate_rotated_ssh_key(&rho_home, &handle, next_index)?;
    let public_key = fs::read_to_string(&public_key_path)
        .map_err(|_| format!("ssh public key not found: {}", public_key_path.display()))?;
    let algorithm = public_key_algorithm(&public_key)?;
    let fingerprint = key_fingerprint(&public_key_path)?;
    let created_at = rho_core::now_rfc3339();
    let key_id =
        format!("{identity_id}/{algorithm}-{next_index}").replace("rho://id/", "rho://key/");
    let new_key = IdentityPublicKey {
        id: key_id.clone(),
        kind: "ssh-signing".to_string(),
        algorithm: algorithm.clone(),
        public_key: public_key.trim().to_string(),
        fingerprint: fingerprint.clone(),
        created_at,
    };
    local
        .local_identity
        .identity
        .public_keys
        .retain(|key| key.id != key_id);
    local.local_identity.identity.public_keys.insert(0, new_key);
    local.local_identity.signing_key = LocalSigningKey {
        kind: "ssh-signing".to_string(),
        algorithm: algorithm.clone(),
        public_key_path: public_key_path.display().to_string(),
        private_key_ref: PrivateKeyRef {
            backend: "ssh-file".to_string(),
            path: private_key_path.display().to_string(),
        },
    };

    let provider_url = github_provider_url(&handle);
    let claim = proof_claim(&handle, &algorithm, &fingerprint);
    let proof_url = proof_url(&provider_url, &claim);
    if let Ok(proof) = github_profile_proof_mut(&mut local.local_identity.identity) {
        proof.provider_url = Some(provider_url);
        proof.claim = Some(claim.clone());
        proof.proof_url = proof_url.clone();
        proof.verified_at = None;
    } else {
        local.local_identity.identity.proofs.push(IdentityProof {
            kind: "github-profile-fragment-claim".to_string(),
            provider_url: Some(provider_url),
            claim: Some(claim.clone()),
            proof_url: proof_url.clone(),
            verified_at: None,
        });
    }

    fs::write(&path, to_yaml(&local)?)?;
    println!("rotated signing key");
    println!("identity: {identity_id}");
    println!("local manifest: {}", path.display());
    println!("public key: {}", public_key_path.display());
    println!("private key: {}", private_key_path.display());
    println!("key_id: {key_id}");
    println!("github proof: unverified");
    println!("github profile proof URL:");
    println!("{proof_url}");
    println!();
    println!("GitHub verification:");
    println!("1. Replace the old Rho claim in your public GitHub profile with:");
    println!("{proof_url}");
    println!("   If GitHub only shows the link target partially, make sure this claim text");
    println!("   is visible somewhere on the fetched profile page:");
    println!("{claim}");
    println!("2. Verify it with:");
    println!("rho id verify-github --identity {identity_id}");
    Ok(())
}

fn maybe_bind_github_commit_identity(local: &mut LocalIdentityManifest) -> RhoResult<()> {
    println!();
    println!("GitHub CLI account:");
    match Command::new("gh").args(["auth", "status"]).status() {
        Ok(status) if status.success() => {}
        Ok(_) => {
            println!("gh auth status failed; skipping GitHub commit binding");
            return Ok(());
        }
        Err(_) => {
            println!("gh is not available; skipping GitHub commit binding");
            return Ok(());
        }
    }
    let Some((login, id)) = gh_user() else {
        println!("could not read active gh account; skipping GitHub commit binding");
        return Ok(());
    };
    let default_email = format!("{id}+{login}@users.noreply.github.com");
    println!("Active gh account: {login}");
    println!("Default commit author: {login} <{default_email}>");
    println!("Use this gh account for rho commits with this identity? [Y/n] ");
    let mut answer = String::new();
    std::io::stdin().read_line(&mut answer)?;
    let bind_answer = answer.trim().to_ascii_lowercase();
    if bind_answer == "n" || bind_answer == "no" {
        return Ok(());
    }
    let mut name = login.clone();
    answer.clear();
    println!("Commit name [{name}]: ");
    std::io::stdin().read_line(&mut answer)?;
    let value = answer.trim();
    if !value.is_empty() {
        name = value.to_string();
    }
    let mut email = default_email;
    answer.clear();
    println!("Commit email [{email}]: ");
    std::io::stdin().read_line(&mut answer)?;
    let value = answer.trim();
    if !value.is_empty() {
        email = value.to_string();
    }
    local.local_identity.git = Some(LocalGitIdentity {
        github_login: login,
        commit_name: name,
        commit_email: email,
    });
    Ok(())
}

fn gh_user() -> Option<(String, String)> {
    let login = gh_api_user_field(".login")?;
    let id = gh_api_user_field(".id")?;
    Some((login, id))
}

fn gh_api_user_field(field: &str) -> Option<String> {
    let output = Command::new("gh")
        .args(["api", "user", "--jq", field])
        .output()
        .ok()?;
    if !output.status.success() {
        return None;
    }
    Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
        .filter(|value| !value.is_empty())
}

fn export(args: &[String]) -> RhoResult<()> {
    let identity_id =
        normalize_actor_id(&require_arg(args, "--identity").unwrap_or_else(|_| usage()))?;
    let out = PathBuf::from(require_arg(args, "--out").unwrap_or_else(|_| usage()));
    let handle = github_handle_from_identity_id(&identity_id)?;
    let local = read_local_identity(&rho_home()?, &handle)?;
    ensure_parent(&out)?;
    fs::write(
        &out,
        to_yaml(&IdentityBundleManifest {
            version: 1,
            identity: local.local_identity.identity,
        })?,
    )?;
    println!("{}", out.display());
    Ok(())
}

fn import(args: &[String]) -> RhoResult<()> {
    let Some(path) = args.first() else {
        usage();
    };
    let text = fs::read_to_string(path)?;
    let bundle: IdentityBundleManifest = from_yaml(&text)?;
    if bundle.identity.kind != "github" {
        return Err(format!("unsupported identity kind: {}", bundle.identity.kind).into());
    }
    validate_github_handle(&bundle.identity.handle)?;
    let target = peer_identity_path(&rho_home()?, &bundle.identity.handle);
    ensure_parent(&target)?;
    fs::write(&target, to_yaml(&bundle)?)?;
    println!("{}", target.display());
    Ok(())
}

fn verify_github(args: &[String]) -> RhoResult<()> {
    let identity_id =
        normalize_actor_id(&require_arg(args, "--identity").unwrap_or_else(|_| usage()))?;
    let handle = github_handle_from_identity_id(&identity_id)?;
    let rho_home = rho_home()?;
    let provider_file = arg_value(args, "--provider-file").map(PathBuf::from);
    let local_path = local_identity_path(&rho_home, &handle);
    let peer_path = peer_identity_path(&rho_home, &handle);

    if local_path.is_file() {
        let text = fs::read_to_string(&local_path)?;
        let mut manifest: LocalIdentityManifest = from_yaml(&text)?;
        let page = provider_page(provider_file.as_deref(), &manifest.local_identity.identity)?;
        verify_github_identity(&mut manifest.local_identity.identity, &page)?;
        fs::write(&local_path, to_yaml(&manifest)?)?;
        println!("verified {}", manifest.local_identity.identity.id);
        println!("{}", local_path.display());
        return Ok(());
    }

    if peer_path.is_file() {
        let text = fs::read_to_string(&peer_path)?;
        let mut manifest: IdentityBundleManifest = from_yaml(&text)?;
        let page = provider_page(provider_file.as_deref(), &manifest.identity)?;
        verify_github_identity(&mut manifest.identity, &page)?;
        fs::write(&peer_path, to_yaml(&manifest)?)?;
        println!("verified {}", manifest.identity.id);
        println!("{}", peer_path.display());
        return Ok(());
    }

    Err(format!("identity not found: {identity_id}").into())
}

fn trust(args: &[String]) -> RhoResult<()> {
    let Some(identity_id) = args.first() else {
        usage();
    };
    let identity_id = normalize_actor_id(identity_id)?;
    let handle = github_handle_from_identity_id(&identity_id)?;
    let rho_home = rho_home()?;
    let peer = peer_identity_path(&rho_home, &handle);
    if !peer.is_file() && !local_identity_path(&rho_home, &handle).is_file() {
        return Err(format!("identity has not been imported or initialized: {identity_id}").into());
    }
    let target = trust_path(&rho_home, &handle);
    ensure_parent(&target)?;
    fs::write(
        &target,
        to_yaml(&TrustManifest {
            version: 1,
            trust: TrustRecord {
                identity_id,
                decision: "trusted".to_string(),
                trusted_at: rho_core::now_rfc3339(),
                source: "local-user".to_string(),
            },
        })?,
    )?;
    println!("{}", target.display());
    Ok(())
}

fn list() -> RhoResult<()> {
    let rho_home = rho_home()?;
    println!("local identities:");
    print_identity_dir(&rho_home.join("identities/github"))?;
    println!("peer identities:");
    print_identity_dir(&rho_home.join("peers/github"))?;
    Ok(())
}

fn show(args: &[String]) -> RhoResult<()> {
    let Some(identity_id) = args.first() else {
        usage();
    };
    let identity_id = normalize_actor_id(identity_id)?;
    let handle = github_handle_from_identity_id(&identity_id)?;
    let rho_home = rho_home()?;
    let local_path = local_identity_path(&rho_home, &handle);
    if local_path.is_file() {
        print!("{}", fs::read_to_string(local_path)?);
        return Ok(());
    }
    let peer_path = peer_identity_path(&rho_home, &handle);
    if peer_path.is_file() {
        print!("{}", fs::read_to_string(peer_path)?);
        return Ok(());
    }
    Err(format!("identity not found: {identity_id}").into())
}

fn status(args: &[String]) -> RhoResult<()> {
    let Some(identity_id) = args.first() else {
        usage();
    };
    let identity_id = normalize_actor_id(identity_id)?;
    let handle = github_handle_from_identity_id(&identity_id)?;
    let rho_home = rho_home()?;
    let local_path = local_identity_path(&rho_home, &handle);
    if local_path.is_file() {
        let local: LocalIdentityManifest = from_yaml(&fs::read_to_string(&local_path)?)?;
        print_identity_status(
            &rho_home,
            &local_path,
            &local.local_identity.identity,
            true,
            local.local_identity.git.as_ref().map(|git| {
                (
                    git.github_login.as_str(),
                    git.commit_name.as_str(),
                    git.commit_email.as_str(),
                )
            }),
        );
        return Ok(());
    }

    let peer_path = peer_identity_path(&rho_home, &handle);
    if peer_path.is_file() {
        let peer: IdentityBundleManifest = from_yaml(&fs::read_to_string(&peer_path)?)?;
        print_identity_status(&rho_home, &peer_path, &peer.identity, false, None);
        return Ok(());
    }

    Err(format!("identity not found: {identity_id}").into())
}

fn print_identity_status(
    rho_home: &Path,
    path: &Path,
    identity: &IdentityBundle,
    has_private_key: bool,
    git: Option<(&str, &str, &str)>,
) {
    println!("identity: {}", identity.id);
    println!("kind: {}", identity.kind);
    println!("handle: {}", identity.handle);
    println!("rho home: {}", rho_home.display());
    println!("manifest: {}", path.display());
    println!(
        "private key: {}",
        if has_private_key {
            "present"
        } else {
            "not-present"
        }
    );
    if let Some((github_login, commit_name, commit_email)) = git {
        println!("github commit login: {github_login}");
        println!("git commit author: {commit_name} <{commit_email}>");
    }
    match github_profile_proof(identity) {
        Ok(proof) => {
            if let Some(verified_at) = &proof.verified_at {
                println!("github proof: verified");
                println!("verified_at: {verified_at}");
            } else {
                println!("github proof: unverified");
            }
            if let Some(provider_url) = &proof.provider_url {
                println!("provider_url: {provider_url}");
            }
            if let Some(claim) = &proof.claim {
                println!("claim: {claim}");
            }
            if !proof.proof_url.is_empty() {
                println!("proof_url: {}", proof.proof_url);
            }
        }
        Err(error) => {
            println!("github proof: missing ({error})");
        }
    }
}

fn print_identity_dir(dir: &Path) -> RhoResult<()> {
    if !dir.is_dir() {
        return Ok(());
    }
    let mut paths: Vec<_> = fs::read_dir(dir)?
        .filter_map(Result::ok)
        .map(|entry| entry.path())
        .filter(|path| path.extension().and_then(|value| value.to_str()) == Some("yaml"))
        .collect();
    paths.sort();
    for path in paths {
        let text = fs::read_to_string(&path)?;
        if let Ok(local) = from_yaml::<LocalIdentityManifest>(&text) {
            let verified = local
                .local_identity
                .identity
                .proofs
                .iter()
                .any(|proof| proof.verified_at.is_some());
            println!(
                "  {}  private-key=yes verified={}  {}",
                local.local_identity.identity.id,
                if verified { "yes" } else { "no" },
                path.display()
            );
            continue;
        }
        if let Ok(peer) = from_yaml::<IdentityBundleManifest>(&text) {
            let verified = peer
                .identity
                .proofs
                .iter()
                .any(|proof| proof.verified_at.is_some());
            println!(
                "  {}  private-key=no verified={}  {}",
                peer.identity.id,
                if verified { "yes" } else { "no" },
                path.display()
            );
            continue;
        }
        println!("  unreadable identity file: {}", path.display());
    }
    Ok(())
}

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

fn local_identity_path(rho_home: &Path, handle: &str) -> PathBuf {
    rho_home
        .join("identities")
        .join("github")
        .join(format!("{handle}.yaml"))
}

fn peer_identity_path(rho_home: &Path, handle: &str) -> PathBuf {
    rho_home
        .join("peers")
        .join("github")
        .join(format!("{handle}.yaml"))
}

fn trust_path(rho_home: &Path, handle: &str) -> PathBuf {
    rho_home
        .join("trust")
        .join("github")
        .join(format!("{handle}.yaml"))
}

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

fn default_ssh_key_pair_exists(rho_home: &Path, handle: &str) -> bool {
    let private = default_ssh_private_key(rho_home, handle);
    private.is_file() && private.with_extension("pub").is_file()
}

fn generate_ssh_key(rho_home: &Path, handle: &str) -> RhoResult<(PathBuf, PathBuf)> {
    let private = default_ssh_private_key(rho_home, handle);
    generate_ssh_key_at(&private, &github_identity_id(handle))
}

fn generate_ssh_key_at(private: &Path, comment: &str) -> RhoResult<(PathBuf, PathBuf)> {
    let public = private.with_extension("pub");
    if private.exists() || public.exists() {
        return Err(format!("ssh key already exists: {}", private.display()).into());
    }
    ensure_parent(private)?;
    let status = Command::new("ssh-keygen")
        .args(["-t", "ed25519", "-N", "", "-C"])
        .arg(comment)
        .arg("-f")
        .arg(private)
        .status()?;
    if !status.success() {
        return Err("ssh-keygen failed".into());
    }
    Ok((public, private.to_path_buf()))
}

fn ensure_default_ssh_key(rho_home: &Path, handle: &str) -> RhoResult<(PathBuf, PathBuf)> {
    let private = default_ssh_private_key(rho_home, handle);
    let public = private.with_extension("pub");
    if private.is_file() && public.is_file() {
        return Ok((public, private));
    }
    if private.exists() || public.exists() {
        return Err(format!(
            "incomplete ssh key pair; expected {} and {}",
            private.display(),
            public.display()
        )
        .into());
    }
    generate_ssh_key(rho_home, handle)
}

fn generate_rotated_ssh_key(
    rho_home: &Path,
    handle: &str,
    index: usize,
) -> RhoResult<(PathBuf, PathBuf)> {
    let private = rotated_ssh_private_key(rho_home, handle, index);
    generate_ssh_key_at(
        &private,
        &format!("{} signing key {index}", github_identity_id(handle)),
    )
}

fn next_signing_key_index(identity: &IdentityBundle) -> usize {
    identity
        .public_keys
        .iter()
        .filter(|key| key.kind == "ssh-signing")
        .filter_map(|key| {
            let suffix = key.id.rsplit('/').next()?;
            let (_, index) = suffix.rsplit_once('-')?;
            index.parse::<usize>().ok()
        })
        .max()
        .unwrap_or(0)
        + 1
}

fn default_x25519_key_pair_exists(rho_home: &Path, handle: &str) -> bool {
    default_x25519_private_key(rho_home, handle).is_file()
        && default_x25519_public_key(rho_home, handle).is_file()
}

fn generate_x25519_key(rho_home: &Path, handle: &str) -> RhoResult<(PathBuf, PathBuf)> {
    let private = default_x25519_private_key(rho_home, handle);
    let public = default_x25519_public_key(rho_home, handle);
    if private.exists() || public.exists() {
        return Err(format!("encryption key already exists: {}", private.display()).into());
    }
    ensure_parent(&private)?;
    let secret = StaticSecret::from(random_bytes::<32>()?);
    let public_key = X25519PublicKey::from(&secret);
    fs::write(&private, format!("{}\n", BASE64.encode(secret.to_bytes())))?;
    fs::write(
        &public,
        format!("{}\n", BASE64.encode(public_key.as_bytes())),
    )?;
    Ok((public, private))
}

fn ensure_default_x25519_key(rho_home: &Path, handle: &str) -> RhoResult<(PathBuf, PathBuf)> {
    let private = default_x25519_private_key(rho_home, handle);
    let public = default_x25519_public_key(rho_home, handle);
    if private.is_file() && public.is_file() {
        return Ok((public, private));
    }
    if private.exists() || public.exists() {
        return Err(format!(
            "incomplete encryption key pair; expected {} and {}",
            private.display(),
            public.display()
        )
        .into());
    }
    generate_x25519_key(rho_home, handle)
}

fn default_ssh_private_key(rho_home: &Path, handle: &str) -> PathBuf {
    rho_home
        .join("keys")
        .join("github")
        .join(handle)
        .join("signing_ed25519")
}

fn rotated_ssh_private_key(rho_home: &Path, handle: &str, index: usize) -> PathBuf {
    let timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_secs())
        .unwrap_or(0);
    rho_home
        .join("keys")
        .join("github")
        .join(handle)
        .join(format!("signing_ed25519-{index}-{timestamp}"))
}

fn default_x25519_private_key(rho_home: &Path, handle: &str) -> PathBuf {
    rho_home
        .join("keys")
        .join("github")
        .join(handle)
        .join("encryption_x25519.key")
}

fn default_x25519_public_key(rho_home: &Path, handle: &str) -> PathBuf {
    rho_home
        .join("keys")
        .join("github")
        .join(handle)
        .join("encryption_x25519.pub")
}

fn random_bytes<const N: usize>() -> RhoResult<[u8; N]> {
    let mut bytes = [0u8; N];
    getrandom::getrandom(&mut bytes).map_err(|error| error.to_string())?;
    Ok(bytes)
}

fn selected_ssh_key(value: &str) -> RhoResult<(PathBuf, PathBuf)> {
    let public = expand_home(value);
    if !public.is_file() {
        return Err(format!("ssh public key not found: {}", public.display()).into());
    }
    let private = if public.extension().and_then(|value| value.to_str()) == Some("pub") {
        public.with_extension("")
    } else {
        return Err("--ssh-key must point to a .pub file".into());
    };
    Ok((public, private))
}

fn expand_home(value: &str) -> PathBuf {
    if let Some(rest) = value.strip_prefix("~/")
        && let Ok(home) = env::var("HOME")
    {
        return PathBuf::from(home).join(rest);
    }
    PathBuf::from(value)
}

fn public_key_algorithm(public_key: &str) -> RhoResult<String> {
    let algorithm = public_key
        .split_whitespace()
        .next()
        .ok_or("ssh public key is empty")?;
    match algorithm {
        "ssh-ed25519" => Ok(algorithm.to_string()),
        _ => Err(format!("unsupported ssh key algorithm for v1: {algorithm}").into()),
    }
}

fn key_fingerprint(public_key_path: &Path) -> RhoResult<String> {
    let output = Command::new("ssh-keygen")
        .args(["-l", "-f"])
        .arg(public_key_path)
        .output();
    if let Ok(output) = output
        && output.status.success()
    {
        let text = String::from_utf8_lossy(&output.stdout);
        if let Some(value) = text.split_whitespace().nth(1) {
            return Ok(value.to_string());
        }
    }
    Ok(format!("sha256-{}", &file_digest(public_key_path)?[..32]))
}

fn provider_page(provider_file: Option<&Path>, identity: &IdentityBundle) -> RhoResult<String> {
    if let Some(path) = provider_file {
        return Ok(fs::read_to_string(path)?);
    }
    let proof = github_profile_proof(identity)?;
    let provider_url = proof
        .provider_url
        .as_deref()
        .ok_or("github proof is missing provider_url")?;
    let output = Command::new("curl")
        .args(["-fsSL", provider_url])
        .output()
        .map_err(|error| format!("failed to fetch {provider_url}: {error}"))?;
    if !output.status.success() {
        return Err(format!("failed to fetch {provider_url}").into());
    }
    Ok(String::from_utf8(output.stdout)?)
}

fn verify_github_identity(identity: &mut IdentityBundle, provider_page: &str) -> RhoResult<()> {
    if identity.kind != "github" {
        return Err(format!("unsupported identity kind: {}", identity.kind).into());
    }
    let identity_handle = github_handle_from_identity_id(&identity.id)?;
    if identity.handle != identity_handle {
        return Err(format!(
            "identity handle mismatch: id has {identity_handle}, bundle has {}",
            identity.handle
        )
        .into());
    }

    let proof = github_profile_proof(identity)?;
    let provider_url = proof
        .provider_url
        .as_deref()
        .ok_or("github proof is missing provider_url")?;
    let claim = proof
        .claim
        .as_deref()
        .ok_or("github proof is missing claim")?;
    let provider_handle = github_handle_from_provider_url(provider_url)?;
    if provider_handle != identity_handle {
        return Err(format!(
            "provider handle mismatch: provider has {provider_handle}, identity has {identity_handle}"
        )
        .into());
    }
    let parsed = parse_proof_claim(claim)?;
    if parsed.handle != identity_handle {
        return Err(format!(
            "claim handle mismatch: claim has {}, identity has {identity_handle}",
            parsed.handle
        )
        .into());
    }
    let expected_url = proof_url(provider_url, claim);
    if proof.proof_url != expected_url {
        return Err(format!(
            "proof_url mismatch: expected {expected_url}, got {}",
            proof.proof_url
        )
        .into());
    }
    let key = identity
        .public_keys
        .iter()
        .find(|key| key.algorithm == parsed.algorithm)
        .ok_or_else(|| format!("no public key with algorithm {}", parsed.algorithm))?;
    if claim_fingerprint(&key.fingerprint) != parsed.fingerprint {
        return Err(format!(
            "fingerprint mismatch: claim has {}, key has {}",
            parsed.fingerprint, key.fingerprint
        )
        .into());
    }
    if !provider_page.contains(claim) {
        return Err(format!("provider page does not contain claim: {claim}").into());
    }

    let verified_at = rho_core::now_rfc3339();
    let proof = github_profile_proof_mut(identity)?;
    proof.verified_at = Some(verified_at);
    Ok(())
}

fn github_profile_proof(identity: &IdentityBundle) -> RhoResult<&IdentityProof> {
    identity
        .proofs
        .iter()
        .find(|proof| proof.kind == "github-profile-fragment-claim")
        .ok_or("identity has no github-profile-fragment-claim proof".into())
}

fn github_profile_proof_mut(identity: &mut IdentityBundle) -> RhoResult<&mut IdentityProof> {
    identity
        .proofs
        .iter_mut()
        .find(|proof| proof.kind == "github-profile-fragment-claim")
        .ok_or("identity has no github-profile-fragment-claim proof".into())
}

fn github_identity_id(handle: &str) -> String {
    format!("rho://id/github/{handle}")
}

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());
    };
    validate_github_handle(handle)?;
    Ok(handle.to_string())
}

fn github_provider_url(handle: &str) -> String {
    format!("https://github.com/{handle}")
}

fn github_handle_from_provider_url(provider_url: &str) -> RhoResult<String> {
    let Some(handle) = provider_url.strip_prefix("https://github.com/") else {
        return Err(format!("unsupported github provider_url: {provider_url}").into());
    };
    let handle = handle.trim_end_matches('/');
    if handle.contains('/') || handle.contains('#') || handle.contains('?') {
        return Err(format!("unsupported github provider_url: {provider_url}").into());
    }
    validate_github_handle(handle)?;
    Ok(handle.to_string())
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct ParsedProofClaim {
    handle: String,
    algorithm: String,
    fingerprint: String,
}

fn parse_proof_claim(claim: &str) -> RhoResult<ParsedProofClaim> {
    let parts: Vec<&str> = claim.split('/').collect();
    if parts.len() != 7
        || parts[0] != "rho.claim"
        || parts[1] != "id"
        || parts[2] != "github"
        || parts[4] != "key"
    {
        return Err(format!("unsupported github proof claim: {claim}").into());
    }
    validate_github_handle(parts[3])?;
    Ok(ParsedProofClaim {
        handle: parts[3].to_string(),
        algorithm: parts[5].to_string(),
        fingerprint: parts[6].to_string(),
    })
}

fn proof_claim(handle: &str, algorithm: &str, fingerprint: &str) -> String {
    format!(
        "rho.claim/id/github/{handle}/key/{algorithm}/{}",
        claim_fingerprint(fingerprint)
    )
}

fn claim_fingerprint(fingerprint: &str) -> String {
    fingerprint
        .replace("SHA256:", "sha256-")
        .replace(['/', '+', '='], "-")
}

fn proof_url(provider_url: &str, claim: &str) -> String {
    format!("{provider_url}#{claim}")
}

fn validate_github_handle(handle: &str) -> RhoResult<()> {
    if handle.is_empty() || handle.len() > 39 {
        return Err("github handle must be 1-39 characters".into());
    }
    if handle.starts_with('-') || handle.ends_with('-') {
        return Err(format!("invalid github handle: {handle}").into());
    }
    let valid = handle
        .chars()
        .all(|ch| ch.is_ascii_alphanumeric() || ch == '-');
    if !valid || handle.contains("--") {
        return Err(format!("invalid github handle: {handle}").into());
    }
    Ok(())
}

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

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

    #[test]
    fn rejects_invalid_github_handles() {
        assert!(validate_github_handle("-bad").is_err());
        assert!(validate_github_handle("bad-").is_err());
        assert!(validate_github_handle("bad_name").is_err());
        assert!(validate_github_handle("bad--name").is_err());
    }

    #[test]
    fn proof_claim_is_url_safe() {
        assert_eq!(
            proof_claim("madhavajay", "ssh-ed25519", "SHA256:abc/def+ghi="),
            "rho.claim/id/github/madhavajay/key/ssh-ed25519/sha256-abc-def-ghi-"
        );
    }

    #[test]
    fn proof_url_uses_provider_fragment() {
        assert_eq!(
            proof_url(
                "https://github.com/madhavajay",
                "rho.claim/id/github/madhavajay/key/ssh-ed25519/sha256-abc"
            ),
            "https://github.com/madhavajay#rho.claim/id/github/madhavajay/key/ssh-ed25519/sha256-abc"
        );
    }

    #[test]
    fn parses_proof_claim() {
        let parsed =
            parse_proof_claim("rho.claim/id/github/madhavajay/key/ssh-ed25519/sha256-abc").unwrap();
        assert_eq!(parsed.handle, "madhavajay");
        assert_eq!(parsed.algorithm, "ssh-ed25519");
        assert_eq!(parsed.fingerprint, "sha256-abc");
    }

    #[test]
    fn parses_github_provider_url() {
        assert_eq!(
            github_handle_from_provider_url("https://github.com/madhavajay").unwrap(),
            "madhavajay"
        );
        assert_eq!(
            github_handle_from_provider_url("https://github.com/madhavajay/").unwrap(),
            "madhavajay"
        );
        assert!(github_handle_from_provider_url("https://github.com/madhavajay/repo").is_err());
    }
}
