use chrono::Utc;
use miette::{Result, miette};
use serde::Deserialize;
use serde_json::json;
use sha2::{Digest, Sha256};

use crate::{
    context::Context,
    dashboard::{DashboardSessionTitle, DashboardState},
    events::{EventPayload, EventView},
    reasoning::{
        prompts::{
            SESSION_TITLE_SYSTEM_REQUIREMENTS, SESSION_TITLE_SYSTEM_ROLE,
            SESSION_TITLE_TOOL_DESCRIPTION, SESSION_TITLE_USER_MESSAGE_PREFIX,
        },
        runtime::{HistoryMessage, PromptRequest},
    },
};

const SESSION_TITLE_REFRESH_INTERVAL_MS: i64 = 5 * 60 * 1000;
const MAX_TITLE_CHARS: usize = 64;
const MAX_EXCERPT_ITEMS: usize = 16;
const MAX_EXCERPT_ITEM_CHARS: usize = 360;

#[derive(Debug, Clone, Default)]
pub struct SessionTitleState {
    current: Option<DashboardSessionTitle>,
    last_activity_signature: Option<String>,
    last_generated_signature: Option<String>,
    last_generated_at_ms: Option<i64>,
}

impl SessionTitleState {
    pub fn snapshot(&self) -> Option<DashboardSessionTitle> {
        self.current.clone()
    }

    fn apply_placeholder(&mut self, signature: &str, title: String, now_ms: i64) -> bool {
        self.last_activity_signature = Some(signature.to_string());
        if self
            .current
            .as_ref()
            .is_some_and(|current| current.generated || current.title.trim() == title.trim())
        {
            return false;
        }
        self.current = Some(DashboardSessionTitle {
            title,
            generated: false,
            updated_at_ms: now_ms,
        });
        true
    }

    fn should_generate(&self, signature: &str, now_ms: i64) -> bool {
        if self.last_generated_signature.as_deref() == Some(signature) {
            return false;
        }
        match self.last_generated_at_ms {
            None => true,
            Some(last) => now_ms.saturating_sub(last) >= SESSION_TITLE_REFRESH_INTERVAL_MS,
        }
    }

    fn apply_generated(&mut self, signature: String, title: String, now_ms: i64) -> bool {
        self.last_activity_signature = Some(signature.clone());
        self.last_generated_signature = Some(signature);
        self.last_generated_at_ms = Some(now_ms);
        if self
            .current
            .as_ref()
            .is_some_and(|current| current.generated && current.title.trim() == title.trim())
        {
            return false;
        }
        self.current = Some(DashboardSessionTitle {
            title,
            generated: true,
            updated_at_ms: now_ms,
        });
        true
    }
}

pub fn sync_session_title_placeholder(
    context: &mut Context,
    tx: &tokio::sync::watch::Sender<DashboardState>,
) {
    let Some(input) = SessionTitleInput::from_context(context) else {
        return;
    };
    let now_ms = Utc::now().timestamp_millis();
    if context.session_title.apply_placeholder(
        &input.activity_signature,
        input.placeholder_title,
        now_ms,
    ) {
        sync_dashboard_session_title(context, tx);
    }
}

pub async fn refresh_session_title_after_activity(
    context: &mut Context,
    tx: &tokio::sync::watch::Sender<DashboardState>,
) -> Result<()> {
    let Some(input) = SessionTitleInput::from_context(context) else {
        return Ok(());
    };
    let now_ms = Utc::now().timestamp_millis();
    if context.session_title.apply_placeholder(
        &input.activity_signature,
        input.placeholder_title.clone(),
        now_ms,
    ) {
        sync_dashboard_session_title(context, tx);
    }
    if !context
        .session_title
        .should_generate(&input.activity_signature, now_ms)
    {
        return Ok(());
    }

    let generated = generate_session_title(context, &input.excerpt).await?;
    if context
        .session_title
        .apply_generated(input.activity_signature, generated, now_ms)
    {
        sync_dashboard_session_title(context, tx);
    }
    Ok(())
}

fn sync_dashboard_session_title(
    context: &Context,
    tx: &tokio::sync::watch::Sender<DashboardState>,
) {
    let session_title = context.session_title.snapshot();
    tx.send_modify(|state| {
        state.session_title = session_title;
    });
}

