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

use rho_core::{
    RhoResult, ensure_parent, from_yaml, 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]"
    );
    std::process::exit(2);
}

pub fn run(args: &[String]) -> RhoResult<()> {
    if args.first().map(String::as_str) != Some("send") {
        usage();
    }
    let rest = &args[1..];
    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\n\
message:\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\n\
message:\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(())
}

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

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

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