use crate::agent::*;
use crate::events::TaskOutcome;

#[allow(clippy::too_many_arguments)]
pub(super) async fn maybe_handle_stop_command(
    agent: &Agent,
    session_id: &str,
    user_text: &str,
    user_role: UserRole,
    channel_ctx: &ChannelContext,
    status_tx: Option<mpsc::Sender<StatusUpdate>>,
    task_id: &str,
    emitter: &crate::events::EventEmitter,
) -> anyhow::Result<Option<String>> {
    let lower_trimmed = user_text.trim().to_ascii_lowercase();
    let is_stop_command = matches!(lower_trimmed.as_str(), "stop" | "cancel" | "abort");
    if !is_stop_command {
        return Ok(None);
    }

    let early_task_start = Instant::now();
    if user_role != UserRole::Owner {
        let reply = "Only the owner can cancel running work in this session.";
        let reply = emit_bootstrap_direct_reply(
            agent,
            emitter,
            task_id,
            session_id,
            early_task_start,
            reply,
        )
        .await?;
        return Ok(Some(reply));
    }

    let cancel_result = agent
        .execute_tool_with_watchdog(
            "cli_agent",
            r#"{"action": "cancel_all"}"#,
            &tool_exec::ToolExecCtx {
                session_id,
                task_id: Some(task_id),
                status_tx,
                channel_visibility: channel_ctx.visibility,
                channel_id: channel_ctx.channel_id.as_deref(),
                project_scope: None,
                trusted: channel_ctx.trusted,
                user_role,
            },
        )
        .await;
    let cli_cancel_msg = cancel_result.ok();

    // Cancel any active goals for this session as well (background task leads/executors).
    let cancelled_goals = agent.cancel_active_goals_for_session(session_id).await;

    let cli_cancelled_any = cli_cancel_msg
        .as_deref()
        .is_some_and(|m| !m.contains("No running CLI agents"));

    let reply = if cli_cancelled_any || !cancelled_goals.is_empty() {
        let mut reply = String::new();
        if cli_cancelled_any {
            reply.push_str(cli_cancel_msg.as_deref().unwrap_or_default());
        }
        if !cancelled_goals.is_empty() {
            if !reply.is_empty() {
                reply.push('\n');
                reply.push('\n');
            }
            if cancelled_goals.len() == 1 {
                reply.push_str(&format!("cancelled goal: {}", cancelled_goals[0]));
            } else {
                reply.push_str(&format!(
                    "cancelled {} goals:\n{}",
                    cancelled_goals.len(),
                    cancelled_goals
                        .iter()
                        .map(|d| format!("- {}", d))
                        .collect::<Vec<_>>()
                        .join("\n")
                ));
            }
        }
        info!(session_id, "Cancelled work on stop command");
        reply
    } else {
        "No running task to cancel.".to_string()
    };

    let reply = emit_bootstrap_direct_reply(
        agent,
        emitter,
        task_id,
        session_id,
        early_task_start,
        &reply,
    )
    .await?;
    Ok(Some(reply))
}