struct SessionTitleInput {
    placeholder_title: String,
    activity_signature: String,
    excerpt: String,
}

impl SessionTitleInput {
    fn from_context(context: &Context) -> Option<Self> {
        let events = context.events.views();
        let messages = context.memory.runtime_conversation_messages();
        let placeholder_title =
            first_event_title(&events).or_else(|| first_visible_history_title(&messages))?;
        let excerpt = conversation_excerpt(&events, &messages);
        if excerpt.trim().is_empty() {
            return None;
        }
        Some(Self {
            placeholder_title,
            activity_signature: activity_signature(&events, &messages),
            excerpt,
        })
    }
}

#[derive(Deserialize)]
struct TitleOutput {
    title: String,
}

async fn generate_session_title(context: &Context, excerpt: &str) -> Result<String> {
    let request = PromptRequest {
        tool_name: "set_session_title".to_string(),
        tool_description: SESSION_TITLE_TOOL_DESCRIPTION.to_string(),
        output_schema: json!({
            "type": "object",
            "properties": {
                "title": {
                    "type": "string",
                    "description": "A concise human-readable session title."
                }
            },
            "required": ["title"],
            "additionalProperties": false
        }),
        system_messages: vec![
            SESSION_TITLE_SYSTEM_ROLE.to_string(),
            SESSION_TITLE_SYSTEM_REQUIREMENTS.to_string(),
        ],
        long_term_memory_messages: Vec::new(),
        history_messages: Vec::new(),
        current_user_message: format!("{SESSION_TITLE_USER_MESSAGE_PREFIX}\n{excerpt}"),
        retry_messages: Vec::new(),
    };
    let value = context.efficient_llm.run_json(context, request).await?;
    let output = serde_json::from_value::<TitleOutput>(value)
        .map_err(|err| miette!("decode session title output failed: {err}"))?;
    normalize_session_title(&output.title).ok_or_else(|| miette!("session title output was empty"))
}

fn first_event_title(events: &[EventView]) -> Option<String> {
    events
        .iter()
        .filter_map(event_text)
        .find_map(first_sentence_title)
}

fn first_visible_history_title(messages: &[HistoryMessage]) -> Option<String> {
    messages
        .iter()
        .filter(|message| !message.is_system() && !message.is_tool())
        .filter_map(|message| message.text_content())
        .filter(|content| !is_runtime_context_text(content))
        .find_map(first_sentence_title)
}

fn conversation_excerpt(events: &[EventView], messages: &[HistoryMessage]) -> String {
    let mut lines = Vec::new();
    for event in events {
        if lines.len() >= MAX_EXCERPT_ITEMS {
            break;
        }
        if let Some(text) = event_text(event) {
            push_excerpt_line(&mut lines, "User", &text);
        }
    }
    for message in messages {
        if lines.len() >= MAX_EXCERPT_ITEMS {
            break;
        }
        if message.is_system() || message.is_tool() {
            continue;
        }
        let Some(text) = message.text_content() else {
            continue;
        };
        if is_runtime_context_text(text) {
            continue;
        }
        let role = if message.is_user() {
            "User"
        } else {
            "Assistant"
        };
        push_excerpt_line(&mut lines, role, text);
    }
    lines.join("\n")
}

fn push_excerpt_line(lines: &mut Vec<String>, role: &str, text: &str) {
    let compact = compact_inline(text);
    if compact.is_empty() {
        return;
    }
    lines.push(format!(
        "{role}: {}",
        truncate_chars(&compact, MAX_EXCERPT_ITEM_CHARS)
    ));
}

fn activity_signature(events: &[EventView], messages: &[HistoryMessage]) -> String {
    let mut hasher = Sha256::new();
    for event in events {
        hasher.update(event.event_id.as_bytes());
        if let Some(text) = event_text(event) {
            hasher.update(text.as_bytes());
        }
    }
    for message in messages {
        if message.is_system() || message.is_tool() {
            continue;
        }
        let Some(text) = message.text_content() else {
            continue;
        };
        if is_runtime_context_text(text) {
            continue;
        }
        hasher.update(message.role_name().as_bytes());
        hasher.update(text.as_bytes());
    }
    format!("{:x}", hasher.finalize())
}

