//! RLM history rendering: turns the chronological turn view into the LLM
//! message sequence the model sees each iteration.
//!
//! Contract:
//! - **History == emission.** A prior executed step renders as an `Assistant`
//!   message holding the canonical cell `{prose}\n<lashlang>\n{code}\n</lashlang>`
//!   (`render_lashlang_cell_text`), followed by a `User` message holding that
//!   step's printed output, images, error, and final value. A plain user turn
//!   renders its content verbatim as a `User` message. There is no
//!   `--- history[N] ---` meta-format: what the model sees as history is exactly
//!   the grammar it must emit, so a continuation lands in that grammar.
//! - **Folding.** A step is stored as two consecutive entries — an assistant
//!   prose `Message` then a `RlmTrajectoryEntry`. They fold into one assistant
//!   message. `visit_turn_view` is a push visitor with no lookahead, so the
//!   prose is buffered (`PendingProse`) and either folded into the next step or
//!   flushed as a standalone assistant message (a prose-only finish).
//! - **Cache fence.** The last history message is marked with a
//!   `cache_breakpoint` (`mark_last_history_text_cache_breakpoint`) so the
//!   provider can reuse the stable history prefix across iterations. Only the
//!   volatile `=== CURRENT ITERATION ===` tail — iteration number, turn events,
//!   finalization, required-output schema, context budget — is appended uncached
//!   by `append_current_iteration_message`.
//! - **Re-fetch handle.** A lossy-projected step output is tagged
//!   `full: history[N].output[M]` on its user message; the `history` projected
//!   binding (`projection/context.rs`) carries the full untruncated value, keyed
//!   by the step's `entry.index`, so the model can recover it by re-printing the
//!   reference. Proven by
//!   `projection::context::tests::history_step_output_resolves_full_untruncated_value`.
//! - **Variables.** The live variable namespace is surfaced separately as the
//!   "Bound Variables" prompt contribution (`crate::rlm_support::render_bound_variables`),
//!   not here — history carries the trajectory, not current globals.

use std::collections::HashSet;
use std::fmt::Write as _;
use std::sync::Arc;

use lash_core::llm::types::{LlmAttachment, LlmContentBlock, LlmMessage, LlmRole};
use lash_core::{BorrowedChronologicalEntry, BorrowedChronologicalPayload, head_tail_truncate};
use lash_rlm_types::{RlmAttachmentRef, RlmImageRef};
use lashlang::{Value as FlowValue, ValueProjectionContext};

use crate::cell_scan::render_lashlang_cell_text;
use crate::projection::{decode_rlm_protocol_event, json_to_flow_value};

pub(super) struct RlmHistoryRenderInput<'a> {
    pub(super) events: &'a [lash_core::SessionEventRecord],
    pub(super) turn_messages: &'a lash_core::MessageSequence,
    pub(super) turn_causes: &'a [lash_core::TurnCause],
    pub(super) max_output_chars: usize,
    pub(super) protocol_iteration: usize,
    pub(super) finalization: &'a str,
    pub(super) required_output: Option<&'a str>,
    pub(super) final_answer_format: Option<&'a str>,
    pub(super) budget_suffix: Option<&'a str>,
}

#[derive(Clone, Copy)]
pub(super) struct CurrentIterationMessageInput<'a> {
    pub(super) saw_history: bool,
    pub(super) protocol_iteration: usize,
    pub(super) turn_causes: &'a [lash_core::TurnCause],
    pub(super) finalization: &'a str,
    pub(super) required_output: Option<&'a str>,
    pub(super) final_answer_format: Option<&'a str>,
    pub(super) budget_suffix: Option<&'a str>,
}

/// Assistant prose awaiting a fold into the next lashlang step. Buffered because
/// `visit_turn_view` is a push visitor with no lookahead.
struct PendingProse {
    text: String,
    image_blocks: Vec<LlmContentBlock>,
}

pub(super) fn build_rlm_history_messages_from_turn(
    input: RlmHistoryRenderInput<'_>,
    attachments: &mut Vec<LlmAttachment>,
) -> Vec<LlmMessage> {
    let mut messages = render_history_messages(&input, attachments);
    let saw_history = !messages.is_empty();
    append_current_iteration_message(
        &mut messages,
        CurrentIterationMessageInput {
            saw_history,
            protocol_iteration: input.protocol_iteration,
            turn_causes: input.turn_causes,
            finalization: input.finalization,
            required_output: input.required_output,
            final_answer_format: input.final_answer_format,
            budget_suffix: input.budget_suffix,
        },
    );
    messages
}

