//! Determinism support for Temporal replay

use std::sync::Arc;

use sha2::{Digest, Sha256};

use crate::decision::{ClaudeDecision, DecisionContext};
use crate::error::ClaudeError;

/// Trait for storing and retrieving recorded decisions
#[allow(async_fn_in_trait)]
pub trait DecisionStore: Send + Sync {
    /// Get a recorded decision by context hash
    async fn get_decision(&self, context_hash: &str) -> Result<Option<ClaudeDecision>, ClaudeError>;

    /// Record a decision for future replay
    async fn record_decision(
        &self,
        context_hash: String,
        decision: ClaudeDecision,
    ) -> Result<(), ClaudeError>;

    /// Check if a decision exists for the given context hash
    async fn has_decision(&self, context_hash: &str) -> Result<bool, ClaudeError> {
        Ok(self.get_decision(context_hash).await?.is_some())
    }
}

/// Records and replays Claude decisions for Temporal determinism
pub struct DecisionRecorder<S: DecisionStore> {
    store: Arc<S>,
}

impl<S: DecisionStore> DecisionRecorder<S> {
    /// Create a new decision recorder
    #[must_use]
    pub fn new(store: Arc<S>) -> Self {
        Self { store }
    }

    /// Compute a hash of the decision context for lookup
    #[must_use]
    pub fn compute_context_hash(&self, context: &DecisionContext) -> String {
        let serialized = serde_json::to_string(context).unwrap_or_default();
        let mut hasher = Sha256::new();
        hasher.update(serialized.as_bytes());
        let result = hasher.finalize();
        hex::encode(result)
    }

    /// Get a recorded decision or invoke Claude to make a new one
    ///
    /// # Errors
    /// Returns an error if the decision cannot be retrieved or recorded
    pub async fn get_or_record_decision(
        &self,
        context: DecisionContext,
        is_replaying: bool,
        invoke_claude: impl AsyncFnOnce(&DecisionContext) -> Result<ClaudeDecision, ClaudeError>,
    ) -> Result<ClaudeDecision, ClaudeError> {
        let context_hash = self.compute_context_hash(&context);

        if is_replaying {
            // During replay, we must return the recorded decision
            return self
                .store
                .get_decision(&context_hash)
                .await?
                .ok_or_else(|| ClaudeError::DecisionNotFound(context_hash));
        }

        // Check if we already have a recorded decision
        if let Some(decision) = self.store.get_decision(&context_hash).await? {
            return Ok(decision);
        }

        // Invoke Claude and record the decision
        let decision = invoke_claude(&context).await?;
        self.store
            .record_decision(context_hash, decision.clone())
            .await?;
        Ok(decision)
    }
}

/// Helper module for hex encoding
mod hex {
    /// Encode bytes as hexadecimal string
    pub fn encode(bytes: impl AsRef<[u8]>) -> String {
        bytes
            .as_ref()
            .iter()
            .map(|b| format!("{b:02x}"))
            .collect()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashMap;
    use std::sync::Mutex;

    struct InMemoryDecisionStore {
        decisions: Mutex<HashMap<String, ClaudeDecision>>,
    }

    impl InMemoryDecisionStore {
        fn new() -> Self {
            Self {
                decisions: Mutex::new(HashMap::new()),
            }
        }
    }

    impl DecisionStore for InMemoryDecisionStore {
        async fn get_decision(
            &self,
            context_hash: &str,
        ) -> Result<Option<ClaudeDecision>, ClaudeError> {
            let decisions = self.decisions.lock().unwrap();
            Ok(decisions.get(context_hash).cloned())
        }

        async fn record_decision(
            &self,
            context_hash: String,
            decision: ClaudeDecision,
        ) -> Result<(), ClaudeError> {
            let mut decisions = self.decisions.lock().unwrap();
            decisions.insert(context_hash, decision);
            Ok(())
        }
    }

    #[test]
    fn test_context_hash_determinism() {
        let store = Arc::new(InMemoryDecisionStore::new());
        let recorder = DecisionRecorder::new(store);

        let context = DecisionContext::new();
        let hash1 = recorder.compute_context_hash(&context);
        let hash2 = recorder.compute_context_hash(&context);

        assert_eq!(hash1, hash2);
    }
}
