//! Provisional synthesis generation over note clusters and tensions.

use std::collections::{BTreeMap, BTreeSet, HashSet};

use crate::research::types::{
    GhostLifecycleState, GhostNote, GhostNoteClass, GhostTone, GhostVisibilityState, LinkType,
    MemoSectionKind, NoteLink, NoteType, ResearchNote, ResearchWorkspace,
};
use crate::research::util::{now_rfc3339, sha256_hex};

pub(crate) fn generate_ghost_notes(
    workspace: &ResearchWorkspace,
    notes: &[ResearchNote],
    links: &[NoteLink],
) -> Vec<GhostNote> {
    let mut ghosts = Vec::new();
    ghosts.extend(generate_missing_evidence_prompts(workspace, notes, links));
    ghosts.extend(generate_contradiction_alerts(workspace, notes, links));
    ghosts.extend(generate_candidate_risks(workspace, notes));
    ghosts.extend(generate_candidate_catalysts(workspace, notes));
    ghosts.extend(generate_valuation_bridges(workspace, notes));
    if let Some(thesis) = generate_candidate_thesis(workspace, notes, links) {
        ghosts.push(thesis);
    }

    rank_and_limit_visibility(&mut ghosts);
    ghosts
}

fn generate_missing_evidence_prompts(
    workspace: &ResearchWorkspace,
    notes: &[ResearchNote],
    links: &[NoteLink],
) -> Vec<GhostNote> {
    notes.iter()
        .filter(|note| matches!(note.note_type, NoteType::Claim | NoteType::Thesis | NoteType::SubThesis))
        .filter(|note| {
            !links.iter().any(|link| {
                link.from_note_id == note.id && matches!(link.link_type, LinkType::SourcedBy | LinkType::Supports)
            })
        })
        .map(|note| ghost(
            workspace,
            GhostNoteClass::MissingEvidencePrompt,
            vec![note.id.clone()],
            Vec::new(),
            note.source_id.iter().cloned().collect(),
            "Missing evidence for claim".to_string(),
            format!(
                "Observed: this claim is currently unsupported by a linked source note. Inference: the argument may be directionally useful but should not be treated as evidence yet. What would confirm/refute: add a filing, transcript, article, or model-backed source."
            ),
            0.42,
            false,
            GhostVisibilityState::Collapsed,
            None,
        ))
        .collect()
}

fn generate_contradiction_alerts(
    workspace: &ResearchWorkspace,
    notes: &[ResearchNote],
    links: &[NoteLink],
) -> Vec<GhostNote> {
    let notes_by_id = notes
        .iter()
        .map(|note| (note.id.as_str(), note))
        .collect::<BTreeMap<_, _>>();
    links.iter()
        .filter(|link| matches!(link.link_type, LinkType::Contradicts | LinkType::ManagementVsReality))
        .filter_map(|link| {
            let left = notes_by_id.get(link.from_note_id.as_str())?;
            let right = notes_by_id.get(link.to_note_id.as_str())?;
            Some(ghost(
                workspace,
                GhostNoteClass::ContradictionAlert,
                vec![left.id.clone(), right.id.clone()],
                vec![right.id.clone()],
                collect_source_ids([*left, *right]),
                "Contradiction alert".to_string(),
                format!(
                    "Observed: {}. Inference: this conflicts with {}. What would confirm/refute: reconcile the newer datapoint, source freshness, and management framing before treating either statement as settled.",
                    left.cleaned_text, right.cleaned_text
                ),
                0.86,
                true,
                GhostVisibilityState::Visible,
                Some(MemoSectionKind::RiskRegister),
            ))
        })
        .collect()
}

fn generate_candidate_risks(
    workspace: &ResearchWorkspace,
    notes: &[ResearchNote],
) -> Vec<GhostNote> {
    let risk_notes = notes
        .iter()
        .filter(|note| {
            matches!(
                note.note_type,
                NoteType::Risk | NoteType::Contradiction | NoteType::ChannelCheck
            )
        })
        .collect::<Vec<_>>();
    if risk_notes.len() < 3 {
        return Vec::new();
    }

    let supporting_ids = risk_notes
        .iter()
        .map(|note| note.id.clone())
        .collect::<Vec<_>>();
    vec![ghost(
        workspace,
        GhostNoteClass::CandidateRisk,
        supporting_ids,
        Vec::new(),
        collect_source_ids(risk_notes.into_iter()),
        "Possible emerging risk cluster".to_string(),
        "Observed: several notes point to downside pressure around the same operating area. Inference: this may represent an investable risk theme, but it should remain provisional until the source trail is tightened. What would confirm/refute: corroborate with fresh operating evidence or management disclosures.".to_string(),
        0.73,
        true,
        GhostVisibilityState::Visible,
        Some(MemoSectionKind::RiskRegister),
    )]
}

