//! Output quality guards: EmptyResponse, OutputContract, NonRepetition,
//! LowValueParroting, UserEcho.

use std::collections::HashSet;

use crate::api::routes::agent::guard_registry::requested_exact_bullet_count;
use crate::api::routes::agent::guard_registry::{Guard, GuardContext, GuardId, GuardVerdict};
use crate::api::routes::agent::intent_registry::Intent;

// ── 0. EmptyResponseGuard ────────────────────────────────────────────────

pub(in crate::api::routes::agent) struct EmptyResponseGuard;

impl Guard for EmptyResponseGuard {
    fn id(&self) -> GuardId {
        GuardId::EmptyResponse
    }

    fn is_relevant(&self, _ctx: &GuardContext) -> bool {
        true
    }

    fn evaluate(&self, content: &str, _ctx: &GuardContext) -> GuardVerdict {
        if content.trim().is_empty() {
            tracing::warn!("guard[EmptyResponse]: response is blank, requesting retry");
            GuardVerdict::RetryRequested {
                reason: "response was empty — no useful content for the user".into(),
            }
        } else {
            GuardVerdict::Pass
        }
    }
}

// ── 4. OutputContractGuard ───────────────────────────────────────────────

pub(in crate::api::routes::agent) struct OutputContractGuard;

impl Guard for OutputContractGuard {
    fn id(&self) -> GuardId {
        GuardId::OutputContract
    }

    fn is_relevant(&self, ctx: &GuardContext) -> bool {
        requested_exact_bullet_count(ctx.user_prompt).is_some()
    }

    fn evaluate(&self, content: &str, ctx: &GuardContext) -> GuardVerdict {
        let Some(expected_bullets) = requested_exact_bullet_count(ctx.user_prompt) else {
            return GuardVerdict::Pass;
        };
        let analysis = analyze_bullet_contract(content);
        if analysis.bullet_count == expected_bullets && !analysis.has_non_bullet_content {
            return GuardVerdict::Pass;
        }
        tracing::warn!(
            expected_bullets,
            actual_bullets = analysis.bullet_count,
            has_non_bullet_content = analysis.has_non_bullet_content,
            "guard[OutputContract]: output shape violated exact bullet contract"
        );
        GuardVerdict::RetryRequested {
            reason: format!(
                "response violated exact bullet contract: expected exactly {expected_bullets} bullet points and no extra framing"
            ),
        }
    }
}

#[derive(Debug, Clone, Copy)]
struct BulletContractAnalysis {
    bullet_count: usize,
    has_non_bullet_content: bool,
}

fn is_bullet_line(trimmed: &str) -> bool {
    trimmed.starts_with("- ")
        || trimmed.starts_with("* ")
        || trimmed.starts_with("• ")
        || trimmed.starts_with("-\t")
        || trimmed.starts_with("*\t")
        || trimmed.starts_with("•\t")
}

fn analyze_bullet_contract(content: &str) -> BulletContractAnalysis {
    let mut bullet_count = 0usize;
    let mut has_non_bullet_content = false;
    for line in content.lines() {
        let trimmed = line.trim();
        if trimmed.is_empty() {
            continue;
        }
        let is_bullet = is_bullet_line(trimmed);
        if is_bullet {
            bullet_count += 1;
        } else {
            has_non_bullet_content = true;
        }
    }
    BulletContractAnalysis {
        bullet_count,
        has_non_bullet_content,
    }
}

// ── 9. NonRepetitionGuard ────────────────────────────────────────────────

pub(in crate::api::routes::agent) struct NonRepetitionGuard;

impl Guard for NonRepetitionGuard {
    fn id(&self) -> GuardId {
        GuardId::NonRepetition
    }

    fn is_relevant(&self, ctx: &GuardContext) -> bool {
        ctx.previous_assistant.is_some()
    }

