//! AI enrichment abstraction with deterministic fallback and live LLM gateway.

use std::collections::HashSet;

use futures::future::BoxFuture;
use serde::{Deserialize, Serialize};
use tauri::Runtime;

use crate::agent::{resolve_runtime, AgentSettingsService, TaskProfile};
use crate::research::errors::ResearchError;
use crate::research::heuristics::classify_note;
use crate::research::types::{
    ModelInfo, NoteType, ResearchFilePreviewKind, ResearchNote, ValuationRef,
};

#[derive(Debug, Clone, PartialEq)]
pub(crate) struct AiEnrichmentResult {
    pub note_type: Option<NoteType>,
    pub annotation: Option<String>,
    pub tags: Vec<String>,
    pub risks: Vec<String>,
    pub catalysts: Vec<String>,
    pub valuation_refs: Vec<ValuationRef>,
    pub missing_evidence: bool,
}

#[derive(Debug, Clone, PartialEq)]
pub(crate) struct GhostFileContext {
    pub note_id: String,
    pub file_name: String,
    pub preview_kind: ResearchFilePreviewKind,
    pub extracted_text: Option<String>,
    pub truncated: bool,
}

pub(crate) trait ResearchAiGateway: Send + Sync {
    fn enrich_note(
        &self,
        note: ResearchNote,
        model_info: Option<ModelInfo>,
    ) -> BoxFuture<'static, AiEnrichmentResult>;

    fn synthesize_ghost_body(
        &self,
        ghost_class: String,
        supporting_summaries: Vec<String>,
        supporting_files: Vec<GhostFileContext>,
        existing_body: String,
    ) -> BoxFuture<'static, String>;
}

#[derive(Debug, Clone, Default)]
#[allow(dead_code)]
pub(crate) struct DeterministicResearchAiGateway;

impl ResearchAiGateway for DeterministicResearchAiGateway {
    fn enrich_note(
        &self,
        note: ResearchNote,
        _model_info: Option<ModelInfo>,
    ) -> BoxFuture<'static, AiEnrichmentResult> {
        Box::pin(async move { deterministic_enrich(&note) })
    }

    fn synthesize_ghost_body(
        &self,
        _ghost_class: String,
        _supporting_summaries: Vec<String>,
        _supporting_files: Vec<GhostFileContext>,
        existing_body: String,
    ) -> BoxFuture<'static, String> {
        Box::pin(async move { existing_body })
    }
}

pub(crate) struct LiveResearchAiGateway<R: Runtime> {
    settings: AgentSettingsService<R>,
    http: reqwest::Client,
}

impl<R: Runtime> Clone for LiveResearchAiGateway<R> {
    fn clone(&self) -> Self {
        Self {
            settings: self.settings.clone(),
            http: self.http.clone(),
        }
    }
}

impl<R: Runtime + 'static> LiveResearchAiGateway<R> {
    pub fn new(settings: AgentSettingsService<R>) -> Self {
        Self {
            settings,
            http: reqwest::Client::builder()
                .timeout(std::time::Duration::from_secs(30))
                .build()
                .unwrap_or_default(),
        }
    }

    fn resolve_enrichment_runtime(&self) -> Option<crate::agent::AgentRuntimeConfig> {
        let stored = self.settings.load().ok()?;
        resolve_runtime(&stored, TaskProfile::NoteEnrichment, None).ok()
    }

    fn resolve_ghost_runtime(&self) -> Option<crate::agent::AgentRuntimeConfig> {
        let stored = self.settings.load().ok()?;
        resolve_runtime(&stored, TaskProfile::GhostSynthesis, None)
            .ok()
            .or_else(|| resolve_runtime(&stored, TaskProfile::NoteEnrichment, None).ok())
    }
}

impl<R: Runtime + 'static> ResearchAiGateway for LiveResearchAiGateway<R> {
    fn enrich_note(
        &self,
        note: ResearchNote,
        _model_info: Option<ModelInfo>,
    ) -> BoxFuture<'static, AiEnrichmentResult> {
        let gateway = self.clone();
        Box::pin(async move {
            let Some(runtime) = gateway.resolve_enrichment_runtime() else {
                return deterministic_enrich(&note);
            };

            let system_prompt = build_enrichment_system_prompt();
            let user_prompt = build_enrichment_user_prompt(&note);

            match call_chat(&gateway.http, &runtime, &system_prompt, &user_prompt).await {
                Ok(response_text) => parse_enrichment_response(&response_text, &note),
                Err(_) => deterministic_enrich(&note),
            }
        })
    }

    fn synthesize_ghost_body(
        &self,
        ghost_class: String,
        supporting_summaries: Vec<String>,
        supporting_files: Vec<GhostFileContext>,
        existing_body: String,
    ) -> BoxFuture<'static, String> {
        let gateway = self.clone();
        Box::pin(async move {
            let Some(runtime) = gateway.resolve_ghost_runtime() else {
                return existing_body;
            };

            let system_prompt = build_ghost_synthesis_system_prompt();
            let user_prompt = build_ghost_synthesis_user_prompt(
                &ghost_class,
                &supporting_summaries,
                &supporting_files,
                &existing_body,
            );

            match call_chat(&gateway.http, &runtime, &system_prompt, &user_prompt).await {
                Ok(response_text) => {
                    let cleaned = response_text.trim().to_string();
                    if cleaned.is_empty() {
                        existing_body
                    } else {
                        cleaned
                    }
                }
                Err(_) => existing_body,
            }
        })
    }
}

