//! Deterministic first-pass note typing and extraction rules.

use regex::Regex;

use crate::research::types::{
    NotePriority, NoteType, ScenarioKind, SourceKind, TimeHorizon, ValuationRef,
};

#[derive(Debug, Clone, PartialEq)]
pub(crate) struct HeuristicTypingResult {
    pub note_type: NoteType,
    pub subtype: Option<String>,
    pub confidence: f32,
    pub tags: Vec<String>,
    pub catalysts: Vec<String>,
    pub risks: Vec<String>,
    pub valuation_refs: Vec<ValuationRef>,
    pub time_horizon: Option<TimeHorizon>,
    pub scenario: Option<ScenarioKind>,
    pub priority: NotePriority,
}

#[must_use]
pub(crate) fn classify_note(
    cleaned_text: &str,
    source_kind: SourceKind,
    override_type: Option<NoteType>,
) -> HeuristicTypingResult {
    if let Some(note_type) = override_type {
        let mut result = baseline_result(note_type, 0.99);
        result.tags = extract_tags(cleaned_text);
        result.valuation_refs = extract_valuation_refs(cleaned_text);
        return result;
    }

    let lower = cleaned_text.to_ascii_lowercase();
    let mut result = if looks_like_quote(cleaned_text, source_kind) {
        baseline_result(NoteType::Quote, 0.92)
    } else if lower.contains("management says")
        || lower.contains("mgmt says")
        || (matches!(source_kind, SourceKind::Transcript)
            && has_any(&lower, &["expect", "seeing", "confident", "guidance"]))
    {
        baseline_result(NoteType::ManagementSignal, 0.83)
    } else if has_any(
        &lower,
        &[
            "ev/ebitda",
            "p/e",
            "fcf yield",
            "multiple",
            "price target",
            "rerating",
            "valuation",
        ],
    ) {
        baseline_result(NoteType::ValuationPoint, 0.87)
    } else if has_any(
        &lower,
        &[
            "risk",
            "downside",
            "headwind",
            "pressure",
            "weakness",
            "inventory",
        ],
    ) {
        baseline_result(NoteType::Risk, 0.78)
    } else if has_any(
        &lower,
        &[
            "catalyst",
            "launch",
            "approval",
            "guidance",
            "next quarter",
            "earnings",
            "rerating",
        ],
    ) {
        baseline_result(NoteType::Catalyst, 0.74)
    } else if has_any(
        &lower,
        &[
            "if ",
            "assume",
            "base case",
            "bull case",
            "bear case",
            "scenario",
        ],
    ) {
        baseline_result(NoteType::ScenarioAssumption, 0.8)
    } else if cleaned_text.ends_with('?')
        || has_any(&lower, &["what if", "why is", "how does", "question"])
    {
        baseline_result(NoteType::Question, 0.91)
    } else if has_any(
        &lower,
        &[
            "channel check",
            "retail",
            "sell-through",
            "inventory in channel",
        ],
    ) {
        baseline_result(NoteType::ChannelCheck, 0.82)
    } else if has_any(
        &lower,
        &["peer", "vs ", "versus", "relative to", "competitor"],
    ) {
        baseline_result(NoteType::CompetitorComparison, 0.77)
    } else if has_any(&lower, &["industry", "category", "market", "sector"]) {
        baseline_result(NoteType::IndustryObservation, 0.73)
    } else if has_any(
        &lower,
        &["thesis", "stock can", "we think", "market is missing"],
    ) {
        baseline_result(NoteType::Thesis, 0.71)
    } else if has_any(
        &lower,
        &["follow up", "check", "verify", "ask ir", "need to"],
    ) {
        baseline_result(NoteType::FollowUpTask, 0.85)
    } else if has_any(
        &lower,
        &["call takeaway", "takeaway", "earnings recap", "event"],
    ) {
        baseline_result(NoteType::EventTakeaway, 0.76)
    } else if looks_like_fact(cleaned_text, source_kind) {
        baseline_result(NoteType::Fact, 0.72)
    } else {
        baseline_result(NoteType::Claim, 0.58)
    };

    result.tags = extract_tags(cleaned_text);
    result.catalysts = extract_keyword_bucket(
        cleaned_text,
        &[
            "launch",
            "approval",
            "guidance",
            "margin",
            "enterprise demand",
        ],
    );
    result.risks = extract_keyword_bucket(
        cleaned_text,
        &[
            "inventory",
            "pricing",
            "churn",
            "competition",
            "demand softness",
        ],
    );
    result.valuation_refs = extract_valuation_refs(cleaned_text);
    result.time_horizon = infer_time_horizon(&lower);
    result.scenario = infer_scenario(&lower);
    result.priority = infer_priority(&result.note_type, &lower);
    result
}

#[must_use]
pub(crate) fn derive_title(cleaned_text: &str, note_type: NoteType) -> Option<String> {
    if cleaned_text.is_empty() {
        return None;
    }

    let prefix = match note_type {
        NoteType::Risk => "Risk",
        NoteType::Catalyst => "Catalyst",
        NoteType::ValuationPoint => "Valuation",
        NoteType::ManagementSignal => "Mgmt",
        NoteType::Question => "Question",
        NoteType::Contradiction => "Conflict",
        NoteType::FollowUpTask => "Follow up",
        _ => "Note",
    };

    Some(format!(
        "{prefix}: {}",
        crate::research::util::clean_title(cleaned_text, 72)
    ))
}

