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

use rho_core::{
    RhoResult, RunManifest, ensure_parent, file_digest, from_json, from_yaml, has_flag,
    is_rho_encrypted_text, normalize_actor_id, normalize_repo_id, providers, require_arg,
    validate_request_id, yaml_quote,
};
use serde::Deserialize;
use sha2::{Digest, Sha256};

fn usage() -> ! {
    eprintln!(
        "usage:\n  rho result publish --shared-root <path> --request-id <id> --result-id <id> --from <id> --to <id> --artifact <path> [--text <text>]\n  rho result release <req-id> --run-id <id> --to <id> [--from <id>] [--root <repo>] [--output <path>|--value <text>] [--inline-max-bytes <n>] [--runner <name>] [--no-branch] [--no-stage] [--allow-missing-request] [--commit] [--push] [--pr]\n  rho result release-pr <pr-number> --run-id <id> --to <id> [--request-id <id>] [--root <repo>] [--output <path>|--value <text>] [--inline-max-bytes <n>] [--runner <name>] [--no-sync] [--commit] [--push] [--pr]\n  rho result verify <req-id> [--root <repo>] [--to <id>]"
    );
    std::process::exit(2);
}

const DEFAULT_INLINE_RESULT_MAX_BYTES: usize = 4096;

pub fn run(args: &[String]) -> RhoResult<()> {
    let Some(command) = args.first().map(String::as_str) else {
        usage();
    };
    match command {
        "publish" => publish(&args[1..]),
        "release" => release(&args[1..]),
        "release-pr" => release_pr(&args[1..]),
        "verify" => verify(&args[1..]),
        _ => usage(),
    }
}

fn publish(rest: &[String]) -> RhoResult<()> {
    let shared_root = PathBuf::from(require_arg(rest, "--shared-root").unwrap_or_else(|_| usage()));
    let request_id = require_arg(rest, "--request-id").unwrap_or_else(|_| usage());
    let result_id = require_arg(rest, "--result-id").unwrap_or_else(|_| usage());
    let from = normalize_actor_id(&require_arg(rest, "--from").unwrap_or_else(|_| usage()))?;
    let to = normalize_actor_id(&require_arg(rest, "--to").unwrap_or_else(|_| usage()))?;
    let artifact = require_arg(rest, "--artifact").unwrap_or_else(|_| usage());
    let text = rho_core::arg_value(rest, "--text")
        .unwrap_or_else(|| format!("Result released for {request_id}."));
    let manifest = shared_root
        .join(".rho/results")
        .join(format!("{request_id}.yaml"));
    ensure_parent(&manifest)?;
    fs::write(
        &manifest,
        format!(
            "version: 1\nresult:\n  id: {}\n  request_id: {}\n  from: {}\n  to: {}\n  status: \"released\"\n  artifact_paths:\n    - {}\n",
            yaml_quote(&result_id),
            yaml_quote(&request_id),
            yaml_quote(&from),
            yaml_quote(&to),
            yaml_quote(&artifact),
        ),
    )?;

    let inbox = shared_root
        .join(".rho/inbox")
        .join(&to)
        .join(format!("result-{request_id}.yaml"));
    ensure_parent(&inbox)?;
    fs::write(
        &inbox,
        format!(
            "version: 1\nmessage:\n  id: {}\n  from: {}\n  to: {}\n  type: \"result_available\"\n  related_request_id: {}\n  body:\n    text: {}\n    result_manifest: {}\n",
            yaml_quote(&format!("result-{request_id}")),
            yaml_quote(&from),
            yaml_quote(&to),
            yaml_quote(&request_id),
            yaml_quote(&text),
            yaml_quote(&manifest.display().to_string()),
        ),
    )?;
    println!("{}", manifest.display());
    Ok(())
}

