//! Signing Policy Engine
//!
//! Evaluates whether a transaction should be signed based on:
//! - Mode-specific policy constraints
//! - Proof validity (private mode)
//! - Transfer limits and destination allowlists
//! - Replay protection
//!
//! Each mode has different requirements:
//! - **Public**: Standard validation (amount, destination, format)
//! - **Private**: Full ZK proof chain + standard validation + proof binding

use coldstar_zk::types::{ProofBundle, TransactionMode, TransferEnvelope};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fmt;
use std::time::{SystemTime, UNIX_EPOCH};

/// Result of a single policy check.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PolicyCheckResult {
    /// Check passed.
    Pass,
    /// Check failed — blocks signing.
    Fail,
    /// Check was skipped (not applicable for this mode).
    Skip,
}

impl fmt::Display for PolicyCheckResult {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            PolicyCheckResult::Pass => write!(f, "pass"),
            PolicyCheckResult::Fail => write!(f, "fail"),
            PolicyCheckResult::Skip => write!(f, "skip"),
        }
    }
}

/// A single policy check with name, result, and details.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyCheck {
    /// Name of the check (e.g., "amount_limit", "replay").
    pub name: String,
    /// Whether the check passed, failed, or was skipped.
    pub result: PolicyCheckResult,
    /// Human-readable detail about the check outcome.
    #[serde(default)]
    pub detail: String,
}

/// Complete result of policy evaluation for a transaction.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyEvaluation {
    /// Whether the transaction was approved by all policy checks.
    pub approved: bool,
    /// Transaction mode that was evaluated.
    pub mode: TransactionMode,
    /// Individual check results.
    pub checks: Vec<PolicyCheck>,
    /// Human-readable reason for rejection (empty if approved).
    #[serde(default)]
    pub reason: String,
    /// Unix timestamp of the evaluation.
    pub timestamp: f64,
}

impl PolicyEvaluation {
    /// Returns all checks that failed.
    pub fn failed_checks(&self) -> Vec<&PolicyCheck> {
        self.checks
            .iter()
            .filter(|c| c.result == PolicyCheckResult::Fail)
            .collect()
    }

    /// Returns a human-readable display string for the evaluation.
    pub fn display(&self) -> String {
        let mut lines = vec![
            format!(
                "Policy Evaluation: {}",
                if self.approved { "APPROVED" } else { "REJECTED" }
            ),
            format!("Mode: {}", self.mode.as_str()),
        ];

        for check in &self.checks {
            let icon = match check.result {
                PolicyCheckResult::Pass => "V",
                PolicyCheckResult::Fail => "X",
                PolicyCheckResult::Skip => "-",
            };
            lines.push(format!("  {} {}: {}", icon, check.name, check.detail));
        }

        if !self.reason.is_empty() {
            lines.push(format!("Reason: {}", self.reason));
        }

        lines.join("\n")
    }
}

/// Evaluates signing policy for a transfer envelope.
///
/// This is a lightweight policy layer on top of `ZkProofEngine::validate_envelope()`.
/// It wraps the [`TransferEnvelope`] type from `coldstar-zk` directly.
///
/// # Policy Checks
///
/// 1. **Mode validity** — mode is recognized
/// 2. **Envelope structure** — sender, recipient, and amount are present and valid
/// 3. **Amount limit** — transfer does not exceed configured maximum
/// 4. **Destination allowlist** — recipient is on the approved list (if enforced)
/// 5. **Replay protection** — transaction nonce has not been seen before
/// 6. **Proof completeness** — (private mode only) ownership proof present,
///    range proof and policy proofs checked
/// 7. **Proof binding** — (private mode only) binding hash is present
pub struct SigningPolicyEngine {
    max_transfer_lamports: u64,
    allowed_destinations: HashSet<String>,
    require_destination_allowlist: bool,
    seen_nonces: HashSet<String>,
}

impl SigningPolicyEngine {
    /// Create a new policy engine with the given configuration.
    ///
    /// # Arguments
    ///
    /// * `max_transfer_lamports` - Maximum transfer amount in lamports (default: 10 SOL)
    /// * `allowed_destinations` - Set of allowed recipient addresses
    /// * `require_destination_allowlist` - Whether to enforce the destination allowlist
    pub fn new(
        max_transfer_lamports: u64,
        allowed_destinations: Option<HashSet<String>>,
        require_destination_allowlist: bool,
    ) -> Self {
        Self {
            max_transfer_lamports,
            allowed_destinations: allowed_destinations.unwrap_or_default(),
            require_destination_allowlist,
            seen_nonces: HashSet::new(),
        }
    }

