pub const DECOMPOSE_TASK_SYSTEM: &str = "\
You are a task-decomposition assistant. Given a task description, produce \
between 3 and 7 concrete, physical micro-steps. Each step must be a short \
imperative sentence, actionable today, with no commentary. Output ONLY a \
JSON array of strings.";

// Phase 9 content-tag extraction. The model emits a {topic, intent}
// JSON pair under a strict GBNF (see grammars::CONTENT_TAGS_GRAMMAR).
// CONTENT_TAGS_SYSTEM is the system message; the user message wraps
// the transcript text.
pub const CONTENT_TAGS_SYSTEM: &str = "\
You tag a transcript with ONE topic and ONE intent. \
TOPIC is a 1 to 3 token lowercase hyphen-joined noun phrase naming the \
dominant subject. Examples: interview-prep, grant-application, \
daily-standup. \
INTENT is exactly one of: planning, reflection, venting, capture, \
decision, question. \
Return JSON only, with this exact shape: \
{\"topic\":\"...\",\"intent\":\"...\"}";

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ContentTags {
    pub topic: String,
    pub intent: String,
}

pub const INTENT_CLOSED_SET: &[&str] = &[
    "planning",
    "reflection",
    "venting",
    "capture",
    "decision",
    "question",
];

pub fn is_valid_intent(s: &str) -> bool {
    INTENT_CLOSED_SET.contains(&s)
}

pub const EXTRACT_TASKS_SYSTEM: &str = "\
You are a task-extraction assistant. Given a transcript of spoken notes, \
output a JSON array of action items the speaker committed to. Each item must \
be a short imperative sentence. Omit observations, wishes, and background \
context that are not explicit commitments. Output an empty array if there are \
no action items.";

/// Compact representation of a human-in-the-loop feedback example used
/// for few-shot prompt conditioning. Built by lumotia-storage and fed to the
/// prompt builder below; we keep this struct local to the LLM crate so
/// lumotia-llm does not depend on lumotia-storage.
#[derive(Debug, Clone)]
pub struct FeedbackExample {
    /// What the AI was given as input (e.g. the parent task text, or
    /// the transcript chunk). Kept verbatim.
    pub input: String,
    /// What the AI produced originally. `None` if the user only
    /// gave a thumbs-up without a prior edit (positive signal
    /// without a paired correction).
    pub original_output: Option<String>,
    /// What the user changed it to. `None` for thumbs-only rows.
    /// This is the highest-value signal — when present, inject it
    /// as the "good" output in the few-shot example.
    pub corrected_output: Option<String>,
}

/// Render a feedback example into the exemplar block used in prompt
/// conditioning. Returns `None` for rows that carry no usable pairing
/// (e.g. a thumbs-up with no input context).
fn render_feedback_exemplar(ex: &FeedbackExample) -> Option<String> {
    if ex.input.trim().is_empty() {
        return None;
    }
    let good = ex
        .corrected_output
        .as_deref()
        .or(ex.original_output.as_deref())?;
    let good = good.trim();
    if good.is_empty() {
        return None;
    }
    Some(format!("Input: {}\nGood output: {}", ex.input.trim(), good))
}

/// Build a system prompt that combines the base task system prompt
/// with a few-shot block assembled from recent HITL examples. If no
/// usable examples are available, returns the base prompt unchanged
/// so early users see the generic behaviour and the LLM is not
/// confused by an empty exemplar section.
///
/// The exemplars are ordered most-recent-first (caller's order is
/// preserved) so the LLM weights the user's current style over
/// earlier noise, mirroring what a human reviewer would do.
pub fn build_conditioned_system_prompt(base: &str, examples: &[FeedbackExample]) -> String {
    let rendered: Vec<String> = examples
        .iter()
        .filter_map(render_feedback_exemplar)
        .collect();
    if rendered.is_empty() {
        return base.to_string();
    }
    let block = rendered
        .iter()
        .map(|s| format!("- {s}"))
        .collect::<Vec<_>>()
        .join("\n");
    format!(
        "{base}\n\nHere are examples of the style this user prefers, in the \
         user's own words. Match this style closely when producing your output:\n{block}"
    )
}

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

    #[test]
    fn builds_plain_prompt_when_no_examples() {
        let out = build_conditioned_system_prompt(DECOMPOSE_TASK_SYSTEM, &[]);
        assert_eq!(out, DECOMPOSE_TASK_SYSTEM);
    }

    #[test]
    fn skips_empty_input_examples() {
        let examples = vec![FeedbackExample {
            input: String::new(),
            original_output: None,
            corrected_output: Some("ignored".into()),
        }];
        let out = build_conditioned_system_prompt(DECOMPOSE_TASK_SYSTEM, &examples);
        assert_eq!(out, DECOMPOSE_TASK_SYSTEM);
    }

    #[test]
    fn prefers_corrected_over_original() {
        let examples = vec![FeedbackExample {
            input: "Clean room".into(),
            original_output: Some("Organise your bedroom".into()),
            corrected_output: Some("Pick up one shirt from the floor".into()),
        }];
        let out = build_conditioned_system_prompt(DECOMPOSE_TASK_SYSTEM, &examples);
        assert!(out.contains("Pick up one shirt from the floor"));
        assert!(!out.contains("Organise your bedroom"));
    }

    #[test]
    fn falls_back_to_original_when_no_correction() {
        let examples = vec![FeedbackExample {
            input: "Write report".into(),
            original_output: Some("Open a blank document".into()),
            corrected_output: None,
        }];
        let out = build_conditioned_system_prompt(DECOMPOSE_TASK_SYSTEM, &examples);
        assert!(out.contains("Open a blank document"));
    }
}
