use super::*;

/// Asks for a title short enough to fit a sidebar row.
const TITLE_INSTRUCTION: &str = "Give this conversation a short title of at most six words. \
Reply with the title alone: no quotes, no trailing period, no explanation.";
const TITLE_MAX_TOKENS: i32 = 48;
const TITLE_MAX_CHARS: usize = 60;

/// A one-shot generation that produces a session title. It runs against the
/// transient KV cache, so it never creates or touches a stored session.
#[cfg(target_os = "macos")]
pub(super) struct TitleRequest {
    session_id: i32,
    /// Title the session had when the request was queued. If it changed since,
    /// somebody renamed it by hand and that newer intent wins.
    expected: String,
    active: ActiveGeneration,
    content: String,
}

#[derive(Clone, Debug)]
pub(crate) struct ChatMessage {
    pub(super) id: i32,
    pub(super) user: bool,
    pub(super) reasoning: Option<String>,
    pub(super) reasoning_complete: bool,
    pub(super) reasoning_open: bool,
    pub(super) content: String,
    pub(super) markdown: Vec<markdown::Item>,
}

impl ChatMessage {
    pub(super) fn append(&mut self, reasoning: bool, chunk: &str) {
        if reasoning {
            self.reasoning.get_or_insert_default().push_str(chunk);
        } else {
            self.reasoning_complete = true;
            self.content.push_str(chunk);
        }
    }

    pub(super) fn refresh_markdown(&mut self) {
        if !self.user {
            let content = if self.reasoning.is_some() {
                self.content.trim_start()
            } else {
                &self.content
            };
            self.markdown = markdown::parse(content).collect();
        }
    }
}

impl From<StoredMessage> for ChatMessage {
    fn from(message: StoredMessage) -> Self {
        let mut message = Self {
            id: message.id,
            user: message.user,
            reasoning: message.reasoning,
            reasoning_complete: message.reasoning_complete,
            reasoning_open: false,
            content: message.content,
            markdown: Vec::new(),
        };
        message.refresh_markdown();
        message
    }
}

impl App {
    pub(super) fn start_generation(&mut self) {
        if self.generating || self.selected_project.is_none() {
            return;
        }
        let prompt = self.composer.trim().to_owned();
        if prompt.is_empty() {
            return;
        }
        let model = match ModelChoice::from_id(&self.preferences.selected_model) {
            Some(model) => model,
            None => {
                self.error = Some("The selected model is not supported.".into());
                return;
            }
        };
        let effective = self.preferences.generation().and_then(|generation| {
            self.preferences.runtime().and_then(|runtime| {
                crate::settings::effective_settings(model, &generation, &runtime, &models_path())
            })
        });
        let effective = match effective {
            Ok(settings) => settings,
            Err(error) => {
                self.error = Some(error);
                return;
            }
        };
        let assistant_reasoning = effective.turn.reasoning_mode != ReasoningMode::Direct;
        #[cfg(target_os = "macos")]
        let mut messages = self
            .conversation
            .iter()
            .map(|message| ChatTurn {
                user: message.user,
                skip_previous_eos: false,
                reasoning: message.reasoning.clone(),
                reasoning_complete: message.reasoning_complete,
                content: message.content.clone(),
            })
            .collect::<Vec<_>>();
        #[cfg(target_os = "macos")]
        messages.push(ChatTurn {
            user: true,
            skip_previous_eos: false,
            reasoning: None,
            reasoning_complete: true,
            content: prompt.clone(),
        });

        #[cfg(target_os = "macos")]
        {
            // A draft session only reaches the database once there is a turn to store.
            let opening_turn = self.selected_session.is_none();
            let session_id = match self.selected_session {
                Some(session_id) => session_id,
                None => {
                    let Some(project_id) = self.selected_project else {
                        return;
                    };
                    match self.persist_session(project_id) {
                        Ok(session_id) => session_id,
                        Err(error) => {
                            self.error = Some(format!("Could not create the session: {error}"));
                            return;
                        }
                    }
                }
            };
            let Some(service) = &self.generation_service else {
                self.error = Some("The model runtime is unavailable.".into());
                return;
            };
            let Some(database) = &mut self.database else {
                return;
            };
            let saved = match database.start_chat_turn(session_id, &prompt, assistant_reasoning) {
                Ok(turn) => turn,
                Err(error) => {
                    self.error = Some(format!("Could not save the chat turn: {error}"));
                    return;
                }
            };
            let idle_timeout =
                Duration::from_secs(self.preferences.idle_timeout_minutes.max(1) as u64 * 60);
            self.active_generation = match service.generate(
                effective.engine,
                effective.turn,
                messages,
                CheckpointTarget::Local(session_checkpoint_path(session_id)),
                idle_timeout,
            ) {
                Ok(active) => Some(active),
                Err(error) => {
                    self.generation_service = None;
                    self.error = Some(error);
                    return;
                }
            };
            let user = ChatMessage::from(saved.0);
            let mut assistant = ChatMessage::from(saved.1);
            assistant.reasoning_open = assistant_reasoning;
            self.composer.clear();
            self.conversation.push(user);
            self.conversation.push(assistant);
            self.generating = true;
            self.tokens_per_second = None;
            self.error = None;
            if opening_turn {
                // Queued behind the reply, so the answer is never delayed by it.
                self.request_first_title(session_id);
            }
        }
        #[cfg(not(target_os = "macos"))]
        {
            let _ = effective;
            self.error = Some("Local Metal generation requires macOS.".into());
            return;
        }
    }