async fn call_chat(
    client: &reqwest::Client,
    runtime: &crate::agent::AgentRuntimeConfig,
    system_prompt: &str,
    user_prompt: &str,
) -> std::result::Result<String, ResearchError> {
    let request_body = ChatCompletionRequest {
        model: runtime.model.clone(),
        messages: vec![
            ChatCompletionMessage {
                role: "system".to_string(),
                content: system_prompt.to_string(),
            },
            ChatCompletionMessage {
                role: "user".to_string(),
                content: user_prompt.to_string(),
            },
        ],
        temperature: 0.2,
    };

    let response = client
        .post(format!(
            "{}/chat/completions",
            runtime.base_url.trim_end_matches('/')
        ))
        .header(
            "Authorization",
            format!("Bearer {}", runtime.api_key.as_deref().unwrap_or("")),
        )
        .header("Content-Type", "application/json")
        .json(&request_body)
        .send()
        .await?;

    if !response.status().is_success() {
        let status = response.status();
        let body = response.text().await.unwrap_or_default();
        return Err(ResearchError::Validation(format!(
            "LLM enrichment request failed with status {status}: {body}"
        )));
    }

    let parsed: ChatCompletionResponse = response.json().await?;
    let content = parsed
        .choices
        .first()
        .and_then(|choice| choice.message.content.clone())
        .unwrap_or_default();

    Ok(content)
}

#[derive(Serialize)]
struct ChatCompletionRequest {
    model: String,
    messages: Vec<ChatCompletionMessage>,
    temperature: f32,
}

#[derive(Serialize, Deserialize)]
struct ChatCompletionMessage {
    role: String,
    content: String,
}

#[derive(Deserialize)]
struct ChatCompletionResponse {
    choices: Vec<ChatCompletionChoice>,
}

#[derive(Deserialize)]
struct ChatCompletionChoice {
    message: ChatCompletionResponseMessage,
}

#[derive(Deserialize)]
struct ChatCompletionResponseMessage {
    content: Option<String>,
}

#[derive(Deserialize)]
struct EnrichmentJsonResponse {
    #[serde(default)]
    note_type: Option<NoteType>,
    #[serde(default)]
    annotation: Option<String>,
    #[serde(default)]
    tags: Vec<String>,
    #[serde(default)]
    risks: Vec<String>,
    #[serde(default)]
    catalysts: Vec<String>,
    #[serde(default)]
    missing_evidence: Option<bool>,
}

fn build_enrichment_system_prompt() -> String {
    let note_types = [
        "fact",
        "quote",
        "management_signal",
        "claim",
        "thesis",
        "sub_thesis",
        "risk",
        "catalyst",
        "valuation_point",
        "scenario_assumption",
        "industry_observation",
        "competitor_comparison",
        "question",
        "contradiction",
        "uncertainty",
        "follow_up_task",
        "event_takeaway",
        "channel_check",
        "mosaic_insight",
    ];

    format!(
        "You are an equity research note classification assistant. Given a research note about a \
         stock or company, classify it and provide enrichment metadata.\n\n\
         Respond with valid JSON matching this schema (no markdown fences):\n\
         {{\n\
         \x20 \"note_type\": \"one of [{}]\",\n\
         \x20 \"annotation\": \"A brief 1-2 sentence analyst-facing note about how this note \
         should be used in research\",\n\
         \x20 \"tags\": [\"relevant\", \"keywords\"],\n\
         \x20 \"risks\": [\"identified downside risk keywords\"],\n\
         \x20 \"catalysts\": [\"identified catalyst keywords\"],\n\
         \x20 \"missing_evidence\": true or false\n\
         }}\n\n\
         Rules:\n\
         - note_type must match the closest category from the list above\n\
         - annotation should be concise and actionable for an analyst\n\
         - tags should be 2-6 lowercase descriptive keywords\n\
         - risks and catalysts should list specific risk/catalyst themes found in the text\n\
         - missing_evidence should be true if the note makes a claim without supporting data or source",
        note_types.join(", "),
    )
}