/// Detect explicit "pivot" turns (e.g. "wait stop... actually ... instead")
/// and cancel any in-flight background work before continuing with the new
/// instruction in the same turn.
#[allow(clippy::too_many_arguments)]
pub(super) async fn maybe_cancel_work_for_mid_task_pivot(
    agent: &Agent,
    session_id: &str,
    user_text: &str,
    user_role: UserRole,
    channel_ctx: &ChannelContext,
    status_tx: Option<mpsc::Sender<StatusUpdate>>,
    task_id: &str,
) {
    if agent.depth != 0 || user_role != UserRole::Owner || !looks_like_mid_task_pivot(user_text) {
        return;
    }

    let cancel_result = agent
        .execute_tool_with_watchdog(
            "cli_agent",
            r#"{"action": "cancel_all"}"#,
            &tool_exec::ToolExecCtx {
                session_id,
                task_id: Some(task_id),
                status_tx,
                channel_visibility: channel_ctx.visibility,
                channel_id: channel_ctx.channel_id.as_deref(),
                project_scope: None,
                trusted: channel_ctx.trusted,
                user_role,
            },
        )
        .await;

    let cli_cancel_msg = match cancel_result {
        Ok(msg) => Some(msg),
        Err(e) => {
            warn!(
                session_id,
                task_id = %task_id,
                error = %e,
                "Failed to cancel in-flight cli_agent work during pivot"
            );
            None
        }
    };

    let cancelled_goals = agent.cancel_active_goals_for_session(session_id).await;
    let cli_cancelled_any = cli_cancel_msg
        .as_deref()
        .is_some_and(|m| !m.contains("No running CLI agents"));

    if cli_cancelled_any || !cancelled_goals.is_empty() {
        info!(
            session_id,
            task_id = %task_id,
            cli_cancelled_any,
            cancelled_goals = cancelled_goals.len(),
            "Detected mid-task pivot; cancelled in-flight session work"
        );
    }
}