fn release(rest: &[String]) -> RhoResult<()> {
    let request_id = rest.first().cloned().unwrap_or_else(|| usage());
    validate_request_id(&request_id)?;
    let root = rho_core::arg_value(rest, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    let run_id = require_arg(rest, "--run-id").unwrap_or_else(|_| usage());
    let to = normalize_actor_id(&require_arg(rest, "--to").unwrap_or_else(|_| usage()))?;
    let from = rho_core::arg_value(rest, "--from")
        .map(|value| normalize_actor_id(&value))
        .transpose()?
        .or_else(active_identity)
        .ok_or("missing --from and RHO_IDENTITY is not set")?;
    let runner = rho_core::arg_value(rest, "--runner").unwrap_or_else(|| "unknown".to_string());
    let repo_id = read_repo_id(&root)?;

    if !has_flag(rest, "--allow-missing-request") {
        require_committed_request(&root, &request_id)?;
    }
    let from_handle = github_handle_from_identity_id(&from)?;
    if !has_flag(rest, "--no-branch") {
        switch_branch(&root, &format!("{from_handle}/result-{request_id}"))?;
    }

    let release_output = read_release_output(&root, &run_id, rest)?;
    preflight_run_input_commitment(&root, &request_id, &run_id)?;

    let result_dir = root
        .join("rho/messages/inbox")
        .join(identity_inbox_relative_path(&to)?)
        .join(&request_id);
    let result_path = result_dir.join("result.yaml");
    let signature_path = result_dir.join("result.rhosig.yaml");
    let receipt_path = root
        .join("rho/run-receipts")
        .join(&request_id)
        .join(format!("{run_id}.yaml"));
    let receipt_signature_path = root
        .join("rho/run-receipts")
        .join(&request_id)
        .join(format!("{run_id}.rhosig.yaml"));
    ensure_parent(&result_path)?;
    let (output_yaml, attachment_path) = match &release_output.payload {
        ReleasePayload::Inline { value } => (
            format!(
                "  output: {}\n  output_sha256: {}\n",
                yaml_quote(value),
                yaml_quote(&release_output.output_sha256),
            ),
            None,
        ),
        ReleasePayload::Attachment { file_name, bytes } => {
            let attachment_relative = format!("attachments/{file_name}");
            let attachment_path = result_dir.join(&attachment_relative);
            ensure_parent(&attachment_path)?;
            fs::write(&attachment_path, bytes)?;
            (
                format!(
                    "  output_sha256: {}\n  attachments:\n    - path: {}\n      original_name: {}\n      sha256: {}\n",
                    yaml_quote(&release_output.output_sha256),
                    yaml_quote(&attachment_relative),
                    yaml_quote(file_name),
                    yaml_quote(&release_output.output_sha256),
                ),
                Some(attachment_path),
            )
        }
    };
    fs::write(
        &result_path,
        format!(
            "version: 1\nresult:\n  request_id: {}\n  from: {}\n  to: {}\n{}  run_id: {}\n  runner: {}\n  note: \"encrypted result released from approved run\"\n",
            yaml_quote(&request_id),
            yaml_quote(&from),
            yaml_quote(&to),
            output_yaml,
            yaml_quote(&run_id),
            yaml_quote(&runner),
        ),
    )?;
    sign_file_with_context(SignatureWrite {
        signed_path: &result_path,
        signature_path: &signature_path,
        identity: &from,
        repo_id: &repo_id,
        request_id: &request_id,
        recipient: &to,
        purpose: "rho.result",
    })?;
    let wrote_receipt = write_run_receipt_if_available(RunReceiptWrite {
        root: &root,
        repo_id: &repo_id,
        request_id: &request_id,
        run_id: &run_id,
        runner: &runner,
        result_path: &result_path,
        receipt_path: &receipt_path,
        receipt_signature_path: &receipt_signature_path,
        output_sha256: &release_output.output_sha256,
        signer: &from,
        recipient: &to,
    })?;

    if !has_flag(rest, "--no-stage") {
        if let Some(path) = &attachment_path {
            git_add_with_identity(&root, path, Some(&from))?;
            verify_staged_encrypted(&root, path)?;
        }
        git_add_with_identity(&root, &result_path, Some(&from))?;
        git_add_with_identity(&root, &signature_path, Some(&from))?;
        verify_staged_encrypted(&root, &result_path)?;
        verify_staged_encrypted(&root, &signature_path)?;
        println!("staged encrypted result files");
        if wrote_receipt {
            git_add(&root, &receipt_path)?;
            git_add(&root, &receipt_signature_path)?;
            println!("staged signed run receipt");
        }
    }

    println!("{}", result_path.display());
    if let Some(path) = &attachment_path {
        println!("{}", path.display());
    }
    println!("{}", signature_path.display());
    if wrote_receipt {
        println!("{}", receipt_path.display());
        println!("{}", receipt_signature_path.display());
    }
    println!("result branch: {from_handle}/result-{request_id}");
    let mut release_paths = Vec::new();
    if let Some(path) = &attachment_path {
        release_paths.push(path.as_path());
    }
    release_paths.push(result_path.as_path());
    release_paths.push(signature_path.as_path());
    if wrote_receipt {
        release_paths.push(receipt_path.as_path());
        release_paths.push(receipt_signature_path.as_path());
    }
    finish_branch_work(rest, &root, &request_id, &release_paths)?;
    println!("next: rho commit -m \"Release encrypted result for {request_id}\"");
    print_create_pr_next_step(
        &root,
        &format!("Release encrypted result for {request_id}"),
        &format!("Release signed encrypted result {request_id} to {to}."),
    );
    Ok(())
}

fn release_pr(rest: &[String]) -> RhoResult<()> {
    let pr_number = rest.first().cloned().unwrap_or_else(|| usage());
    let root = rho_core::arg_value(rest, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    if !has_flag(rest, "--no-sync") {
        run_rho_command(&[
            "repo".to_string(),
            "sync".to_string(),
            "--root".to_string(),
            root.display().to_string(),
        ])?;
    }
    let pr = gh_pr_metadata(&root, &pr_number)?;
    gh_pr_checkout(&root, &pr_number)?;
    let request_id = match rho_core::arg_value(rest, "--request-id") {
        Some(request_id) => request_id,
        None => find_single_repo_request_id(&root)?,
    };
    validate_request_id(&request_id)?;

    let want_pr = has_flag(rest, "--pr");
    let want_commit = want_pr || has_flag(rest, "--commit");
    let want_push = want_pr || has_flag(rest, "--push");
    let mut release_args = vec![
        request_id.clone(),
        "--root".to_string(),
        root.display().to_string(),
        "--no-branch".to_string(),
    ];
    for flag in [
        "--run-id",
        "--to",
        "--from",
        "--output",
        "--value",
        "--inline-max-bytes",
        "--runner",
    ] {
        if let Some(value) = rho_core::arg_value(rest, flag) {
            release_args.push(flag.to_string());
            release_args.push(value);
        }
    }
    for flag in ["--allow-missing-request", "--no-stage"] {
        if has_flag(rest, flag) {
            release_args.push(flag.to_string());
        }
    }
    if want_commit {
        release_args.push("--commit".to_string());
    }
    release(&release_args)?;

    if want_push {
        let target = github_ssh_url(&format!(
            "{}/{}",
            pr.head_repository_owner.login, pr.head_repository.name
        ));
        git_push_head_to_url(&root, &target, &pr.head_ref_name)?;
        println!(
            "pushed: {}:{}",
            pr.head_repository_owner.login, pr.head_ref_name
        );
    }
    if want_pr {
        println!("pull request: {}", pr.url);
    }
    Ok(())
}

fn print_create_pr_next_step(root: &Path, title: &str, body: &str) {
    let root_arg = root.display().to_string();
    println!(
        "next: rho repo create-pr --root {} --title {} --body {}",
        shell_quote(&root_arg),
        shell_quote(title),
        shell_quote(body)
    );
}

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

fn verify(rest: &[String]) -> RhoResult<()> {
    let request_id = rest.first().cloned().unwrap_or_else(|| usage());
    validate_request_id(&request_id)?;
    let root = rho_core::arg_value(rest, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    let expected_recipient = rho_core::arg_value(rest, "--to")
        .map(|value| normalize_actor_id(&value))
        .transpose()?;
    let repo_id = read_repo_id(&root)?;
    let result_path = locate_result_path(&root, &request_id, expected_recipient.as_deref())?;
    let result_signature_path = result_path.with_file_name("result.rhosig.yaml");
    refresh_result_file_if_needed(&root, &result_path)?;
    refresh_result_file_if_needed(&root, &result_signature_path)?;
    let result: ResultVerifyManifest = from_yaml(&fs::read_to_string(&result_path)?)?;
    let result = result.result;

    if result.request_id != request_id {
        return Err(format!(
            "result request_id mismatch: expected {request_id}, got {}",
            result.request_id
        )
        .into());
    }
    if let Some(expected) = expected_recipient.as_deref()
        && result.to != expected
    {
        return Err(format!(
            "result recipient mismatch: expected {expected}, got {}",
            result.to
        )
        .into());
    }
    verify_file_with_context(SignatureVerify {
        signed_path: &result_path,
        signature_path: &result_signature_path,
        identity: &result.from,
        repo_root: &root,
        repo_id: &repo_id,
        request_id: &request_id,
        recipient: &result.to,
        purpose: "rho.result",
    })?;

    let receipt_path = root
        .join("rho/run-receipts")
        .join(&request_id)
        .join(format!("{}.yaml", result.run_id));
    let receipt_signature_path = root
        .join("rho/run-receipts")
        .join(&request_id)
        .join(format!("{}.rhosig.yaml", result.run_id));
    let receipt: RunReceiptVerifyManifest = from_yaml(&fs::read_to_string(&receipt_path)?)?;
    let receipt = receipt.run_receipt;
    check_result_receipt_binding(&root, &result_path, &result, &receipt)?;
    verify_file_with_context(SignatureVerify {
        signed_path: &receipt_path,
        signature_path: &receipt_signature_path,
        identity: &result.from,
        repo_root: &root,
        repo_id: &repo_id,
        request_id: &request_id,
        recipient: &result.to,
        purpose: "rho.run_receipt",
    })?;

    let action_id = action_id_for_run_id(&result.run_id)?;
    let grant_path = root
        .join("rho/approval-grants")
        .join(&request_id)
        .join(format!("{action_id}.yaml"));
    let grant_signature_path = root
        .join("rho/approval-grants")
        .join(&request_id)
        .join(format!("{action_id}.rhosig.yaml"));
    let grant: ApprovalGrantVerifyManifest = from_yaml(&fs::read_to_string(&grant_path)?)?;
    let grant = grant.approval_grant;
    check_receipt_grant_binding(&result.from, &receipt, &grant, &action_id)?;
    verify_file_with_context(SignatureVerify {
        signed_path: &grant_path,
        signature_path: &grant_signature_path,
        identity: &result.from,
        repo_root: &root,
        repo_id: &repo_id,
        request_id: &request_id,
        recipient: &result.to,
        purpose: "rho.approval_grant",
    })?;

    println!(
        "result: verified {}",
        repo_relative_path(&root, &result_path)?
    );
    println!("  request_id: {}", result.request_id);
    println!("  run_id: {}", result.run_id);
    println!("  from: {}", result.from);
    println!("  to: {}", result.to);
    println!("  output_sha256: {}", result.output_sha256);
    println!(
        "run receipt: verified {}",
        repo_relative_path(&root, &receipt_path)?
    );
    println!("  runner: {}", receipt.runner);
    println!("  tier: {}", receipt.tier);
    println!("  code_sha256: {}", receipt.code_sha256);
    println!("  input_sha256: {}", receipt.input_sha256);
    println!(
        "approval grant: verified {}",
        repo_relative_path(&root, &grant_path)?
    );
    println!("  action_id: {}", grant.action_id);
    println!("  decision: {}", grant.decision);
    println!("  granted_by: {}", grant.granted_by);
    Ok(())
}

struct ReleaseOutput {
    output_sha256: String,
    payload: ReleasePayload,
}

enum ReleasePayload {
    Inline { value: String },
    Attachment { file_name: String, bytes: Vec<u8> },
}

#[derive(Debug, Deserialize)]
struct GhPrMetadata {
    #[serde(rename = "headRefName")]
    head_ref_name: String,
    #[serde(rename = "headRepository")]
    head_repository: GhPrRepository,
    #[serde(rename = "headRepositoryOwner")]
    head_repository_owner: GhPrOwner,
    url: String,
}

#[derive(Debug, Deserialize)]
struct GhPrRepository {
    name: String,
}

#[derive(Debug, Deserialize)]
struct GhPrOwner {
    login: String,
}

#[derive(Debug, Deserialize)]
struct ResultVerifyManifest {
    result: ResultVerifyRecord,
}

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

#[derive(Debug, Deserialize)]
struct RunReceiptVerifyManifest {
    run_receipt: RunReceiptVerifyRecord,
}

#[derive(Debug, Deserialize)]
struct RunReceiptVerifyRecord {
    request_id: String,
    run_id: String,
    runner: String,
    status: String,
    tier: String,
    code_path: String,
    code_sha256: String,
    input_sha256: String,
    output_sha256: String,
    result_path: String,
}

#[derive(Debug, Deserialize)]
struct ApprovalGrantVerifyManifest {
    approval_grant: ApprovalGrantVerifyRecord,
}

#[derive(Debug, Deserialize)]
struct ApprovalGrantVerifyRecord {
    action_id: String,
    request_id: String,
    decision: String,
    granted_by: String,
    code: ApprovalGrantCodeVerifyRecord,
    private_input: ApprovalGrantPrivateInputVerifyRecord,
}

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

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

fn read_release_output(root: &Path, run_id: &str, rest: &[String]) -> RhoResult<ReleaseOutput> {
    if let Some(value) = rho_core::arg_value(rest, "--value") {
        let output_sha256 = sha256_hex(value.as_bytes());
        if value.len() > inline_result_max_bytes(rest)? {
            return Ok(ReleaseOutput {
                output_sha256,
                payload: ReleasePayload::Attachment {
                    file_name: "output.txt".to_string(),
                    bytes: value.into_bytes(),
                },
            });
        }
        return Ok(ReleaseOutput {
            output_sha256,
            payload: ReleasePayload::Inline { value },
        });
    }

    let path = if let Some(path) = rho_core::arg_value(rest, "--output") {
        PathBuf::from(path)
    } else {
        root.join(".rho/runs").join(run_id).join("stdout.txt")
    };
    let bytes = fs::read(&path).map_err(|_| {
        format!(
            "missing run output; pass --output or --value: {}",
            path.display()
        )
    })?;
    let file_name = attachment_file_name(&path)?;
    let output_sha256 = sha256_hex(&bytes);
    Ok(ReleaseOutput {
        output_sha256,
        payload: ReleasePayload::Attachment { file_name, bytes },
    })
}

fn inline_result_max_bytes(rest: &[String]) -> RhoResult<usize> {
    rho_core::arg_value(rest, "--inline-max-bytes")
        .map(|value| {
            value
                .parse::<usize>()
                .map_err(|_| format!("invalid --inline-max-bytes: {value}").into())
        })
        .unwrap_or(Ok(DEFAULT_INLINE_RESULT_MAX_BYTES))
}

fn locate_result_path(
    root: &Path,
    request_id: &str,
    expected_recipient: Option<&str>,
) -> RhoResult<PathBuf> {
    if let Some(recipient) = expected_recipient {
        for relative in [
            identity_inbox_relative_path(recipient)?,
            legacy_identity_inbox_relative_path(recipient)?,
        ] {
            let path = root
                .join("rho/messages/inbox")
                .join(relative)
                .join(request_id)
                .join("result.yaml");
            if path.is_file() {
                return Ok(path);
            }
        }
        return Err(format!("result not found for {request_id} and recipient {recipient}").into());
    }

    let inbox = root.join("rho/messages/inbox");
    let mut matches = Vec::new();
    if inbox.is_dir() {
        collect_result_paths(&inbox, request_id, &mut matches)?;
    }
    matches.sort();
    match matches.len() {
        1 => Ok(matches.remove(0)),
        0 => Err(format!("result not found for request {request_id}").into()),
        _ => Err(format!(
            "multiple results found for request {request_id}; pass --to to select recipient"
        )
        .into()),
    }
}

fn collect_result_paths(dir: &Path, request_id: &str, matches: &mut Vec<PathBuf>) -> RhoResult<()> {
    for entry in fs::read_dir(dir)? {
        let path = entry?.path();
        if !path.is_dir() {
            continue;
        }
        let result_path = path.join(request_id).join("result.yaml");
        if result_path.is_file() {
            matches.push(result_path);
        }
        collect_result_paths(&path, request_id, matches)?;
    }
    Ok(())
}

fn check_result_receipt_binding(
    root: &Path,
    result_path: &Path,
    result: &ResultVerifyRecord,
    receipt: &RunReceiptVerifyRecord,
) -> RhoResult<()> {
    if receipt.request_id != result.request_id {
        return Err(format!(
            "run receipt request_id mismatch: expected {}, got {}",
            result.request_id, receipt.request_id
        )
        .into());
    }
    if receipt.run_id != result.run_id {
        return Err(format!(
            "run receipt run_id mismatch: expected {}, got {}",
            result.run_id, receipt.run_id
        )
        .into());
    }
    if receipt.status != "completed" {
        return Err(format!("run receipt status is not completed: {}", receipt.status).into());
    }
    if receipt.output_sha256 != result.output_sha256 {
        return Err(format!(
            "run receipt output digest mismatch: expected {}, got {}",
            result.output_sha256, receipt.output_sha256
        )
        .into());
    }
    let expected_result_path = repo_relative_path(root, result_path)?;
    if receipt.result_path != expected_result_path {
        return Err(format!(
            "run receipt result_path mismatch: expected {expected_result_path}, got {}",
            receipt.result_path
        )
        .into());
    }
    for (label, value) in [
        ("runner", receipt.runner.as_str()),
        ("tier", receipt.tier.as_str()),
        ("code_path", receipt.code_path.as_str()),
        ("code_sha256", receipt.code_sha256.as_str()),
        ("output_sha256", receipt.output_sha256.as_str()),
    ] {
        if value.trim().is_empty() {
            return Err(format!("run receipt missing {label}").into());
        }
    }
    if receipt.tier == "real" && receipt.input_sha256.trim().is_empty() {
        return Err("real run receipt missing input_sha256".into());
    }
    Ok(())
}

fn check_receipt_grant_binding(
    expected_owner: &str,
    receipt: &RunReceiptVerifyRecord,
    grant: &ApprovalGrantVerifyRecord,
    action_id: &str,
) -> RhoResult<()> {
    if grant.action_id != action_id {
        return Err(format!(
            "approval grant action_id mismatch: expected {action_id}, got {}",
            grant.action_id
        )
        .into());
    }
    if grant.request_id != receipt.request_id {
        return Err(format!(
            "approval grant request_id mismatch: expected {}, got {}",
            receipt.request_id, grant.request_id
        )
        .into());
    }
    if grant.decision != "approved" {
        return Err(format!(
            "approval grant decision is not approved: {}",
            grant.decision
        )
        .into());
    }
    if grant.granted_by != expected_owner {
        return Err(format!(
            "approval grant granted_by mismatch: expected {expected_owner}, got {}",
            grant.granted_by
        )
        .into());
    }
    if grant.code.sha256 != receipt.code_sha256 {
        return Err(format!(
            "approval grant code digest mismatch: expected {}, got {}",
            receipt.code_sha256, grant.code.sha256
        )
        .into());
    }
    if grant.private_input.tier != receipt.tier {
        return Err(format!(
            "approval grant tier mismatch: expected {}, got {}",
            receipt.tier, grant.private_input.tier
        )
        .into());
    }
    if grant.private_input.sha256 != receipt.input_sha256 {
        return Err(format!(
            "approval grant input digest mismatch: expected {}, got {}",
            receipt.input_sha256, grant.private_input.sha256
        )
        .into());
    }
    Ok(())
}

fn action_id_for_run_id(run_id: &str) -> RhoResult<String> {
    run_id
        .strip_prefix("run-")
        .map(|suffix| format!("act-{suffix}"))
        .ok_or_else(|| {
            format!("run id has no conventional approval grant mapping: {run_id}").into()
        })
}

fn attachment_file_name(path: &Path) -> RhoResult<String> {
    let file_name = path
        .file_name()
        .and_then(|value| value.to_str())
        .ok_or_else(|| format!("output path has no valid filename: {}", path.display()))?;
    if file_name.is_empty()
        || file_name == "."
        || file_name == ".."
        || file_name.contains('/')
        || file_name.contains('\\')
        || file_name.chars().any(char::is_control)
    {
        return Err(format!("unsupported output filename: {file_name}").into());
    }
    Ok(file_name.to_string())
}

struct RunReceiptWrite<'a> {
    root: &'a Path,
    repo_id: &'a str,
    request_id: &'a str,
    run_id: &'a str,
    runner: &'a str,
    result_path: &'a Path,
    receipt_path: &'a Path,
    receipt_signature_path: &'a Path,
    output_sha256: &'a str,
    signer: &'a str,
    recipient: &'a str,
}

fn write_run_receipt_if_available(receipt: RunReceiptWrite<'_>) -> RhoResult<bool> {
    let run_manifest_path = receipt
        .root
        .join(".rho/runs")
        .join(format!("{}.yaml", receipt.run_id));
    if !run_manifest_path.is_file() {
        return Ok(false);
    }
    let text = fs::read_to_string(&run_manifest_path)?;
    let manifest: RunManifest = from_yaml(&text)?;
    if manifest.run.id != receipt.run_id {
        return Err(format!(
            "run manifest id mismatch: expected {}, got {}",
            receipt.run_id, manifest.run.id
        )
        .into());
    }
    if manifest.run.request_id != receipt.request_id {
        return Err(format!(
            "run manifest request mismatch: expected {}, got {}",
            receipt.request_id, manifest.run.request_id
        )
        .into());
    }
    let result_repo_path = repo_relative_path(receipt.root, receipt.result_path)?;
    let input_sha256 = run_input_commitment(receipt.root, &manifest.run)?;
    if receipt.receipt_path.is_file() {
        validate_existing_run_receipt(&manifest, &receipt, input_sha256.as_deref())?;
        verify_file_with_context(SignatureVerify {
            signed_path: receipt.receipt_path,
            signature_path: receipt.receipt_signature_path,
            identity: receipt.signer,
            repo_root: receipt.root,
            repo_id: receipt.repo_id,
            request_id: receipt.request_id,
            recipient: receipt.recipient,
            purpose: "rho.run_receipt",
        })?;
        return Ok(true);
    }
    ensure_parent(receipt.receipt_path)?;
    fs::write(
        receipt.receipt_path,
        format!(
            "version: 1\nrun_receipt:\n  request_id: {}\n  run_id: {}\n  runner: {}\n  status: {}\n  tier: {}\n  code_path: {}\n  code_sha256: {}\n  input_sha256: {}\n  output_sha256: {}\n  result_path: {}\n  created_at: {}\n",
            yaml_quote(receipt.request_id),
            yaml_quote(receipt.run_id),
            yaml_quote(manifest.run.runner.as_deref().unwrap_or(receipt.runner)),
            yaml_quote(&manifest.run.status),
            yaml_quote(&manifest.run.tier),
            yaml_quote(manifest.run.code_path.as_deref().unwrap_or("")),
            yaml_quote(manifest.run.code_sha256.as_deref().unwrap_or("")),
            yaml_quote(input_sha256.as_deref().unwrap_or("")),
            yaml_quote(receipt.output_sha256),
            yaml_quote(&result_repo_path),
            yaml_quote(&rho_core::now_rfc3339()),
        ),
    )?;
    sign_file_with_context(SignatureWrite {
        signed_path: receipt.receipt_path,
        signature_path: receipt.receipt_signature_path,
        identity: receipt.signer,
        repo_id: receipt.repo_id,
        request_id: receipt.request_id,
        recipient: receipt.recipient,
        purpose: "rho.run_receipt",
    })?;
    Ok(true)
}

fn validate_existing_run_receipt(
    manifest: &RunManifest,
    receipt: &RunReceiptWrite<'_>,
    input_sha256: Option<&str>,
) -> RhoResult<()> {
    if !receipt.receipt_signature_path.is_file() {
        return Err(format!(
            "existing run receipt signature missing: {}",
            receipt.receipt_signature_path.display()
        )
        .into());
    }
    let existing: RunReceiptVerifyManifest = from_yaml(&fs::read_to_string(receipt.receipt_path)?)?;
    let existing = existing.run_receipt;
    let result_repo_path = repo_relative_path(receipt.root, receipt.result_path)?;
    for (label, expected, actual) in [
        (
            "request_id",
            receipt.request_id,
            existing.request_id.as_str(),
        ),
        ("run_id", receipt.run_id, existing.run_id.as_str()),
        (
            "status",
            manifest.run.status.as_str(),
            existing.status.as_str(),
        ),
        ("tier", manifest.run.tier.as_str(), existing.tier.as_str()),
        (
            "output_sha256",
            receipt.output_sha256,
            existing.output_sha256.as_str(),
        ),
        (
            "result_path",
            result_repo_path.as_str(),
            existing.result_path.as_str(),
        ),
    ] {
        if expected != actual {
            return Err(format!(
                "existing run receipt {label} mismatch: expected {expected}, got {actual}"
            )
            .into());
        }
    }
    if let Some(runner) = manifest.run.runner.as_deref()
        && existing.runner != runner
    {
        return Err(format!(
            "existing run receipt runner mismatch: expected {runner}, got {}",
            existing.runner
        )
        .into());
    }
    if let Some(code_sha256) = manifest.run.code_sha256.as_deref()
        && existing.code_sha256 != code_sha256
    {
        return Err(format!(
            "existing run receipt code digest mismatch: expected {code_sha256}, got {}",
            existing.code_sha256
        )
        .into());
    }
    let expected_input = input_sha256.unwrap_or("");
    if existing.input_sha256 != expected_input {
        return Err(format!(
            "existing run receipt input digest mismatch: expected {expected_input}, got {}",
            existing.input_sha256
        )
        .into());
    }
    Ok(())
}

fn preflight_run_input_commitment(root: &Path, request_id: &str, run_id: &str) -> RhoResult<()> {
    let run_manifest_path = root.join(".rho/runs").join(format!("{run_id}.yaml"));
    if !run_manifest_path.is_file() {
        return Ok(());
    }
    let text = fs::read_to_string(&run_manifest_path)?;
    let manifest: RunManifest = from_yaml(&text)?;
    if manifest.run.id != run_id {
        return Err(format!(
            "run manifest id mismatch: expected {run_id}, got {}",
            manifest.run.id
        )
        .into());
    }
    if manifest.run.request_id != request_id {
        return Err(format!(
            "run manifest request mismatch: expected {request_id}, got {}",
            manifest.run.request_id
        )
        .into());
    }
    let _ = run_input_commitment(root, &manifest.run)?;
    Ok(())
}

fn run_input_commitment(root: &Path, run: &rho_core::RunRecord) -> RhoResult<Option<String>> {
    let Some(dataset_csv) = run.dataset_csv.as_deref() else {
        if run.tier == "real" {
            return Err("real run receipt requires dataset_csv for input commitment".into());
        }
        return Ok(None);
    };
    let path = Path::new(dataset_csv);
    let path = if path.is_absolute() {
        path.to_path_buf()
    } else {
        root.join(path)
    };
    let digest = file_digest(&path)
        .map_err(|error| format!("failed to digest run input {}: {error}", path.display()))?;
    Ok(Some(digest))
}

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

fn require_committed_request(root: &Path, request_id: &str) -> RhoResult<()> {
    let inbox = root.join("rho/messages/inbox");
    if request_exists(&inbox, request_id)? {
        return Ok(());
    }
    Err(format!(
        "request not found in repo; merge the request first or pass --allow-missing-request: {request_id}"
    )
    .into())
}

fn request_exists(dir: &Path, request_id: &str) -> RhoResult<bool> {
    if !dir.is_dir() {
        return Ok(false);
    }
    for entry in fs::read_dir(dir)? {
        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 Ok(true);
            }
            if request_exists(&path, request_id)? {
                return Ok(true);
            }
        }
    }
    Ok(false)
}

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

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

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