    /// Update the maximum transfer limit.
    pub fn set_max_transfer(&mut self, lamports: u64) {
        self.max_transfer_lamports = lamports;
    }

    /// Add an address to the destination allowlist.
    pub fn add_allowed_destination(&mut self, address: &str) {
        self.allowed_destinations.insert(address.to_string());
    }

    /// Remove an address from the destination allowlist.
    pub fn remove_allowed_destination(&mut self, address: &str) {
        self.allowed_destinations.remove(address);
    }

    /// Run all policy checks for a transfer envelope.
    pub fn evaluate(&mut self, envelope: &TransferEnvelope) -> PolicyEvaluation {
        let mode = envelope.mode;
        let mut checks: Vec<PolicyCheck> = Vec::new();

        // 1. Mode validity
        checks.push(PolicyCheck {
            name: "mode_valid".to_string(),
            result: PolicyCheckResult::Pass,
            detail: format!("Mode: {}", mode.as_str()),
        });

        // 2. Envelope structure
        checks.push(self.check_structure(envelope));

        // 3. Amount within limits
        checks.push(self.check_amount(envelope));

        // 4. Destination check
        checks.push(self.check_destination(envelope));

        // 5. Replay protection
        checks.push(self.check_replay(envelope));

        // 6. Private-mode-only checks
        if mode == TransactionMode::Private {
            match &envelope.proof_bundle {
                None => {
                    checks.push(PolicyCheck {
                        name: "proof_bundle_present".to_string(),
                        result: PolicyCheckResult::Fail,
                        detail: "Private mode requires a proof bundle".to_string(),
                    });
                }
                Some(bundle) => {
                    checks.push(PolicyCheck {
                        name: "proof_bundle_present".to_string(),
                        result: PolicyCheckResult::Pass,
                        detail: "Proof bundle provided".to_string(),
                    });
                    checks.extend(self.check_proof_completeness(bundle));
                    checks.push(self.check_proof_binding(envelope));
                }
            }
        } else {
            checks.push(PolicyCheck {
                name: "proof_bundle_present".to_string(),
                result: PolicyCheckResult::Skip,
                detail: "Not required for public mode".to_string(),
            });
        }

        // Determine approval
        let failed: Vec<&PolicyCheck> = checks
            .iter()
            .filter(|c| c.result == PolicyCheckResult::Fail)
            .collect();
        let approved = failed.is_empty();
        let reason = if approved {
            String::new()
        } else {
            failed
                .iter()
                .map(|c| format!("{}: {}", c.name, c.detail))
                .collect::<Vec<_>>()
                .join("; ")
        };

        // Record nonce if approved
        let nonce = &envelope.transaction.nonce;
        if approved && !nonce.is_empty() {
            self.seen_nonces.insert(nonce.clone());
        }

        let timestamp = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map(|d| d.as_secs_f64())
            .unwrap_or(0.0);

        PolicyEvaluation {
            approved,
            mode,
            checks,
            reason,
            timestamp,
        }
    }

    // -- Individual Checks ------------------------------------------------

    fn check_structure(&self, envelope: &TransferEnvelope) -> PolicyCheck {
        let tx = &envelope.transaction;
        let mut issues = Vec::new();

        if tx.from_pubkey.is_empty() {
            issues.push("missing sender");
        }
        if tx.to_pubkey.is_empty() {
            issues.push("missing recipient");
        }
        // amount_lamports is u64, so cannot be negative

        if issues.is_empty() {
            PolicyCheck {
                name: "structure".to_string(),
                result: PolicyCheckResult::Pass,
                detail: "Envelope structure valid".to_string(),
            }
        } else {
            PolicyCheck {
                name: "structure".to_string(),
                result: PolicyCheckResult::Fail,
                detail: format!("Invalid: {}", issues.join(", ")),
            }
        }
    }

    fn check_amount(&self, envelope: &TransferEnvelope) -> PolicyCheck {
        let amount = envelope.transaction.amount_lamports;
        if amount > self.max_transfer_lamports {
            PolicyCheck {
                name: "amount_limit".to_string(),
                result: PolicyCheckResult::Fail,
                detail: format!(
                    "Amount {} exceeds limit {}",
                    amount, self.max_transfer_lamports
                ),
            }
        } else {
            PolicyCheck {
                name: "amount_limit".to_string(),
                result: PolicyCheckResult::Pass,
                detail: format!(
                    "Amount {} within limit {}",
                    amount, self.max_transfer_lamports
                ),
            }
        }
    }