/// The history portion only (no current-iteration tail): each prior step as an
/// assistant cell message + a user observation message, with prose folded in.
pub(super) fn render_history_messages(
    input: &RlmHistoryRenderInput<'_>,
    attachments: &mut Vec<LlmAttachment>,
) -> Vec<LlmMessage> {
    let mut messages = Vec::new();
    let active_cause_ids = input
        .turn_causes
        .iter()
        .map(|cause| cause.id.as_str())
        .collect::<HashSet<_>>();
    let mut pending: Option<PendingProse> = None;

    lash_core::visit_turn_view(input.events, input.turn_messages, |entry| {
        if borrowed_entry_is_active_cause(entry, &active_cause_ids) {
            return;
        }
        match entry.payload {
            BorrowedChronologicalPayload::Message(message)
                if matches!(message.role, lash_core::MessageRole::Assistant) =>
            {
                // Assistant prose: buffer to fold into the next lashlang step.
                flush_pending_prose(&mut messages, &mut pending);
                let mut image_blocks = Vec::new();
                append_borrowed_entry_image_blocks(entry, attachments, &mut image_blocks);
                pending = Some(PendingProse {
                    text: message_history_text_parts(message.parts),
                    image_blocks,
                });
            }
            BorrowedChronologicalPayload::ProtocolEvent(event) => {
                let Some(lash_rlm_types::RlmProtocolEvent::RlmTrajectoryEntry(step)) =
                    decode_rlm_protocol_event(event)
                else {
                    return;
                };
                // Fold buffered prose into one assistant message: prose + cell,
                // byte-identical to the model's own emission.
                let prose = pending.take();
                let prose_text = prose.as_ref().map(|p| p.text.as_str()).unwrap_or("");
                let cell = render_lashlang_cell_text(prose_text, step.code.trim());
                let mut cell_blocks = vec![text_block(cell, false)];
                if let Some(prose) = prose {
                    cell_blocks.extend(prose.image_blocks);
                }
                messages.push(LlmMessage::new(LlmRole::Assistant, cell_blocks));

                // The step's printed outputs become a user observation message.
                let image_refs = step
                    .images
                    .iter()
                    .map(|image| RlmImageRef {
                        id: image.id.to_string(),
                        media_type: image.media_type,
                        width: image.width,
                        height: image.height,
                        bytes: image.byte_len as usize,
                        label: image.label.clone(),
                    })
                    .collect::<Vec<_>>();
                let obs_text = step_output_text(
                    entry.index,
                    &step.output,
                    &image_refs,
                    step.error.as_deref(),
                    step.final_output.as_ref(),
                );
                let mut obs_blocks = vec![text_block(obs_text, false)];
                append_borrowed_entry_image_blocks(entry, attachments, &mut obs_blocks);
                messages.push(LlmMessage::new(LlmRole::User, obs_blocks));
            }
            BorrowedChronologicalPayload::Message(message) => {
                // User / system / event turn: rendered verbatim by role.
                flush_pending_prose(&mut messages, &mut pending);
                let text = message_text(
                    entry.index,
                    &message_history_text_parts(message.parts),
                    &message_attachment_refs(message.parts),
                    input.max_output_chars,
                );
                let mut blocks = vec![text_block(text, false)];
                append_borrowed_entry_image_blocks(entry, attachments, &mut blocks);
                let role = match message.role {
                    lash_core::MessageRole::User | lash_core::MessageRole::Event => LlmRole::User,
                    lash_core::MessageRole::System => LlmRole::System,
                    lash_core::MessageRole::Assistant => LlmRole::Assistant,
                };
                messages.push(LlmMessage::new(role, blocks));
            }
        }
    });
    flush_pending_prose(&mut messages, &mut pending);
    messages
}

/// Emit a buffered prose as a standalone assistant message (a prose-only
/// finish). Carries nothing for empty prose with no images.
fn flush_pending_prose(messages: &mut Vec<LlmMessage>, pending: &mut Option<PendingProse>) {
    if let Some(prose) = pending.take() {
        if prose.text.trim().is_empty() && prose.image_blocks.is_empty() {
            return;
        }
        let mut blocks = vec![text_block(prose.text, false)];
        blocks.extend(prose.image_blocks);
        messages.push(LlmMessage::new(LlmRole::Assistant, blocks));
    }
}

