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

use rho_core::{
    RhoResult, ensure_parent, from_yaml, is_rho_encrypted_text, normalize_actor_id,
    normalize_repo_id, require_arg, uuid_like, yaml_quote,
};
use serde::Deserialize;

fn usage() -> ! {
    eprintln!(
        "usage: rho message send (--root <repo>|--shared-root <path>) --from <id> --to <id> --type <type> --text <text> [--related-request-id <id>] [--id <id>] [--no-stage]\n  rho message verify <message-id> --root <repo> [--to <id>]"
    );
    std::process::exit(2);
}

pub fn run(args: &[String]) -> RhoResult<()> {
    let Some(command) = args.first().map(String::as_str) else {
        usage();
    };
    let rest = &args[1..];
    if command == "verify" {
        return verify_repo_message(rest);
    }
    if command != "send" {
        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 message_type = require_arg(rest, "--type").unwrap_or_else(|_| usage());
    let text = require_arg(rest, "--text").unwrap_or_else(|_| usage());
    let id = rho_core::arg_value(rest, "--id").unwrap_or_else(|| format!("msg-{}", uuid_like()));
    validate_message_id(&id)?;
    let related = rho_core::arg_value(rest, "--related-request-id");
    if let Some(root) = rho_core::arg_value(rest, "--root") {
        if rho_core::arg_value(rest, "--shared-root").is_some() {
            return Err("--root and --shared-root cannot be used together".into());
        }
        return send_repo_message(SendMessage {
            root: PathBuf::from(root),
            from,
            to,
            message_type,
            text,
            id,
            related,
            stage: !rho_core::has_flag(rest, "--no-stage"),
        });
    }
    let shared_root = PathBuf::from(require_arg(rest, "--shared-root").unwrap_or_else(|_| usage()));
    send_legacy_message(
        &shared_root,
        &from,
        &to,
        &message_type,
        &text,
        &id,
        related.as_deref(),
    )
}

fn send_legacy_message(
    shared_root: &Path,
    from: &str,
    to: &str,
    message_type: &str,
    text: &str,
    id: &str,
    related: Option<&str>,
) -> RhoResult<()> {
    let path = shared_root
        .join(".rho/inbox")
        .join(to)
        .join(format!("{id}.yaml"));

    ensure_parent(&path)?;
    let related_line = related
        .map(|value| format!("  related_request_id: {}\n", yaml_quote(value)))
        .unwrap_or_default();
    fs::write(
        &path,
        format!(
            "version: 1\nmessage:\n  id: {}\n  from: {}\n  to: {}\n  type: {}\n{}  created_at: {}\n  body:\n    text: {}\n",
            yaml_quote(id),
            yaml_quote(from),
            yaml_quote(to),
            yaml_quote(message_type),
            related_line,
            yaml_quote(&rho_core::now_rfc3339()),
            yaml_quote(text),
        ),
    )?;
    println!("{}", path.display());
    Ok(())
}

struct SendMessage {
    root: PathBuf,
    from: String,
    to: String,
    message_type: String,
    text: String,
    id: String,
    related: Option<String>,
    stage: bool,
}

fn send_repo_message(message: SendMessage) -> RhoResult<()> {
    let repo_id = read_repo_id(&message.root)?;
    let message_dir = message
        .root
        .join("rho/messages/inbox")
        .join(identity_inbox_relative_path(&message.to)?)
        .join(&message.id);
    let message_path = message_dir.join("message.yaml");
    let signature_path = message_dir.join("message.rhosig.yaml");
    ensure_parent(&message_path)?;
    let related_line = message
        .related
        .as_ref()
        .map(|value| format!("  related_request_id: {}\n", yaml_quote(value)))
        .unwrap_or_default();
    fs::write(
        &message_path,
        format!(
            "version: 1\nmessage:\n  id: {}\n  from: {}\n  to: {}\n  type: {}\n{}  created_at: {}\n  body:\n    text: {}\n",
            yaml_quote(&message.id),
            yaml_quote(&message.from),
            yaml_quote(&message.to),
            yaml_quote(&message.message_type),
            related_line,
            yaml_quote(&rho_core::now_rfc3339()),
            yaml_quote(&message.text),
        ),
    )?;
    sign_message_file(
        &message.root,
        &repo_id,
        &message.id,
        &message.from,
        &message.to,
        &message_path,
        &signature_path,
    )?;
    if message.stage {
        git_add_with_identity(&message.root, &message_path, &message.from)?;
        git_add_with_identity(&message.root, &signature_path, &message.from)?;
        println!("staged encrypted message files");
    }
    println!("message: {}", message.id);
    println!(
        "path: {}",
        repo_relative_or_display(&message.root, &message_path)
    );
    println!(
        "signature: {}",
        repo_relative_or_display(&message.root, &signature_path)
    );
    Ok(())
}

fn verify_repo_message(rest: &[String]) -> RhoResult<()> {
    let id = rest.first().cloned().unwrap_or_else(|| usage());
    validate_message_id(&id)?;
    let root = PathBuf::from(require_arg(rest, "--root").unwrap_or_else(|_| usage()));
    let expected_recipient = rho_core::arg_value(rest, "--to")
        .map(|value| normalize_actor_id(&value))
        .transpose()?;
    let repo_id = read_repo_id(&root)?;
    let message_path = locate_message_path(&root, &id, expected_recipient.as_deref())?;
    let signature_path = message_path.with_file_name("message.rhosig.yaml");
    refresh_message_file_if_needed(&root, &message_path)?;
    refresh_message_file_if_needed(&root, &signature_path)?;
    let manifest: MessageVerifyManifest = from_yaml(&fs::read_to_string(&message_path)?)?;
    let message = manifest.message;
    if message.id != id {
        return Err(format!("message id mismatch: expected {id}, got {}", message.id).into());
    }
    if let Some(expected) = expected_recipient.as_deref()
        && message.to != expected
    {
        return Err(format!(
            "message recipient mismatch: expected {expected}, got {}",
            message.to
        )
        .into());
    }
    verify_message_file(VerifyMessage {
        root: &root,
        repo_id: &repo_id,
        message_id: &id,
        signer: &message.from,
        recipient: &message.to,
        message_path: &message_path,
        signature_path: &signature_path,
    })?;
    println!(
        "message: verified {}",
        repo_relative_or_display(&root, &message_path)
    );
    println!("  id: {}", message.id);
    println!("  from: {}", message.from);
    println!("  to: {}", message.to);
    println!("  type: {}", message.message_type);
    Ok(())
}

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

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

#[derive(Debug, Deserialize)]
struct MessageVerifyManifest {
    message: MessageVerifyRecord,
}

#[derive(Debug, Deserialize)]
struct MessageVerifyRecord {
    id: String,
    from: String,
    to: String,
    #[serde(rename = "type")]
    message_type: 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)
}