fn event_text(event: &EventView) -> Option<String> {
    match &event.payload {
        EventPayload::TelegramIncoming(payload) => Some(payload.incoming_text.clone()),
        EventPayload::TerminalIncoming(payload) => Some(payload.incoming_text.clone()),
    }
    .map(|text| text.trim().to_string())
    .filter(|text| !text.is_empty())
}

fn first_sentence_title(text: impl AsRef<str>) -> Option<String> {
    let compact = compact_inline(text.as_ref());
    if compact.is_empty() {
        return None;
    }
    let sentence_end = compact.char_indices().find_map(|(index, ch)| {
        matches!(ch, '.' | '!' | '?' | '。' | '！' | '？').then_some(index)
    });
    let candidate = match sentence_end {
        Some(index) => compact[..index].trim(),
        None => compact.trim(),
    };
    normalize_session_title(candidate)
}

fn normalize_session_title(title: &str) -> Option<String> {
    let compact = compact_inline(title)
        .trim_matches(|ch: char| matches!(ch, '"' | '\'' | '`' | '“' | '”' | '‘' | '’'))
        .trim()
        .to_string();
    if compact.is_empty() {
        return None;
    }
    Some(truncate_chars(&compact, MAX_TITLE_CHARS))
}

fn compact_inline(text: &str) -> String {
    text.split_whitespace().collect::<Vec<_>>().join(" ")
}

fn truncate_chars(text: &str, max_chars: usize) -> String {
    let mut chars = text.chars();
    let truncated = chars.by_ref().take(max_chars).collect::<String>();
    if chars.next().is_some() {
        truncated.trim_end().to_string()
    } else {
        truncated
    }
}

fn is_runtime_context_text(text: &str) -> bool {
    let text = text.trim_start();
    text.starts_with("<preturn_context") || text.starts_with("<afterclaim_context")
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        events::{EventSource, EventStatus, TelegramIncomingEvent},
        reasoning::runtime::HistoryMessage,
    };

    fn event(id: &str, text: &str) -> EventView {
        EventView {
            event_id: uuid::Uuid::parse_str(id).expect("uuid"),
            source: EventSource::Telegram,
            status: EventStatus::Resolved,
            reply_message: None,
            arrived_at_ms: 0,
            payload: EventPayload::TelegramIncoming(TelegramIncomingEvent {
                chat_id: "1".to_string(),
                chat_kind: "private".to_string(),
                chat_title: "chat".to_string(),
                sender: "alice".to_string(),
                incoming_text: text.to_string(),
                telegram_update_id: 1,
                telegram_message_id: None,
                telegram_message_date: None,
                attachments: Vec::new(),
            }),
            last_error: None,
        }
    }

    #[test]
    fn placeholder_uses_first_event_sentence() {
        let events = vec![event(
            "11111111-1111-4111-8111-111111111111",
            "Please inspect the repository. Then commit it.",
        )];

        assert_eq!(
            first_event_title(&events).as_deref(),
            Some("Please inspect the repository")
        );
    }

    #[test]
    fn visible_history_title_skips_runtime_context() {
        let messages = vec![
            HistoryMessage::user("<preturn_context>state</preturn_context>"),
            HistoryMessage::user("Fix the Telegram session routing. Thanks"),
        ];

        assert_eq!(
            first_visible_history_title(&messages).as_deref(),
            Some("Fix the Telegram session routing")
        );
    }

    #[test]
    fn generation_requires_changed_activity_and_interval() {
        let mut state = SessionTitleState::default();
        assert!(state.should_generate("a", 0));
        assert!(state.apply_generated("a".to_string(), "Initial".to_string(), 0));
        assert!(!state.should_generate("a", SESSION_TITLE_REFRESH_INTERVAL_MS + 1));
        assert!(!state.should_generate("b", SESSION_TITLE_REFRESH_INTERVAL_MS - 1));
        assert!(state.should_generate("b", SESSION_TITLE_REFRESH_INTERVAL_MS));
    }
}