fn generate_candidate_catalysts(
    workspace: &ResearchWorkspace,
    notes: &[ResearchNote],
) -> Vec<GhostNote> {
    let catalyst_notes = notes
        .iter()
        .filter(|note| {
            matches!(
                note.note_type,
                NoteType::Catalyst | NoteType::EventTakeaway | NoteType::ManagementSignal
            )
        })
        .collect::<Vec<_>>();
    if catalyst_notes.len() < 2 {
        return Vec::new();
    }

    let supporting_ids = catalyst_notes
        .iter()
        .map(|note| note.id.clone())
        .collect::<Vec<_>>();
    vec![ghost(
        workspace,
        GhostNoteClass::CandidateCatalyst,
        supporting_ids,
        Vec::new(),
        collect_source_ids(catalyst_notes.into_iter()),
        "Possible catalyst cluster".to_string(),
        "Observed: multiple notes point toward an identifiable event or operating trigger. Inference: this could matter for the next stock move if timing and evidence quality hold. What would confirm/refute: map the catalyst to dated milestones and an observable KPI.".to_string(),
        0.7,
        true,
        GhostVisibilityState::Visible,
        Some(MemoSectionKind::CatalystCalendar),
    )]
}

fn generate_valuation_bridges(
    workspace: &ResearchWorkspace,
    notes: &[ResearchNote],
) -> Vec<GhostNote> {
    let valuation_notes = notes
        .iter()
        .filter(|note| note.note_type == NoteType::ValuationPoint)
        .collect::<Vec<_>>();
    let driver_notes = notes
        .iter()
        .filter(|note| {
            matches!(
                note.note_type,
                NoteType::IndustryObservation
                    | NoteType::ManagementSignal
                    | NoteType::Fact
                    | NoteType::Catalyst
            )
        })
        .collect::<Vec<_>>();

    if valuation_notes.len() < 2 || driver_notes.is_empty() {
        return Vec::new();
    }

    let mut support = valuation_notes
        .iter()
        .map(|note| note.id.clone())
        .collect::<Vec<_>>();
    support.extend(driver_notes.iter().take(2).map(|note| note.id.clone()));
    vec![ghost(
        workspace,
        GhostNoteClass::ValuationBridge,
        support,
        Vec::new(),
        collect_source_ids(valuation_notes.into_iter().chain(driver_notes.into_iter())),
        "Possible valuation bridge".to_string(),
        "Observed: valuation notes point to a discount while operating notes suggest a driver that could narrow that gap. Inference: there may be a rerating bridge if the operating evidence persists. What would confirm/refute: track the KPI that should transmit into multiple expansion.".to_string(),
        0.76,
        true,
        GhostVisibilityState::Visible,
        Some(MemoSectionKind::ValuationWriteUp),
    )]
}

fn generate_candidate_thesis(
    workspace: &ResearchWorkspace,
    notes: &[ResearchNote],
    links: &[NoteLink],
) -> Option<GhostNote> {
    let source_count = notes
        .iter()
        .filter_map(|note| note.source_id.as_ref())
        .collect::<HashSet<_>>()
        .len();
    let family_count = notes
        .iter()
        .map(|note| note.note_type)
        .collect::<HashSet<_>>()
        .len();
    let corroborated_count = notes
        .iter()
        .filter(|note| {
            matches!(
                note.note_type,
                NoteType::Fact
                    | NoteType::Quote
                    | NoteType::ManagementSignal
                    | NoteType::ValuationPoint
            )
        })
        .count();
    let has_catalyst_or_valuation = notes.iter().any(|note| {
        matches!(
            note.note_type,
            NoteType::Catalyst | NoteType::ValuationPoint
        )
    });
    let has_unresolved_contradiction = links.iter().any(|link| {
        matches!(
            link.link_type,
            LinkType::Contradicts | LinkType::ManagementVsReality
        ) && link.confidence > 0.75
    });

    if notes.len() < 4
        || family_count < 2
        || source_count < 2
        || corroborated_count < 2
        || !has_catalyst_or_valuation
    {
        return None;
    }

    let headline = if has_unresolved_contradiction {
        "Possible thesis emerging, but tension remains"
    } else {
        "Possible candidate thesis"
    };
    let body = if has_unresolved_contradiction {
        "Observed: enough connected evidence exists to suggest an investable pattern, but at least one unresolved contradiction remains. Inference: a thesis may be forming, though conviction should stay tempered until the conflict is resolved. What would confirm/refute: close the contradiction with fresher operating evidence."
    } else {
        "Observed: multiple notes across evidence, catalyst, and valuation categories are pointing in the same direction. Inference: a coherent thesis may be emerging, though it should remain provisional until explicitly accepted by the analyst. What would confirm/refute: one more corroborating datapoint tied to the key driver."
    };

    Some(ghost(
        workspace,
        GhostNoteClass::CandidateThesis,
        notes.iter().take(6).map(|note| note.id.clone()).collect(),
        Vec::new(),
        collect_source_ids(notes.iter()),
        headline.to_string(),
        body.to_string(),
        if has_unresolved_contradiction {
            0.68
        } else {
            0.82
        },
        !has_unresolved_contradiction,
        GhostVisibilityState::Visible,
        Some(MemoSectionKind::InvestmentMemo),
    ))
}

