//! Credit Management
//!
//! Promotional credits, SLA credits, and referral rewards.

use crate::{AccountId, SynorDecimal};
use chrono::{DateTime, Duration, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};

/// Credit type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CreditType {
    /// Welcome bonus for new accounts
    Welcome,
    /// Promotional campaign
    Promotional,
    /// Referral reward
    Referral,
    /// SLA violation compensation
    SlaCompensation,
    /// Support ticket resolution
    Support,
    /// Bug bounty reward
    BugBounty,
    /// Manual adjustment
    Manual,
}

impl std::fmt::Display for CreditType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            CreditType::Welcome => write!(f, "Welcome Bonus"),
            CreditType::Promotional => write!(f, "Promotional"),
            CreditType::Referral => write!(f, "Referral Reward"),
            CreditType::SlaCompensation => write!(f, "SLA Compensation"),
            CreditType::Support => write!(f, "Support Credit"),
            CreditType::BugBounty => write!(f, "Bug Bounty"),
            CreditType::Manual => write!(f, "Manual Adjustment"),
        }
    }
}

/// Account credit record
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Credit {
    /// Unique credit ID
    pub id: String,
    /// Account receiving the credit
    pub account_id: AccountId,
    /// Credit amount in SYNOR
    pub amount: SynorDecimal,
    /// Original amount (before any usage)
    pub original_amount: SynorDecimal,
    /// Amount used
    pub used_amount: SynorDecimal,
    /// Credit type
    pub credit_type: CreditType,
    /// Reason/description
    pub reason: String,
    /// Date created
    pub created_at: DateTime<Utc>,
    /// Expiration date (optional)
    pub expires_at: Option<DateTime<Utc>>,
    /// Whether the credit has been fully used or expired
    pub is_active: bool,
    /// Reference ID (e.g., referral code, ticket number)
    pub reference_id: Option<String>,
    /// Approved by (admin user)
    pub approved_by: Option<String>,
}

impl Credit {
    /// Create a new credit
    pub fn new(
        account_id: impl Into<String>,
        amount: SynorDecimal,
        reason: impl Into<String>,
    ) -> Self {
        Self {
            id: generate_credit_id(),
            account_id: account_id.into(),
            amount,
            original_amount: amount,
            used_amount: Decimal::ZERO,
            credit_type: CreditType::Manual,
            reason: reason.into(),
            created_at: Utc::now(),
            expires_at: None,
            is_active: true,
            reference_id: None,
            approved_by: None,
        }
    }

    /// Create welcome credit
    pub fn welcome(account_id: impl Into<String>, amount: SynorDecimal) -> Self {
        Self::new(account_id, amount, "Welcome to Synor!")
            .with_type(CreditType::Welcome)
            .with_expiry_days(90) // 90 day expiry
    }

    /// Create referral credit
    pub fn referral(
        account_id: impl Into<String>,
        amount: SynorDecimal,
        referrer_id: impl Into<String>,
    ) -> Self {
        Self::new(account_id, amount, "Referral reward")
            .with_type(CreditType::Referral)
            .with_reference(referrer_id)
    }

    /// Create SLA compensation credit
    pub fn sla_compensation(
        account_id: impl Into<String>,
        amount: SynorDecimal,
        incident_id: impl Into<String>,
    ) -> Self {
        Self::new(account_id, amount, "SLA violation compensation")
            .with_type(CreditType::SlaCompensation)
            .with_reference(incident_id)
    }

    /// Set credit type
    pub fn with_type(mut self, credit_type: CreditType) -> Self {
        self.credit_type = credit_type;
        self
    }

    /// Set expiration date
    pub fn with_expiry(mut self, expires_at: DateTime<Utc>) -> Self {
        self.expires_at = Some(expires_at);
        self
    }

    /// Set expiration days from now
    pub fn with_expiry_days(mut self, days: i64) -> Self {
        self.expires_at = Some(Utc::now() + Duration::days(days));
        self
    }

    /// Set reference ID
    pub fn with_reference(mut self, reference_id: impl Into<String>) -> Self {
        self.reference_id = Some(reference_id.into());
        self
    }

    /// Set approver
    pub fn with_approver(mut self, approver: impl Into<String>) -> Self {
        self.approved_by = Some(approver.into());
        self
    }

    /// Use some of the credit
    pub fn use_credit(&mut self, amount: SynorDecimal) -> Result<SynorDecimal, CreditError> {
        if !self.is_active {
            return Err(CreditError::CreditInactive);
        }

        if self.is_expired() {
            self.is_active = false;
            return Err(CreditError::CreditExpired);
        }

        let available = self.amount;
        if amount > available {
            return Err(CreditError::InsufficientCredit {
                requested: amount,
                available,
            });
        }

        self.amount -= amount;
        self.used_amount += amount;

        if self.amount == Decimal::ZERO {
            self.is_active = false;
        }

        Ok(amount)
    }