struct SignatureWrite<'a> {
    signed_path: &'a Path,
    signature_path: &'a Path,
    identity: &'a str,
    repo_id: &'a str,
    request_id: &'a str,
    recipient: &'a str,
    purpose: &'a str,
}

struct SignatureVerify<'a> {
    signed_path: &'a Path,
    signature_path: &'a Path,
    identity: &'a str,
    repo_root: &'a Path,
    repo_id: &'a str,
    request_id: &'a str,
    recipient: &'a str,
    purpose: &'a str,
}

fn sign_file_with_context(signature: SignatureWrite<'_>) -> RhoResult<()> {
    let mut rho_crypto = rho_crypto_command();
    let status = rho_crypto
        .arg("sign")
        .arg(signature.signed_path)
        .arg("--identity")
        .arg(signature.identity)
        .arg("--out")
        .arg(signature.signature_path)
        .arg("--repo-id")
        .arg(signature.repo_id)
        .arg("--request-id")
        .arg(signature.request_id)
        .arg("--recipient")
        .arg(signature.recipient)
        .arg("--purpose")
        .arg(signature.purpose)
        .status()?;
    if !status.success() {
        return Err(format!("failed to sign file: {}", signature.signed_path.display()).into());
    }
    Ok(())
}

fn verify_file_with_context(signature: SignatureVerify<'_>) -> RhoResult<()> {
    if !signature.signature_path.is_file() {
        return Err(format!("signature missing: {}", signature.signature_path.display()).into());
    }
    let output = rho_crypto_command()
        .arg("verify")
        .arg(signature.signed_path)
        .arg("--signature")
        .arg(signature.signature_path)
        .arg("--identity")
        .arg(signature.identity)
        .arg("--repo-root")
        .arg(signature.repo_root)
        .arg("--repo-id")
        .arg(signature.repo_id)
        .arg("--request-id")
        .arg(signature.request_id)
        .arg("--recipient")
        .arg(signature.recipient)
        .arg("--purpose")
        .arg(signature.purpose)
        .output()?;
    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(format!(
            "signature verification failed for {}: {}",
            signature.signed_path.display(),
            stderr.trim()
        )
        .into());
    }
    Ok(())
}

