// Lets the command modules (which refer to crate items as `rho_core::...`) compile
// both here in the library and in the `rho` binary.
extern crate self as rho_core;

use std::ffi::OsStr;
use std::fs::{self, File};
use std::io::{self, Read};
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};

use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};

pub type RhoResult<T> = Result<T, Box<dyn std::error::Error>>;

pub mod commands;
pub mod providers;
pub mod storage;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RequestManifest {
    pub version: u32,
    pub request: RunRequest,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RunRequest {
    pub id: String,
    pub from: String,
    pub to: String,
    pub tool_id: String,
    pub dataset_uuid: String,
    pub code_paths: Vec<String>,
    pub code_sha256: String,
    pub command: Vec<String>,
    pub requested_tier: String,
    pub created_at: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ApprovalManifest {
    pub version: u32,
    pub approval: Approval,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Approval {
    pub request_id: String,
    pub decision: String,
    pub approver: String,
    pub note: String,
    pub created_at: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RunManifest {
    pub version: u32,
    pub run: RunRecord,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RunRecord {
    pub id: String,
    pub request_id: String,
    pub status: String,
    pub tier: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub runner: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub dataset_csv: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub code_path: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub code_sha256: Option<String>,
    pub command: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub exit_code: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
    pub stdout_path: String,
    pub stderr_path: String,
    pub created_at: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SandboxRunManifest {
    pub version: u32,
    pub sandbox_run: SandboxRunRecord,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SandboxRunRecord {
    pub id: String,
    pub request_id: String,
    pub runner: String,
    pub tier: String,
    pub dataset_csv: String,
    pub code_path: String,
    pub command: Vec<String>,
    pub artifact_dir: String,
    pub stdout_path: String,
    pub stderr_path: String,
    pub mounts: Vec<SandboxMount>,
    pub network: SandboxNetworkPolicy,
    pub created_at: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SandboxMount {
    pub host_path: String,
    pub guest_path: String,
    pub mode: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SandboxNetworkPolicy {
    pub default_deny: bool,
    #[serde(default)]
    pub allow_hosts: Vec<String>,
    #[serde(default)]
    pub tcp_maps: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ControlledActionManifest {
    pub version: u32,
    pub kind: String,
    pub action: ControlledAction,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ControlledAction {
    pub action_id: String,
    pub request_id: String,
    pub tool_id: String,
    pub requested_by: String,
    pub requested_for: String,
    pub action_type: String,
    pub summary: String,
    pub reason: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub input_path: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub script_path: Option<String>,
    pub output_path: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ProposedActionManifest {
    pub version: u32,
    pub proposed_action: ProposedAction,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ProposedAction {
    pub action_id: String,
    pub request_id: String,
    pub tool_id: String,
    pub requested_by: String,
    pub requested_for: String,
    pub action_type: String,
    pub script_path: String,
    pub output_path: String,
    pub summary: String,
    pub reason: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ToolManifest {
    pub version: u32,
    pub tool: Tool,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Tool {
    pub id: String,
    pub action_type: String,
    pub owner: String,
    pub approval_required: bool,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub command_template: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ActionGrantManifest {
    pub version: u32,
    pub action_grant: ActionGrant,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ActionGrant {
    pub action_id: String,
    pub request_id: String,
    pub tool_id: String,
    pub action_type: String,
    pub decision: String,
    pub granted_by: String,
    pub created_at: String,
    pub action: GrantedActionFile,
    pub repo: GrantedRepoState,
    #[serde(default)]
    pub inputs: Vec<GrantedInput>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct GrantedActionFile {
    pub path: String,
    pub sha256: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct GrantedRepoState {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub git_commit: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct GrantedInput {
    pub kind: String,
    pub path: String,
    pub sha256: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ControlledActionStatusManifest {
    pub version: u32,
    pub status: ControlledActionStatus,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ControlledActionStatus {
    pub action_id: String,
    pub request_id: String,
    pub status: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub run_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stdout_path: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
    pub created_at: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct IdentityBundleManifest {
    pub version: u32,
    pub identity: IdentityBundle,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub self_signature: Option<SignatureRecord>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct IdentityBundle {
    pub id: String,
    pub kind: String,
    pub handle: String,
    pub display_name: String,
    pub public_keys: Vec<IdentityPublicKey>,
    pub proofs: Vec<IdentityProof>,
    pub created_at: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct IdentityPublicKey {
    pub id: String,
    pub kind: String,
    pub algorithm: String,
    pub public_key: String,
    pub fingerprint: String,
    pub created_at: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct IdentityProof {
    pub kind: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub provider_url: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub claim: Option<String>,
    pub proof_url: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub verified_at: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LocalIdentityManifest {
    pub version: u32,
    pub local_identity: LocalIdentity,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LocalIdentity {
    pub identity: IdentityBundle,
    pub signing_key: LocalSigningKey,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub encryption_key: Option<LocalEncryptionKey>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub git: Option<LocalGitIdentity>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LocalGitIdentity {
    pub github_login: String,
    pub commit_name: String,
    pub commit_email: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LocalSigningKey {
    pub kind: String,
    pub algorithm: String,
    pub public_key_path: String,
    pub private_key_ref: PrivateKeyRef,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LocalEncryptionKey {
    pub kind: String,
    pub algorithm: String,
    pub public_key_path: String,
    pub private_key_ref: PrivateKeyRef,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PrivateKeyRef {
    pub backend: String,
    pub path: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TrustManifest {
    pub version: u32,
    pub trust: TrustRecord,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TrustRecord {
    pub identity_id: String,
    pub decision: String,
    pub trusted_at: String,
    pub source: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SignatureManifest {
    pub version: u32,
    pub signature: SignatureRecord,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SignatureRecord {
    pub signed_path: String,
    pub signed_sha256: String,
    pub signer: String,
    pub key_id: String,
    pub algorithm: String,
    pub namespace: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub context: Option<SignatureContext>,
    pub signature: String,
    pub created_at: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SignatureContext {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub repo_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub request_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub message_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub recipient_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub purpose: Option<String>,
}

pub fn to_yaml<T: Serialize>(value: &T) -> RhoResult<String> {
    Ok(serde_yaml::to_string(value)?)
}

pub fn from_yaml<T: for<'de> Deserialize<'de>>(value: &str) -> RhoResult<T> {
    Ok(serde_yaml::from_str(value)?)
}

pub fn bytes_digest(bytes: &[u8]) -> String {
    let mut hasher = Sha256::new();
    hasher.update(bytes);
    hex_lower_bytes(&hasher.finalize())
}

fn hex_lower_bytes(bytes: &[u8]) -> String {
    let mut out = String::with_capacity(bytes.len() * 2);
    for byte in bytes {
        out.push_str(&format!("{byte:02x}"));
    }
    out
}

pub fn yaml_top_level_kind(value: &str) -> Option<String> {
    let parsed: serde_yaml::Value = serde_yaml::from_str(value).ok()?;
    let mapping = parsed.as_mapping()?;
    mapping
        .get(serde_yaml::Value::String("kind".to_string()))?
        .as_str()
        .map(ToOwned::to_owned)
}

pub fn is_rho_encrypted_text(value: &str) -> bool {
    matches!(
        yaml_top_level_kind(value).as_deref(),
        Some("rho_recipient_envelope" | "rho_transparent_file")
    )
}

pub fn to_json_pretty<T: Serialize>(value: &T) -> RhoResult<String> {
    Ok(serde_json::to_string_pretty(value)? + "\n")
}

pub fn from_json<T: for<'de> Deserialize<'de>>(value: &str) -> RhoResult<T> {
    Ok(serde_json::from_str(value)?)
}

pub fn validate_actor_id(value: &str) -> RhoResult<()> {
    if value.starts_with("rho://id/") {
        providers::parse_identity_id(value)?;
        return Ok(());
    }
    validate_simple_id(value, "actor id", true)
}

pub fn normalize_actor_id(value: &str) -> RhoResult<String> {
    if value.starts_with("rho://id/") {
        validate_actor_id(value)?;
        return Ok(value.to_string());
    }
    if let Some(handle) = value.strip_prefix("github/") {
        return providers::github::identity_id(handle);
    }
    providers::github::identity_id(value)
}

pub fn normalize_repo_id(value: &str) -> RhoResult<String> {
    if value.starts_with("rho://repo/") {
        validate_relative_safe_path(value.trim_start_matches("rho://repo/"))?;
        return Ok(value.to_string());
    }
    let path = value
        .strip_prefix("https://github.com/")
        .or_else(|| value.strip_prefix("git@github.com:"))
        .unwrap_or(value)
        .trim_end_matches(".git")
        .trim_matches('/');
    let parts: Vec<&str> = path.split('/').collect();
    if parts.len() != 2 {
        return Err(format!(
            "repo id must be rho://repo/..., https://github.com/owner/repo, or owner/repo: {value}"
        )
        .into());
    }
    validate_simple_id(parts[0], "github owner", true)?;
    validate_simple_id(parts[1], "github repo", true)?;
    Ok(format!("rho://repo/github/{}/{}", parts[0], parts[1]))
}

pub fn validate_request_id(value: &str) -> RhoResult<()> {
    if !value.starts_with("req-") {
        return Err(format!("request id must start with req-: {value}").into());
    }
    validate_simple_id(value, "request id", true)
}

pub fn validate_run_id(value: &str) -> RhoResult<()> {
    if !value.starts_with("run-") {
        return Err(format!("run id must start with run-: {value}").into());
    }
    validate_simple_id(value, "run id", true)
}

pub fn validate_action_id(value: &str) -> RhoResult<()> {
    if !value.starts_with("act-") {
        return Err(format!("action id must start with act-: {value}").into());
    }
    validate_simple_id(value, "action id", true)
}

pub fn validate_tool_id(value: &str) -> RhoResult<()> {
    validate_simple_id(value, "tool id", true)
}

pub fn validate_action_type(value: &str) -> RhoResult<()> {
    match value {
        "run_mock_data" | "run_real_data" | "release_results" => Ok(()),
        _ => Err(format!("unsupported action type: {value}").into()),
    }
}

pub fn validate_tier(value: &str) -> RhoResult<()> {
    match value {
        "public" | "mock" | "real" => Ok(()),
        _ => Err(format!("unsupported tier: {value}").into()),
    }
}

pub fn validate_relative_safe_path(value: &str) -> RhoResult<()> {
    if value.is_empty() || value.contains('\0') {
        return Err("path must be non-empty and contain no NUL bytes".into());
    }
    let path = Path::new(value);
    for component in path.components() {
        match component {
            std::path::Component::ParentDir => {
                return Err(format!("path must not contain ..: {value}").into());
            }
            std::path::Component::RootDir | std::path::Component::Prefix(_) => {
                return Err(format!("path must be relative: {value}").into());
            }
            _ => {}
        }
    }
    Ok(())
}

pub fn path_matches_pattern(path: &str, pattern: &str) -> bool {
    let path = normalize_match_path(path);
    let pattern = normalize_match_path(pattern);
    if path.is_empty() || pattern.is_empty() {
        return path == pattern;
    }
    let path_segments = path.split('/').collect::<Vec<_>>();
    let pattern_segments = pattern.split('/').collect::<Vec<_>>();
    match_path_segments(&path_segments, &pattern_segments)
}

fn normalize_match_path(value: &str) -> String {
    value
        .replace('\\', "/")
        .trim_start_matches("./")
        .trim_matches('/')
        .to_string()
}

fn match_path_segments(path: &[&str], pattern: &[&str]) -> bool {
    match pattern.split_first() {
        None => path.is_empty(),
        Some((first, rest)) if *first == "**" => {
            match_path_segments(path, rest)
                || (!path.is_empty() && match_path_segments(&path[1..], pattern))
        }
        Some((first, rest)) => {
            let Some((path_first, path_rest)) = path.split_first() else {
                return false;
            };
            segment_matches(path_first, first) && match_path_segments(path_rest, rest)
        }
    }
}

fn segment_matches(value: &str, pattern: &str) -> bool {
    let value = value.as_bytes();
    let pattern = pattern.as_bytes();
    let mut value_index = 0usize;
    let mut pattern_index = 0usize;
    let mut star_index = None;
    let mut value_after_star = 0usize;

    while value_index < value.len() {
        if pattern_index < pattern.len()
            && pattern[pattern_index] != b'*'
            && pattern[pattern_index] == value[value_index]
        {
            value_index += 1;
            pattern_index += 1;
        } else if pattern_index < pattern.len() && pattern[pattern_index] == b'*' {
            star_index = Some(pattern_index);
            pattern_index += 1;
            value_after_star = value_index;
        } else if let Some(star) = star_index {
            pattern_index = star + 1;
            value_after_star += 1;
            value_index = value_after_star;
        } else {
            return false;
        }
    }

    pattern[pattern_index..].iter().all(|byte| *byte == b'*')
}

fn validate_simple_id(value: &str, label: &str, allow_hyphen: bool) -> RhoResult<()> {
    if value.is_empty() || value.len() > 96 {
        return Err(format!("{label} must be 1-96 characters").into());
    }
    let valid = value.chars().all(|ch| {
        ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_' || (allow_hyphen && ch == '-')
    });
    if !valid {
        return Err(format!("{label} has invalid characters: {value}").into());
    }
    Ok(())
}

pub fn arg_value(args: &[String], flag: &str) -> Option<String> {
    args.windows(2)
        .find(|window| window[0] == flag)
        .map(|window| window[1].clone())
}

pub fn has_flag(args: &[String], flag: &str) -> bool {
    args.iter().any(|arg| arg == flag)
}

pub fn require_arg(args: &[String], flag: &str) -> RhoResult<String> {
    arg_value(args, flag).ok_or_else(|| format!("missing required argument: {flag}").into())
}

pub fn ensure_parent(path: &Path) -> io::Result<()> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }
    Ok(())
}

pub fn copy_dir_recursive(source: &Path, target: &Path) -> io::Result<()> {
    fs::create_dir_all(target)?;
    for entry in fs::read_dir(source)? {
        let entry = entry?;
        let source_path = entry.path();
        let target_path = target.join(entry.file_name());
        let file_type = entry.file_type()?;
        if file_type.is_dir() {
            copy_dir_recursive(&source_path, &target_path)?;
        } else if file_type.is_file() {
            fs::copy(&source_path, &target_path)?;
        }
    }
    Ok(())
}

pub fn remove_dir_if_exists(path: &Path) -> io::Result<()> {
    if path.exists() {
        fs::remove_dir_all(path)?;
    }
    Ok(())
}

pub fn yaml_quote(value: &str) -> String {
    let escaped = value
        .replace('\\', "\\\\")
        .replace('"', "\\\"")
        .replace('\n', "\\n")
        .replace('\r', "");
    format!("\"{escaped}\"")
}

pub fn now_rfc3339() -> String {
    let seconds = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs();
    format_unix_seconds_rfc3339(seconds)
}

pub fn uuid_like() -> String {
    let mut bytes = [0u8; 16];
    getrandom::getrandom(&mut bytes).expect("secure random UUID generation failed");
    bytes[6] = (bytes[6] & 0x0f) | 0x40;
    bytes[8] = (bytes[8] & 0x3f) | 0x80;
    format!(
        "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
        bytes[0],
        bytes[1],
        bytes[2],
        bytes[3],
        bytes[4],
        bytes[5],
        bytes[6],
        bytes[7],
        bytes[8],
        bytes[9],
        bytes[10],
        bytes[11],
        bytes[12],
        bytes[13],
        bytes[14],
        bytes[15]
    )
}

pub fn file_digest(path: &Path) -> RhoResult<String> {
    let mut file = File::open(path)?;
    let mut hasher = Sha256::new();
    let mut buffer = [0u8; 16 * 1024];
    loop {
        let read = file.read(&mut buffer)?;
        if read == 0 {
            break;
        }
        hasher.update(&buffer[..read]);
    }
    Ok(format!("{:x}", hasher.finalize()))
}

pub fn mime_type(path: &Path) -> String {
    let Some(extension) = path
        .extension()
        .and_then(OsStr::to_str)
        .map(|value| value.to_ascii_lowercase())
    else {
        return "application/octet-stream".to_string();
    };
    match extension.as_str() {
        "csv" => "text/csv",
        "json" => "application/json",
        "yaml" | "yml" => "application/yaml",
        "txt" | "md" => "text/plain",
        "py" => "text/x-python",
        "rs" => "text/rust",
        "pdf" => "application/pdf",
        "png" => "image/png",
        "jpg" | "jpeg" => "image/jpeg",
        "gif" => "image/gif",
        "svg" => "image/svg+xml",
        "zip" => "application/zip",
        "tar" => "application/x-tar",
        "gz" => "application/gzip",
        "rhoenc" => "application/vnd.rho.envelope",
        _ => "application/octet-stream",
    }
    .to_string()
}

fn format_unix_seconds_rfc3339(seconds: u64) -> String {
    let days = (seconds / 86_400) as i64;
    let seconds_of_day = seconds % 86_400;
    let (year, month, day) = civil_from_days(days);
    let hour = seconds_of_day / 3_600;
    let minute = (seconds_of_day % 3_600) / 60;
    let second = seconds_of_day % 60;
    format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z")
}

fn civil_from_days(days_since_unix_epoch: i64) -> (i64, u64, u64) {
    let z = days_since_unix_epoch + 719_468;
    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
    let day_of_era = z - era * 146_097;
    let year_of_era =
        (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
    let year = year_of_era + era * 400;
    let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
    let month_prime = (5 * day_of_year + 2) / 153;
    let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
    let month = month_prime + if month_prime < 10 { 3 } else { -9 };
    let year = year + if month <= 2 { 1 } else { 0 };
    (year, month as u64, day as u64)
}

pub fn file_name(path: &Path) -> RhoResult<String> {
    path.file_name()
        .and_then(OsStr::to_str)
        .map(ToOwned::to_owned)
        .ok_or_else(|| format!("path has no valid file name: {}", path.display()).into())
}

pub fn canonical_display(path: &Path) -> String {
    path.canonicalize()
        .unwrap_or_else(|_| PathBuf::from(path))
        .display()
        .to_string()
}

pub fn read_to_string_if_exists(path: &Path) -> io::Result<Option<String>> {
    match fs::read_to_string(path) {
        Ok(value) => Ok(Some(value)),
        Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
        Err(error) => Err(error),
    }
}

pub fn split_command_line(value: &str) -> RhoResult<Vec<String>> {
    let mut parts = Vec::new();
    let mut current = String::new();
    let mut chars = value.chars().peekable();
    let mut quote: Option<char> = None;

    while let Some(ch) = chars.next() {
        match (quote, ch) {
            (Some(active), next) if next == active => quote = None,
            (Some(_), '\\') => {
                if let Some(next) = chars.next() {
                    current.push(next);
                }
            }
            (Some(_), next) => current.push(next),
            (None, '\'' | '"') => quote = Some(ch),
            (None, next) if next.is_whitespace() => {
                if !current.is_empty() {
                    parts.push(std::mem::take(&mut current));
                }
            }
            (None, next) => current.push(next),
        }
    }

    if quote.is_some() {
        return Err("unterminated quote in command_text".into());
    }
    if !current.is_empty() {
        parts.push(current);
    }
    if parts.is_empty() {
        return Err("command_text is empty".into());
    }
    Ok(parts)
}

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

    #[test]
    fn split_command_line_preserves_quoted_args() {
        let parts = split_command_line("python3 \"sum prices.py\" DATASET_CSV").unwrap();
        assert_eq!(parts, vec!["python3", "sum prices.py", "DATASET_CSV"]);
    }

    #[test]
    fn split_command_line_rejects_unterminated_quotes() {
        assert!(split_command_line("python3 \"unterminated").is_err());
    }

    #[test]
    fn validates_expected_ids() {
        assert!(validate_actor_id("agent1").is_ok());
        assert!(validate_request_id("req-abc-123").is_ok());
        assert!(validate_run_id("run-real-123").is_ok());
        assert!(validate_actor_id("test-runner").is_ok());
        assert!(validate_actor_id("Agent1").is_err());
        assert!(validate_request_id("abc-123").is_err());
        assert!(validate_run_id("real-123").is_err());
    }

    #[test]
    fn rejects_unsafe_relative_paths() {
        assert!(validate_relative_safe_path("workspace/sum_prices.py").is_ok());
        assert!(validate_relative_safe_path("../private/data.csv").is_err());
        assert!(validate_relative_safe_path("/tmp/data.csv").is_err());
        assert!(validate_relative_safe_path("").is_err());
    }

    #[test]
    fn path_patterns_are_segment_aware_globs() {
        assert!(path_matches_pattern(
            "rho/messages/inbox/id/github/rho-owner/request.yaml",
            "rho/messages/inbox/id/github/rho-owner/**"
        ));
        assert!(path_matches_pattern(
            "rho/messages/inbox/id/github/rho-owner",
            "rho/messages/inbox/id/github/rho-owner/**"
        ));
        assert!(path_matches_pattern(
            "rho/messages/inbox/id/github/rho-owner/req-123/request.yaml",
            "rho/messages/inbox/id/github/*/req-*/request.yaml"
        ));
        assert!(path_matches_pattern(
            "./rho/messages/inbox/id/github/rho-owner/req-123/request.yaml",
            "rho/messages/**/request.*"
        ));
        assert!(!path_matches_pattern(
            "rho/messages/inbox/id/github/rho-owner/req-123/request.yaml",
            "rho/messages/inbox/id/github/*/request.yaml"
        ));
        assert!(!path_matches_pattern(
            "rho/messages/inbox/id/github/rho-owner/req-123/request.yaml",
            "rho/messages/inbox/id/github/rho-owner/req-124/**"
        ));
        assert!(!path_matches_pattern(
            "rho/messages/inbox/id/github/rho-owner/req-123/request.yaml",
            "rho/messages/inbox/id/github/rho-*"
        ));
    }

    #[test]
    fn file_digest_uses_sha256() {
        let path = std::env::temp_dir().join(format!("rho-file-digest-{}.txt", uuid_like()));
        fs::write(&path, b"abc").unwrap();
        let digest = file_digest(&path).unwrap();
        fs::remove_file(&path).unwrap();

        assert_eq!(
            digest,
            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
        );
        assert!(!digest.starts_with("fallback-noncryptographic-"));
    }

    #[test]
    fn formats_unix_seconds_as_rfc3339_utc() {
        assert_eq!(format_unix_seconds_rfc3339(0), "1970-01-01T00:00:00Z");
        assert_eq!(
            format_unix_seconds_rfc3339(946_684_800),
            "2000-01-01T00:00:00Z"
        );
        assert_eq!(
            format_unix_seconds_rfc3339(1_609_459_200),
            "2021-01-01T00:00:00Z"
        );
    }

    #[test]
    fn uuid_like_generates_uuid_v4_shape() {
        let value = uuid_like();
        assert_eq!(value.len(), 36);
        assert_eq!(&value[8..9], "-");
        assert_eq!(&value[13..14], "-");
        assert_eq!(&value[18..19], "-");
        assert_eq!(&value[23..24], "-");
        assert_eq!(&value[14..15], "4");
        assert!(matches!(&value[19..20], "8" | "9" | "a" | "b"));
        assert!(value.chars().all(|ch| ch.is_ascii_hexdigit() || ch == '-'));
    }

    #[test]
    fn mime_type_uses_deterministic_extension_mapping() {
        assert_eq!(mime_type(Path::new("prices.csv")), "text/csv");
        assert_eq!(mime_type(Path::new("manifest.yaml")), "application/yaml");
        assert_eq!(mime_type(Path::new("report.pdf")), "application/pdf");
        assert_eq!(
            mime_type(Path::new("unknown.nope")),
            "application/octet-stream"
        );
    }

    #[test]
    fn encrypted_envelope_detection_requires_top_level_yaml_kind() {
        assert!(is_rho_encrypted_text(
            "version: 1\nkind: rho_recipient_envelope\n"
        ));
        assert!(is_rho_encrypted_text(
            "version: 1\nkind: rho_transparent_file\n"
        ));
        assert!(!is_rho_encrypted_text(
            "message:\n  kind: rho_recipient_envelope\n"
        ));
        assert!(!is_rho_encrypted_text(
            "not yaml: [\nkind: rho_recipient_envelope\n"
        ));
    }

    #[test]
    fn request_manifest_round_trips_as_yaml() {
        let manifest = RequestManifest {
            version: 1,
            request: RunRequest {
                id: "req-abc-123".to_string(),
                from: "agent2".to_string(),
                to: "agent1".to_string(),
                tool_id: "run_real".to_string(),
                dataset_uuid: "11111111-2222-3333-4444-555555555555".to_string(),
                code_paths: vec!["sandbox/two-console/shared/workspace/sum_prices.py".to_string()],
                code_sha256: "abc123".to_string(),
                command: vec![
                    "python3".to_string(),
                    "sum_prices.py".to_string(),
                    "DATASET_CSV".to_string(),
                ],
                requested_tier: "real".to_string(),
                created_at: "2026-04-30T00:00:00Z".to_string(),
            },
        };

        let yaml = to_yaml(&manifest).unwrap();
        assert!(yaml.contains("command:"));
        assert!(!yaml.contains("command_text"));
        let parsed: RequestManifest = from_yaml(&yaml).unwrap();
        assert_eq!(parsed, manifest);
    }

    #[test]
    fn run_manifest_omits_absent_optional_fields() {
        let manifest = RunManifest {
            version: 1,
            run: RunRecord {
                id: "run-blocked-123".to_string(),
                request_id: "req-abc-123".to_string(),
                status: "blocked".to_string(),
                tier: "real".to_string(),
                runner: None,
                dataset_csv: None,
                code_path: Some("sandbox/two-console/shared/workspace/sum_prices.py".to_string()),
                code_sha256: None,
                command: vec!["python3".to_string()],
                exit_code: None,
                error: Some("requires approval".to_string()),
                stdout_path: "sandbox/two-console/shared/.rho/runs/run-blocked-123/stdout.txt"
                    .to_string(),
                stderr_path: "sandbox/two-console/shared/.rho/runs/run-blocked-123/stderr.txt"
                    .to_string(),
                created_at: "2026-04-30T00:00:00Z".to_string(),
            },
        };

        let yaml = to_yaml(&manifest).unwrap();
        assert!(!yaml.contains("dataset_csv"));
        assert!(!yaml.contains("runner"));
        assert!(!yaml.contains("exit_code"));
        assert!(yaml.contains("requires approval"));
    }

    #[test]
    fn sandbox_run_manifest_round_trips_as_yaml() {
        let manifest = SandboxRunManifest {
            version: 1,
            sandbox_run: SandboxRunRecord {
                id: "run-run-mock".to_string(),
                request_id: "req-mock-123".to_string(),
                runner: "local".to_string(),
                tier: "mock".to_string(),
                dataset_csv: "sandbox/two-console/shared/datasets/id/mock/prices.csv".to_string(),
                code_path: "sandbox/two-console/shared/workspace/sum_prices.py".to_string(),
                command: vec![
                    "python3".to_string(),
                    "sandbox/two-console/shared/workspace/sum_prices.py".to_string(),
                    "sandbox/two-console/shared/datasets/id/mock/prices.csv".to_string(),
                ],
                artifact_dir: "sandbox/two-console/shared/.rho/runs/run-run-mock".to_string(),
                stdout_path: "sandbox/two-console/shared/.rho/runs/run-run-mock/stdout.txt"
                    .to_string(),
                stderr_path: "sandbox/two-console/shared/.rho/runs/run-run-mock/stderr.txt"
                    .to_string(),
                mounts: vec![SandboxMount {
                    host_path: "sandbox/two-console/shared/workspace".to_string(),
                    guest_path: "/workspace".to_string(),
                    mode: "ro".to_string(),
                }],
                network: SandboxNetworkPolicy {
                    default_deny: true,
                    allow_hosts: vec![],
                    tcp_maps: vec![],
                },
                created_at: "2026-04-30T00:00:00Z".to_string(),
            },
        };

        let yaml = to_yaml(&manifest).unwrap();
        assert!(yaml.contains("sandbox_run:"));
        assert!(yaml.contains("runner: local"));
        let parsed: SandboxRunManifest = from_yaml(&yaml).unwrap();
        assert_eq!(parsed, manifest);
    }

    #[test]
    fn controlled_action_round_trips_as_json() {
        let manifest = ControlledActionManifest {
            version: 1,
            kind: "controlled_action".to_string(),
            action: ControlledAction {
                action_id: "act-run-real".to_string(),
                request_id: "req-abc-123".to_string(),
                tool_id: "run_real".to_string(),
                requested_by: "agent2".to_string(),
                requested_for: "agent1".to_string(),
                action_type: "run_real_data".to_string(),
                summary: "Run approved script".to_string(),
                reason: "Need aggregate".to_string(),
                input_path: None,
                script_path: Some("sandbox/two-console/shared/workspace/sum_prices.py".to_string()),
                output_path: "sandbox/two-console/shared/.rho/runs/run-act-run-real/stdout.txt"
                    .to_string(),
            },
        };

        let json = to_json_pretty(&manifest).unwrap();
        assert!(json.contains("\"controlled_action\""));
        let parsed: ControlledActionManifest = from_json(&json).unwrap();
        assert_eq!(parsed, manifest);
    }

    #[test]
    fn validates_action_ids_and_types() {
        assert!(validate_action_id("act-run-real").is_ok());
        assert!(validate_action_id("run-real").is_err());
        assert!(validate_tool_id("run_real").is_ok());
        assert!(validate_action_type("run_real_data").is_ok());
        assert!(validate_action_type("run_mock_data").is_ok());
        assert!(validate_action_type("delete_private_data").is_err());
    }

    #[test]
    fn tool_manifest_round_trips_as_yaml() {
        let manifest = ToolManifest {
            version: 1,
            tool: Tool {
                id: "run_real".to_string(),
                action_type: "run_real_data".to_string(),
                owner: "agent1".to_string(),
                approval_required: true,
                command_template: vec![
                    "python3".to_string(),
                    "CODE_PATH".to_string(),
                    "DATASET_CSV".to_string(),
                ],
            },
        };
        let yaml = to_yaml(&manifest).unwrap();
        assert!(yaml.contains("run_real_data"));
        assert!(yaml.contains("command_template"));
        let parsed: ToolManifest = from_yaml(&yaml).unwrap();
        assert_eq!(parsed, manifest);
    }

    #[test]
    fn action_grant_manifest_round_trips_as_yaml() {
        let manifest = ActionGrantManifest {
            version: 1,
            action_grant: ActionGrant {
                action_id: "act-run-real".to_string(),
                request_id: "req-abc-123".to_string(),
                tool_id: "run_real".to_string(),
                action_type: "run_real_data".to_string(),
                decision: "approved".to_string(),
                granted_by: "agent1".to_string(),
                created_at: "2026-04-30T00:00:00Z".to_string(),
                action: GrantedActionFile {
                    path: "sandbox/two-console/control/outbox/act-run-real.json".to_string(),
                    sha256: "action-sha".to_string(),
                },
                repo: GrantedRepoState {
                    git_commit: Some("commit-sha".to_string()),
                },
                inputs: vec![GrantedInput {
                    kind: "code".to_string(),
                    path: "sandbox/two-console/shared/workspace/sum_prices.py".to_string(),
                    sha256: "input-sha".to_string(),
                }],
            },
        };

        let yaml = to_yaml(&manifest).unwrap();
        assert!(yaml.contains("action_grant:"));
        assert!(yaml.contains("action-sha"));
        assert!(yaml.contains("input-sha"));
        let parsed: ActionGrantManifest = from_yaml(&yaml).unwrap();
        assert_eq!(parsed, manifest);
    }

    #[test]
    fn github_provider_parses_standard_and_alias_remotes() {
        use crate::providers::github::repo_candidate_from_remote_with_host_resolver;

        assert_eq!(
            repo_candidate_from_remote_with_host_resolver(
                "git@github.com:madhavajay/rho-live.git",
                |_| false,
            ),
            Some("madhavajay/rho-live".to_string())
        );
        assert_eq!(
            repo_candidate_from_remote_with_host_resolver(
                "git@github-madhavajay:madhavajay/rho-live.git",
                |host| host == "github-madhavajay",
            ),
            Some("madhavajay/rho-live".to_string())
        );
        assert_eq!(
            repo_candidate_from_remote_with_host_resolver(
                "git@example.com:madhavajay/rho-live.git",
                |_| false,
            ),
            None
        );
    }
}