pub(super) async fn maybe_handle_pending_goal_confirmation(
    agent: &Agent,
    session_id: &str,
    user_text: &str,
    user_role: UserRole,
    task_id: &str,
    emitter: &crate::events::EventEmitter,
) -> anyhow::Result<Option<String>> {
    let early_task_start = Instant::now();
    let pending_goals = agent
        .state
        .get_pending_confirmation_goals(session_id)
        .await
        .unwrap_or_default();

    if pending_goals.is_empty() {
        return Ok(None);
    }

    if user_role == UserRole::Owner {
        let lower_trimmed = user_text.trim().to_lowercase();
        let is_confirm = ["confirm", "yes", "go ahead", "schedule it", "do it"]
            .iter()
            .any(|kw| contains_keyword_as_words(&lower_trimmed, kw));
        let is_reject = ["no", "cancel", "never mind", "nevermind"]
            .iter()
            .any(|kw| contains_keyword_as_words(&lower_trimmed, kw));

        if is_confirm {
            let mut activated = Vec::new();
            let mut activation_errors = Vec::new();
            let tz_label = crate::cron_utils::system_timezone_display();

            for goal in &pending_goals {
                match agent.state.activate_goal(&goal.id).await {
                    Ok(true) => {
                        if let Some(ref registry) = agent.goal_token_registry {
                            registry.register(&goal.id).await;
                        }
                        let schedules = agent
                            .state
                            .get_schedules_for_goal(&goal.id)
                            .await
                            .unwrap_or_default();
                        let next_run = schedules
                            .iter()
                            .filter_map(|s| {
                                chrono::DateTime::parse_from_rfc3339(&s.next_run_at).ok()
                            })
                            .min_by_key(|dt| dt.timestamp())
                            .map(|dt| {
                                dt.with_timezone(&chrono::Local)
                                    .format("%Y-%m-%d %H:%M %Z")
                                    .to_string()
                            })
                            .unwrap_or_else(|| "unscheduled".to_string());
                        activated.push(format!("{} (next: {})", goal.description, next_run));
                    }
                    Ok(false) => {}
                    Err(e) => activation_errors.push(e.to_string()),
                }
            }

            let msg = if !activated.is_empty() && activation_errors.is_empty() {
                if activated.len() == 1 {
                    format!(
                        "Scheduled: {}. I'll execute it when the time comes. System timezone: {}.",
                        activated[0], tz_label
                    )
                } else {
                    format!(
                        "Scheduled {} goals:\n- {}\nSystem timezone: {}.",
                        activated.len(),
                        activated.join("\n- "),
                        tz_label
                    )
                }
            } else if !activated.is_empty() {
                format!(
                    "Scheduled {} goals:\n- {}\nBut {} could not be activated: {}",
                    activated.len(),
                    activated.join("\n- "),
                    activation_errors.len(),
                    activation_errors.join("; ")
                )
            } else {
                format!(
                    "I couldn't activate scheduled goals: {}",
                    activation_errors.join("; ")
                )
            };

            let msg = emit_bootstrap_direct_reply(
                agent,
                emitter,
                task_id,
                session_id,
                early_task_start,
                &msg,
            )
            .await?;
            return Ok(Some(msg));
        }

        if is_reject {
            let mut cancelled = 0usize;
            for goal in &pending_goals {
                let mut updated = goal.clone();
                updated.status = "cancelled".to_string();
                updated.completed_at = Some(chrono::Utc::now().to_rfc3339());
                updated.updated_at = chrono::Utc::now().to_rfc3339();
                if agent.state.update_goal(&updated).await.is_ok() {
                    cancelled += 1;
                }
                // Best-effort cleanup: schedules were created before confirmation.
                // Cancelled goals should not retain schedules.
                if let Ok(schedules) = agent.state.get_schedules_for_goal(&updated.id).await {
                    for s in &schedules {
                        let _ = agent.state.delete_goal_schedule(&s.id).await;
                    }
                }
            }

            let msg = if cancelled == 1 {
                "OK, cancelled the scheduled goal.".to_string()
            } else {
                format!("OK, cancelled {} scheduled goals.", cancelled)
            };

            let msg = emit_bootstrap_direct_reply(
                agent,
                emitter,
                task_id,
                session_id,
                early_task_start,
                &msg,
            )
            .await?;
            return Ok(Some(msg));
        }

        // User moved on without explicit confirmation/rejection.
        // Auto-cancel pending confirmations to avoid stale intents.
        for goal in &pending_goals {
            let mut updated = goal.clone();
            updated.status = "cancelled".to_string();
            updated.completed_at = Some(chrono::Utc::now().to_rfc3339());
            updated.updated_at = chrono::Utc::now().to_rfc3339();
            let _ = agent.state.update_goal(&updated).await;
            // Best-effort cleanup: remove any schedules created pre-confirmation.
            if let Ok(schedules) = agent.state.get_schedules_for_goal(&updated.id).await {
                for s in &schedules {
                    let _ = agent.state.delete_goal_schedule(&s.id).await;
                }
            }
        }
        return Ok(None);
    }

    // Non-owner: if they typed confirm/reject keywords,
    // return owner-only message immediately (no LLM call).
    let lower_trimmed = user_text.trim().to_lowercase();
    let is_confirm_or_reject = [
        "confirm",
        "yes",
        "go ahead",
        "schedule it",
        "do it",
        "no",
        "cancel",
        "never mind",
        "nevermind",
    ]
    .iter()
    .any(|kw| contains_keyword_as_words(&lower_trimmed, kw));
    if is_confirm_or_reject {
        let msg = "Only the owner can confirm or cancel scheduled goals.";
        let msg =
            emit_bootstrap_direct_reply(agent, emitter, task_id, session_id, early_task_start, msg)
                .await?;
        return Ok(Some(msg));
    }

    // Otherwise: ignore pending goals, proceed normally.
    // Don't confirm, reject, or auto-cancel.
    Ok(None)
}

pub(super) async fn maybe_handle_non_resolving_confirmation_shortcut(
    agent: &Agent,
    session_id: &str,
    user_text: &str,
    task_id: &str,
    emitter: &crate::events::EventEmitter,
) -> anyhow::Result<Option<String>> {
    if agent.depth != 0 || !is_bare_confirmation(user_text) {
        return Ok(None);
    }

    let history = agent
        .state
        .get_history(session_id, 12)
        .await
        .unwrap_or_default();
    let prev_assistant = history
        .iter()
        .rev()
        .find(|msg| msg.role == "assistant")
        .and_then(|msg| msg.content.as_deref());
    let Some(prev_assistant) = prev_assistant else {
        return Ok(None);
    };

    if !assistant_question_requires_specific_answer(prev_assistant) {
        return Ok(None);
    }

    if prev_assistant
        .trim()
        .starts_with("I still need the specific")
    {
        return Ok(None);
    }

    let reply = build_specific_answer_request(prev_assistant);
    let reply =
        emit_bootstrap_direct_reply(agent, emitter, task_id, session_id, Instant::now(), &reply)
            .await?;
    Ok(Some(reply))
}