    /// Check if credit is expired
    pub fn is_expired(&self) -> bool {
        self.expires_at
            .map(|exp| Utc::now() > exp)
            .unwrap_or(false)
    }

    /// Get remaining amount
    pub fn remaining(&self) -> SynorDecimal {
        if self.is_expired() || !self.is_active {
            Decimal::ZERO
        } else {
            self.amount
        }
    }

    /// Get days until expiration
    pub fn days_until_expiry(&self) -> Option<i64> {
        self.expires_at.map(|exp| {
            let diff = exp - Utc::now();
            diff.num_days()
        })
    }
}

/// Generate unique credit ID
fn generate_credit_id() -> String {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    format!("crd_{:x}", nanos)
}

/// Credit errors
#[derive(Debug, Clone, PartialEq)]
pub enum CreditError {
    /// Credit is no longer active
    CreditInactive,
    /// Credit has expired
    CreditExpired,
    /// Insufficient credit balance
    InsufficientCredit {
        requested: SynorDecimal,
        available: SynorDecimal,
    },
    /// Exceeds maximum credit allowed per account
    ExceedsMaxCredit {
        current: SynorDecimal,
        requested: SynorDecimal,
        maximum: SynorDecimal,
    },
}

impl std::fmt::Display for CreditError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            CreditError::CreditInactive => write!(f, "Credit is no longer active"),
            CreditError::CreditExpired => write!(f, "Credit has expired"),
            CreditError::InsufficientCredit { requested, available } => {
                write!(
                    f,
                    "Insufficient credit: requested {}, available {}",
                    requested, available
                )
            }
            CreditError::ExceedsMaxCredit { current, requested, maximum } => {
                write!(
                    f,
                    "Credit exceeds maximum: current {}, requested {}, maximum {}",
                    current, requested, maximum
                )
            }
        }
    }
}

impl std::error::Error for CreditError {}

/// Credit policy configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreditPolicy {
    /// Welcome credit amount
    pub welcome_amount: SynorDecimal,
    /// Referral credit for referrer
    pub referral_referrer_amount: SynorDecimal,
    /// Referral credit for referee
    pub referral_referee_amount: SynorDecimal,
    /// Maximum credit per account
    pub max_credit_per_account: SynorDecimal,
    /// Default expiry days
    pub default_expiry_days: i64,
}

impl Default for CreditPolicy {
    fn default() -> Self {
        Self {
            welcome_amount: Decimal::new(10, 0), // 10 SYNOR
            referral_referrer_amount: Decimal::new(25, 0), // 25 SYNOR
            referral_referee_amount: Decimal::new(10, 0), // 10 SYNOR
            max_credit_per_account: Decimal::new(1000, 0), // 1000 SYNOR
            default_expiry_days: 365,
        }
    }
}

/// Credit manager for issuing and managing credits based on policy
pub struct CreditManager {
    /// Credit policy configuration
    policy: CreditPolicy,
    /// Credits by account ID
    credits: std::collections::HashMap<AccountId, Vec<Credit>>,
}

impl CreditManager {
    /// Create a new credit manager with default policy
    pub fn new() -> Self {
        Self::with_policy(CreditPolicy::default())
    }

    /// Create a credit manager with custom policy
    pub fn with_policy(policy: CreditPolicy) -> Self {
        Self {
            policy,
            credits: std::collections::HashMap::new(),
        }
    }

    /// Issue welcome credit to a new account
    pub fn issue_welcome_credit(&mut self, account_id: impl Into<String>) -> Credit {
        let account_id = account_id.into();
        let credit = Credit::welcome(&account_id, self.policy.welcome_amount)
            .with_expiry_days(self.policy.default_expiry_days);

        self.credits
            .entry(account_id)
            .or_default()
            .push(credit.clone());

        credit
    }

    /// Issue referral credits to both referrer and referee
    pub fn issue_referral_credits(
        &mut self,
        referrer_id: impl Into<String>,
        referee_id: impl Into<String>,
    ) -> (Credit, Credit) {
        let referrer_id = referrer_id.into();
        let referee_id = referee_id.into();

        // Credit for the referrer
        let referrer_credit = Credit::referral(&referrer_id, self.policy.referral_referrer_amount, &referee_id)
            .with_expiry_days(self.policy.default_expiry_days);

        // Credit for the referee
        let referee_credit = Credit::referral(&referee_id, self.policy.referral_referee_amount, &referrer_id)
            .with_expiry_days(self.policy.default_expiry_days);

        self.credits
            .entry(referrer_id)
            .or_default()
            .push(referrer_credit.clone());

        self.credits
            .entry(referee_id)
            .or_default()
            .push(referee_credit.clone());

        (referrer_credit, referee_credit)
    }

