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

use rho_core::{
    RhoResult, RunManifest, ensure_parent, file_digest, from_yaml, has_flag, normalize_actor_id,
    normalize_repo_id, 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>] [--runner <name>] [--no-branch] [--no-stage] [--allow-missing-request]"
    );
    std::process::exit(2);
}

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..]),
        _ => 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)?;
    let to_handle = github_handle_from_identity_id(&to)?;
    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(&to_handle)
        .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(&root, path)?;
            verify_staged_encrypted(&root, path)?;
        }
        git_add(&root, &result_path)?;
        git_add(&root, &signature_path)?;
        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}");
    println!("next: rho commit -m \"Release encrypted result for {request_id}\"");
    Ok(())
}

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

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

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

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 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<()> {
    let status = Command::new("git")
        .arg("-C")
        .arg(root)
        .arg("add")
        .arg(path)
        .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 text.contains("kind: rho_recipient_envelope") || text.contains("kind: rho_transparent_file")
    {
        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> {
    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())
}
