//! Client-side scheme abstractions for x402 payment handling.
//!
//! This module provides the traits and types needed by payment clients to
//! examine 402 responses, generate payment candidates, and sign payments.

use std::fmt::{Debug, Formatter};
use std::future::Future;
use std::pin::Pin;

use crate::chain::{ChainId, ChainIdPattern};
use crate::proto;

/// Trait for scheme clients that can process payment requirements.
///
/// Implementations examine 402 responses and generate payment candidates
/// for requirements they can fulfill.
pub trait SchemeClient: super::SchemeId + Send + Sync {
    /// Generates payment candidates for the given payment requirements.
    fn accept(&self, payment_required: &proto::PaymentRequired) -> Vec<PaymentCandidate>;
}

/// A payment option that can be signed and submitted.
///
/// Payment candidates are generated by scheme clients when they find
/// a matching payment requirement they can fulfill.
pub struct PaymentCandidate {
    /// The chain where payment will be made.
    pub chain_id: ChainId,
    /// The token asset address.
    pub asset: String,
    /// The payment amount in the token's smallest unit (as a decimal string).
    pub amount: String,
    /// The payment scheme name.
    pub scheme: String,
    /// The recipient address.
    pub pay_to: String,
    /// The signer that can authorize this payment.
    pub signer: Box<dyn PaymentCandidateSigner + Send + Sync>,
}

impl Debug for PaymentCandidate {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("PaymentCandidate")
            .field("chain_id", &self.chain_id)
            .field("asset", &self.asset)
            .field("amount", &self.amount)
            .field("scheme", &self.scheme)
            .field("pay_to", &self.pay_to)
            .field("signer", &"<dyn PaymentCandidateSigner>")
            .finish()
    }
}

impl PaymentCandidate {
    /// Signs this payment candidate, producing a payment payload.
    ///
    /// # Errors
    ///
    /// Returns [`ClientError`] if signing fails.
    pub async fn sign(&self) -> Result<String, ClientError> {
        self.signer.sign_payment().await
    }
}

/// Trait for signing payment authorizations.
pub trait PaymentCandidateSigner {
    /// Signs a payment authorization.
    fn sign_payment(
        &self,
    ) -> Pin<Box<dyn Future<Output = Result<String, ClientError>> + Send + '_>>;
}

/// Errors that can occur during client-side payment processing.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ClientError {
    /// No payment option matched the client's capabilities.
    #[error("No matching payment option found")]
    NoMatchingPaymentOption,

    /// The HTTP request body cannot be cloned (e.g., streaming).
    #[error("Request is not cloneable (streaming body?)")]
    RequestNotCloneable,

    /// Failed to parse the 402 response body.
    #[error("Failed to parse 402 response: {0}")]
    ParseError(String),

    /// Failed to sign the payment authorization.
    #[error("Failed to sign payment: {0}")]
    SigningError(String),

    /// JSON serialization/deserialization error.
    #[error("JSON error: {0}")]
    JsonError(#[from] serde_json::Error),
}

/// Trait for selecting the best payment candidate from available options.
///
/// Implement this trait to customize how payments are selected when
/// multiple options are available.
pub trait PaymentSelector: Send + Sync {
    /// Selects a payment candidate from the available options.
    ///
    /// Accepts a slice of references to allow seamless integration with
    /// the [`PaymentPolicy`] filtering pipeline.
    fn select<'a>(&self, candidates: &[&'a PaymentCandidate]) -> Option<&'a PaymentCandidate>;
}

/// Selector that returns the first matching candidate.
///
/// This is the simplest selection strategy. The order of candidates
/// is determined by the registration order of scheme clients.
#[derive(Debug, Clone, Copy)]
pub struct FirstMatch;

impl PaymentSelector for FirstMatch {
    fn select<'a>(&self, candidates: &[&'a PaymentCandidate]) -> Option<&'a PaymentCandidate> {
        candidates.first().copied()
    }
}

/// Selector that prefers specific chains in priority order.
///
/// Patterns are tried in order; the first matching candidate is returned.
/// If no patterns match, falls back to the first available candidate.
#[derive(Debug)]
pub struct PreferChain(Vec<ChainIdPattern>);

