//! Ed25519 keypair management for actor witness signing (ADR-024, D26).
//!
//! Each actor declared in `.ontoref/actors.ncl` may carry an Ed25519 public
//! key. The corresponding private key is stored under
//! `~/.config/ontoref/keys/<actor>.key` with `0600` permissions and is
//! loaded into memory when the actor opens a session. Witnesses produced by
//! the operations runtime are signed under the concatenation
//! `op_id || state_root || effects_hash`.
//!
//! When an actor invokes an operation without a registered keypair the
//! daemon falls back to its own admin key, attaching
//! `on_behalf_of = <actor_id>` metadata. This is acceptable for piloto
//! self-host but should be an exception, not the norm.

use std::io::Write;
use std::path::{Path, PathBuf};

use argon2::password_hash::rand_core::{OsRng, RngCore};
use ed25519_dalek::{Signer, SigningKey};
use ontoref_types::signature::{PublicKey, Sig, SignatureError};

/// Failures emitted by keypair operations.
#[derive(Debug, thiserror::Error)]
pub enum KeypairError {
    /// Disk IO failure with the operation context that triggered it.
    #[error("io error in {context}: {source}")]
    Io {
        context: String,
        #[source]
        source: std::io::Error,
    },
    /// The key file contents are not valid hex.
    #[error("invalid hex in keyfile '{0}'")]
    InvalidHexFile(PathBuf),
    /// The decoded seed is not the expected 32 bytes.
    #[error("invalid seed length in '{path}': expected 32 bytes, got {got}")]
    InvalidSeedLength { path: PathBuf, got: usize },
    /// Actor id violates the `[a-z0-9_-]+` whitelist.
    #[error("invalid actor id '{0}': must match [a-z0-9_-]+ and be non-empty")]
    InvalidActorId(String),
    /// `actors.ncl` contains malformed content the appender cannot extend.
    #[error("malformed actors.ncl '{path}': {reason}")]
    MalformedActorsNcl { path: PathBuf, reason: String },
    /// Underlying signature verification failure.
    #[error("signature: {0}")]
    Signature(#[from] SignatureError),
}

/// Validate an actor id against the `[a-z0-9_-]+` whitelist (non-empty).
pub fn validate_actor_id(id: &str) -> Result<(), KeypairError> {
    let ok = !id.is_empty()
        && id
            .chars()
            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_');
    if !ok {
        return Err(KeypairError::InvalidActorId(id.to_owned()));
    }
    Ok(())
}

/// Compute the filesystem path of an actor's private key file.
pub fn keyfile_path(key_dir: &Path, actor_id: &str) -> PathBuf {
    key_dir.join(format!("{actor_id}.key"))
}

/// Generate a fresh Ed25519 keypair for `actor_id` and persist the private
/// key to `<key_dir>/<actor_id>.key` with `0600` permissions on Unix.
///
/// Returns the derived [`PublicKey`] for registration in `actors.ncl`.
///
/// # Errors
///
/// - [`KeypairError::InvalidActorId`] when the actor id is empty or contains
///   characters outside `[a-z0-9_-]`.
/// - [`KeypairError::Io`] on directory creation or file write failure.
pub fn gen_for_actor(actor_id: &str, key_dir: &Path) -> Result<PublicKey, KeypairError> {
    let (signing_key, public_key) = generate_keypair();
    persist_signing_key(actor_id, key_dir, &signing_key)?;
    Ok(public_key)
}

/// Generate a fresh Ed25519 keypair from the OS CSPRNG without touching
/// disk. Used by key rotation (ADR-044), which must hold the new signing
/// key in memory to record the succession before persisting it.
pub fn generate_keypair() -> (SigningKey, PublicKey) {
    let mut seed = [0u8; 32];
    OsRng.fill_bytes(&mut seed);
    let signing_key = SigningKey::from_bytes(&seed);
    let public_key = PublicKey::from_signing_key(&signing_key);
    (signing_key, public_key)
}

/// Persist `signing_key`'s seed to `<key_dir>/<actor_id>.key` with `0600`
/// permissions on Unix, creating `key_dir` if needed. Overwrites an
/// existing file — the caller orders this after recording any succession.
///
/// # Errors
///
/// [`KeypairError::InvalidActorId`] for a malformed id; [`KeypairError::Io`]
/// on directory creation or file write failure.
pub fn persist_signing_key(
    actor_id: &str,
    key_dir: &Path,
    signing_key: &SigningKey,
) -> Result<(), KeypairError> {
    validate_actor_id(actor_id)?;
    std::fs::create_dir_all(key_dir).map_err(|e| KeypairError::Io {
        context: format!("creating directory {}", key_dir.display()),
        source: e,
    })?;
    let path = keyfile_path(key_dir, actor_id);
    let hex = hex_encode(&signing_key.to_bytes());
    write_secure(&path, hex.as_bytes())
}

/// Load an actor's private signing key from `<key_dir>/<actor_id>.key`.
///
/// # Errors
///
/// - [`KeypairError::InvalidActorId`] for malformed ids.
/// - [`KeypairError::Io`] when the file is missing or unreadable.
/// - [`KeypairError::InvalidHexFile`] when content is not hex.
/// - [`KeypairError::InvalidSeedLength`] when the seed is not 32 bytes.
pub fn load_for_actor(actor_id: &str, key_dir: &Path) -> Result<SigningKey, KeypairError> {
    validate_actor_id(actor_id)?;
    let path = keyfile_path(key_dir, actor_id);
    let raw = std::fs::read_to_string(&path).map_err(|e| KeypairError::Io {
        context: format!("reading {}", path.display()),
        source: e,
    })?;
    let trimmed = raw.trim();
    let bytes = hex_decode(trimmed).ok_or_else(|| KeypairError::InvalidHexFile(path.clone()))?;
    if bytes.len() != 32 {
        return Err(KeypairError::InvalidSeedLength {
            path,
            got: bytes.len(),
        });
    }
    let mut seed = [0u8; 32];
    seed.copy_from_slice(&bytes);
    Ok(SigningKey::from_bytes(&seed))
}

/// Sign the canonical witness payload `op_id || state_root || effects_hash`.
pub fn sign_witness_payload(
    key: &SigningKey,
    op_id: &[u8],
    state_root: &[u8],
    effects_hash: &[u8],
) -> Sig {
    let mut buf = Vec::with_capacity(op_id.len() + state_root.len() + effects_hash.len());
    buf.extend_from_slice(op_id);
    buf.extend_from_slice(state_root);
    buf.extend_from_slice(effects_hash);
    Sig(key.sign(&buf).to_bytes())
}

/// Hex-encode a public key for inclusion in `actors.ncl`.
pub fn public_key_hex(pk: &PublicKey) -> String {
    hex_encode(&pk.0)
}

/// Sign an arbitrary payload (e.g. a substrate-view adjustment digest, ADR-046
/// C2) with the actor's Ed25519 key. Returns `(signature_hex, public_key_hex)`.
pub fn sign_payload_for_actor(
    actor_id: &str,
    key_dir: &Path,
    payload: &[u8],
) -> Result<(String, String), KeypairError> {
    let key = load_for_actor(actor_id, key_dir)?;
    let sig = sign_witness_payload(&key, payload, &[], &[]);
    let pk = PublicKey::from_signing_key(&key);
    Ok((hex_encode(&sig.0), public_key_hex(&pk)))
}

/// Verify a payload signature given a hex-encoded public key (32 bytes) and a
/// hex-encoded Ed25519 signature (64 bytes). Returns `false` for any malformed
/// input rather than erroring — the caller treats verification as a boolean
/// gate.
pub fn verify_payload_hex(public_key_hex: &str, payload: &[u8], signature_hex: &str) -> bool {
    let (Some(pk_bytes), Some(sig_bytes)) = (hex_decode(public_key_hex), hex_decode(signature_hex))
    else {
        return false;
    };
    if pk_bytes.len() != 32 || sig_bytes.len() != 64 {
        return false;
    }
    let mut pk = [0u8; 32];
    pk.copy_from_slice(&pk_bytes);
    let mut sg = [0u8; 64];
    sg.copy_from_slice(&sig_bytes);
    PublicKey(pk).verify(payload, &Sig(sg)).is_ok()
}

/// Register `actor_id` with its public key in `actors.ncl`. Creates the file
/// with the canonical `{ actors = [...] }` shell when missing; appends the
/// entry; replaces the entry in place if the actor id already appears.
///
/// `created_at` is recorded verbatim (typically an ISO-8601 timestamp).
///
/// # Errors
///
/// - [`KeypairError::Io`] on read/write failure.
/// - [`KeypairError::MalformedActorsNcl`] when the existing file does not
///   contain the canonical `actors = [...]` array close (`  ],`).
pub fn register_actor_in_ncl(
    actors_ncl: &Path,
    actor_id: &str,
    public_key: &PublicKey,
    created_at: &str,
) -> Result<(), KeypairError> {
    validate_actor_id(actor_id)?;
    let pk_hex = public_key_hex(public_key);
    let entry = format!(
        "    {{ id = \"{actor_id}\", public_key = \"{pk_hex}\", created_at = \"{created_at}\" \
         }},\n"
    );

    if !actors_ncl.exists() {
        if let Some(parent) = actors_ncl.parent() {
            std::fs::create_dir_all(parent).map_err(|e| KeypairError::Io {
                context: format!("creating parent of {}", actors_ncl.display()),
                source: e,
            })?;
        }
        let body = format!("{{\n  actors = [\n{entry}  ],\n}}\n");
        std::fs::write(actors_ncl, body).map_err(|e| KeypairError::Io {
            context: format!("creating {}", actors_ncl.display()),
            source: e,
        })?;
        return Ok(());
    }

    let content = std::fs::read_to_string(actors_ncl).map_err(|e| KeypairError::Io {
        context: format!("reading {}", actors_ncl.display()),
        source: e,
    })?;

    let id_marker = format!("id = \"{actor_id}\"");
    let updated = if content.contains(&id_marker) {
        replace_actor_line(&content, &id_marker, &entry)
    } else {
        append_before_array_close(&content, &entry).ok_or_else(|| {
            KeypairError::MalformedActorsNcl {
                path: actors_ncl.to_path_buf(),
                reason: "missing `  ],` array close to anchor append".to_owned(),
            }
        })?
    };

    std::fs::write(actors_ncl, updated).map_err(|e| KeypairError::Io {
        context: format!("writing {}", actors_ncl.display()),
        source: e,
    })?;
    Ok(())
}

/// Provision an actor's keypair on a Tier1→Tier2 ascent (ADR-036): generate
/// the private key (`0600`) if absent and publish its public key in
/// `actors.ncl`, so an actor entering tier-2 gains a witness-signing key.
///
/// Idempotent — when the key file already exists this is a no-op returning
/// `Ok(None)` (the actor was provisioned on an earlier ascent); otherwise it
/// returns the freshly generated `Some(public_key)`. The ascent decision
/// (`from == Tier1 && to == Tier2`) stays in the caller so this module
/// remains tier-agnostic.
///
/// # Errors
///
/// Propagates [`gen_for_actor`] and [`register_actor_in_ncl`] failures, plus
/// [`KeypairError::InvalidActorId`] for a malformed id.
pub fn provision_actor(
    actor_id: &str,
    key_dir: &Path,
    actors_ncl: &Path,
    created_at: &str,
) -> Result<Option<PublicKey>, KeypairError> {
    validate_actor_id(actor_id)?;
    if keyfile_path(key_dir, actor_id).exists() {
        return Ok(None);
    }
    let public_key = gen_for_actor(actor_id, key_dir)?;
    register_actor_in_ncl(actors_ncl, actor_id, &public_key, created_at)?;
    Ok(Some(public_key))
}

fn replace_actor_line(content: &str, id_marker: &str, entry: &str) -> String {
    let trimmed_entry = entry.trim_end_matches('\n');
    let mut lines: Vec<String> = content
        .lines()
        .map(|line| {
            if line.contains(id_marker) {
                trimmed_entry.to_owned()
            } else {
                line.to_owned()
            }
        })
        .collect();
    if content.ends_with('\n') {
        lines.push(String::new());
    }
    lines.join("\n")
}

fn append_before_array_close(content: &str, entry: &str) -> Option<String> {
    let idx = content.rfind("  ],")?;
    let mut out = String::with_capacity(content.len() + entry.len());
    out.push_str(&content[..idx]);
    out.push_str(entry);
    out.push_str(&content[idx..]);
    Some(out)
}

fn write_secure(path: &Path, data: &[u8]) -> Result<(), KeypairError> {
    use std::fs::OpenOptions;

    let mut opts = OpenOptions::new();
    opts.write(true).create(true).truncate(true);

    #[cfg(unix)]
    {
        use std::os::unix::fs::OpenOptionsExt;
        opts.mode(0o600);
    }

    let mut file = opts.open(path).map_err(|e| KeypairError::Io {
        context: format!("opening {} for write", path.display()),
        source: e,
    })?;
    file.write_all(data).map_err(|e| KeypairError::Io {
        context: format!("writing {}", path.display()),
        source: e,
    })
}

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

fn hex_decode(s: &str) -> Option<Vec<u8>> {
    if !s.len().is_multiple_of(2) {
        return None;
    }
    (0..s.len())
        .step_by(2)
        .map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
        .collect()
}

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