pub(super) async fn maybe_handle_trivial_ack_shortcut(
    agent: &Agent,
    session_id: &str,
    user_text: &str,
    task_id: &str,
    emitter: &crate::events::EventEmitter,
) -> anyhow::Result<Option<String>> {
    // Cheap local acknowledgment shortcut: avoid an LLM call for trivial turns like
    // "thanks" or a single emoji reaction. Keep this conservative to avoid eating
    // genuine requests.
    if agent.depth != 0 {
        return Ok(None);
    }

    let trimmed = user_text.trim();
    let normalized = trimmed
        .trim_matches(|c: char| c.is_ascii_punctuation() || c.is_whitespace())
        .to_ascii_lowercase();
    let is_thanks = matches!(normalized.as_str(), "thanks" | "thank you" | "thx");
    let is_ok = matches!(normalized.as_str(), "ok" | "okay");
    let is_single_emoji_reaction = {
        let char_count = trimmed.chars().count();
        char_count > 0
            && char_count <= 4
            && !trimmed.is_ascii()
            && trimmed
                .chars()
                .all(|c| !c.is_ascii_alphanumeric() && !c.is_ascii_whitespace())
    };

    // If the previous assistant turn ended with a question, treat "ok/okay" as non-terminal
    // and let the LLM re-ask for missing info rather than replying "Got it.".
    let ok_is_safe_to_short_circuit = if is_ok {
        let history = agent
            .state
            .get_history(session_id, 12)
            .await
            .unwrap_or_default();
        let last_assistant = history.iter().rev().find(|m| m.role == "assistant");
        !last_assistant
            .and_then(|m| m.content.as_deref())
            .is_some_and(|c| c.contains('?'))
    } else {
        true
    };

    let trivial_reply = if is_thanks {
        Some("You're welcome.".to_string())
    } else if is_single_emoji_reaction || (is_ok && ok_is_safe_to_short_circuit) {
        Some("Got it.".to_string())
    } else {
        None
    };

    if let Some(reply) = trivial_reply {
        let reply = emit_bootstrap_direct_reply(
            agent,
            emitter,
            task_id,
            session_id,
            Instant::now(),
            &reply,
        )
        .await?;
        return Ok(Some(reply));
    }

    Ok(None)
}

pub(super) async fn maybe_handle_time_query_shortcut(
    agent: &Agent,
    session_id: &str,
    user_text: &str,
    task_id: &str,
    emitter: &crate::events::EventEmitter,
) -> anyhow::Result<Option<String>> {
    // Cheap local time shortcut: avoid an LLM call for "what time is it?" style requests.
    // Keep this strict (exact-match after normalization) so we don't mis-handle timezone
    // or location-specific queries (e.g., "what time is it in Tokyo?").
    if agent.depth != 0 {
        return Ok(None);
    }

    let trimmed = user_text.trim();
    let normalized = trimmed
        .chars()
        .map(|c| {
            if c.is_ascii_alphanumeric() || c.is_whitespace() {
                c.to_ascii_lowercase()
            } else {
                ' '
            }
        })
        .collect::<String>();
    let normalized = normalized.split_whitespace().collect::<Vec<_>>().join(" ");
    let is_time_query = matches!(
        normalized.as_str(),
        "what time is it"
            | "what time is it now"
            | "what time is it right now"
            | "what is the time"
            | "what s the time"
            | "whats the time"
            | "current time"
            | "time now"
            | "time"
    );

    if !is_time_query {
        return Ok(None);
    }

    let now = chrono::Local::now();
    let reply = format!("It is {}.", now.format("%Y-%m-%d %H:%M:%S %Z (UTC%:z)"));
    let reply =
        emit_bootstrap_direct_reply(agent, emitter, task_id, session_id, Instant::now(), &reply)
            .await?;
    Ok(Some(reply))
}