    /// Issue promotional credit
    pub fn issue_promotional_credit(
        &mut self,
        account_id: impl Into<String>,
        amount: SynorDecimal,
        campaign: impl Into<String>,
    ) -> Result<Credit, CreditError> {
        let account_id = account_id.into();

        // Check if adding this credit exceeds the maximum
        let current_total = self.get_total_credit(&account_id);
        if current_total + amount > self.policy.max_credit_per_account {
            return Err(CreditError::ExceedsMaxCredit {
                current: current_total,
                requested: amount,
                maximum: self.policy.max_credit_per_account,
            });
        }

        let credit = Credit::new(&account_id, amount, campaign.into())
            .with_type(CreditType::Promotional)
            .with_expiry_days(self.policy.default_expiry_days);

        self.credits
            .entry(account_id)
            .or_default()
            .push(credit.clone());

        Ok(credit)
    }

    /// Issue SLA compensation credit
    pub fn issue_sla_credit(
        &mut self,
        account_id: impl Into<String>,
        amount: SynorDecimal,
        incident_id: impl Into<String>,
    ) -> Credit {
        let account_id = account_id.into();
        let credit = Credit::sla_compensation(&account_id, amount, incident_id);

        self.credits
            .entry(account_id)
            .or_default()
            .push(credit.clone());

        credit
    }

    /// Get total active credit for an account
    pub fn get_total_credit(&self, account_id: &str) -> SynorDecimal {
        self.credits
            .get(account_id)
            .map(|credits| {
                credits
                    .iter()
                    .filter(|c| c.is_active && !c.is_expired())
                    .map(|c| c.amount)
                    .sum()
            })
            .unwrap_or(Decimal::ZERO)
    }

    /// Get all active credits for an account
    pub fn get_active_credits(&self, account_id: &str) -> Vec<&Credit> {
        self.credits
            .get(account_id)
            .map(|credits| {
                credits
                    .iter()
                    .filter(|c| c.is_active && !c.is_expired())
                    .collect()
            })
            .unwrap_or_default()
    }

    /// Use credits from an account (applies oldest first - FIFO)
    pub fn use_credits(
        &mut self,
        account_id: &str,
        amount: SynorDecimal,
    ) -> Result<SynorDecimal, CreditError> {
        let total = self.get_total_credit(account_id);
        if total < amount {
            return Err(CreditError::InsufficientCredit {
                requested: amount,
                available: total,
            });
        }

        let credits = self.credits.get_mut(account_id).unwrap();
        let mut remaining = amount;

        // Sort by expiry date (soonest first) for FIFO
        credits.sort_by(|a, b| {
            match (&a.expires_at, &b.expires_at) {
                (Some(a_exp), Some(b_exp)) => a_exp.cmp(b_exp),
                (Some(_), None) => std::cmp::Ordering::Less,
                (None, Some(_)) => std::cmp::Ordering::Greater,
                (None, None) => a.created_at.cmp(&b.created_at),
            }
        });

        for credit in credits.iter_mut() {
            if remaining <= Decimal::ZERO {
                break;
            }
            if credit.is_active && !credit.is_expired() {
                let used = credit.use_credit(remaining).unwrap_or(Decimal::ZERO);
                remaining -= used;
            }
        }

        Ok(amount - remaining)
    }

    /// Get policy
    pub fn policy(&self) -> &CreditPolicy {
        &self.policy
    }
}

impl Default for CreditManager {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use rust_decimal_macros::dec;

    #[test]
    fn test_credit_creation() {
        let credit = Credit::new("account_123", dec!(50), "Test credit");

        assert_eq!(credit.account_id, "account_123");
        assert_eq!(credit.amount, dec!(50));
        assert_eq!(credit.original_amount, dec!(50));
        assert!(credit.is_active);
    }

    #[test]
    fn test_welcome_credit() {
        let credit = Credit::welcome("new_user", dec!(10));

        assert_eq!(credit.credit_type, CreditType::Welcome);
        assert!(credit.expires_at.is_some());
        assert!(credit.days_until_expiry().unwrap() <= 90);
    }

    #[test]
    fn test_use_credit() {
        let mut credit = Credit::new("test", dec!(100), "Test");

        // Use partial
        let used = credit.use_credit(dec!(30)).unwrap();
        assert_eq!(used, dec!(30));
        assert_eq!(credit.amount, dec!(70));
        assert_eq!(credit.used_amount, dec!(30));
        assert!(credit.is_active);

        // Use remaining
        let used = credit.use_credit(dec!(70)).unwrap();
        assert_eq!(used, dec!(70));
        assert_eq!(credit.amount, dec!(0));
        assert!(!credit.is_active);
    }

    #[test]
    fn test_insufficient_credit() {
        let mut credit = Credit::new("test", dec!(50), "Test");

        let result = credit.use_credit(dec!(100));
        assert!(matches!(
            result,
            Err(CreditError::InsufficientCredit { .. })
        ));
    }

    #[test]
    fn test_referral_credit() {
        let credit = Credit::referral("new_user", dec!(25), "referrer_123");

        assert_eq!(credit.credit_type, CreditType::Referral);
        assert_eq!(credit.reference_id, Some("referrer_123".to_string()));
    }
}
