use crate::domain::vault::{Vault, VaultStatus, VaultTriggerConfig, VaultTriggerType};
use crate::domain::beneficiary::{Beneficiary, VaultKey};
use crate::domain::record_id::RecordId;
use crate::ports::repository::{VaultRepositoryPort, JotRepositoryPort, BeneficiaryRepositoryPort, UserRepositoryPort, AccessLinkRepositoryPort};
use crate::ports::email::EmailPort;
use crate::ports::id_generator::IdGeneratorPort;
use crate::domain::access_link::{AccessLink, AccessLinkStatus};
use crate::error::{Result, JottamError};
use chrono::Utc;
use std::collections::HashSet;

pub struct VaultUseCase<
    V: VaultRepositoryPort, 
    J: JotRepositoryPort, 
    B: BeneficiaryRepositoryPort,
    U: UserRepositoryPort,
    A: AccessLinkRepositoryPort,
    E: EmailPort,
    G: IdGeneratorPort,
> {
    vault_repo: V,
    jot_repo: J,
    ben_repo: B,
    user_repo: U,
    access_link_repo: A,
    email_port: E,
    id_gen: G,
}

impl<
    V: VaultRepositoryPort, 
    J: JotRepositoryPort, 
    B: BeneficiaryRepositoryPort,
    U: UserRepositoryPort,
    A: AccessLinkRepositoryPort,
    E: EmailPort,
    G: IdGeneratorPort,