fn append_current_iteration_message(
    messages: &mut Vec<LlmMessage>,
    input: CurrentIterationMessageInput<'_>,
) {
    if !input.saw_history {
        messages.push(LlmMessage::new(
            LlmRole::User,
            vec![text_block(
                "=== HISTORY ===\n\nNo chronological history is available.",
                false,
            )],
        ));
    } else {
        mark_last_history_text_cache_breakpoint(messages);
    }
    let mut current_prompt = format!(
        "\n\n\n=== CURRENT ITERATION: {} ===",
        input.protocol_iteration
    );
    if let Some(turn_events) = lash_core::render_turn_causes_prompt(input.turn_causes) {
        current_prompt.push_str("\n\n");
        current_prompt.push_str(&turn_events);
    }
    current_prompt.push_str("\n\n\n=== FINALIZATION ===\n\n");
    current_prompt.push_str(input.finalization);
    if let Some(block) = input.required_output {
        current_prompt.push_str("\n\n=== REQUIRED OUTPUT ===\n\n");
        current_prompt.push_str(block);
    }
    if let Some(guidance) = input.final_answer_format {
        current_prompt.push_str("\n\n=== FINAL ANSWER FORMAT ===\n\n");
        current_prompt.push_str(guidance);
    }
    if let Some(suffix) = input.budget_suffix {
        current_prompt.push_str("\n\n=== CONTEXT BUDGET ===\n\n");
        current_prompt.push_str(suffix);
    }
    messages.push(LlmMessage::new(
        LlmRole::User,
        vec![text_block(current_prompt, false)],
    ));
}

fn borrowed_entry_is_active_cause(
    entry: BorrowedChronologicalEntry<'_>,
    active_cause_ids: &HashSet<&str>,
) -> bool {
    matches!(
        entry.payload,
        BorrowedChronologicalPayload::Message(message)
            if matches!(message.role, lash_core::MessageRole::Event)
                && active_cause_ids.contains(message.id)
    )
}

fn text_block(text: impl Into<Arc<str>>, cache_breakpoint: bool) -> LlmContentBlock {
    LlmContentBlock::Text {
        text: text.into(),
        response_meta: None,
        cache_breakpoint,
    }
}

fn mark_last_history_text_cache_breakpoint(messages: &mut [LlmMessage]) {
    for message in messages.iter_mut().rev() {
        let Some(blocks) = Arc::get_mut(&mut message.blocks) else {
            continue;
        };
        for block in blocks.iter_mut().rev() {
            if let LlmContentBlock::Text {
                text,
                cache_breakpoint,
                ..
            } = block
                && !text.trim().is_empty()
            {
                *cache_breakpoint = true;
                return;
            }
        }
    }
}

fn append_borrowed_entry_image_blocks(
    entry: BorrowedChronologicalEntry<'_>,
    attachments: &mut Vec<LlmAttachment>,
    blocks: &mut Vec<LlmContentBlock>,
) {
    match entry.payload {
        BorrowedChronologicalPayload::Message(message) => {
            for part in message.parts {
                let Some(attachment) = part.attachment.as_ref() else {
                    continue;
                };
                let attachment_idx = attachments.len();
                attachments.push(LlmAttachment::reference(attachment.reference.clone()));
                blocks.push(LlmContentBlock::Image { attachment_idx });
            }
        }
        BorrowedChronologicalPayload::ProtocolEvent(event) => {
            if let Some(lash_rlm_types::RlmProtocolEvent::RlmTrajectoryEntry(entry)) =
                decode_rlm_protocol_event(event)
            {
                for image in &entry.images {
                    let attachment_idx = attachments.len();
                    attachments.push(LlmAttachment::reference(image.clone()));
                    blocks.push(LlmContentBlock::Image { attachment_idx });
                }
            }
        }
    }
}

/// Verbatim content for a plain (non-step) message. No `--- history[N] ---`
/// wrapper; keeps the truncation re-fetch handle and the attachment listing.
fn message_text(
    index: usize,
    content: &str,
    attachments: &[RlmAttachmentRef],
    max_output_chars: usize,
) -> String {
    let (preview, raw_len) = head_tail_truncate(content, max_output_chars);
    let mut out = preview.to_string();
    if raw_len > max_output_chars {
        let _ = write!(
            out,
            "\n\n(preview only — full value retained; re-print history[{index}].content for the rest)"
        );
    }
    if !attachments.is_empty() {
        out.push_str("\n\nAttachments:");
        for (attachment_index, attachment) in attachments.iter().enumerate() {
            let rendered = serde_json::to_string(attachment)
                .unwrap_or_else(|_| "{\"error\":\"unrenderable attachment\"}".to_string());
            let _ = write!(
                out,
                "\n- history[{index}].attachments[{attachment_index}]: {rendered}"
            );
        }
    }
    out
}