fn refresh_result_file_if_needed(root: &Path, path: &Path) -> RhoResult<()> {
    let text = fs::read_to_string(path)?;
    if !is_rho_encrypted_text(&text) {
        return Ok(());
    }
    let relative_path = repo_relative_path(root, path)?;
    let plaintext = smudge_result_file_text(root, &relative_path, &text)?;
    if is_rho_encrypted_text(&plaintext) {
        return Err(format!(
            "cannot decrypt protected result file {}; run with a RHO_HOME/RHO_IDENTITY that can open it",
            path.display()
        )
        .into());
    }
    fs::write(path, plaintext)?;
    println!("refreshed protected file: {relative_path}");
    Ok(())
}

fn smudge_result_file_text(root: &Path, relative_path: &str, text: &str) -> RhoResult<String> {
    let mut rho_crypto = rho_crypto_command();
    let mut child = rho_crypto
        .arg("smudge")
        .arg("--repo-root")
        .arg(root)
        .arg("--path")
        .arg(relative_path)
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(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() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(format!(
            "rho crypto smudge failed for {relative_path}: {}",
            stderr.trim()
        )
        .into());
    }
    Ok(String::from_utf8(output.stdout)?)
}

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 git_add(root: &Path, path: &Path) -> RhoResult<()> {
    git_add_with_identity(root, path, None)
}

