//! Staleness recalculation for research notes based on source freshness and time decay.

use crate::research::types::{FreshnessBucket, NoteType, ResearchNote, SourceRecord, StaleReason};
use crate::research::util::now_rfc3339;

const STALE_AFTER_DAYS: i64 = 90;
const AGING_AFTER_DAYS: i64 = 30;

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct StalenessVerdict {
    pub fresh: bool,
    pub reason: Option<StaleReason>,
}

#[must_use]
pub(crate) fn recalculate_staleness(
    notes: &[ResearchNote],
    sources: &[SourceRecord],
) -> Vec<(String, StalenessVerdict)> {
    let source_map: std::collections::HashMap<&str, &SourceRecord> = sources
        .iter()
        .map(|source| (source.id.as_str(), source))
        .collect();

    let now = now_rfc3339();
    let now_time = parse_iso_timestamp(&now);

    notes
        .iter()
        .filter(|note| !note.archived)
        .map(|note| {
            let verdict = evaluate_note_staleness(note, &source_map, now_time);
            (note.id.clone(), verdict)
        })
        .collect()
}

fn evaluate_note_staleness(
    note: &ResearchNote,
    source_map: &std::collections::HashMap<&str, &SourceRecord>,
    now_time: i64,
) -> StalenessVerdict {
    if matches!(
        note.note_type,
        NoteType::Question | NoteType::FollowUpTask | NoteType::Uncertainty
    ) {
        return StalenessVerdict {
            fresh: true,
            reason: None,
        };
    }

    if let Some(source_id) = note.source_id.as_deref() {
        if let Some(source) = source_map.get(source_id) {
            if source.superseded_by_source_id.is_some() {
                return StalenessVerdict {
                    fresh: false,
                    reason: Some(StaleReason::SourceExpired),
                };
            }

            if matches!(source.freshness_bucket, FreshnessBucket::Stale) {
                return StalenessVerdict {
                    fresh: false,
                    reason: Some(StaleReason::NewFiling),
                };
            }
        }
    }

    let updated_at = parse_iso_timestamp(&note.updated_at);
    let age_days = (now_time - updated_at) / 86_400;

    if age_days > STALE_AFTER_DAYS {
        return StalenessVerdict {
            fresh: false,
            reason: Some(StaleReason::SourceExpired),
        };
    }

    if age_days > AGING_AFTER_DAYS {
        return StalenessVerdict {
            fresh: true,
            reason: None,
        };
    }

    StalenessVerdict {
        fresh: true,
        reason: None,
    }
}

fn parse_iso_timestamp(iso: &str) -> i64 {
    iso.get(..19)
        .and_then(|prefix| chrono::DateTime::parse_from_rfc3339(&format!("{prefix}Z")).ok())
        .map(|dt| dt.timestamp())
        .unwrap_or(0)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::research::types::{
        AnalystStatus, EvidenceStatus, GhostStatus, NotePriority, NoteProvenance, ProvenanceActor,
        SourceKind, ThesisStatus,
    };
    use crate::research::util::{now_rfc3339, sha256_hex};

    #[test]
    fn fresh_note_is_not_stale() {
        let note = test_note("note-1", NoteType::Fact, None);
        let sources = vec![];
        let verdicts = recalculate_staleness(&[note], &sources);
        assert_eq!(verdicts.len(), 1);
        assert!(verdicts[0].1.fresh);
    }

    #[test]
    fn note_with_superseded_source_is_stale() {
        let source = test_source("source-1", Some("source-2".to_string()));
        let note = test_note("note-1", NoteType::Fact, Some("source-1".to_string()));
        let verdicts = recalculate_staleness(&[note], &[source]);
        assert!(!verdicts[0].1.fresh);
        assert_eq!(verdicts[0].1.reason, Some(StaleReason::SourceExpired));
    }

    #[test]
    fn question_notes_never_go_stale() {
        let note = test_note("note-1", NoteType::Question, None);
        let verdicts = recalculate_staleness(&[note], &[]);
        assert!(verdicts[0].1.fresh);
    }

    fn test_note(id: &str, note_type: NoteType, source_id: Option<String>) -> ResearchNote {
        let now = now_rfc3339();
        ResearchNote {
            id: id.to_string(),
            workspace_id: "ws-1".to_string(),
            company_id: None,
            ticker: Some("AAPL".to_string()),
            source_id,
            raw_text: "test".to_string(),
            cleaned_text: "test".to_string(),
            body_html: None,
            embeds: Vec::new(),
            file_ids: Vec::new(),
            title: None,
            note_type,
            subtype: None,
            analyst_status: AnalystStatus::Captured,
            ai_annotation: None,
            confidence: 0.8,
            evidence_status: EvidenceStatus::Unsourced,
            inferred_links: Vec::new(),
            ghost_status: GhostStatus::None,
            thesis_status: ThesisStatus::None,
            created_at: now.clone(),
            updated_at: now,
            provenance: NoteProvenance {
                created_by: ProvenanceActor::Manual,
                capture_method: crate::research::types::CaptureMethod::QuickEntry,
                source_kind: SourceKind::Manual,
                origin_note_id: None,
                origin_ghost_id: None,
                model_info: None,
                created_at: now_rfc3339(),
                raw_input_hash: sha256_hex("test"),
            },
            tags: Vec::new(),
            catalysts: Vec::new(),
            risks: Vec::new(),
            valuation_refs: Vec::new(),
            time_horizon: None,
            scenario: None,
            priority: NotePriority::Normal,
            pinned: false,
            archived: false,
            revision: 1,
            source_excerpt: None,
            last_enriched_at: None,
            last_linked_at: None,
            stale_reason: None,
            superseded_by_note_id: None,
        }
    }

    fn test_source(id: &str, superseded_by: Option<String>) -> SourceRecord {
        let now = now_rfc3339();
        SourceRecord {
            id: id.to_string(),
            workspace_id: "ws-1".to_string(),
            kind: SourceKind::Article,
            ticker: Some("AAPL".to_string()),
            title: "Test source".to_string(),
            publisher: None,
            url: None,
            canonical_url: None,
            filing_accession: None,
            form_type: None,
            published_at: None,
            as_of_date: None,
            ingested_at: now,
            freshness_bucket: FreshnessBucket::Fresh,
            checksum: None,
            metadata_json: serde_json::json!({}),
            superseded_by_source_id: superseded_by,
        }
    }
}