/// The user observation message for a step: printed outputs (with re-fetch
/// handles), images, error, and final value. Never empty.
fn step_output_text(
    index: usize,
    output: &[String],
    images: &[RlmImageRef],
    error: Option<&str>,
    final_output: Option<&serde_json::Value>,
) -> String {
    let mut out = String::new();
    for (output_index, item) in output.iter().enumerate() {
        let (preview, projected_lossy) = project_history_output(item);
        let raw_len = item.chars().count();
        let full_ref = projected_ref(
            projected_lossy,
            &format!("history[{index}].output[{output_index}]"),
        );
        if !out.is_empty() {
            out.push_str("\n\n");
        }
        let _ = write!(
            out,
            "history[{index}].output[{output_index}] ({raw_len} chars{full_ref}):\n{preview}"
        );
    }
    if !images.is_empty() {
        if !out.is_empty() {
            out.push_str("\n\n");
        }
        out.push_str("Images:");
        for (image_index, image) in images.iter().enumerate() {
            let rendered = serde_json::to_string(image)
                .unwrap_or_else(|_| "{\"error\":\"unrenderable image\"}".to_string());
            let _ = write!(
                out,
                "\n- history[{index}].images[{image_index}]: {rendered}"
            );
        }
    }
    if let Some(error) = error {
        if !out.is_empty() {
            out.push_str("\n\n");
        }
        out.push_str("Error:\n");
        out.push_str(error);
    }
    if let Some(final_output) = final_output {
        if !out.is_empty() {
            out.push_str("\n\n");
        }
        out.push_str("Final output:\n");
        out.push_str(
            &serde_json::to_string_pretty(final_output)
                .unwrap_or_else(|_| final_output.to_string()),
        );
    }
    if out.is_empty() {
        out.push_str("(no printed output)");
    }
    out
}

fn message_attachment_refs(parts: &[lash_core::Part]) -> Vec<RlmAttachmentRef> {
    parts
        .iter()
        .filter_map(|part| {
            let attachment = part.attachment.as_ref()?;
            Some(RlmAttachmentRef {
                id: part.id.clone(),
                media_type: attachment.reference.media_type,
                label: attachment.reference.label.clone(),
                reference: attachment.reference.id.to_string(),
            })
        })
        .collect()
}

fn message_history_text_parts(parts: &[lash_core::Part]) -> String {
    let chunks = parts
        .iter()
        .filter(|part| {
            matches!(
                part.kind,
                lash_core::PartKind::Text | lash_core::PartKind::Prose
            )
        })
        .map(|part| part.content.trim())
        .filter(|part| !part.is_empty())
        .collect::<Vec<_>>();
    chunks.join("\n\n")
}

fn projected_ref(projected_lossy: bool, reference: &str) -> String {
    if projected_lossy {
        // State retention explicitly: a truncated preview is display-only, the
        // full value is still live and re-printable. Models otherwise misread a
        // short preview as lost state and stop mid-task.
        format!(" — preview only, full value retained; re-print {reference} for the rest")
    } else {
        String::new()
    }
}

fn project_history_output(item: &str) -> (String, bool) {
    let value = history_output_value(item);
    let projected = crate::rlm_support::print_history_projector()
        .project_blocking(ValueProjectionContext::new(&value));
    let lossy = projection_is_lossy(item, &projected);
    (projected, lossy)
}

fn history_output_value(item: &str) -> FlowValue {
    let trimmed = item.trim_start();
    if (trimmed.starts_with('{') || trimmed.starts_with('['))
        && let Ok(value) = serde_json::from_str::<serde_json::Value>(item)
    {
        return json_to_flow_value(value);
    }
    FlowValue::String(item.into())
}

fn projection_is_lossy(original: &str, projected: &str) -> bool {
    projected.contains("truncated")
        || projected.contains("omitted")
        || projected.contains("max depth")
        || projected.chars().count() < original.chars().count()
}