fn finish_branch_work(
    args: &[String],
    root: &Path,
    request_id: &str,
    stage_paths: &[&Path],
) -> RhoResult<()> {
    let want_pr = rho_core::has_flag(args, "--pr");
    let want_commit = want_pr || rho_core::has_flag(args, "--commit");
    let want_push = want_pr || rho_core::has_flag(args, "--push");
    if !want_commit && !want_push && !want_pr {
        return Ok(());
    }
    if want_commit {
        for path in stage_paths {
            git_add(root, path)?;
        }
        git_commit(root, &format!("Release encrypted result for {request_id}"))?;
        println!("committed: Release encrypted result for {request_id}");
    }
    if want_push {
        git_push_current_branch(root)?;
        println!("pushed: {}", current_branch(root)?);
    }
    if want_pr {
        create_pr(
            root,
            &format!("Release encrypted result for {request_id}"),
            &format!("Signed encrypted result for {request_id}."),
        )?;
    }
    Ok(())
}

fn git_commit(root: &Path, message: &str) -> RhoResult<()> {
    let exe = env::current_exe()?;
    let status = Command::new(exe)
        .current_dir(root)
        .arg("commit")
        .arg("-m")
        .arg(message)
        .status()?;
    if !status.success() {
        return Err(format!("rho commit failed in {}", root.display()).into());
    }
    Ok(())
}

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

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