impl PreferChain {
    /// Creates a new chain preference selector.
    pub fn new<P: Into<Vec<ChainIdPattern>>>(patterns: P) -> Self {
        Self(patterns.into())
    }

    /// Adds additional chain patterns with lower priority.
    #[must_use]
    pub fn or_chain<P: Into<Vec<ChainIdPattern>>>(self, patterns: P) -> Self {
        Self(self.0.into_iter().chain(patterns.into()).collect())
    }
}

impl PaymentSelector for PreferChain {
    fn select<'a>(&self, candidates: &[&'a PaymentCandidate]) -> Option<&'a PaymentCandidate> {
        for pattern in &self.0 {
            if let Some(candidate) = candidates.iter().find(|c| pattern.matches(&c.chain_id)) {
                return Some(*candidate);
            }
        }
        candidates.first().copied()
    }
}

/// Selector that only accepts payments up to a maximum amount.
///
/// Useful for limiting spending or implementing budget controls.
#[derive(Debug, Clone, Copy)]
pub struct MaxAmount(pub u128);

impl PaymentSelector for MaxAmount {
    fn select<'a>(&self, candidates: &[&'a PaymentCandidate]) -> Option<&'a PaymentCandidate> {
        candidates
            .iter()
            .find(|c| c.amount.parse::<u128>().is_ok_and(|a| a <= self.0))
            .copied()
    }
}

/// Trait for filtering or transforming payment candidates before selection.
///
/// Policies are applied in sequence, forming a pipeline that progressively
/// narrows the set of acceptable payment options. This mirrors the official
/// x402 Go SDK's `PaymentPolicy` function type.
///
/// Unlike [`PaymentSelector`] which picks one candidate, policies operate on
/// the full candidate list and return a (potentially smaller) list.
pub trait PaymentPolicy: Send + Sync {
    /// Filters the candidates, returning only those that pass this policy.
    fn apply<'a>(&self, candidates: Vec<&'a PaymentCandidate>) -> Vec<&'a PaymentCandidate>;
}

/// Policy that restricts payments to specific networks.
///
/// Only candidates whose chain ID matches one of the allowed patterns are kept.
#[derive(Debug)]
pub struct NetworkPolicy(Vec<ChainIdPattern>);

impl NetworkPolicy {
    /// Creates a policy that only allows payments on the specified networks.
    pub fn new<P: Into<Vec<ChainIdPattern>>>(patterns: P) -> Self {
        Self(patterns.into())
    }
}

impl PaymentPolicy for NetworkPolicy {
    fn apply<'a>(&self, candidates: Vec<&'a PaymentCandidate>) -> Vec<&'a PaymentCandidate> {
        candidates
            .into_iter()
            .filter(|c| self.0.iter().any(|p| p.matches(&c.chain_id)))
            .collect()
    }
}

/// Policy that restricts payments to specific scheme names.
///
/// Only candidates whose scheme matches one of the allowed names are kept.
#[derive(Debug)]
pub struct SchemePolicy(Vec<String>);

impl SchemePolicy {
    /// Creates a policy that only allows the specified payment schemes.
    pub fn new<S: Into<String>, I: IntoIterator<Item = S>>(schemes: I) -> Self {
        Self(schemes.into_iter().map(Into::into).collect())
    }
}

impl PaymentPolicy for SchemePolicy {
    fn apply<'a>(&self, candidates: Vec<&'a PaymentCandidate>) -> Vec<&'a PaymentCandidate> {
        candidates
            .into_iter()
            .filter(|c| self.0.iter().any(|s| s == &c.scheme))
            .collect()
    }
}

/// Policy that restricts payments to a maximum amount.
///
/// Only candidates whose amount (parsed as `u128`) is at most the specified
/// limit are kept. Candidates with unparseable amounts are excluded.
#[derive(Debug, Clone, Copy)]
pub struct MaxAmountPolicy(pub u128);

impl PaymentPolicy for MaxAmountPolicy {
    fn apply<'a>(&self, candidates: Vec<&'a PaymentCandidate>) -> Vec<&'a PaymentCandidate> {
        candidates
            .into_iter()
            .filter(|c| c.amount.parse::<u128>().is_ok_and(|a| a <= self.0))
            .collect()
    }
}