#[must_use]
pub(crate) fn detect_urls(text: &str) -> Vec<String> {
    static URL_REGEX: std::sync::LazyLock<Regex> =
        std::sync::LazyLock::new(|| Regex::new(r"https?://[^\s)]+").unwrap());
    URL_REGEX
        .find_iter(text)
        .map(|capture| capture.as_str().trim_end_matches('.').to_string())
        .collect()
}

#[must_use]
pub(crate) fn extract_tickers(text: &str) -> Vec<String> {
    static TICKER_REGEX: std::sync::LazyLock<Regex> =
        std::sync::LazyLock::new(|| Regex::new(r"\b[A-Z]{2,5}\b").unwrap());
    TICKER_REGEX
        .find_iter(text)
        .map(|capture| capture.as_str().to_string())
        .collect()
}

fn baseline_result(note_type: NoteType, confidence: f32) -> HeuristicTypingResult {
    HeuristicTypingResult {
        note_type,
        subtype: None,
        confidence,
        tags: Vec::new(),
        catalysts: Vec::new(),
        risks: Vec::new(),
        valuation_refs: Vec::new(),
        time_horizon: None,
        scenario: None,
        priority: NotePriority::Normal,
    }
}

fn looks_like_quote(cleaned_text: &str, source_kind: SourceKind) -> bool {
    cleaned_text.contains('\"')
        || matches!(source_kind, SourceKind::Transcript) && cleaned_text.contains(':')
}

fn looks_like_fact(cleaned_text: &str, source_kind: SourceKind) -> bool {
    static FACT_REGEX: std::sync::LazyLock<Regex> =
        std::sync::LazyLock::new(|| Regex::new(r"\b\d+(\.\d+)?(%|x|bps|m|bn)?\b").unwrap());
    let lower = cleaned_text.to_ascii_lowercase();
    matches!(
        source_kind,
        SourceKind::Filing | SourceKind::Transcript | SourceKind::Article
    ) || has_any(
        &lower,
        &["reported", "was", "were", "increased", "decreased"],
    ) || FACT_REGEX.is_match(cleaned_text)
}

fn extract_tags(cleaned_text: &str) -> Vec<String> {
    let mut tags = Vec::new();
    let lower = cleaned_text.to_ascii_lowercase();
    for tag in [
        "margin",
        "demand",
        "inventory",
        "pricing",
        "guidance",
        "subscription",
        "enterprise",
        "valuation",
        "peer",
    ] {
        if lower.contains(tag) {
            tags.push(tag.to_string());
        }
    }
    tags
}

fn extract_keyword_bucket(cleaned_text: &str, keywords: &[&str]) -> Vec<String> {
    let lower = cleaned_text.to_ascii_lowercase();
    keywords
        .iter()
        .filter(|keyword| lower.contains(**keyword))
        .map(|keyword| (*keyword).to_string())
        .collect()
}

fn extract_valuation_refs(cleaned_text: &str) -> Vec<ValuationRef> {
    static VALUATION_REGEX: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
        Regex::new(r"(?P<multiple>\d+(\.\d+)?)x\s+(?P<metric>[A-Za-z/]+)").unwrap()
    });
    VALUATION_REGEX
        .captures_iter(cleaned_text)
        .map(|captures| ValuationRef {
            metric: captures["metric"].to_string(),
            multiple: captures["multiple"].parse::<f64>().ok(),
            unit: Some("x".to_string()),
            basis: None,
        })
        .collect()
}

fn infer_time_horizon(lower: &str) -> Option<TimeHorizon> {
    if has_any(lower, &["next quarter", "next half", "next earnings"]) {
        Some(TimeHorizon::NextEarnings)
    } else if has_any(lower, &["12 month", "twelve month", "next year"]) {
        Some(TimeHorizon::TwelveMonth)
    } else if has_any(lower, &["multi-year", "long term", "3 year", "5 year"]) {
        Some(TimeHorizon::MultiYear)
    } else if has_any(lower, &["near term", "this quarter"]) {
        Some(TimeHorizon::NearTerm)
    } else {
        None
    }
}

fn infer_scenario(lower: &str) -> Option<ScenarioKind> {
    if lower.contains("bull case") {
        Some(ScenarioKind::Bull)
    } else if lower.contains("bear case") {
        Some(ScenarioKind::Bear)
    } else if lower.contains("downside") {
        Some(ScenarioKind::DownsideCase)
    } else if lower.contains("upside") {
        Some(ScenarioKind::UpsideCase)
    } else if lower.contains("base case") {
        Some(ScenarioKind::Base)
    } else {
        None
    }
}

fn infer_priority(note_type: &NoteType, lower: &str) -> NotePriority {
    if matches!(note_type, NoteType::Contradiction | NoteType::Risk)
        && has_any(lower, &["material", "severe", "significant"])
    {
        NotePriority::Critical
    } else if matches!(
        note_type,
        NoteType::Risk | NoteType::Catalyst | NoteType::Thesis | NoteType::ManagementSignal
    ) {
        NotePriority::High
    } else {
        NotePriority::Normal
    }
}

fn has_any(input: &str, needles: &[&str]) -> bool {
    needles.iter().any(|needle| input.contains(needle))
}

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

    use super::classify_note;

    #[test]
    fn classify_note_should_flag_management_signal_for_transcript_language() {
        let result = classify_note(
            "Mgmt says enterprise demand improved sequentially and expects gross margin to exit above 70% next half.",
            SourceKind::Transcript,
            None,
        );

        assert_eq!(result.note_type, NoteType::ManagementSignal);
        assert_eq!(result.time_horizon, Some(TimeHorizon::NextEarnings));
    }
}