fn build_enrichment_user_prompt(note: &ResearchNote) -> String {
    let source_label = format!("{:?}", note.provenance.source_kind).to_ascii_lowercase();
    let evidence_label = format!("{:?}", note.evidence_status).to_ascii_lowercase();
    let has_source = note.source_id.is_some();

    format!(
        "Note text: {}\n\n\
         Source kind: {source_label}\n\
         Evidence status: {evidence_label}\n\
         Has linked source: {has_source}\n\
         Current classification: {:?}",
        note.cleaned_text, note.note_type,
    )
}

fn build_ghost_synthesis_system_prompt() -> String {
    "You are an equity research synthesis assistant. You write concise, \
     provisional investment insights based on clusters of research notes. \
     Your output should follow the Observed / Inference / What would confirm \
     or refute pattern. Write in a direct, analytical tone. Be specific \
     about the operating evidence, not vague. Treat note summaries as primary \
     evidence and linked file text as supplemental context. Use file text only \
     when it sharpens specificity. Do not invent facts that are absent from the \
     notes or extracted file text. If extracted text is noisy, synthesize \
     conservatively. Keep the body under 200 words."
        .to_string()
}

fn build_ghost_synthesis_user_prompt(
    ghost_class: &str,
    summaries: &[String],
    supporting_files: &[GhostFileContext],
    existing_body: &str,
) -> String {
    let notes_block = summaries
        .iter()
        .enumerate()
        .map(|(i, s)| format!("{}. {s}", i + 1))
        .collect::<Vec<_>>()
        .join("\n");
    let files_block = if supporting_files.is_empty() {
        "None".to_string()
    } else {
        supporting_files
            .iter()
            .enumerate()
            .map(|(i, file)| {
                let truncated = if file.truncated { " (truncated)" } else { "" };
                let text = file
                    .extracted_text
                    .as_deref()
                    .filter(|value| !value.trim().is_empty())
                    .unwrap_or("No extracted text available.");
                format!(
                    "{}. Note {} -> {} ({}){}\n{}",
                    i + 1,
                    file.note_id,
                    file.file_name,
                    preview_kind_label(file.preview_kind),
                    truncated,
                    text,
                )
            })
            .collect::<Vec<_>>()
            .join("\n\n")
    };

    format!(
        "Ghost insight type: {ghost_class}\n\n\
         Supporting note summaries:\n{notes_block}\n\n\
         Linked file context:\n{files_block}\n\n\
         Current template body (rewrite with more specific insight):\n{existing_body}"
    )
}

fn preview_kind_label(kind: ResearchFilePreviewKind) -> &'static str {
    match kind {
        ResearchFilePreviewKind::Image => "image",
        ResearchFilePreviewKind::Pdf => "pdf",
        ResearchFilePreviewKind::Text => "text",
        ResearchFilePreviewKind::None => "none",
    }
}

fn parse_enrichment_response(response_text: &str, note: &ResearchNote) -> AiEnrichmentResult {
    let cleaned = response_text
        .trim()
        .trim_start_matches("```json")
        .trim_start_matches("```")
        .trim_end_matches("```")
        .trim();

    let parsed: EnrichmentJsonResponse = match serde_json::from_str(cleaned) {
        Ok(value) => value,
        Err(_) => {
            return deterministic_enrich(note);
        }
    };

    let heuristic = classify_note(
        &note.cleaned_text,
        note.provenance.source_kind,
        parsed.note_type.or(Some(note.note_type)),
    );

    AiEnrichmentResult {
        note_type: parsed.note_type.or(Some(heuristic.note_type)),
        annotation: parsed.annotation.or_else(|| Some(build_annotation(note))),
        tags: merge_unique(heuristic.tags, parsed.tags),
        risks: merge_unique(heuristic.risks, parsed.risks),
        catalysts: merge_unique(heuristic.catalysts, parsed.catalysts),
        valuation_refs: if note.valuation_refs.is_empty() {
            heuristic.valuation_refs
        } else {
            note.valuation_refs.clone()
        },
        missing_evidence: parsed.missing_evidence.unwrap_or(
            note.source_id.is_none()
                && !matches!(
                    note.note_type,
                    NoteType::Question | NoteType::FollowUpTask | NoteType::SourceReference
                ),
        ),
    }
}