fn github_ssh_url(repo_slug: &str) -> String {
    format!("git@github.com:{repo_slug}.git")
}

fn gh_pr_metadata(root: &Path, pr_number: &str) -> RhoResult<GhPrMetadata> {
    let output = Command::new("gh")
        .current_dir(root)
        .args([
            "pr",
            "view",
            pr_number,
            "--json",
            "headRefName,headRepository,headRepositoryOwner,url",
        ])
        .output()?;
    if !output.status.success() {
        return Err(format!("gh pr view failed for {pr_number}").into());
    }
    from_json(&String::from_utf8(output.stdout)?)
}

fn gh_pr_checkout(root: &Path, pr_number: &str) -> RhoResult<()> {
    let status = Command::new("gh")
        .current_dir(root)
        .args(["pr", "checkout", pr_number])
        .status()?;
    if !status.success() {
        return Err(format!("gh pr checkout failed for {pr_number}").into());
    }
    Ok(())
}

fn current_branch(root: &Path) -> RhoResult<String> {
    let output = Command::new("git")
        .arg("-C")
        .arg(root)
        .args(["branch", "--show-current"])
        .output()?;
    if !output.status.success() {
        return Err(format!("git branch --show-current failed in {}", root.display()).into());
    }
    let branch = String::from_utf8(output.stdout)?.trim().to_string();
    if branch.is_empty() {
        return Err("current Git branch is empty".into());
    }
    Ok(branch)
}