    pub(super) fn poll_generation(&mut self) -> bool {
        #[cfg(target_os = "macos")]
        let Some(active) = &mut self.active_generation else {
            self.generating = false;
            return false;
        };
        #[cfg(target_os = "macos")]
        let mut transcript_changed = false;
        #[cfg(target_os = "macos")]
        let mut context_changed = false;
        #[cfg(target_os = "macos")]
        loop {
            match active.events.try_recv() {
                Ok(GenerationEvent::Loading) => {}
                Ok(GenerationEvent::Chunk { reasoning, content }) => {
                    if let Some(message) = self.conversation.last_mut()
                        && !message.user
                    {
                        message.append(reasoning, &content);
                        transcript_changed = true;
                    }
                }
                Ok(GenerationEvent::Context {
                    used,
                    limit,
                    tokens_per_second,
                }) => {
                    self.context_used = used;
                    self.context_limit = limit;
                    self.tokens_per_second = tokens_per_second;
                    context_changed = true;
                }
                Ok(GenerationEvent::Finished(result)) => {
                    self.generating = false;
                    if let Err(error) = result {
                        self.error = Some(error);
                    }
                    self.active_generation = None;
                    break;
                }
                Err(TryRecvError::Empty) => break,
                Err(TryRecvError::Disconnected) => {
                    self.generating = false;
                    self.active_generation = None;
                    self.error = Some("The model runtime stopped unexpectedly.".into());
                    break;
                }
            }
        }
        #[cfg(target_os = "macos")]
        if transcript_changed && let Some(message) = self.conversation.last_mut() {
            message.refresh_markdown();
        }
        #[cfg(target_os = "macos")]
        if transcript_changed
            && let Some(message) = self.conversation.last()
            && let Some(database) = &mut self.database
            && let Err(error) = database.update_message(
                message.id,
                message.reasoning.as_deref(),
                message.reasoning_complete,
                &message.content,
            )
        {
            if let Some(active) = &self.active_generation {
                active.cancel.store(true, Ordering::Relaxed);
            }
            self.error = Some(format!("Could not save generated chat text: {error}"));
        }
        #[cfg(target_os = "macos")]
        if context_changed
            && let Some(session_id) = self.selected_session
            && let Some(database) = &mut self.database
        {
            if let Err(error) = database.update_session_context(
                session_id,
                self.context_used,
                self.context_limit,
                self.tokens_per_second,
            ) {
                self.error = Some(format!("Could not save context usage: {error}"));
            } else if let Some(session) = self
                .projects
                .iter_mut()
                .flat_map(|project| &mut project.sessions)
                .find(|session| session.id == session_id)
            {
                session.context_used = self.context_used as i32;
                session.context_limit = self.context_limit as i32;
                session.last_tokens_per_second = self.tokens_per_second;
            }
        }
        #[cfg(target_os = "macos")]
        return transcript_changed;
        #[cfg(not(target_os = "macos"))]
        false
    }