    fn check_destination(&self, envelope: &TransferEnvelope) -> PolicyCheck {
        let recipient = &envelope.transaction.to_pubkey;

        if !self.require_destination_allowlist {
            return PolicyCheck {
                name: "destination".to_string(),
                result: PolicyCheckResult::Pass,
                detail: "Destination allowlist not enforced".to_string(),
            };
        }

        if self.allowed_destinations.is_empty() {
            return PolicyCheck {
                name: "destination".to_string(),
                result: PolicyCheckResult::Fail,
                detail: "Destination allowlist is enforced but empty".to_string(),
            };
        }

        if self.allowed_destinations.contains(recipient) {
            let prefix = if recipient.len() >= 8 {
                &recipient[..8]
            } else {
                recipient
            };
            PolicyCheck {
                name: "destination".to_string(),
                result: PolicyCheckResult::Pass,
                detail: format!("Destination {}... is allowed", prefix),
            }
        } else {
            let prefix = if recipient.len() >= 8 {
                &recipient[..8]
            } else {
                recipient
            };
            PolicyCheck {
                name: "destination".to_string(),
                result: PolicyCheckResult::Fail,
                detail: format!("Destination {}... not in allowlist", prefix),
            }
        }
    }

    fn check_replay(&self, envelope: &TransferEnvelope) -> PolicyCheck {
        let nonce = &envelope.transaction.nonce;

        if nonce.is_empty() {
            return PolicyCheck {
                name: "replay".to_string(),
                result: PolicyCheckResult::Fail,
                detail: "Missing nonce".to_string(),
            };
        }

        if self.seen_nonces.contains(nonce) {
            let prefix = if nonce.len() >= 16 {
                &nonce[..16]
            } else {
                nonce
            };
            PolicyCheck {
                name: "replay".to_string(),
                result: PolicyCheckResult::Fail,
                detail: format!("Nonce '{}...' already used", prefix),
            }
        } else {
            PolicyCheck {
                name: "replay".to_string(),
                result: PolicyCheckResult::Pass,
                detail: "Nonce is fresh".to_string(),
            }
        }
    }

    fn check_proof_completeness(&self, bundle: &ProofBundle) -> Vec<PolicyCheck> {
        let mut checks = Vec::new();

        // Ownership proof is a required field in ProofBundle, so it's always present
        checks.push(PolicyCheck {
            name: "ownership_proof".to_string(),
            result: PolicyCheckResult::Pass,
            detail: "Ownership proof present".to_string(),
        });

        // Range proof is optional
        match &bundle.range_proof {
            Some(_) => {
                checks.push(PolicyCheck {
                    name: "range_proof".to_string(),
                    result: PolicyCheckResult::Pass,
                    detail: "Range proof present".to_string(),
                });
            }
            None => {
                checks.push(PolicyCheck {
                    name: "range_proof".to_string(),
                    result: PolicyCheckResult::Skip,
                    detail: "Range proof not included".to_string(),
                });
            }
        }

        // Policy proofs
        if bundle.policy_proofs.is_empty() {
            checks.push(PolicyCheck {
                name: "policy_proofs".to_string(),
                result: PolicyCheckResult::Skip,
                detail: "No policy proofs".to_string(),
            });
        } else {
            checks.push(PolicyCheck {
                name: "policy_proofs".to_string(),
                result: PolicyCheckResult::Pass,
                detail: format!("{} policy proof(s)", bundle.policy_proofs.len()),
            });
        }

        checks
    }

    fn check_proof_binding(&self, envelope: &TransferEnvelope) -> PolicyCheck {
        match &envelope.proof_bundle {
            Some(bundle) if !bundle.binding.is_empty() => PolicyCheck {
                name: "proof_binding".to_string(),
                result: PolicyCheckResult::Pass,
                detail: "Proof binding present".to_string(),
            },
            _ => PolicyCheck {
                name: "proof_binding".to_string(),
                result: PolicyCheckResult::Fail,
                detail: "No proof binding".to_string(),
            },
        }
    }
}

