//! Source grounding helpers for notes and citations.

use regex::Regex;
use serde_json::json;

use crate::research::errors::Result;
use crate::research::heuristics::derive_title;
use crate::research::types::{
    AnalystStatus, EvidenceStatus, FreshnessBucket, NotePriority, NoteProvenance, NoteType,
    ResearchNote, SourceExcerpt, SourceRecord, SourceReferenceInput,
};
use crate::research::util::{generate_id, now_rfc3339, sha256_hex};

pub(crate) fn build_source_record(
    workspace_id: &str,
    ticker: Option<&str>,
    input: &SourceReferenceInput,
) -> SourceRecord {
    let now = now_rfc3339();
    let title = input
        .title
        .clone()
        .or_else(|| input.url.as_deref().map(derive_title_from_url))
        .unwrap_or_else(|| "Attached source".to_string());

    SourceRecord {
        id: generate_id("source"),
        workspace_id: workspace_id.to_string(),
        kind: input.kind,
        ticker: ticker.map(ToOwned::to_owned),
        title,
        publisher: input.url.as_deref().and_then(extract_publisher),
        url: input.url.clone(),
        canonical_url: input.url.clone(),
        filing_accession: input.filing_accession.clone(),
        form_type: input.form_type.clone(),
        published_at: input.published_at.clone(),
        as_of_date: input.published_at.clone(),
        ingested_at: now,
        freshness_bucket: FreshnessBucket::Fresh,
        checksum: input.url.as_deref().map(sha256_hex),
        metadata_json: json!({
            "kind": input.kind,
            "locationLabel": input.location_label,
        }),
        superseded_by_source_id: None,
    }
}

pub(crate) fn build_source_reference_note(
    workspace_id: &str,
    ticker: Option<&str>,
    source: &SourceRecord,
    excerpt: Option<&SourceExcerpt>,
) -> ResearchNote {
    let now = now_rfc3339();
    let raw_text = format!(
        "{}{}",
        source.title,
        source
            .url
            .as_deref()
            .map(|url| format!(" ({url})"))
            .unwrap_or_default()
    );

    ResearchNote {
        id: generate_id("note"),
        workspace_id: workspace_id.to_string(),
        company_id: None,
        ticker: ticker.map(ToOwned::to_owned),
        source_id: Some(source.id.clone()),
        raw_text: raw_text.clone(),
        cleaned_text: raw_text,
        title: derive_title(&source.title, NoteType::SourceReference),
        note_type: NoteType::SourceReference,
        subtype: Some(format!("{:?}", source.kind).to_ascii_lowercase()),
        analyst_status: AnalystStatus::Accepted,
        ai_annotation: None,
        confidence: 1.0,
        evidence_status: EvidenceStatus::SourceLinked,
        inferred_links: Vec::new(),
        ghost_status: crate::research::types::GhostStatus::None,
        thesis_status: crate::research::types::ThesisStatus::None,
        created_at: now.clone(),
        updated_at: now.clone(),
        provenance: NoteProvenance {
            created_by: crate::research::types::ProvenanceActor::Import,
            capture_method: crate::research::types::CaptureMethod::ManualLink,
            source_kind: source.kind,
            origin_note_id: None,
            origin_ghost_id: None,
            model_info: None,
            created_at: now,
            raw_input_hash: sha256_hex(&source.title),
        },
        tags: vec!["source".to_string()],
        catalysts: Vec::new(),
        risks: Vec::new(),
        valuation_refs: Vec::new(),
        time_horizon: None,
        scenario: None,
        priority: NotePriority::Low,
        pinned: false,
        archived: false,
        revision: 1,
        source_excerpt: excerpt.cloned(),
        last_enriched_at: None,
        last_linked_at: None,
        stale_reason: None,
        superseded_by_note_id: None,
    }
}

pub(crate) fn source_excerpt_from_input(
    source_id: &str,
    input: &SourceReferenceInput,
) -> Option<SourceExcerpt> {
    if input.excerpt_text.is_none() && input.location_label.is_none() {
        return None;
    }

    Some(SourceExcerpt {
        source_id: source_id.to_string(),
        excerpt_text: input.excerpt_text.clone(),
        location_label: input.location_label.clone(),
        start_offset: None,
        end_offset: None,
    })
}

pub(crate) async fn refresh_source_metadata(source: &SourceRecord) -> Result<SourceRecord> {
    let Some(url) = source.url.as_deref() else {
        return Ok(source.clone());
    };

    let body = reqwest::get(url).await?.text().await?;
    let title = extract_html_title(&body).unwrap_or_else(|| source.title.clone());

    let mut refreshed = source.clone();
    refreshed.title = title;
    refreshed.publisher = extract_publisher(url);
    refreshed.metadata_json = json!({
        "kind": refreshed.kind,
        "refreshedFromUrl": url,
    });
    Ok(refreshed)
}

fn derive_title_from_url(url: &str) -> String {
    let mut trimmed = url
        .trim_start_matches("https://")
        .trim_start_matches("http://")
        .trim_end_matches('/');
    if let Some((host, path)) = trimmed.split_once('/') {
        trimmed = if path.is_empty() { host } else { path };
    }

    trimmed.replace('-', " ")
}

fn extract_publisher(url: &str) -> Option<String> {
    let without_scheme = url
        .trim_start_matches("https://")
        .trim_start_matches("http://");
    let host = without_scheme.split('/').next().unwrap_or_default();
    if host.is_empty() {
        None
    } else {
        Some(host.to_string())
    }
}

fn extract_html_title(body: &str) -> Option<String> {
    let regex = Regex::new(r"(?is)<title>(.*?)</title>").expect("title regex should compile");
    regex
        .captures(body)
        .and_then(|captures| {
            captures
                .get(1)
                .map(|value| value.as_str().trim().to_string())
        })
        .filter(|value| !value.is_empty())
}
