//! Actor key rotation orchestration (bl-017 / ADR-044).
//!
//! Composes the key-file lifecycle (daemon `keypair` module) with the
//! oplog-anchored succession record (`ontoref_types::succession`). Lives in
//! the daemon because rotation needs both the OS-level key store and the
//! project oplog; the succession FORMAT and its verifier stay in the base
//! crate so any peer verifies offline.

use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};

use ontoref_oplog::OpLog;
use ontoref_types::{build_key_succession, Hlc, PublicKey};

use crate::keypair::{self, KeypairError};

/// Failures emitted by [`rotate_actor_key`].
#[derive(Debug, thiserror::Error)]
pub enum RotationError {
    /// Key store failure (load old key, persist new key, publish pubkey).
    #[error("keypair: {0}")]
    Keypair(#[from] KeypairError),
    /// Oplog failure (open, read heads, append the succession).
    #[error("oplog: {0}")]
    OpLog(#[from] ontoref_oplog::Error),
}

/// Rotate `actor_id`'s witness-signing key. Append-first: the succession
/// record — signed by the OLD key — is appended to the project oplog
/// BEFORE the on-disk key is replaced, so the authorization is durably
/// recorded at the point of no return; the key-file swap and `actors.ncl`
/// republish follow. Returns the new public key.
///
/// # Errors
///
/// [`RotationError::Keypair`] if the old key cannot be loaded or the new
/// one persisted/published; [`RotationError::OpLog`] if the succession
/// cannot be appended.
pub fn rotate_actor_key(
    actor_id: &str,
    key_dir: &Path,
    project_root: &Path,
    created_at: &str,
) -> Result<PublicKey, RotationError> {
    let old_signing = keypair::load_for_actor(actor_id, key_dir)?;
    let (new_signing, new_pub) = keypair::generate_keypair();

    // 1. Record the succession first, signed by the retiring key.
    let oplog_path = project_root
        .join(".ontoref")
        .join("substrate")
        .join("oplog.redb");
    let oplog = OpLog::open(&oplog_path)?;
    let parents = oplog.heads()?;
    let mut max_parent_ts = Hlc::genesis();
    for parent in &parents {
        if let Some(op) = oplog.get(parent)? {
            if op.body.timestamp > max_parent_ts {
                max_parent_ts = op.body.timestamp;
            }
        }
    }
    let now_physical = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| u64::try_from(d.as_nanos()).unwrap_or(u64::MAX))
        .unwrap_or(0);
    let succession = build_key_succession(
        actor_id,
        &old_signing,
        new_pub,
        parents,
        Hlc::tick(max_parent_ts, now_physical),
    );
    oplog.append(&succession)?;

    // 2. Swap the on-disk key and republish the public key.
    keypair::persist_signing_key(actor_id, key_dir, &new_signing)?;
    let actors_ncl = project_root.join(".ontoref").join("actors.ncl");
    keypair::register_actor_in_ncl(&actors_ncl, actor_id, &new_pub, created_at)?;
    Ok(new_pub)
}

#[cfg(test)]
mod tests {
    use ontoref_types::{operation::OpPayload, valid_keys_for_actor, PublicKey};

    use super::*;

    #[test]
    fn rotation_records_succession_and_preserves_old_key_validity() {
        let tmp = tempfile::tempdir().unwrap();
        let key_dir = tmp.path().join("keys");
        let project_root = tmp.path().join("project");

        // Initial key for the actor + a bootstrapped oplog.
        let old_pub = keypair::gen_for_actor("agent", &key_dir).unwrap();
        let substrate_dir = project_root.join(".ontoref").join("substrate");
        std::fs::create_dir_all(&substrate_dir).unwrap();
        {
            let _ = OpLog::open(substrate_dir.join("oplog.redb")).unwrap();
        }

        let new_pub = rotate_actor_key("agent", &key_dir, &project_root, "t0").unwrap();
        assert_ne!(new_pub, old_pub, "rotation yields a fresh key");

        // The on-disk key is now the new one.
        let reloaded = keypair::load_for_actor("agent", &key_dir).unwrap();
        assert_eq!(PublicKey::from_signing_key(&reloaded), new_pub);

        // actors.ncl publishes the new public key.
        let ncl =
            std::fs::read_to_string(project_root.join(".ontoref").join("actors.ncl")).unwrap();
        assert!(ncl.contains(&keypair::public_key_hex(&new_pub)));

        // The oplog holds exactly the succession, signed by the OLD key,
        // and the chain keeps the old key valid for the actor.
        let oplog = OpLog::open(substrate_dir.join("oplog.redb")).unwrap();
        let ops = oplog.all_ops_in_hlc_order().unwrap();
        assert_eq!(ops.len(), 1);
        ops[0].verify_signature().expect("succession verifies");
        assert!(matches!(
            ops[0].body.payload,
            OpPayload::KeySuccession { .. }
        ));
        assert_eq!(ops[0].body.actor, old_pub, "signed by the retiring key");

        let valid = valid_keys_for_actor(&ops, "agent", new_pub);
        assert!(
            valid.contains(&old_pub),
            "witnesses under the old key still verify"
        );
        assert!(valid.contains(&new_pub));
    }
}
