//! Vault-level settings: trash retention, history retention, generator
//! defaults, attachment caps, autofill TOFU acks.

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VaultSettings {
    pub trash_retention: TrashRetention,
    pub field_history_retention: HistoryRetention,
    pub generator_defaults: GeneratorRequest,
    pub attachment_caps: AttachmentCaps,
    /// hostname → unix-seconds first-acked
    #[serde(default)]
    pub autofill_origin_acks: HashMap<String, i64>,
}

impl Default for VaultSettings {
    fn default() -> Self {
        Self {
            trash_retention: TrashRetention::Days(30),
            field_history_retention: HistoryRetention::Forever,
            generator_defaults: GeneratorRequest::default(),
            attachment_caps: AttachmentCaps::default(),
            autofill_origin_acks: HashMap::new(),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum TrashRetention {
    Days(u32),
    Forever,
}

impl TrashRetention {
    pub fn should_purge(&self, trashed_at: i64, now: i64) -> bool {
        match self {
            TrashRetention::Forever => false,
            TrashRetention::Days(d) => now - trashed_at > (*d as i64) * 86_400,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum HistoryRetention {
    LastN(u32),
    Days(u32),
    Forever,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum GeneratorRequest {
    Bip39 {
        word_count: u32,
        separator: String,
        capitalization: Capitalization,
    },
    Random {
        length: u32,
        classes: CharClasses,
        symbol_charset: SymbolCharset,
    },
}

impl Default for GeneratorRequest {
    fn default() -> Self {
        GeneratorRequest::Random {
            length: 20,
            classes: CharClasses { lower: true, upper: true, digits: true, symbols: true },
            symbol_charset: SymbolCharset::SafeOnly,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Capitalization {
    Lower,
    Upper,
    FirstOfEach,
    Title,
    Mixed,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct CharClasses {
    pub lower: bool,
    pub upper: bool,
    pub digits: bool,
    pub symbols: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum SymbolCharset {
    SafeOnly,
    Extended,
    Custom(String),
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct AttachmentCaps {
    pub per_attachment_max_bytes: u64,
    pub per_item_max_count: u32,
    pub per_vault_soft_cap_bytes: u64,
    pub per_vault_hard_cap_bytes: u64,
}

impl Default for AttachmentCaps {
    fn default() -> Self {
        Self {
            per_attachment_max_bytes: 10 * 1024 * 1024,
            per_item_max_count: 20,
            per_vault_soft_cap_bytes: 100 * 1024 * 1024,
            per_vault_hard_cap_bytes: 500 * 1024 * 1024,
        }
    }
}

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

    #[test]
    fn defaults_match_spec() {
        let s = VaultSettings::default();
        assert!(matches!(s.trash_retention, TrashRetention::Days(30)));
        assert!(matches!(s.field_history_retention, HistoryRetention::Forever));
        assert_eq!(s.attachment_caps.per_attachment_max_bytes, 10 * 1024 * 1024);
        assert_eq!(s.attachment_caps.per_item_max_count, 20);
    }

    #[test]
    fn trash_retention_purges_after_days() {
        let r = TrashRetention::Days(30);
        let now = 1_000_000_000;
        let recently_trashed = now - 29 * 86_400;
        let long_trashed = now - 31 * 86_400;
        assert!(!r.should_purge(recently_trashed, now));
        assert!(r.should_purge(long_trashed, now));
    }

    #[test]
    fn trash_retention_forever_never_purges() {
        let r = TrashRetention::Forever;
        assert!(!r.should_purge(0, 1_000_000_000));
    }

    #[test]
    fn settings_round_trip() {
        let s = VaultSettings::default();
        let json = serde_json::to_string(&s).unwrap();
        let parsed: VaultSettings = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.attachment_caps.per_attachment_max_bytes,
                   s.attachment_caps.per_attachment_max_bytes);
    }

    #[test]
    fn random_generator_default_is_20_safe() {
        match VaultSettings::default().generator_defaults {
            GeneratorRequest::Random { length, classes, symbol_charset } => {
                assert_eq!(length, 20);
                assert!(classes.lower && classes.upper && classes.digits && classes.symbols);
                assert!(matches!(symbol_charset, SymbolCharset::SafeOnly));
            }
            _ => panic!("expected Random default"),
        }
    }

    #[test]
    fn symbol_charset_custom_round_trips() {
        let c = SymbolCharset::Custom("!@#".into());
        let json = serde_json::to_string(&c).unwrap();
        let parsed: SymbolCharset = serde_json::from_str(&json).unwrap();
        match parsed {
            SymbolCharset::Custom(s) => assert_eq!(s, "!@#"),
            other => panic!("expected Custom, got {:?}", other),
        }
    }
}