fn sign_message_file(
    root: &Path,
    repo_id: &str,
    message_id: &str,
    signer: &str,
    recipient: &str,
    message_path: &Path,
    signature_path: &Path,
) -> RhoResult<()> {
    let status = rho_crypto_command()
        .arg("sign")
        .arg(message_path)
        .arg("--identity")
        .arg(signer)
        .arg("--out")
        .arg(signature_path)
        .arg("--repo-id")
        .arg(repo_id)
        .arg("--message-id")
        .arg(message_id)
        .arg("--recipient")
        .arg(recipient)
        .arg("--purpose")
        .arg("rho.message")
        .current_dir(root)
        .status()?;
    if !status.success() {
        return Err(format!("failed to sign message: {}", message_path.display()).into());
    }
    Ok(())
}

struct VerifyMessage<'a> {
    root: &'a Path,
    repo_id: &'a str,
    message_id: &'a str,
    signer: &'a str,
    recipient: &'a str,
    message_path: &'a Path,
    signature_path: &'a Path,
}

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

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

fn rho_crypto_command() -> Command {
    let rho = std::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 locate_message_path(
    root: &Path,
    id: &str,
    expected_recipient: Option<&str>,
) -> RhoResult<PathBuf> {
    if let Some(recipient) = expected_recipient {
        let path = root
            .join("rho/messages/inbox")
            .join(identity_inbox_relative_path(recipient)?)
            .join(id)
            .join("message.yaml");
        if path.is_file() {
            return Ok(path);
        }
        return Err(format!("message not found for {id} and recipient {recipient}").into());
    }
    let inbox = root.join("rho/messages/inbox");
    let mut matches = Vec::new();
    if inbox.is_dir() {
        collect_message_paths(&inbox, id, &mut matches)?;
    }
    matches.sort();
    match matches.len() {
        1 => Ok(matches.remove(0)),
        0 => Err(format!("message not found: {id}").into()),
        _ => Err(format!("multiple messages found for {id}; pass --to to select recipient").into()),
    }
}

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

fn refresh_message_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_message_file_text(root, &relative_path, &text)?;
    if is_rho_encrypted_text(&plaintext) {
        return Err(format!(
            "cannot decrypt protected message 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_message_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 identity_inbox_relative_path(identity_id: &str) -> RhoResult<PathBuf> {
    let Some(handle) = identity_id.strip_prefix("rho://id/github/") else {
        return Err(format!("unsupported inbox identity: {identity_id}").into());
    };
    if handle.is_empty() || handle.contains('/') || handle.contains("..") {
        return Err(format!("invalid github identity handle: {identity_id}").into());
    }
    Ok(PathBuf::from("id").join("github").join(handle))
}

fn repo_relative_path(root: &Path, path: &Path) -> RhoResult<String> {
    Ok(path
        .strip_prefix(root)
        .map_err(|_| {
            format!(
                "path must be inside repo root {}: {}",
                root.display(),
                path.display()
            )
        })?
        .display()
        .to_string())
}

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

fn validate_message_id(value: &str) -> RhoResult<()> {
    if value.is_empty() {
        return Err("message id must not be empty".into());
    }
    if value.len() > 128 {
        return Err("message id must be 128 characters or fewer".into());
    }
    if !value
        .chars()
        .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '.' | '_' | '-'))
    {
        return Err(format!(
            "message id must contain only ASCII letters, digits, '.', '_', or '-': {value}"
        )
        .into());
    }
    Ok(())
}