    fn evaluate(&self, content: &str, ctx: &GuardContext) -> GuardVerdict {
        if let Some(prev) = ctx.previous_assistant
            && looks_repetitive(content, prev)
            && user_requests_fresh_delta(ctx.user_prompt)
        {
            return GuardVerdict::Rewritten(
                "No verified delta since my last report. Name the exact check you want \
                 and I will run it now."
                    .into(),
            );
        }
        if let Some(echo) = (!ctx.prior_assistant_messages.is_empty())
            .then(|| find_self_echo_across_history(content, ctx.prior_assistant_messages))
            .flatten()
        {
            tracing::warn!(
                echo_len = echo.len(),
                "guard[NonRepetition]: self-echo from earlier turn detected"
            );
            return GuardVerdict::RetryRequested {
                reason: format!(
                    "Your response reuses prose from an earlier turn verbatim: \"{}\". \
                     Generate fresh content — do not recycle your own earlier descriptions.",
                    if echo.len() > 60 {
                        format!("{}...", &echo[..57])
                    } else {
                        echo
                    }
                ),
            };
        }
        GuardVerdict::Pass
    }
}

fn find_self_echo_across_history(output: &str, prior_messages: &[String]) -> Option<String> {
    let output_lower = output.to_ascii_lowercase();
    let output_words: Vec<&str> = output_lower.split_whitespace().collect();
    let min_words = 10;
    if output_words.len() < min_words {
        return None;
    }
    for prior in prior_messages {
        let prior_lower = prior.to_ascii_lowercase();
        for start in 0..=(output_words.len() - min_words) {
            let phrase = output_words[start..start + min_words].join(" ");
            if prior_lower.contains(&phrase) {
                let mut end = start + min_words;
                while end < output_words.len() {
                    let extended = output_words[start..=end].join(" ");
                    if !prior_lower.contains(&extended) {
                        break;
                    }
                    end += 1;
                }
                return Some(output_words[start..end].join(" "));
            }
        }
    }
    None
}

fn user_requests_fresh_delta(user_prompt: &str) -> bool {
    let lower = user_prompt.to_ascii_lowercase();
    const MARKERS: &[&str] = &[
        "status",
        "update",
        "what changed",
        "anything changed",
        "fresh check",
        "check again",
        "still",
        "latest",
        "current",
        "sitrep",
    ];
    MARKERS.iter().any(|m| lower.contains(m))
}

fn repeat_tokens(text: &str) -> HashSet<String> {
    text.to_ascii_lowercase()
        .split(|c: char| !c.is_ascii_alphanumeric())
        .filter(|tok| tok.len() >= 3)
        .map(|tok| tok.to_string())
        .collect()
}

fn common_prefix_ratio(a: &str, b: &str) -> f64 {
    let a_chars: Vec<char> = a.chars().collect();
    let b_chars: Vec<char> = b.chars().collect();
    let max_len = a_chars.len().max(b_chars.len());
    if max_len == 0 {
        return 0.0;
    }
    let shared = a_chars
        .iter()
        .zip(b_chars.iter())
        .take_while(|(ac, bc)| ac == bc)
        .count();
    shared as f64 / max_len as f64
}

fn looks_repetitive(current: &str, previous: &str) -> bool {
    let cur = current.trim();
    let prev = previous.trim();
    if cur.is_empty() || prev.is_empty() {
        return false;
    }
    if cur.eq_ignore_ascii_case(prev) {
        return true;
    }
    if cur.len() < 80 || prev.len() < 80 {
        return false;
    }
    let a = repeat_tokens(cur);
    let b = repeat_tokens(prev);
    if a.is_empty() || b.is_empty() {
        return false;
    }
    let overlap = a.intersection(&b).count() as f64;
    let denom = a.len().max(b.len()) as f64;
    let overlap_ratio = overlap / denom;
    let prefix_ratio = common_prefix_ratio(&cur.to_ascii_lowercase(), &prev.to_ascii_lowercase());
    overlap_ratio >= 0.86 || (overlap_ratio >= 0.72 && prefix_ratio >= 0.55)
}

// ── 10. LowValueParrotingGuard ───────────────────────────────────────────

pub(in crate::api::routes::agent) struct LowValueParrotingGuard;

impl Guard for LowValueParrotingGuard {
    fn id(&self) -> GuardId {
        GuardId::LowValueParroting
    }

    fn is_relevant(&self, ctx: &GuardContext) -> bool {
        ctx.tool_results.is_empty() && !ctx.has_intent(Intent::Execution)
    }

    fn evaluate(&self, content: &str, ctx: &GuardContext) -> GuardVerdict {
        if is_low_value_response(content, ctx.intents)
            || is_parroting_user_prompt(ctx.user_prompt, content)
        {
            tracing::warn!("guard[LowValueParroting]: low-value or parroting response detected");
            GuardVerdict::RetryRequested {
                reason: "low-value placeholder or parroting response".into(),
            }
        } else {
            GuardVerdict::Pass
        }
    }
}