fn find_single_repo_request_id(root: &Path) -> RhoResult<String> {
    let mut ids = Vec::new();
    collect_repo_request_ids(&root.join("rho/messages/inbox"), &mut ids)?;
    ids.sort();
    ids.dedup();
    match ids.as_slice() {
        [id] => Ok(id.clone()),
        [] => Err("no request found in checked-out PR; pass --request-id".into()),
        _ => Err(format!(
            "multiple requests found in checked-out PR: {}; pass --request-id",
            ids.join(", ")
        )
        .into()),
    }
}

fn collect_repo_request_ids(dir: &Path, ids: &mut Vec<String>) -> RhoResult<()> {
    if !dir.is_dir() {
        return Ok(());
    }
    for entry in fs::read_dir(dir)? {
        let entry = entry?;
        let path = entry.path();
        if path.is_dir() {
            if path.join("request.yaml").is_file()
                && let Some(id) = path.file_name().and_then(|value| value.to_str())
            {
                ids.push(id.to_string());
            }
            collect_repo_request_ids(&path, ids)?;
        }
    }
    Ok(())
}

fn create_pr(root: &Path, title: &str, body: &str) -> RhoResult<()> {
    let exe = env::current_exe()?;
    let status = Command::new(exe)
        .arg("repo")
        .arg("create-pr")
        .arg("--root")
        .arg(root)
        .arg("--title")
        .arg(title)
        .arg("--body")
        .arg(body)
        .status()?;
    if !status.success() {
        return Err(format!("rho repo create-pr failed in {}", root.display()).into());
    }
    Ok(())
}