fn deterministic_enrich(note: &ResearchNote) -> AiEnrichmentResult {
    let heuristic = classify_note(
        &note.cleaned_text,
        note.provenance.source_kind,
        Some(note.note_type),
    );
    let annotation = Some(build_annotation(note));

    AiEnrichmentResult {
        note_type: Some(heuristic.note_type),
        annotation,
        tags: heuristic.tags,
        risks: heuristic.risks,
        catalysts: heuristic.catalysts,
        valuation_refs: if note.valuation_refs.is_empty() {
            heuristic.valuation_refs
        } else {
            note.valuation_refs.clone()
        },
        missing_evidence: note.source_id.is_none()
            && !matches!(
                note.note_type,
                NoteType::Question | NoteType::FollowUpTask | NoteType::SourceReference
            ),
    }
}

fn build_annotation(note: &ResearchNote) -> String {
    let kind = match note.note_type {
        NoteType::ManagementSignal => "Management is signaling a directional read that should be checked against operating evidence.",
        NoteType::ValuationPoint => "This note matters if the valuation frame can be tied to a concrete operating driver.",
        NoteType::Risk => "This note points to downside that should be sized and sourced before it informs conviction.",
        NoteType::Catalyst => "This note suggests a possible stock-moving trigger and should be paired with timing evidence.",
        NoteType::Fact => "This datapoint is most useful when linked directly into a claim, risk, or valuation bridge.",
        NoteType::Quote => "Treat the quote as evidence, then separate the analyst interpretation into linked notes.",
        NoteType::Claim | NoteType::Thesis | NoteType::SubThesis => "This statement is an inference until supporting evidence and explicit counterpoints are attached.",
        NoteType::ChannelCheck => "This datapoint is potentially informative but should be kept caveated unless corroborated.",
        _ => "This note may be more valuable once it is linked into a driver, risk, catalyst, or source trail.",
    };

    if note.source_id.is_none()
        && !matches!(note.note_type, NoteType::Question | NoteType::FollowUpTask)
    {
        format!("{kind} Evidence is still missing or indirect.")
    } else {
        kind.to_string()
    }
}

pub(crate) fn build_model_info(
    model: &str,
    task_profile: &str,
    provider: &str,
) -> Option<ModelInfo> {
    if model.trim().is_empty() {
        return None;
    }

    Some(ModelInfo {
        task_profile: task_profile.to_string(),
        model: model.to_string(),
        provider: Some(provider.to_string()),
    })
}

fn merge_unique(mut left: Vec<String>, right: Vec<String>) -> Vec<String> {
    let mut seen: HashSet<String> = left.iter().cloned().collect();
    for value in right {
        if seen.insert(value.clone()) {
            left.push(value);
        }
    }
    left
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_enrichment_response_falls_back_on_invalid_json() {
        let note = test_note(
            "Management says demand is improving.",
            NoteType::ManagementSignal,
        );
        let result = parse_enrichment_response("not json at all", &note);
        assert_eq!(result.note_type, Some(NoteType::ManagementSignal));
    }

    #[test]
    fn parse_enrichment_response_parses_valid_json() {
        let note = test_note("Inventory days increased 12% sequentially.", NoteType::Fact);
        let json = r#"{"note_type":"risk","annotation":"Rising inventory days signal potential demand weakness.","tags":["inventory","demand"],"risks":["inventory build"],"catalysts":[],"missing_evidence":false}"#;
        let result = parse_enrichment_response(json, &note);
        assert_eq!(result.note_type, Some(NoteType::Risk));
        assert_eq!(result.tags, vec!["inventory", "demand"]);
        assert_eq!(result.risks, vec!["inventory build"]);
    }

    #[test]
    fn parse_enrichment_response_strips_markdown_fences() {
        let note = test_note("Gross margin expanded to 72%.", NoteType::Fact);
        let json = "```json\n{\"note_type\":\"valuation_point\",\"annotation\":\"Margin expansion.\",\"tags\":[\"margin\"],\"risks\":[],\"catalysts\":[\"margin\"],\"missing_evidence\":false}\n```";
        let result = parse_enrichment_response(json, &note);
        assert_eq!(result.note_type, Some(NoteType::ValuationPoint));
    }

    use crate::research::types::{
        AnalystStatus, EvidenceStatus, GhostStatus, NotePriority, NoteProvenance, ProvenanceActor,
        SourceKind, ThesisStatus,
    };
    use crate::research::util::{now_rfc3339, sha256_hex};

    fn test_note(text: &str, note_type: NoteType) -> ResearchNote {
        let now = now_rfc3339();
        ResearchNote {
            id: "note-test".to_string(),
            workspace_id: "ws-1".to_string(),
            company_id: None,
            ticker: Some("AAPL".to_string()),
            source_id: None,
            raw_text: text.to_string(),
            cleaned_text: text.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.clone(),
            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,
                raw_input_hash: sha256_hex(text),
            },
            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,
        }
    }
}