impl Default for SigningPolicyEngine {
    fn default() -> Self {
        Self::new(10_000_000_000, None, false)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use coldstar_zk::types::{OwnershipProof, TransactionContext};

    fn make_tx_context(mode: TransactionMode, nonce: &str) -> TransactionContext {
        TransactionContext {
            unsigned_tx_b64: "AAAA".to_string(),
            from_pubkey: "SenderPubkey11111111111111111111111111111111".to_string(),
            to_pubkey: "RecipientPubkey1111111111111111111111111111".to_string(),
            amount_lamports: 1_000_000_000,
            fee_lamports: 5000,
            recent_blockhash: "BlockHash111111111111111111111111111111111".to_string(),
            mode,
            nonce: nonce.to_string(),
            created_at: "2026-01-01T00:00:00Z".to_string(),
        }
    }

    fn make_envelope(mode: TransactionMode, nonce: &str) -> TransferEnvelope {
        TransferEnvelope {
            version: "1.0.0".to_string(),
            mode,
            created_at: "2026-01-01T00:00:00Z".to_string(),
            transaction: make_tx_context(mode, nonce),
            proof_bundle: None,
            integrity: "deadbeef".to_string(),
        }
    }

    fn make_proof_bundle() -> ProofBundle {
        ProofBundle {
            ownership_proof: OwnershipProof {
                public_key: "aabb".to_string(),
                commitment_r: "ccdd".to_string(),
                challenge: "eeff".to_string(),
                response: "1122".to_string(),
                context_hash: "3344".to_string(),
            },
            range_proof: None,
            policy_proofs: vec![],
            binding: "deadbeefcafebabe".to_string(),
            nonce: "abcd1234".to_string(),
            version: "1.0.0".to_string(),
        }
    }

    #[test]
    fn public_envelope_passes() {
        let mut engine = SigningPolicyEngine::default();
        let envelope = make_envelope(TransactionMode::Public, "nonce_001");
        let result = engine.evaluate(&envelope);
        assert!(result.approved);
        assert!(result.failed_checks().is_empty());
    }

    #[test]
    fn missing_nonce_fails() {
        let mut engine = SigningPolicyEngine::default();
        let envelope = make_envelope(TransactionMode::Public, "");
        let result = engine.evaluate(&envelope);
        assert!(!result.approved);
        assert!(result.reason.contains("replay"));
    }

    #[test]
    fn replay_detected() {
        let mut engine = SigningPolicyEngine::default();
        let envelope = make_envelope(TransactionMode::Public, "nonce_replay");
        let r1 = engine.evaluate(&envelope);
        assert!(r1.approved);

        let r2 = engine.evaluate(&envelope);
        assert!(!r2.approved);
        assert!(r2.reason.contains("already used"));
    }

    #[test]
    fn amount_over_limit() {
        let mut engine = SigningPolicyEngine::new(500_000_000, None, false);
        let mut envelope = make_envelope(TransactionMode::Public, "nonce_big");
        envelope.transaction.amount_lamports = 1_000_000_000;
        let result = engine.evaluate(&envelope);
        assert!(!result.approved);
        assert!(result.reason.contains("amount_limit"));
    }

    #[test]
    fn destination_allowlist_enforced() {
        let mut engine = SigningPolicyEngine::new(10_000_000_000, None, true);
        engine.add_allowed_destination("AllowedAddr1111111111111111111111111111111");

        let envelope = make_envelope(TransactionMode::Public, "nonce_dest");
        let result = engine.evaluate(&envelope);
        assert!(!result.approved);
        assert!(result.reason.contains("destination"));
    }

    #[test]
    fn private_without_proofs_fails() {
        let mut engine = SigningPolicyEngine::default();
        let envelope = make_envelope(TransactionMode::Private, "nonce_priv");
        let result = engine.evaluate(&envelope);
        assert!(!result.approved);
        assert!(result.reason.contains("proof_bundle_present"));
    }

    #[test]
    fn private_with_proofs_passes() {
        let mut engine = SigningPolicyEngine::default();
        let mut envelope = make_envelope(TransactionMode::Private, "nonce_priv_ok");
        envelope.proof_bundle = Some(make_proof_bundle());
        let result = engine.evaluate(&envelope);
        assert!(result.approved);
    }

    #[test]
    fn display_output_contains_mode() {
        let mut engine = SigningPolicyEngine::default();
        let envelope = make_envelope(TransactionMode::Public, "nonce_disp");
        let result = engine.evaluate(&envelope);
        let display = result.display();
        assert!(display.contains("APPROVED"));
        assert!(display.contains("public"));
    }
}