fn run_rho_command(args: &[String]) -> RhoResult<()> {
    let exe = env::current_exe()?;
    let status = Command::new(exe).args(args).status()?;
    if !status.success() {
        return Err(format!("rho command failed: {}", args.join(" ")).into());
    }
    Ok(())
}

fn git_add_with_identity(root: &Path, path: &Path, identity: Option<&str>) -> RhoResult<()> {
    let mut command = Command::new("git");
    command.arg("-C").arg(root).arg("add").arg(path);
    if let Some(identity) = identity {
        command.env("RHO_IDENTITY", identity);
    }
    let status = command.status()?;
    if !status.success() {
        return Err(format!("git add failed: {}", path.display()).into());
    }
    Ok(())
}

fn verify_staged_encrypted(root: &Path, path: &Path) -> RhoResult<()> {
    let relative = path
        .strip_prefix(root)
        .map_err(|_| format!("path is not under repo root: {}", path.display()))?;
    let output = Command::new("git")
        .arg("-C")
        .arg(root)
        .args(["show", &format!(":{}", relative.display())])
        .output()?;
    if !output.status.success() {
        return Err(format!("staged blob not found: {}", relative.display()).into());
    }
    let text = String::from_utf8_lossy(&output.stdout);
    if is_rho_encrypted_text(&text) {
        return Ok(());
    }
    Err(format!("staged result is not encrypted: {}", relative.display()).into())
}

fn repo_relative_path(root: &Path, path: &Path) -> RhoResult<String> {
    let relative = path
        .strip_prefix(root)
        .map_err(|_| format!("path is not under repo root: {}", path.display()))?;
    Ok(relative
        .to_str()
        .ok_or_else(|| -> Box<dyn std::error::Error> { "path is not valid UTF-8".into() })?
        .to_string())
}

fn sha256_hex(bytes: &[u8]) -> String {
    let digest = Sha256::digest(bytes);
    digest.iter().map(|byte| format!("{byte:02x}")).collect()
}

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 github_handle_from_identity_id(identity_id: &str) -> RhoResult<String> {
    providers::github::handle_from_identity_id(identity_id)
}

fn identity_inbox_relative_path(identity_id: &str) -> RhoResult<PathBuf> {
    providers::identity_inbox_relative_path(identity_id)
}

fn legacy_identity_inbox_relative_path(identity_id: &str) -> RhoResult<PathBuf> {
    Ok(PathBuf::from(github_handle_from_identity_id(identity_id)?))
}

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

    #[test]
    fn release_value_uses_attachment_when_over_inline_limit() {
        let small_args = vec![
            "--value".to_string(),
            "small".to_string(),
            "--inline-max-bytes".to_string(),
            "8".to_string(),
        ];
        let small = read_release_output(Path::new("."), "run-test", &small_args).unwrap();
        assert!(matches!(small.payload, ReleasePayload::Inline { .. }));

        let large_args = vec![
            "--value".to_string(),
            "too large".to_string(),
            "--inline-max-bytes".to_string(),
            "3".to_string(),
        ];
        let large = read_release_output(Path::new("."), "run-test", &large_args).unwrap();
        match large.payload {
            ReleasePayload::Attachment { file_name, bytes } => {
                assert_eq!(file_name, "output.txt");
                assert_eq!(bytes, b"too large");
            }
            ReleasePayload::Inline { .. } => panic!("large value should become an attachment"),
        }
    }

    #[test]
    fn finds_single_request_id_from_repo_inbox() {
        let root = env::temp_dir().join(format!(
            "rho-result-request-test-{}-{}",
            std::process::id(),
            rho_core::uuid_like()
        ));
        let request_dir = root.join("rho/messages/inbox/id/github/owner/req-test-001");
        fs::create_dir_all(&request_dir).unwrap();
        fs::write(request_dir.join("request.yaml"), "version: 1\n").unwrap();
        assert_eq!(
            find_single_repo_request_id(&root).unwrap(),
            "req-test-001".to_string()
        );
        fs::remove_dir_all(root).unwrap();
    }
}