pub(super) async fn emit_bootstrap_direct_reply(
    agent: &Agent,
    emitter: &crate::events::EventEmitter,
    task_id: &str,
    session_id: &str,
    task_start: Instant,
    reply: &str,
) -> anyhow::Result<String> {
    let reply_text = reply.to_string();
    let assistant_msg = Message {
        id: Uuid::new_v4().to_string(),
        session_id: session_id.to_string(),
        role: "assistant".to_string(),
        content: Some(reply_text.clone()),
        tool_call_id: None,
        tool_name: None,
        tool_calls_json: None,
        created_at: Utc::now(),
        importance: 0.5,
        ..Message::runtime_defaults()
    };
    agent
        .append_assistant_message_with_event(emitter, &assistant_msg, "system", None, None)
        .await?;

    if agent.harness_eval_enabled() {
        let turn_id = agent.current_turn_ids.read().await.get(session_id).cloned();
        let mut harness_eval = HarnessEvalAccumulator::new(HarnessEvalSeed {
            task_id: task_id.to_string(),
            turn_id,
            depth: agent.depth as u32,
            parent_task_id: agent.task_id.clone(),
            completion_task_kind: "conversational".to_string(),
            followup_mode: None,
            config: agent.harness_eval_config.clone(),
        });
        harness_eval.record_bootstrap("bootstrap_direct_return", vec![], None, false);
        agent.install_harness_eval(harness_eval).await;
    }

    agent
        .emit_direct_return_task_end(
            emitter,
            task_id,
            TaskStatus::Completed,
            TaskOutcome::Succeeded,
            task_start,
            0,
            0,
            None,
            Some(reply_text.chars().take(200).collect()),
            true,
        )
        .await;

    Ok(reply_text)
}

fn looks_like_mid_task_pivot(user_text: &str) -> bool {
    let lower = user_text.trim().to_ascii_lowercase();
    if lower.is_empty() {
        return false;
    }

    // Exact stop/cancel/abort is handled by maybe_handle_stop_command and should
    // return immediately to the user, not continue as a pivot.
    if matches!(lower.as_str(), "stop" | "cancel" | "abort") {
        return false;
    }

    let has_cancel_cue = [
        "stop",
        "cancel",
        "abort",
        "scratch that",
        "forget that",
        "never mind",
        "nevermind",
    ]
    .iter()
    .any(|kw| contains_keyword_as_words(&lower, kw));

    if !has_cancel_cue {
        return false;
    }

    let has_pivot_cue = [
        "actually",
        "instead",
        "rather",
        "new plan",
        "change of plan",
        "let's",
        "lets",
    ]
    .iter()
    .any(|kw| contains_keyword_as_words(&lower, kw));

    has_pivot_cue && lower.split_whitespace().count() >= 5
}

fn is_bare_confirmation(user_text: &str) -> bool {
    let normalized = user_text
        .trim()
        .trim_matches(|c: char| c.is_ascii_punctuation() || c.is_whitespace())
        .to_ascii_lowercase();
    matches!(
        normalized.as_str(),
        "yes"
            | "yes please"
            | "yep"
            | "yep please"
            | "yeah"
            | "yeah please"
            | "sure"
            | "sure please"
            | "ok"
            | "okay"
            | "go ahead"
            | "please do"
            | "do it"
            | "sounds good"
            | "confirm"
            | "confirmed"
            | "proceed"
    )
}

fn extract_last_question_line(message: &str) -> Option<&str> {
    message
        .lines()
        .rev()
        .map(str::trim)
        .find(|line| !line.is_empty() && line.contains('?'))
}