fn rank_and_limit_visibility(ghosts: &mut [GhostNote]) {
    ghosts.sort_by(|left, right| right.confidence.total_cmp(&left.confidence));
    let mut visible_count = 0usize;
    for ghost in ghosts {
        if matches!(
            ghost.visibility_state,
            GhostVisibilityState::Visible | GhostVisibilityState::Pinned
        ) {
            if visible_count >= 3 {
                ghost.visibility_state = GhostVisibilityState::Hidden;
                ghost.state = GhostLifecycleState::Generated;
            } else {
                visible_count += 1;
                ghost.state = GhostLifecycleState::Visible;
            }
        }
    }
}

fn ghost(
    workspace: &ResearchWorkspace,
    ghost_class: GhostNoteClass,
    supporting_note_ids: Vec<String>,
    contradicting_note_ids: Vec<String>,
    source_ids: Vec<String>,
    headline: String,
    body: String,
    confidence: f32,
    evidence_threshold_met: bool,
    visibility_state: GhostVisibilityState,
    memo_section_hint: Option<MemoSectionKind>,
) -> GhostNote {
    let now = now_rfc3339();
    let mut key_parts = BTreeSet::new();
    key_parts.extend(supporting_note_ids.iter().cloned());
    key_parts.extend(contradicting_note_ids.iter().cloned());
    let ghost_key = format!(
        "{ghost_class:?}-{}",
        key_parts.into_iter().collect::<Vec<_>>().join(",")
    );

    GhostNote {
        id: format!("ghost-{}", &sha256_hex(&ghost_key)[..16]),
        workspace_id: workspace.id.clone(),
        ghost_class,
        headline,
        body,
        tone: GhostTone::Tentative,
        confidence,
        visibility_state,
        state: GhostLifecycleState::Generated,
        supporting_note_ids,
        contradicting_note_ids,
        source_ids,
        evidence_threshold_met,
        created_at: now.clone(),
        updated_at: now,
        superseded_by_ghost_id: None,
        promoted_note_id: None,
        memo_section_hint,
    }
}

fn collect_source_ids<'a>(notes: impl IntoIterator<Item = &'a ResearchNote>) -> Vec<String> {
    notes
        .into_iter()
        .filter_map(|note| note.source_id.clone())
        .collect::<HashSet<_>>()
        .into_iter()
        .collect()
}

#[cfg(test)]
mod tests {
    use crate::research::types::{
        AnalystStatus, EvidenceStatus, GhostStatus, NotePriority, NoteProvenance, NoteType,
        ProvenanceActor, ResearchNote, ResearchWorkspace, ThesisStatus, WorkspaceScope,
        WorkspaceViewKind,
    };
    use crate::research::util::{now_rfc3339, sha256_hex};

    use super::generate_ghost_notes;

    fn workspace() -> ResearchWorkspace {
        let now = now_rfc3339();
        ResearchWorkspace {
            id: "workspace-1".to_string(),
            name: "AAPL".to_string(),
            primary_ticker: "AAPL".to_string(),
            scope: WorkspaceScope::SingleCompany,
            stage: crate::research::types::ResearchStage::Thesis,
            default_view: WorkspaceViewKind::ThesisBuilder,
            pinned_note_ids: Vec::new(),
            archived: false,
            created_at: now.clone(),
            updated_at: now,
        }
    }

    fn note(id: &str, note_type: NoteType, source_id: Option<&str>) -> ResearchNote {
        let now = now_rfc3339();
        ResearchNote {
            id: id.to_string(),
            workspace_id: "workspace-1".to_string(),
            company_id: None,
            ticker: Some("AAPL".to_string()),
            source_id: source_id.map(ToOwned::to_owned),
            raw_text: id.to_string(),
            cleaned_text: id.to_string(),
            title: None,
            note_type,
            subtype: None,
            analyst_status: AnalystStatus::Captured,
            ai_annotation: None,
            confidence: 0.8,
            evidence_status: EvidenceStatus::SourceLinked,
            inferred_links: Vec::new(),
            ghost_status: GhostStatus::None,
            thesis_status: ThesisStatus::None,
            created_at: now.clone(),
            updated_at: now.clone(),
            provenance: NoteProvenance {
                created_by: 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: sha256_hex(id),
            },
            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,
        }
    }

    #[test]
    fn generate_ghost_notes_should_surface_candidate_thesis_when_evidence_threshold_met() {
        let ghosts = generate_ghost_notes(
            &workspace(),
            &[
                note("fact-1", NoteType::Fact, Some("source-1")),
                note("quote-1", NoteType::Quote, Some("source-2")),
                note("catalyst-1", NoteType::Catalyst, Some("source-2")),
                note("valuation-1", NoteType::ValuationPoint, Some("source-1")),
            ],
            &[],
        );

        assert!(ghosts
            .iter()
            .any(|ghost| ghost.ghost_class
                == crate::research::types::GhostNoteClass::CandidateThesis));
    }
}
