//! Read-model projections for frontend workspace views.

use crate::research::types::{
    GhostNote, GraphEdge, GraphNode, KanbanColumn, MemoBlockCandidate, MemoSectionKind, NoteLink,
    NoteType, ResearchNote, ResearchWorkspace, TimelineEvent, WorkspaceProjection,
    WorkspaceViewKind,
};

#[must_use]
pub(crate) fn build_workspace_projection(
    workspace: ResearchWorkspace,
    requested_view: WorkspaceViewKind,
    notes: Vec<ResearchNote>,
    links: Vec<NoteLink>,
    ghosts: Vec<GhostNote>,
) -> WorkspaceProjection {
    let memo_blocks = build_memo_blocks(&notes, &ghosts);
    let graph_nodes = notes
        .iter()
        .map(|note| GraphNode {
            id: note.id.clone(),
            label: note
                .title
                .clone()
                .unwrap_or_else(|| note.cleaned_text.clone()),
            kind: format!("{:?}", note.note_type).to_ascii_lowercase(),
            confidence: note.confidence,
            evidence_status: note.evidence_status,
        })
        .collect();
    let graph_edges = links
        .iter()
        .map(|link| GraphEdge {
            id: link.id.clone(),
            from: link.from_note_id.clone(),
            to: link.to_note_id.clone(),
            link_type: link.link_type,
            strength: link.strength,
            confidence: link.confidence,
        })
        .collect();
    let kanban_columns = build_kanban_columns(&notes);
    let timeline_events = build_timeline(&notes, &ghosts);

    WorkspaceProjection {
        workspace,
        active_view: requested_view,
        notes,
        links,
        ghosts,
        memo_blocks,
        graph_nodes,
        graph_edges,
        kanban_columns,
        timeline_events,
    }
}

#[must_use]
pub(crate) fn build_memo_blocks(
    notes: &[ResearchNote],
    ghosts: &[GhostNote],
) -> Vec<MemoBlockCandidate> {
    let mut blocks = notes
        .iter()
        .filter(|note| {
            !note.archived
                && !matches!(
                    note.note_type,
                    NoteType::Question | NoteType::FollowUpTask | NoteType::SourceReference
                )
                && !matches!(
                    note.evidence_status,
                    crate::research::types::EvidenceStatus::Unsourced
                )
        })
        .filter_map(|note| {
            let section_kind = section_for_note(note.note_type)?;
            Some(MemoBlockCandidate {
                section_kind,
                headline: note
                    .title
                    .clone()
                    .unwrap_or_else(|| note.cleaned_text.clone()),
                body: note
                    .ai_annotation
                    .clone()
                    .unwrap_or_else(|| note.cleaned_text.clone()),
                source_note_ids: vec![note.id.clone()],
                citation_refs: note.source_id.iter().cloned().collect(),
                confidence: note.confidence,
                accepted: matches!(
                    note.thesis_status,
                    crate::research::types::ThesisStatus::AcceptedSupport
                        | crate::research::types::ThesisStatus::AcceptedCore
                        | crate::research::types::ThesisStatus::BullCase
                        | crate::research::types::ThesisStatus::BearCase
                ),
            })
        })
        .collect::<Vec<_>>();

    blocks.extend(
        ghosts
            .iter()
            .filter(|ghost| {
                matches!(
                    ghost.state,
                    crate::research::types::GhostLifecycleState::Accepted
                        | crate::research::types::GhostLifecycleState::Converted
                )
            })
            .filter_map(|ghost| {
                Some(MemoBlockCandidate {
                    section_kind: ghost.memo_section_hint?,
                    headline: ghost.headline.clone(),
                    body: ghost.body.clone(),
                    source_note_ids: ghost.supporting_note_ids.clone(),
                    citation_refs: ghost.source_ids.clone(),
                    confidence: ghost.confidence,
                    accepted: true,
                })
            }),
    );

    blocks
}

fn build_kanban_columns(notes: &[ResearchNote]) -> Vec<KanbanColumn> {
    let mut columns = Vec::new();
    for note_type in [
        NoteType::Fact,
        NoteType::ManagementSignal,
        NoteType::Claim,
        NoteType::Risk,
        NoteType::Catalyst,
        NoteType::ValuationPoint,
        NoteType::Question,
        NoteType::SourceReference,
    ] {
        columns.push(KanbanColumn {
            key: format!("{note_type:?}").to_ascii_lowercase(),
            label: format!("{note_type:?}")
                .replace("Point", "")
                .replace("Signal", " Signal"),
            notes: notes
                .iter()
                .filter(|note| note.note_type == note_type && !note.archived)
                .cloned()
                .collect(),
        });
    }
    columns
}

