//! Witness payload construction and signing.
//!
//! Canonical witness payload: `blake3("ontoref-witness-v1") || op_id ||
//! state_root || effects_hash || extra_payload`. The signature is
//! Ed25519 over the payload bytes. Verifiers reconstruct the payload
//! from the published `(op_id, state_root, effects, extra_payload)`
//! values and verify against the actor's public key from
//! `.ontoref/actors.ncl`.

use ed25519_dalek::{Signer, SigningKey};
use ontoref_commit::StateRoot;
use ontoref_types::signature::{PublicKey, Sig, SignatureError};
use serde::{Deserialize, Serialize};

use crate::output::OpOutput;

/// Witness produced by the pipeline after a successful op.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineWitness {
    /// Operation id this witness attests.
    pub op_id: String,
    /// Substrate state root at the moment of attestation.
    pub state_root: StateRoot,
    /// Blake3 hash over the canonical encoding of the op's effects.
    pub effects_hash: [u8; 32],
    /// Domain-specific payload the op body emitted (e.g. canonical
    /// op-body bytes for replay).
    pub extra_payload: Vec<u8>,
    /// Witness shape tag declared on the op (`"state_transition"`,
    /// `"node_upsert"`, …). Tied to the op's catalog declaration.
    pub witness_shape: String,
    /// Actor public key derived from the signing key.
    pub actor: PublicKey,
    /// Ed25519 signature over [`canonical_payload`](Self::canonical_payload).
    pub signature: Sig,
}

impl PipelineWitness {
    /// Canonical bytes that the signature covers. Verifiers must
    /// reconstruct this from the same inputs to verify the signature.
    pub fn canonical_payload(&self) -> Vec<u8> {
        let mut buf = Vec::with_capacity(
            self.op_id.len()
                + self.state_root.len()
                + self.effects_hash.len()
                + self.extra_payload.len()
                + 16,
        );
        buf.extend_from_slice(b"ontoref-witness-v1");
        buf.extend_from_slice(self.op_id.as_bytes());
        buf.extend_from_slice(&self.state_root);
        buf.extend_from_slice(&self.effects_hash);
        buf.extend_from_slice(&self.extra_payload);
        buf
    }

    /// Verify the witness's signature against its declared actor.
    ///
    /// # Errors
    ///
    /// - [`SignatureError::InvalidPublicKey`] if the actor's key is malformed.
    /// - [`SignatureError::BadSignature`] if the signature does not verify the
    ///   canonical payload.
    pub fn verify(&self) -> Result<(), SignatureError> {
        let payload = self.canonical_payload();
        self.actor.verify(&payload, &self.signature)
    }
}

/// Sign a witness payload with `signing_key`, producing a
/// [`PipelineWitness`] over the inputs.
pub fn sign_witness(
    signing_key: &SigningKey,
    op_id: &str,
    state_root: StateRoot,
    output: &OpOutput,
    witness_shape: &str,
) -> PipelineWitness {
    let effects_hash = hash_effects(&output.effects);
    let actor = PublicKey::from_signing_key(signing_key);
    let mut buf = Vec::with_capacity(64 + op_id.len() + output.witness_payload.len());
    buf.extend_from_slice(b"ontoref-witness-v1");
    buf.extend_from_slice(op_id.as_bytes());
    buf.extend_from_slice(&state_root);
    buf.extend_from_slice(&effects_hash);
    buf.extend_from_slice(&output.witness_payload);
    let signature = Sig(signing_key.sign(&buf).to_bytes());
    PipelineWitness {
        op_id: op_id.to_owned(),
        state_root,
        effects_hash,
        extra_payload: output.witness_payload.clone(),
        witness_shape: witness_shape.to_owned(),
        actor,
        signature,
    }
}

fn hash_effects(effects: &[crate::output::EffectRecord]) -> [u8; 32] {
    let mut hasher = blake3::Hasher::new();
    hasher.update(b"ontoref-effects-v1");
    for e in effects {
        hasher.update(e.kind.as_bytes());
        hasher.update(b"|");
        hasher.update(e.entity.as_bytes());
        hasher.update(b"|");
        for attr in &e.attrs {
            hasher.update(attr.as_bytes());
            hasher.update(b",");
        }
        hasher.update(b"|");
        for dim in &e.dimensions {
            hasher.update(dim.as_bytes());
            hasher.update(b",");
        }
        hasher.update(b";");
    }
    *hasher.finalize().as_bytes()
}
