//! Item envelope, sections, and custom fields.
//!
//! `FieldKind` and `FieldValue` are kept as parallel enums (rather than collapsing
//! to a single tagged enum) so the kind can be queried without inspecting the value.
//! Validation invariant: kind and value's discriminants must match — enforced at
//! construction (`Field::new`) and during deserialization (`Field::validate`).

use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
use url::Url;
use zeroize::Zeroizing;

use crate::error::{RelicarioError, Result};
use crate::ids::{AttachmentId, FieldId};
use crate::item_types::TotpConfig;
use crate::time::MonthYear;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FieldKind {
    Text,
    Multiline,
    Password,
    Concealed,
    Url,
    Email,
    Phone,
    Date,
    MonthYear,
    Totp,
    Reference,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum FieldValue {
    Text(String),
    Multiline(String),
    Password(Zeroizing<String>),
    Concealed(Zeroizing<String>),
    Url(Url),
    Email(String),
    Phone(String),
    Date(NaiveDate),
    MonthYear(MonthYear),
    Totp(TotpConfig),
    Reference(AttachmentId),
}

impl FieldValue {
    pub fn kind(&self) -> FieldKind {
        match self {
            FieldValue::Text(_) => FieldKind::Text,
            FieldValue::Multiline(_) => FieldKind::Multiline,
            FieldValue::Password(_) => FieldKind::Password,
            FieldValue::Concealed(_) => FieldKind::Concealed,
            FieldValue::Url(_) => FieldKind::Url,
            FieldValue::Email(_) => FieldKind::Email,
            FieldValue::Phone(_) => FieldKind::Phone,
            FieldValue::Date(_) => FieldKind::Date,
            FieldValue::MonthYear(_) => FieldKind::MonthYear,
            FieldValue::Totp(_) => FieldKind::Totp,
            FieldValue::Reference(_) => FieldKind::Reference,
        }
    }

    /// True if this kind triggers field-history capture on update.
    pub fn is_history_tracked(&self) -> bool {
        matches!(self, FieldValue::Password(_) | FieldValue::Concealed(_) | FieldValue::Totp(_))
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Field {
    pub id: FieldId,
    pub label: String,
    pub kind: FieldKind,
    pub value: FieldValue,
    #[serde(default)]
    pub hidden_by_default: bool,
}

impl Field {
    /// Construct a field, deriving `kind` from `value`.
    pub fn new(label: String, value: FieldValue) -> Self {
        let kind = value.kind();
        Self {
            id: FieldId::new(),
            label,
            kind,
            value,
            hidden_by_default: matches!(kind, FieldKind::Password | FieldKind::Concealed),
        }
    }

    /// Verify kind/value discriminants match. Called after deserialization.
    pub fn validate(&self) -> Result<()> {
        if self.kind != self.value.kind() {
            return Err(RelicarioError::Format(format!(
                "field {}: kind {:?} does not match value discriminant {:?}",
                self.id.as_str(),
                self.kind,
                self.value.kind()
            )));
        }
        Ok(())
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Section {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    pub fields: Vec<Field>,
}

use std::collections::HashMap;

use crate::attachment::AttachmentRef;
use crate::ids::ItemId;
use crate::item_types::{ItemCore, ItemType};
use crate::time::now_unix;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldHistoryEntry {
    pub value: Zeroizing<String>,
    pub replaced_at: i64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Item {
    pub id: ItemId,
    pub title: String,
    pub r#type: ItemType,
    #[serde(default)]
    pub tags: Vec<String>,
    #[serde(default)]
    pub favorite: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub group: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub notes: Option<String>,
    pub created: i64,
    pub modified: i64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub trashed_at: Option<i64>,
    pub core: ItemCore,
    #[serde(default)]
    pub sections: Vec<Section>,
    #[serde(default)]
    pub attachments: Vec<AttachmentRef>,
    #[serde(default)]
    pub field_history: HashMap<FieldId, Vec<FieldHistoryEntry>>,
}

impl Item {
    /// Construct a new Item from a typed core; auto-fills id, type, timestamps.
    pub fn new(title: String, core: ItemCore) -> Self {
        let now = now_unix();
        let r#type = core.item_type();
        Self {
            id: ItemId::new(),
            title,
            r#type,
            tags: Vec::new(),
            favorite: false,
            group: None,
            notes: None,
            created: now,
            modified: now,
            trashed_at: None,
            core,
            sections: Vec::new(),
            attachments: Vec::new(),
            field_history: HashMap::new(),
        }
    }

    /// Replace a custom field's value, capturing the previous value into
    /// field_history if the field's kind is history-tracked.
    pub fn set_field_value(&mut self, field_id: &FieldId, new_value: FieldValue) -> Result<()> {
        for section in &mut self.sections {
            if let Some(field) = section.fields.iter_mut().find(|f| &f.id == field_id) {
                if field.value.kind() != new_value.kind() {
                    return Err(RelicarioError::Format(format!(
                        "field {}: cannot change kind from {:?} to {:?}",
                        field.id.as_str(), field.value.kind(), new_value.kind()
                    )));
                }
                if field.value.is_history_tracked() {
                    let serialized = serialize_history_value(&field.value)?;
                    self.field_history
                        .entry(field.id.clone())
                        .or_default()
                        .push(FieldHistoryEntry { value: serialized, replaced_at: now_unix() });
                }
                field.value = new_value;
                self.modified = now_unix();
                return Ok(());
            }
        }
        Err(RelicarioError::Format(format!("field {} not found", field_id.as_str())))
    }

    pub fn soft_delete(&mut self) {
        self.trashed_at = Some(now_unix());
        self.modified = now_unix();
    }

    pub fn restore(&mut self) {
        self.trashed_at = None;
        self.modified = now_unix();
    }

    pub fn is_trashed(&self) -> bool {
        self.trashed_at.is_some()
    }

    pub fn prune_history(&mut self, retention: &crate::settings::HistoryRetention, now: i64) {
        use crate::settings::HistoryRetention;
        for history in self.field_history.values_mut() {
            match retention {
                HistoryRetention::Forever => {}
                HistoryRetention::LastN(n) => {
                    let n = *n as usize;
                    if history.len() > n {
                        let drop_count = history.len() - n;
                        history.drain(..drop_count);
                    }
                }
                HistoryRetention::Days(d) => {
                    let cutoff = now - (*d as i64) * 86_400;
                    history.retain(|e| e.replaced_at > cutoff);
                }
            }
        }
    }
}

/// Serialize a FieldValue to the string form stored in field_history.
fn serialize_history_value(value: &FieldValue) -> Result<Zeroizing<String>> {
    let s = match value {
        FieldValue::Password(p) => Zeroizing::new(p.as_str().to_owned()),
        FieldValue::Concealed(c) => Zeroizing::new(c.as_str().to_owned()),
        FieldValue::Totp(cfg) => {
            // Store the base32-encoded secret string for human-recognizability.
            let s = crate::base32::encode_rfc4648(&cfg.secret);
            Zeroizing::new(s)
        }
        _ => return Err(RelicarioError::Format("not a history-tracked kind".into())),
    };
    Ok(s)
}

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

    #[test]
    fn field_value_kind_matches() {
        let v = FieldValue::Text("hello".into());
        assert_eq!(v.kind(), FieldKind::Text);
    }

    #[test]
    fn password_field_marked_history_tracked() {
        assert!(FieldValue::Password(Zeroizing::new("x".into())).is_history_tracked());
        assert!(FieldValue::Concealed(Zeroizing::new("x".into())).is_history_tracked());
        assert!(FieldValue::Totp(TotpConfig::default()).is_history_tracked());
        assert!(!FieldValue::Text("x".into()).is_history_tracked());
        assert!(!FieldValue::Url(Url::parse("https://example.com").unwrap()).is_history_tracked());
    }

    #[test]
    fn field_new_derives_kind_from_value() {
        let f = Field::new("Password".into(), FieldValue::Password(Zeroizing::new("x".into())));
        assert_eq!(f.kind, FieldKind::Password);
        assert!(f.hidden_by_default);
    }

    #[test]
    fn field_new_text_not_hidden() {
        let f = Field::new("Username".into(), FieldValue::Text("alice".into()));
        assert!(!f.hidden_by_default);
    }

    #[test]
    fn field_validate_catches_kind_value_mismatch() {
        let f = Field {
            id: FieldId::new(),
            label: "x".into(),
            kind: FieldKind::Password,
            value: FieldValue::Text("not actually a password".into()),
            hidden_by_default: false,
        };
        assert!(f.validate().is_err());
    }

    #[test]
    fn field_round_trips() {
        let f = Field::new("Recovery code".into(), FieldValue::Concealed(Zeroizing::new("abcd-efgh".into())));
        let json = serde_json::to_string(&f).unwrap();
        let parsed: Field = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.label, "Recovery code");
        assert_eq!(parsed.kind, FieldKind::Concealed);
        parsed.validate().unwrap();
    }

    #[test]
    fn section_round_trip() {
        let s = Section {
            name: Some("Recovery codes".into()),
            fields: vec![
                Field::new("code1".into(), FieldValue::Concealed(Zeroizing::new("abc".into()))),
                Field::new("code2".into(), FieldValue::Concealed(Zeroizing::new("def".into()))),
            ],
        };
        let json = serde_json::to_string(&s).unwrap();
        let parsed: Section = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.name.as_deref(), Some("Recovery codes"));
        assert_eq!(parsed.fields.len(), 2);
    }

    #[test]
    fn new_item_has_timestamps_and_id() {
        let core = ItemCore::SecureNote(crate::item_types::SecureNoteCore::default());
        let item = Item::new("note".into(), core);
        assert_eq!(item.id.0.len(), 16);
        assert_eq!(item.r#type, ItemType::SecureNote);
        assert!(item.created > 0);
        assert_eq!(item.created, item.modified);
        assert!(item.field_history.is_empty());
    }

    #[test]
    fn soft_delete_and_restore_round_trip() {
        let core = ItemCore::Login(crate::item_types::LoginCore::default());
        let mut item = Item::new("login".into(), core);
        assert!(!item.is_trashed());
        item.soft_delete();
        assert!(item.is_trashed());
        item.restore();
        assert!(!item.is_trashed());
    }

    #[test]
    fn set_field_value_captures_history_for_password() {
        let core = ItemCore::Login(crate::item_types::LoginCore::default());
        let mut item = Item::new("login".into(), core);
        let pw_field = Field::new("Password".into(), FieldValue::Password(Zeroizing::new("old".into())));
        let pw_id = pw_field.id.clone();
        item.sections.push(Section { name: None, fields: vec![pw_field] });

        item.set_field_value(&pw_id, FieldValue::Password(Zeroizing::new("new".into()))).unwrap();
        let hist = item.field_history.get(&pw_id).expect("history should exist");
        assert_eq!(hist.len(), 1);
        assert_eq!(hist[0].value.as_str(), "old");
    }

    #[test]
    fn set_field_value_does_not_capture_history_for_text() {
        let core = ItemCore::Login(crate::item_types::LoginCore::default());
        let mut item = Item::new("login".into(), core);
        let f = Field::new("nickname".into(), FieldValue::Text("a".into()));
        let fid = f.id.clone();
        item.sections.push(Section { name: None, fields: vec![f] });

        item.set_field_value(&fid, FieldValue::Text("b".into())).unwrap();
        assert!(item.field_history.get(&fid).is_none_or(|v| v.is_empty()));
    }

    #[test]
    fn set_field_value_rejects_kind_change() {
        let core = ItemCore::Login(crate::item_types::LoginCore::default());
        let mut item = Item::new("login".into(), core);
        let f = Field::new("x".into(), FieldValue::Text("a".into()));
        let fid = f.id.clone();
        item.sections.push(Section { name: None, fields: vec![f] });

        let err = item.set_field_value(&fid, FieldValue::Password(Zeroizing::new("p".into())));
        assert!(err.is_err());
    }

    #[test]
    fn item_serializes_with_minimal_optional_fields() {
        let core = ItemCore::SecureNote(crate::item_types::SecureNoteCore::default());
        let item = Item::new("note".into(), core);
        let json = serde_json::to_string(&item).unwrap();
        // No "trashed_at" or "group" or "notes" should appear when None
        assert!(!json.contains("trashed_at"));
        assert!(!json.contains("\"group\""));
    }

    #[test]
    fn full_item_round_trip() {
        let core = ItemCore::Login(crate::item_types::LoginCore {
            username: Some("alice".into()),
            password: Some(Zeroizing::new("hunter2".into())),
            url: Some(Url::parse("https://github.com").unwrap()),
            totp: None,
        });
        let mut item = Item::new("GitHub".into(), core);
        item.tags = vec!["work".into()];
        item.favorite = true;
        item.notes = Some("notes".into());

        let json = serde_json::to_string(&item).unwrap();
        let parsed: Item = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.title, "GitHub");
        assert_eq!(parsed.tags, vec!["work".to_string()]);
        assert!(parsed.favorite);
        match parsed.core {
            ItemCore::Login(l) => {
                assert_eq!(l.username.as_deref(), Some("alice"));
            }
            other => panic!("expected Login, got {:?}", other),
        }
    }

    #[test]
    fn prune_history_keeps_last_n() {
        use crate::settings::HistoryRetention;

        let core = ItemCore::Login(crate::item_types::LoginCore::default());
        let mut item = Item::new("x".into(), core);
        let f = Field::new("p".into(), FieldValue::Password(Zeroizing::new("v0".into())));
        let fid = f.id.clone();
        item.sections.push(Section { name: None, fields: vec![f] });

        for i in 1..=5 {
            item.set_field_value(&fid, FieldValue::Password(Zeroizing::new(format!("v{i}")))).unwrap();
        }
        assert_eq!(item.field_history[&fid].len(), 5);

        item.prune_history(&HistoryRetention::LastN(3), 0);
        assert_eq!(item.field_history[&fid].len(), 3);
        // Keeps the MOST RECENT 3
        assert_eq!(item.field_history[&fid][0].value.as_str(), "v2");
    }

    #[test]
    fn prune_history_drops_old_entries_by_days() {
        use crate::settings::HistoryRetention;

        let core = ItemCore::Login(crate::item_types::LoginCore::default());
        let mut item = Item::new("x".into(), core);
        let f = Field::new("p".into(), FieldValue::Password(Zeroizing::new("v0".into())));
        let fid = f.id.clone();
        item.sections.push(Section { name: None, fields: vec![f] });

        let now = 1_000_000_000;
        item.field_history.insert(fid.clone(), vec![
            FieldHistoryEntry { value: Zeroizing::new("old".into()),    replaced_at: now - 100 * 86_400 },
            FieldHistoryEntry { value: Zeroizing::new("recent".into()), replaced_at: now - 86_400 },
        ]);

        item.prune_history(&HistoryRetention::Days(30), now);
        assert_eq!(item.field_history[&fid].len(), 1);
        assert_eq!(item.field_history[&fid][0].value.as_str(), "recent");
    }

    #[test]
    fn prune_history_forever_keeps_all() {
        use crate::settings::HistoryRetention;

        let core = ItemCore::Login(crate::item_types::LoginCore::default());
        let mut item = Item::new("x".into(), core);
        item.field_history.insert(FieldId::new(), vec![
            FieldHistoryEntry { value: Zeroizing::new("a".into()), replaced_at: 0 },
            FieldHistoryEntry { value: Zeroizing::new("b".into()), replaced_at: 0 },
        ]);
        item.prune_history(&HistoryRetention::Forever, 1_000_000_000);
        assert_eq!(item.field_history.values().next().unwrap().len(), 2);
    }
}