fn question_requires_specific_answer(question: &str) -> bool {
    let lower = question.trim().to_ascii_lowercase();
    if !lower.contains('?') {
        return false;
    }

    if lower.contains(" or ") {
        return true;
    }

    lower.starts_with("how ")
        || contains_keyword_as_words(&lower, "which")
        || contains_keyword_as_words(&lower, "what")
        || contains_keyword_as_words(&lower, "where")
        || contains_keyword_as_words(&lower, "when")
        || contains_keyword_as_words(&lower, "who")
        || lower.contains("any specific")
        || lower.contains("can you clarify")
        || lower.contains("could you clarify")
}

fn assistant_question_requires_specific_answer(message: &str) -> bool {
    extract_last_question_line(message).is_some_and(question_requires_specific_answer)
}

fn build_specific_answer_request(prev_assistant: &str) -> String {
    if let Some(question) = extract_last_question_line(prev_assistant) {
        let question = question.trim();
        if question.chars().count() <= 160 {
            return format!(
                "I still need the specific answer to my last question before I can continue. Please answer directly: {}",
                question
            );
        }
    }

    "I still need the specific option or missing detail from my last question before I can continue. Please answer with the exact choice or value you want, not just a confirmation.".to_string()
}

#[cfg(test)]
mod tests {
    use super::{
        assistant_question_requires_specific_answer, build_specific_answer_request,
        is_bare_confirmation, looks_like_mid_task_pivot,
    };

    #[test]
    fn test_looks_like_mid_task_pivot_detects_explicit_pivot() {
        assert!(looks_like_mid_task_pivot(
            "Wait stop. Actually scratch React and do plain HTML/CSS/JS instead."
        ));
        assert!(looks_like_mid_task_pivot(
            "Cancel that and instead generate a static page."
        ));
    }

    #[test]
    fn test_looks_like_mid_task_pivot_ignores_plain_stop() {
        assert!(!looks_like_mid_task_pivot("stop"));
        assert!(!looks_like_mid_task_pivot("cancel"));
        assert!(!looks_like_mid_task_pivot("abort"));
    }

    #[test]
    fn test_looks_like_mid_task_pivot_requires_both_cancel_and_pivot_cues() {
        assert!(!looks_like_mid_task_pivot(
            "Actually create a static page instead."
        ));
        assert!(!looks_like_mid_task_pivot("Never mind."));
    }

    #[test]
    fn test_bare_confirmation_detects_short_affirmations() {
        assert!(is_bare_confirmation("Yes"));
        assert!(is_bare_confirmation("go ahead"));
        assert!(!is_bare_confirmation("yes, post it"));
    }

    #[test]
    fn test_specific_answer_required_for_branching_question() {
        assert!(assistant_question_requires_specific_answer(
            "Want me to tweak this or post it?"
        ));
        assert!(assistant_question_requires_specific_answer(
            "What time should I schedule it?"
        ));
        assert!(!assistant_question_requires_specific_answer(
            "Should I post it now?"
        ));
    }

    #[test]
    fn test_specific_answer_request_reuses_last_question_line() {
        let reply = build_specific_answer_request(
            "Persistent context. Not just chat.\n\nWant me to tweak this or post it?",
        );
        assert!(reply.contains("Please answer directly: Want me to tweak this or post it?"));
    }

    #[test]
    fn test_confirmation_shortcut_pass_through() {
        // First nudge
        let reply1 = build_specific_answer_request("Want me to tweak this or post it?");
        assert!(reply1.starts_with("I still need the specific"));

        // Second nudge (fallback)
        let reply2 = build_specific_answer_request(
            "Here is a very long question that exceeds the limit so we use the fallback..."
                .repeat(3)
                .as_str(),
        );
        assert!(reply2.starts_with("I still need the specific"));
    }
}