> VaultUseCase<V, J, B, U, A, E, G> {
    pub fn new(
        vault_repo: V, 
        jot_repo: J, 
        ben_repo: B,
        user_repo: U,
        access_link_repo: A,
        email_port: E,
        id_gen: G,
    ) -> Self {
        Self { 
            vault_repo, 
            jot_repo, 
            ben_repo,
            user_repo,
            access_link_repo,
            email_port,
            id_gen,
        }
    }

    pub async fn create_vault(&self, name: String, description: Option<String>, user_id: RecordId) -> Result<Vault> {
        let current_count = self.vault_repo.count_by_user_id(&user_id).await?;
        if current_count >= 3 {
            return Err(JottamError::Conflict("Maximum limit of 3 vaults reached".to_string()));
        }

        if self.vault_repo.find_by_name_and_user_id(&name, &user_id).await?.is_some() {
            return Err(JottamError::Conflict(format!("Vault with name '{}' already exists", name)));
        }

        let now = Utc::now();
        let trigger_config = VaultTriggerConfig {
            r#type: VaultTriggerType::Inactivity,
            inactivity_duration_days: Some(30),
            fixed_date: None,
            notification_delay_days: None,
            notification_delay_hours: None,
        };
        let effective_trigger_at = Some(now + chrono::Duration::days(30));

        let id = self.id_gen.generate();
        let vault = Vault {
            id,
            name,
            description,
            user_id,
            beneficiary_ids: Vec::new(),
            jot_ids: Vec::new(),
            trigger_config: Some(trigger_config),
            effective_trigger_at,
            status: VaultStatus::Active,
            created_at: now,
            updated_at: now,
            version: 1,
        };

        self.vault_repo.save(vault).await
    }

    pub async fn get_vault(&self, id: &RecordId, owner_id: &RecordId) -> Result<Option<Vault>> {
        let vault = self.vault_repo.find_by_id(id).await?;

        if let Some(ref v) = vault {
            if &v.user_id != owner_id {
                return Err(JottamError::Unauthorized("Unauthorized access to Vault".to_string()));
            }
        }
        Ok(vault)
    }

    pub async fn list_vaults_by_user(&self, user_id: &RecordId, page: u32, limit: u32) -> Result<(Vec<Vault>, u64)> {
        let vaults = self.vault_repo.find_by_user_id(user_id, page, limit).await?;
        let total = self.vault_repo.count_by_user_id(user_id).await?;
        Ok((vaults, total))
    }

    pub async fn update_vault(&self, id: RecordId, owner_id: RecordId, name: Option<String>, description: Option<String>, trigger_config: Option<VaultTriggerConfig>) -> Result<Vault> {
        let mut existing = self.vault_repo.find_by_id(&id).await?
            .ok_or_else(|| JottamError::NotFound(format!("Vault {} not found", id)))?;

        if existing.user_id != owner_id {
            return Err(JottamError::Unauthorized("Unauthorized to update this Vault".to_string()));
        }

        if let Some(n) = name {
            existing.name = n;
        }
        if let Some(d) = description {
            existing.description = Some(d);
        }
        
        if let Some(tc) = trigger_config {
            let mut effective_trigger_at = match tc.r#type {
                VaultTriggerType::Inactivity => {
                    let days = tc.inactivity_duration_days.unwrap_or(30);
                    Some(Utc::now() + chrono::Duration::days(days)) 
                },
                VaultTriggerType::FixedDate => {
                    tc.fixed_date
                }
            };
            
            if let Some(ref mut dt) = effective_trigger_at {
                if let Some(delay_days) = tc.notification_delay_days {
                     *dt += chrono::Duration::days(delay_days);
                }
                if let Some(delay_hours) = tc.notification_delay_hours {
                     *dt += chrono::Duration::hours(delay_hours);
                }
            }

            existing.trigger_config = Some(tc);
            existing.effective_trigger_at = effective_trigger_at;
        }

        existing.updated_at = Utc::now();
        self.vault_repo.update(&id, existing).await
    }

    pub async fn delete_vault(&self, id: &RecordId, owner_id: &RecordId) -> Result<bool> {
        let existing = self.vault_repo.find_by_id(id).await?
            .ok_or_else(|| JottamError::NotFound(format!("Vault {} not found", id)))?;

        if &existing.user_id != owner_id {
            return Err(JottamError::Unauthorized("Unauthorized to delete this Vault".to_string()));
        }

        self.jot_repo.clear_vault_id_for_all_jots_in_vault(id, owner_id).await?;
        let deleted = self.vault_repo.delete(id).await?;
        Ok(deleted)
    }

    pub async fn add_jots_to_vault(&self, vault_id: RecordId, owner_id: RecordId, jot_ids: Vec<RecordId>) -> Result<Vault> {
        let mut target_vault = self.vault_repo.find_by_id(&vault_id).await?
            .ok_or_else(|| JottamError::NotFound(format!("Vault {} not found", vault_id)))?;

        if target_vault.user_id != owner_id {
            return Err(JottamError::Unauthorized("Unauthorized to modify this Vault".to_string()));
        }
        
        let jots = self.jot_repo.find_by_ids(&jot_ids).await?;
            
        for jot in &jots {
            if jot.owner_id != owner_id {
                return Err(JottamError::Unauthorized("Unauthorized: One or more Jots do not belong to you".to_string()));
            }
        }

        for jot in &jots {
            if let Some(ref old_vault_id) = jot.vault_id {
                if old_vault_id != &vault_id {
                    self.vault_repo.remove_jot_from_all_vaults(&jot.id, &owner_id).await?;
                }
            }
        }
        
        self.jot_repo.update_vault_id_for_jots(&jot_ids, &owner_id, Some(vault_id.clone())).await?;
        
        let mut target_jot_ids: HashSet<RecordId> = target_vault.jot_ids.into_iter().collect();
        for id in jot_ids {
            target_jot_ids.insert(id);
        }
        target_vault.jot_ids = target_jot_ids.into_iter().collect();
        target_vault.updated_at = Utc::now();
        
        self.vault_repo.update(&vault_id, target_vault).await
    }

    pub async fn remove_jots_from_vault(&self, vault_id: RecordId, owner_id: RecordId, jot_ids: Vec<RecordId>) -> Result<Vault> {
        let mut vault = self.vault_repo.find_by_id(&vault_id).await?
            .ok_or_else(|| JottamError::NotFound(format!("Vault {} not found", vault_id)))?;

        if vault.user_id != owner_id {
            return Err(JottamError::Unauthorized("Unauthorized to modify this Vault".to_string()));
        }
        
        self.jot_repo.update_vault_id_for_jots(&jot_ids, &owner_id, None).await?;
        
        let jot_id_set: HashSet<RecordId> = jot_ids.into_iter().collect();
        vault.jot_ids.retain(|id| !jot_id_set.contains(id));
        vault.updated_at = Utc::now();
        
        self.vault_repo.update(&vault_id, vault).await
    }

    pub async fn add_beneficiaries_to_vault(&self, vault_id: RecordId, owner_id: RecordId, beneficiaries_with_keys: Vec<(RecordId, VaultKey)>) -> Result<Vault> {
        let mut vault = self.vault_repo.find_by_id(&vault_id).await?
            .ok_or_else(|| JottamError::NotFound(format!("Vault {} not found", vault_id)))?;

        if vault.user_id != owner_id {
            return Err(JottamError::Unauthorized("Unauthorized to modify this Vault".to_string()));
        }
        
        for (ben_id, key) in beneficiaries_with_keys {
            if !vault.beneficiary_ids.contains(&ben_id) {
                vault.beneficiary_ids.push(ben_id.clone());
            }

            let mut beneficiary = self.ben_repo.find_by_id(&ben_id).await?
                 .ok_or_else(|| JottamError::NotFound(format!("Beneficiary {} not found", ben_id)))?;
            
            if beneficiary.user_id != owner_id.to_string() {
                 return Err(JottamError::Unauthorized("Unauthorized access to Beneficiary".to_string()));
            }

            let mut keys = beneficiary.vault_keys.unwrap_or_default();
            keys.retain(|k| k.vault_id != key.vault_id);
            keys.push(key);
            beneficiary.vault_keys = Some(keys);
            
            self.ben_repo.update(beneficiary).await?;
        }
        
        vault.updated_at = Utc::now();
        self.vault_repo.update(&vault_id, vault).await
    }

    pub async fn remove_beneficiaries_from_vault(&self, vault_id: RecordId, owner_id: RecordId, beneficiary_ids: Vec<RecordId>) -> Result<Vault> {
        let mut vault = self.vault_repo.find_by_id(&vault_id).await?
            .ok_or_else(|| JottamError::NotFound(format!("Vault {} not found", vault_id)))?;

        if vault.user_id != owner_id {
            return Err(JottamError::Unauthorized("Unauthorized to modify this Vault".to_string()));
        }
        
        let vault_id_str = vault_id.0.to_string();

        for ben_id in &beneficiary_ids {
            if let Ok(Some(mut beneficiary)) = self.ben_repo.find_by_id(ben_id).await {
                if beneficiary.user_id == owner_id.to_string() {
                    let mut keys = beneficiary.vault_keys.take().unwrap_or_default();
                    let initial_len = keys.len();
                    
                    keys.retain(|k| k.vault_id != vault_id_str);
                    
                    if keys.len() != initial_len {
                        beneficiary.vault_keys = Some(keys);
                        let _ = self.ben_repo.update(beneficiary).await;
                    } else {
                        beneficiary.vault_keys = Some(keys);
                    }
                }
            }
        }
        
        let ben_id_set: HashSet<RecordId> = beneficiary_ids.into_iter().collect();
        vault.beneficiary_ids.retain(|id| !ben_id_set.contains(id));
        
        vault.updated_at = Utc::now();
        self.vault_repo.update(&vault_id, vault).await
    }

    pub async fn process_due_vaults(&self) -> Result<usize> {
        let now = Utc::now();
        let due_vaults = self.vault_repo.find_due_vaults(now).await?;
        let count = due_vaults.len();

        for mut vault in due_vaults {
            vault.status = VaultStatus::Released;
            vault.updated_at = Utc::now();
            
            let vault_id = vault.id.clone();

            // Fetch Owner
            let owner = self.user_repo.find_by_id(&vault.user_id).await?;
                
            if let Some(_owner) = owner {
                if !vault.beneficiary_ids.is_empty() {
                    let beneficiaries = self.ben_repo.find_by_ids(&vault.beneficiary_ids).await?;
                    
                    for beneficiary in beneficiaries {
                        let access_link = AccessLink {
                            id: self.id_gen.generate(),
                            vault_id: vault_id.clone(),
                            beneficiary_id: beneficiary.id.clone(),
                            status: AccessLinkStatus::Pending,
                            attempts: 0,
                            created_at: Utc::now(),
                            updated_at: Utc::now(),
                        };
                        
                        if self.access_link_repo.save(access_link.clone()).await.is_err() {
                            continue;
                        }

                        // Send release notification to beneficiary
                        let _ = self.email_port.send_vault_release_notification(
                            &beneficiary.email,
                            &beneficiary.first_name,
                            &format!("{} {}", _owner.first_name, _owner.last_name),
                            &vault.name,
                            &access_link.id.0.to_string(),
                        ).await;
                    }
                }
            }

            let _ = self.vault_repo.update(&vault_id, vault).await;
        }
        
        Ok(count)
    }

    pub async fn get_access_link(&self, token_id: &RecordId) -> Result<AccessLink> {
        self.access_link_repo.find_by_id(token_id).await?
            .ok_or_else(|| JottamError::NotFound("Access link not found".to_string()))
    }

    pub async fn increment_failed_attempts(&self, token_id: &RecordId) -> Result<()> {
        let mut access_link = self.access_link_repo.find_by_id(token_id).await?
            .ok_or_else(|| JottamError::NotFound("Access link not found".to_string()))?;

        access_link.attempts += 1;
        if access_link.attempts >= 5 {
            access_link.status = AccessLinkStatus::Failed;
        }

        access_link.updated_at = Utc::now();
        self.access_link_repo.update(access_link).await?;
        
        Ok(())
    }

    pub async fn mark_verification_success(&self, token_id: &RecordId) -> Result<()> {
        let mut access_link = self.access_link_repo.find_by_id(token_id).await?
            .ok_or_else(|| JottamError::NotFound("Access link not found".to_string()))?;

        access_link.status = AccessLinkStatus::Success;
        access_link.updated_at = Utc::now();
        
        self.access_link_repo.update(access_link).await?;
        
        Ok(())
    }

    pub async fn get_access_link_details(&self, token_id: &RecordId) -> Result<(AccessLink, Vault, Beneficiary)> {
        let link = self.access_link_repo.find_by_id(token_id).await?
            .ok_or_else(|| JottamError::NotFound("Access link not found".to_string()))?;
        
        let vault = self.vault_repo.find_by_id(&link.vault_id).await?
            .ok_or_else(|| JottamError::NotFound("Vault not found".to_string()))?;
            
        let beneficiary = self.ben_repo.find_by_id(&link.beneficiary_id).await?
            .ok_or_else(|| JottamError::NotFound("Beneficiary not found".to_string()))?;
            
        Ok((link, vault, beneficiary))
    }

    pub async fn get_vault_by_id(&self, id: &RecordId) -> Result<Option<Vault>> {
        self.vault_repo.find_by_id(id).await
    }
}