fn build_timeline(notes: &[ResearchNote], ghosts: &[GhostNote]) -> Vec<TimelineEvent> {
    let mut timeline = notes
        .iter()
        .filter(|note| {
            matches!(
                note.note_type,
                NoteType::EventTakeaway | NoteType::Catalyst | NoteType::ManagementSignal
            )
        })
        .map(|note| TimelineEvent {
            id: note.id.clone(),
            label: note
                .title
                .clone()
                .unwrap_or_else(|| note.cleaned_text.clone()),
            note_id: note.id.clone(),
            at: note
                .source_excerpt
                .as_ref()
                .and_then(|excerpt| excerpt.location_label.clone()),
        })
        .collect::<Vec<_>>();
    timeline.extend(
        ghosts
            .iter()
            .filter(|ghost| {
                matches!(
                    ghost.ghost_class,
                    crate::research::types::GhostNoteClass::ContradictionAlert
                )
            })
            .map(|ghost| TimelineEvent {
                id: ghost.id.clone(),
                label: ghost.headline.clone(),
                note_id: ghost
                    .supporting_note_ids
                    .first()
                    .cloned()
                    .unwrap_or_else(|| ghost.id.clone()),
                at: None,
            }),
    );
    timeline
}

fn section_for_note(note_type: NoteType) -> Option<MemoSectionKind> {
    match note_type {
        NoteType::Thesis | NoteType::SubThesis | NoteType::MosaicInsight => {
            Some(MemoSectionKind::InvestmentMemo)
        }
        NoteType::Risk | NoteType::Contradiction => Some(MemoSectionKind::RiskRegister),
        NoteType::Catalyst => Some(MemoSectionKind::CatalystCalendar),
        NoteType::ValuationPoint | NoteType::ScenarioAssumption => {
            Some(MemoSectionKind::ValuationWriteUp)
        }
        NoteType::EventTakeaway => Some(MemoSectionKind::EarningsRecap),
        NoteType::ChannelCheck => Some(MemoSectionKind::WatchlistUpdate),
        NoteType::Fact
        | NoteType::Quote
        | NoteType::ManagementSignal
        | NoteType::Claim
        | NoteType::IndustryObservation
        | NoteType::CompetitorComparison => Some(MemoSectionKind::StockPitch),
        NoteType::Question
        | NoteType::FollowUpTask
        | NoteType::SourceReference
        | NoteType::Uncertainty => None,
    }
}

#[cfg(test)]
mod tests {
    use crate::research::types::{EvidenceStatus, MemoSectionKind, NoteType};

    use super::build_memo_blocks;

    fn note(
        note_type: NoteType,
        evidence_status: EvidenceStatus,
    ) -> crate::research::types::ResearchNote {
        let now = crate::research::util::now_rfc3339();
        crate::research::types::ResearchNote {
            id: format!("note-{note_type:?}"),
            workspace_id: "workspace-1".to_string(),
            company_id: None,
            ticker: Some("AAPL".to_string()),
            source_id: Some("source-1".to_string()),
            raw_text: "sample".to_string(),
            cleaned_text: "sample".to_string(),
            body_html: None,
            embeds: Vec::new(),
            file_ids: Vec::new(),
            title: Some("Sample".to_string()),
            note_type,
            subtype: None,
            analyst_status: crate::research::types::AnalystStatus::Accepted,
            ai_annotation: Some("annotation".to_string()),
            confidence: 0.8,
            evidence_status,
            inferred_links: Vec::new(),
            ghost_status: crate::research::types::GhostStatus::None,
            thesis_status: crate::research::types::ThesisStatus::AcceptedSupport,
            created_at: now.clone(),
            updated_at: now.clone(),
            provenance: crate::research::types::NoteProvenance {
                created_by: crate::research::types::ProvenanceActor::Manual,
                capture_method: crate::research::types::CaptureMethod::QuickEntry,
                source_kind: crate::research::types::SourceKind::Manual,
                origin_note_id: None,
                origin_ghost_id: None,
                model_info: None,
                created_at: now,
                raw_input_hash: "hash".to_string(),
            },
            tags: Vec::new(),
            catalysts: Vec::new(),
            risks: Vec::new(),
            valuation_refs: Vec::new(),
            time_horizon: None,
            scenario: None,
            priority: crate::research::types::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,
        }
    }

    #[test]
    fn build_memo_blocks_should_exclude_unsourced_questions() {
        let blocks = build_memo_blocks(
            &[
                note(NoteType::Question, EvidenceStatus::Unsourced),
                note(NoteType::Fact, EvidenceStatus::Corroborated),
            ],
            &[],
        );

        assert_eq!(blocks.len(), 1);
        assert_eq!(blocks[0].section_kind, MemoSectionKind::StockPitch);
    }
}