fn is_low_value_response(response: &str, intents: &[Intent]) -> bool {
    if intents.contains(&Intent::Acknowledgement) {
        return false;
    }
    let trimmed = response.trim();
    if trimmed.is_empty() {
        return true;
    }
    let lower = trimmed.to_ascii_lowercase();
    if lower == "ready"
        || lower == "on it"
        || lower == "working on that now"
        || lower == "working on that now."
        || lower == "i await your insights"
        || lower == "i await your insights."
    {
        return true;
    }

    const NOISE_MARKERS: &[&str] = &[
        "ready",
        "i await your insights",
        "acknowledged. working on that now.",
        "acknowledged. working on that now",
    ];
    let lines = trimmed
        .lines()
        .map(str::trim)
        .filter(|l| !l.is_empty())
        .collect::<Vec<_>>();
    if !lines.is_empty()
        && lines.iter().all(|line| {
            let low = line.to_ascii_lowercase();
            NOISE_MARKERS.iter().any(|m| low == *m)
        })
    {
        return true;
    }

    false
}

fn prompt_allows_echo(user_prompt: &str) -> bool {
    let lower = user_prompt.to_ascii_lowercase();
    const MARKERS: &[&str] = &[
        "repeat",
        "echo",
        "quote",
        "verbatim",
        "paraphrase",
        "summarize what i said",
        "summarize my message",
    ];
    MARKERS.iter().any(|m| lower.contains(m))
}

fn is_parroting_user_prompt(user_prompt: &str, response: &str) -> bool {
    if prompt_allows_echo(user_prompt) {
        return false;
    }
    let u = user_prompt.trim();
    let r = response.trim();
    if u.is_empty() || r.is_empty() {
        return false;
    }
    let u_lower = u.to_ascii_lowercase();
    let r_lower = r.to_ascii_lowercase();
    if r_lower == u_lower {
        return true;
    }
    let ut = repeat_tokens(&u_lower);
    let rt = repeat_tokens(&r_lower);
    if ut.is_empty() || rt.is_empty() {
        return false;
    }
    let overlap = ut.intersection(&rt).count() as f64;
    let overlap_vs_prompt = overlap / ut.len() as f64;
    let prefix_ratio = common_prefix_ratio(&u_lower, &r_lower);
    let length_ratio = (r.len() as f64 / u.len().max(1) as f64).clamp(0.0, 10.0);
    overlap_vs_prompt >= 0.88 && prefix_ratio >= 0.55 && length_ratio <= 1.35
}

// ── 13. UserEchoGuard ────────────────────────────────────────────────────

pub(in crate::api::routes::agent) struct UserEchoGuard;

impl Guard for UserEchoGuard {
    fn id(&self) -> GuardId {
        GuardId::UserEcho
    }

    fn is_relevant(&self, _ctx: &GuardContext) -> bool {
        true
    }

    fn evaluate(&self, content: &str, ctx: &GuardContext) -> GuardVerdict {
        if let Some(echo) = find_user_echo(ctx.user_prompt, content, 8) {
            tracing::warn!(
                echo_len = echo.len(),
                "guard[UserEcho]: verbatim echo of user's words detected"
            );
            GuardVerdict::RetryRequested {
                reason: format!(
                    "Response echoed the user's own words verbatim: \"{}\". \
                     React to the sentiment in your own words instead of quoting the user.",
                    if echo.len() > 60 {
                        format!("{}...", &echo[..57])
                    } else {
                        echo
                    }
                ),
            }
        } else {
            GuardVerdict::Pass
        }
    }
}

fn find_user_echo(user_input: &str, output: &str, min_words: usize) -> Option<String> {
    let user_words: Vec<&str> = user_input.split_whitespace().collect();
    let output_lower = output.to_ascii_lowercase();

    if user_words.len() < min_words {
        return None;
    }

    for window_size in (min_words..=user_words.len()).rev() {
        for start in 0..=(user_words.len() - window_size) {
            let phrase = user_words[start..start + window_size]
                .join(" ")
                .to_ascii_lowercase();
            if output_lower.contains(&phrase) {
                return Some(phrase);
            }
        }
    }
    None
}