    #[test]
    fn validate_actor_id_accepts_canonical() {
        assert!(validate_actor_id("test-developer").is_ok());
        assert!(validate_actor_id("agent_42").is_ok());
        assert!(validate_actor_id("ci").is_ok());
    }

    #[test]
    fn validate_actor_id_rejects_malformed() {
        assert!(matches!(
            validate_actor_id(""),
            Err(KeypairError::InvalidActorId(_))
        ));
        assert!(matches!(
            validate_actor_id("Has Upper"),
            Err(KeypairError::InvalidActorId(_))
        ));
        assert!(matches!(
            validate_actor_id("with.dot"),
            Err(KeypairError::InvalidActorId(_))
        ));
    }

    #[test]
    fn provision_actor_generates_then_is_idempotent() {
        let tmp = tempfile::tempdir().unwrap();
        let key_dir = tmp.path().join("keys");
        let actors_ncl = tmp.path().join(".ontoref").join("actors.ncl");

        let first = provision_actor("agent", &key_dir, &actors_ncl, "t0").unwrap();
        assert!(first.is_some(), "first ascent generates a key");
        assert!(
            keyfile_path(&key_dir, "agent").exists(),
            "private key file written"
        );
        let ncl = std::fs::read_to_string(&actors_ncl).unwrap();
        let pk_hex = public_key_hex(&first.unwrap());
        assert!(ncl.contains("id = \"agent\""), "actor registered in ncl");
        assert!(ncl.contains(&pk_hex), "public key published in ncl");

        let second = provision_actor("agent", &key_dir, &actors_ncl, "t1").unwrap();
        assert!(
            second.is_none(),
            "an already-provisioned actor is a no-op (key never regenerated)"
        );
    }

    #[test]
    fn hex_roundtrips() {
        let bytes = [0xde, 0xad, 0xbe, 0xef, 0x00, 0xff];
        let s = hex_encode(&bytes);
        assert_eq!(s, "deadbeef00ff");
        assert_eq!(hex_decode(&s).unwrap(), bytes);
    }

    #[test]
    fn hex_decode_rejects_invalid() {
        assert!(hex_decode("xx").is_none());
        assert!(hex_decode("0").is_none()); // odd length
    }
}
