//! [`SignedOp`], [`OpState`], and the causal queue capacity constant.

use crate::keypair::{Ed25519KeyPair, Ed25519PublicKey, Ed25519Signature};
use ed25519_dalek::Verifier as _;
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, Bytes};

use crate::keypair::{sha256, sign, AuthorId, SignedDigest};
use crate::op::{print_hex, print_path, Op, OpId};

use super::{CrdtNode, JsonValue};

/// Enum representing possible outcomes of applying an operation to a CRDT
#[derive(Debug, PartialEq)]
pub enum OpState {
    /// Operation applied successfully
    Ok,
    /// Tried to apply an operation to a non-CRDT primitive (i.e. f64, bool, etc.)
    /// If you would like a mutable primitive, wrap it in a [`LWWRegisterCRDT`]
    ErrApplyOnPrimitive,
    /// Tried to apply an operation to a static struct CRDT
    /// If you would like a mutable object, use a [`Value`]
    ErrApplyOnStruct,
    /// Tried to apply an operation that contains content of the wrong type.
    /// In other words, the content cannot be coerced to the CRDT at the path specified.
    ErrMismatchedType,
    /// The signed digest of the message did not match the claimed author of the message.
    /// This can happen if the message was tampered with during delivery
    ErrDigestMismatch,
    /// The hash of the message did not match the contents of the message.
    /// This can happen if the author tried to perform an equivocation attack by creating an
    /// operation and modifying it has already been created
    ErrHashMismatch,
    /// Tried to apply an operation to a non-existent path. The author may have forgotten to attach
    /// a causal dependency
    ErrPathMismatch,
    /// Trying to modify/delete the sentinel (zero-th) node element that is used for book-keeping
    ErrListApplyToEmpty,
    /// We have not received all of the causal dependencies of this operation. It has been queued
    /// up and will be executed when its causal dependencies have been delivered
    MissingCausalDependencies,
    /// This op has already been applied (identified by its `signed_digest`).
    /// The CRDT state is unchanged — this is a no-op (idempotent self-loop guard).
    AlreadySeen,
}

/// Maximum total number of ops that may sit in the causal-order hold queue at any
/// one time, summed across all pending dependency buckets.
///
/// **Overflow policy: drop oldest.**
/// When the limit is reached, the oldest pending op in the largest dependency bucket
/// is silently evicted before the new op is queued.  Rationale: a misbehaving or
/// heavily-partitioned peer can send ops whose causal ancestors never arrive, causing
/// unbounded memory growth.  Dropping the oldest entry preserves the most recent
/// information and caps memory use.  The peer can reconnect and receive a fresh bulk
/// state dump to recover any dropped ops.
pub const CAUSAL_QUEUE_MAX: usize = 256;

/// An [`Op<Value>`] with a few bits of extra metadata
#[serde_as]
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
pub struct SignedOp {
    // Note that this can be different from the author of the inner op as the inner op could have been created
    // by a different person
    author: AuthorId,
    /// Signed hash using priv key of author. Effectively [`OpID`] Use this as the ID to figure out what has been delivered already
    #[serde_as(as = "Bytes")]
    pub signed_digest: SignedDigest,
    pub inner: Op<JsonValue>,
    /// List of causal dependencies
    #[serde_as(as = "Vec<Bytes>")]
    pub depends_on: Vec<SignedDigest>,
}

impl SignedOp {
    pub fn id(&self) -> OpId {
        self.inner.id
    }

    pub fn author(&self) -> AuthorId {
        self.author
    }

    /// Creates a digest of the following fields. Any changes in the fields will change the signed digest
    ///  - id (hash of the following)
    ///    - origin
    ///    - author
    ///    - seq
    ///    - is_deleted
    ///  - path
    ///  - dependencies
    fn digest(&self) -> [u8; 32] {
        let path_string = print_path(self.inner.path.clone());
        let dependency_string = self
            .depends_on
            .iter()
            .map(print_hex)
            .collect::<Vec<_>>()
            .join("");
        let fmt_str = format!("{:?},{path_string},{dependency_string}", self.id());
        sha256(fmt_str)
    }

    /// Sign this digest with the given keypair. Shouldn't need to be called manually,
    /// just use [`SignedOp::from_op`] instead
    fn sign_digest(&mut self, keypair: &Ed25519KeyPair) {
        self.signed_digest = sign(keypair, &self.digest()).to_bytes()
    }

    /// Ensure digest was actually signed by the author it claims to be signed by
    pub fn is_valid_digest(&self) -> bool {
        let digest = Ed25519Signature::from_bytes(&self.signed_digest);
        match Ed25519PublicKey::from_bytes(&self.author()) {
            Ok(pubkey) => pubkey.verify(&self.digest(), &digest).is_ok(),
            Err(_) => false,
        }
    }

    /// Sign a normal op and add all the needed metadata
    pub fn from_op<T: CrdtNode>(
        value: Op<T>,
        keypair: &Ed25519KeyPair,
        depends_on: Vec<SignedDigest>,
    ) -> Self {
        let author = keypair.verifying_key().to_bytes();
        let mut new = Self {
            inner: Op {
                content: value.content.map(|c| c.view()),
                origin: value.origin,
                author: value.author,
                seq: value.seq,
                path: value.path,
                is_deleted: value.is_deleted,
                id: value.id,
            },
            author,
            signed_digest: [0u8; 64],
            depends_on,
        };
        new.sign_digest(keypair);
        new
    }
}