    /// Asks the model for a session title without opening a session for it: the
    /// turn runs against the shared transient KV cache and only its text is kept.
    ///
    /// Reports failures to the caller instead of the error banner: the automatic
    /// first-chat titling must stay silent, while the menu action shows them.
    #[cfg(target_os = "macos")]
    fn request_title(&mut self, session_id: i32) -> Result<(), String> {
        if self.active_titling.is_some() {
            return Err("A title is already being generated.".into());
        }
        let model = ModelChoice::from_id(&self.preferences.selected_model)
            .ok_or_else(|| "The selected model is not supported.".to_owned())?;
        let mut effective = self.preferences.generation().and_then(|generation| {
            self.preferences.runtime().and_then(|runtime| {
                crate::settings::effective_settings(model, &generation, &runtime, &models_path())
            })
        })?;
        let database = self
            .database
            .as_mut()
            .ok_or_else(|| "The project database is unavailable.".to_owned())?;
        let stored = database
            .load_messages(session_id)
            .map_err(|error| format!("Could not read the session: {error}"))?;
        // The assistant row is inserted empty when a turn starts; an empty turn
        // is never useful context, and skipping it is what lets the automatic
        // pass summarize the user's opening message on its own.
        let mut messages = stored
            .into_iter()
            .filter(|message| !message.content.trim().is_empty())
            .map(|message| ChatTurn {
                user: message.user,
                skip_previous_eos: false,
                reasoning: None,
                reasoning_complete: true,
                content: message.content,
            })
            .collect::<Vec<_>>();
        if messages.is_empty() {
            return Err("This session has no messages to summarize yet.".into());
        }
        messages.push(ChatTurn {
            user: true,
            skip_previous_eos: false,
            reasoning: None,
            reasoning_complete: true,
            content: TITLE_INSTRUCTION.to_owned(),
        });
        effective.turn.max_generated_tokens = TITLE_MAX_TOKENS;
        effective.turn.reasoning_mode = ReasoningMode::Direct;

        let service = self
            .generation_service
            .as_ref()
            .ok_or_else(|| "The model runtime is unavailable.".to_owned())?;
        let idle_timeout =
            Duration::from_secs(self.preferences.idle_timeout_minutes.max(1) as u64 * 60);
        match service.generate(
            effective.engine,
            effective.turn,
            messages,
            CheckpointTarget::OneShot(transient_cache_path()),
            idle_timeout,
        ) {
            Ok(active) => {
                self.active_titling = Some(TitleRequest {
                    session_id,
                    expected: self.session_title(session_id).unwrap_or_default(),
                    active,
                    content: String::new(),
                });
                Ok(())
            }
            Err(error) => {
                self.generation_service = None;
                Err(error)
            }
        }
    }

    /// The "Retitle with AI" menu action, which reports why it could not run.
    #[cfg(target_os = "macos")]
    pub(super) fn retitle_session(&mut self, session_id: i32) {
        match self.request_title(session_id) {
            Ok(()) => self.error = None,
            Err(error) => self.error = Some(error),
        }
    }

    /// Titles a session from its opening message. Queued behind the reply that
    /// is already running, and skipped without complaint if it cannot start —
    /// the user did not ask for this, so it must never interrupt them.
    #[cfg(target_os = "macos")]
    pub(super) fn request_first_title(&mut self, session_id: i32) {
        let _ = self.request_title(session_id);
    }

    /// Drains the one-shot title generation. Returns true while it is running so
    /// the caller keeps ticking.
    #[cfg(target_os = "macos")]
    pub(super) fn poll_titling(&mut self) -> bool {
        let Some(mut request) = self.active_titling.take() else {
            return false;
        };
        let failure = loop {
            match request.active.events.try_recv() {
                Ok(GenerationEvent::Chunk {
                    reasoning: false,
                    content,
                }) => request.content.push_str(&content),
                Ok(GenerationEvent::Finished(Err(error))) => break Some(error),
                Ok(GenerationEvent::Finished(Ok(_))) => break None,
                Ok(_) => {}
                Err(TryRecvError::Empty) => {
                    self.active_titling = Some(request);
                    return true;
                }
                Err(TryRecvError::Disconnected) => {
                    break Some("The model runtime stopped unexpectedly.".to_owned());
                }
            }
        };
        if let Some(error) = failure {
            self.error = Some(format!("Could not generate a title: {error}"));
        } else if let Some(title) = session_title(&request.content) {
            self.apply_title(&request, &title);
        } else {
            self.error = Some("The model did not return a usable title.".into());
        }
        false
    }

    #[cfg(target_os = "macos")]
    fn session_title(&self, session_id: i32) -> Option<String> {
        self.projects
            .iter()
            .flat_map(|project| &project.sessions)
            .find(|session| session.id == session_id)
            .map(|session| session.title.clone())
    }

    #[cfg(target_os = "macos")]
    fn apply_title(&mut self, request: &TitleRequest, title: &str) {
        let Some(current) = self.session_title(request.session_id) else {
            return;
        };
        if current != request.expected {
            return;
        }
        let Some(database) = &mut self.database else {
            return;
        };
        match database.rename_session(request.session_id, title) {
            Ok(()) => {
                self.error = None;
                self.reload_projects();
            }
            Err(error) => self.error = Some(format!("Could not save the session title: {error}")),
        }
    }
}

/// Reduces a model reply to a single sidebar-sized line, or `None` if nothing
/// usable came back.
pub(super) fn session_title(reply: &str) -> Option<String> {
    let title = reply
        .lines()
        .map(str::trim)
        .find(|line| !line.is_empty())?
        .trim_matches(|character: char| {
            character.is_whitespace() || matches!(character, '"' | '\'' | '`' | '*' | '#' | '.')
        })
        .trim();
    if title.is_empty() {
        return None;
    }
    Some(match title.char_indices().nth(TITLE_MAX_CHARS) {
        Some((index, _)) => format!("{}…", title[..index].trim_end()),
        None => title.to_owned(),
    })
}
