use regex::Regex;

use super::{IntentGateDecision, ENABLE_SCHEDULE_HEURISTICS};

/// Complexity classification for orchestration routing.
#[derive(Debug, Clone, PartialEq)]
pub(super) enum IntentComplexity {
    /// Answer from memory/knowledge, no executor needed.
    Knowledge,
    /// Simple task — falls through to full agent loop.
    Simple,
    /// Multi-step complex task, create a goal and fall through to current agent loop.
    Complex,
    /// User asks for recurring/ongoing behavior but did not provide timing.
    ScheduledMissingTiming,
    /// Scheduled task intent requiring deferred/recurring goal creation.
    Scheduled {
        schedule_raw: String,
        schedule_cron: Option<String>,
        is_one_shot: bool,
        schedule_type_explicit: bool,
    },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ConnectedApiIntent {
    RuntimeCapabilityValidation,
    ReadAction,
    WriteAction,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(super) enum ConnectedContentMode {
    #[default]
    None,
    DraftOnly,
    DeliverOnly,
    DraftThenDeliver,
}

impl ConnectedContentMode {
    pub(super) fn is_authoring_only(self) -> bool {
        matches!(self, Self::DraftOnly)
    }

    pub(super) fn expects_live_delivery(self) -> bool {
        matches!(self, Self::DeliverOnly | Self::DraftThenDeliver)
    }
}

/// Check if a phrase appears as complete words in text (word-boundary matching).
/// Splits on whitespace, trims surrounding punctuation (preserving apostrophes),
/// then checks for consecutive word matches. Case-insensitive.
///
/// Works for single keywords ("deploy"), multi-word phrases ("set up"),
/// and contractions ("i'll check").
pub(crate) fn contains_keyword_as_words(text: &str, keyword: &str) -> bool {
    let normalize = |w: &str| -> String {
        w.trim_matches(|c: char| c.is_ascii_punctuation() && c != '\'')
            .to_lowercase()
    };
    let text_words: Vec<String> = text
        .split_whitespace()
        .map(normalize)
        .filter(|w| !w.is_empty())
        .collect();
    let kw_words: Vec<String> = keyword
        .split_whitespace()
        .map(normalize)
        .filter(|w| !w.is_empty())
        .collect();
    if kw_words.is_empty() {
        return false;
    }
    text_words
        .windows(kw_words.len())
        .any(|window| window == kw_words.as_slice())
}

/// Detect "about this scheduled goal" meta-queries so they aren't misread as
/// fresh scheduling requests when quoted text contains timing words.
fn is_schedule_reference_query(user_text: &str) -> bool {
    let lower = user_text.trim().to_ascii_lowercase();
    if lower.is_empty() {
        return false;
    }

    let mentions_schedule_subject = contains_keyword_as_words(&lower, "scheduled goal")
        || contains_keyword_as_words(&lower, "recurring goal")
        || contains_keyword_as_words(&lower, "schedule");

    if !mentions_schedule_subject {
        return false;
    }

    contains_keyword_as_words(&lower, "details about")
        || contains_keyword_as_words(&lower, "tell me about")
        || contains_keyword_as_words(&lower, "show me")
        || contains_keyword_as_words(&lower, "list")
        || contains_keyword_as_words(&lower, "what is")
        || contains_keyword_as_words(&lower, "what's")
        || contains_keyword_as_words(&lower, "explain")
        || contains_keyword_as_words(&lower, "describe")
        || lower.contains("scheduled goal:")
}

/// Detect obvious scheduling phrases in user text as a fallback when the model
/// omits schedule fields in [INTENT_GATE].
///
/// Returns (schedule_raw, is_one_shot).
///
/// This heuristic is intentionally conservative: it only overrides the LLM's
/// classification when there is strong evidence of scheduling intent.  Bare
/// month-day dates (e.g. "March 3rd") are ambiguous — they could be recall,
/// facts, or past references — so we require a scheduling verb alongside them.
/// More specific patterns like "in 2h" or "tomorrow at 3pm" are inherently
/// forward-looking and don't need the extra verb check.
pub(super) fn detect_schedule_heuristic(user_text: &str) -> Option<(String, bool)> {
    let text = user_text.trim();
    if text.is_empty() {
        return None;
    }
    // Synthetic follow-up messages from background command completions contain
    // command output that often includes dates (e.g., `find -mmin` timestamps).
    // These are never scheduling requests.
    if text.starts_with("[Background command completed]") {
        return None;
    }
    if is_schedule_reference_query(text) {
        return None;
    }
    if is_memory_storage_intent(text) {
        return None;
    }
    if is_file_operation_with_dates(text) {
        return None;
    }

    let result = crate::cron_utils::extract_schedule_from_text(text)?;

    // Bare month-day patterns ("March 3rd", "on October 15th") are ambiguous —
    // they appear in recall queries, fact storage, past-tense references, etc.
    // Only treat them as scheduling when the text also contains an explicit
    // scheduling verb (remind, schedule, alert, notify, etc.).  This trusts the
    // LLM's classification for bare-date messages and only overrides when there
    // is unambiguous scheduling intent.
    if crate::cron_utils::is_bare_month_day_pattern(&result.0) && !has_scheduling_action_verb(text)
    {
        return None;
    }

    Some(result)
}

/// Detect memory/fact-storage intent so that date mentions like "my birthday is
/// October 15" are not hijacked by the schedule extractor.
///
/// Returns true when the message is clearly asking the agent to remember or
/// store information (which may incidentally contain a date).
fn is_memory_storage_intent(user_text: &str) -> bool {
    let lower = user_text.trim().to_ascii_lowercase();
    if lower.is_empty() {
        return false;
    }

    // Memory-storage verb phrases — user wants to store a fact, not schedule.
    // Uses the strict (multi-word) precision level: this classifier gates the
    // schedule heuristic, so a false positive would silently swallow a real
    // reminder request.
    let has_memory_verb = crate::agent::intent_keywords::MEMORY_STORE_STRICT_PHRASES
        .iter()
        .any(|kw| contains_keyword_as_words(&lower, kw));

    // Fact-storage context phrases catch messages without a leading verb
    // (e.g., "here are some facts about me").
    let has_fact_context = crate::agent::intent_keywords::MEMORY_STORE_FACT_CONTEXTS
        .iter()
        .any(|kw| lower.contains(kw));

    if !has_memory_verb && !has_fact_context {
        return false;
    }

    // If the message also contains explicit scheduling verbs, let the
    // scheduler win — the date is meant as a trigger, not a fact.
    let has_scheduling_verb = crate::agent::intent_keywords::SCHEDULING_VERB_PHRASES
        .iter()
        .any(|kw| contains_keyword_as_words(&lower, kw));

    !has_scheduling_verb
}

/// Check whether the user text contains an explicit scheduling action verb.
///
/// Used to gate bare month-day pattern matches (e.g. "March 3rd") — these are
/// too ambiguous on their own (could be recall, facts, past references) and
/// should only trigger scheduling when paired with a clear scheduling verb.
fn has_scheduling_action_verb(user_text: &str) -> bool {
    let lower = user_text.trim().to_ascii_lowercase();
    if lower.is_empty() {
        return false;
    }
    let verbs = [
        "remind",
        "reminder",
        "remind me",
        "schedule",
        "set a reminder",
        "alert me",
        "notify me",
        "ping me",
        "follow up",
    ];
    verbs.iter().any(|kw| contains_keyword_as_words(&lower, kw))
}

/// Detect file-editing/coding intent so that date mentions in messages like
/// "finish the plan covering March 18 through March 31" or "append to file.md
/// starting from where it was truncated" are not hijacked by the schedule extractor.
///
/// Returns true when the message contains both file-operation verbs AND file path
/// references, indicating the dates are part of file content, not a scheduling request.
fn is_file_operation_with_dates(user_text: &str) -> bool {
    let lower = user_text.trim().to_ascii_lowercase();
    if lower.is_empty() {
        return false;
    }

    // File-operation verb phrases — user wants to create/edit/read/fix files.
    let file_verbs = [
        "read the",
        "read my",
        "read file",
        "read_file",
        "write_file",
        "write file",
        "write to",
        "edit_file",
        "edit file",
        "edit the",
        "edit my",
        "append",
        "finish it",
        "finish the",
        "complete the",
        "fix the file",
        "fix my file",
        "fix the script",
        "fix my script",
        "update the file",
        "update my file",
        "create a file",
        "create a script",
        "got cut off",
        "was truncated",
        "got truncated",
    ];
    let has_file_verb = file_verbs.iter().any(|kw| lower.contains(kw));

    if !has_file_verb {
        return false;
    }

    // Must also contain a file-path-like reference to confirm the dates are
    // about file content, not scheduling. Matches:
    //   ~/projects/..., ./foo.md, /path/to/file, file.md, file.json, etc.
    static FILE_PATH_RE: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
        Regex::new(r"(?:~/|[.~/]\S*\.\w{1,10}|\S+\.\w{2,5}(?:\s|$))").expect("file path regex")
    });
    let has_file_ref = FILE_PATH_RE.is_match(&lower);

    if !has_file_ref {
        return false;
    }

    // If the message also contains explicit scheduling verbs, let the scheduler win.
    let scheduling_verbs = [
        "schedule",
        "remind me",
        "set a reminder",
        "alert me",
        "notify me",
    ];
    let has_scheduling_verb = scheduling_verbs
        .iter()
        .any(|kw| contains_keyword_as_words(&lower, kw));

    !has_scheduling_verb
}

/// Detect recurring-intent language when the user did not provide concrete timing.
/// Used to prevent accidental fallback into non-recurring "complex" goals.
pub(super) fn looks_like_recurring_intent_without_timing(user_text: &str) -> bool {
    if detect_schedule_heuristic(user_text).is_some() {
        return false;
    }

    let lower = user_text.trim().to_ascii_lowercase();
    if lower.is_empty() {
        return false;
    }

    let re_times_per = match Regex::new(r"(?i)\b\d+\s+times?\s+per\s+(day|week|month)\b") {
        Ok(re) => re,
        Err(_) => return false,
    };
    if re_times_per.is_match(user_text) {
        return true;
    }

    for kw in [
        "monitor",
        "recurring",
        "ongoing",
        "long-term",
        "long term",
        "regularly",
        "consistently",
        "every day",
        "each day",
        "per day",
        "per week",
        "per month",
    ] {
        if contains_keyword_as_words(&lower, kw) {
            return true;
        }
    }

    false
}

/// Detect legacy internal-maintenance intents that should run via native
/// heartbeat/memory jobs rather than goal orchestration.
pub(super) fn is_internal_maintenance_intent(user_text: &str) -> bool {
    let lower = user_text.trim().to_ascii_lowercase();
    if lower.is_empty() {
        return false;
    }

    if lower == "maintain knowledge base: process embeddings, consolidate memories, decay old facts"
        || lower
            == "maintain memory health: prune old events, clean up retention, remove stale data"
    {
        return true;
    }

    let knowledge_maintenance = contains_keyword_as_words(&lower, "process embeddings")
        && contains_keyword_as_words(&lower, "consolidate memories")
        && (contains_keyword_as_words(&lower, "decay old facts")
            || contains_keyword_as_words(&lower, "memory decay"));

    let memory_health = contains_keyword_as_words(&lower, "prune old events")
        && (contains_keyword_as_words(&lower, "clean up retention")
            || contains_keyword_as_words(&lower, "retention cleanup"))
        && contains_keyword_as_words(&lower, "stale data");

    knowledge_maintenance || memory_health
}

pub(super) fn infer_intent_gate(user_text: &str, _analysis: &str) -> IntentGateDecision {
    let user_text = crate::channels::attachments::user_authored_text(user_text);
    // Deterministic tool-need overrides:
    // 1) explicit filesystem paths
    // 2) explicit local-environment execution/inspection requests
    // 3) connected external API/integration work:
    //    - runtime capability validation
    //    - connected API reads
    //    - connected API writes
    //
    // These requests cannot be satisfied by a text-only first-pass response and
    // must run through the tool loop, regardless of model intent-gate output.
    if super::user_text_references_filesystem_path(&user_text)
        || user_text_requires_local_tool_execution(&user_text)
        || user_text_requests_auth_or_integration_management(&user_text)
        || classify_connected_api_intent(&user_text).is_some()
    {
        return IntentGateDecision {
            can_answer_now: Some(false),
            needs_tools: Some(true),
            needs_clarification: Some(false),
            clarifying_question: None,
            missing_info: Vec::new(),
            complexity: None,
            cancel_intent: None,
            cancel_scope: None,
            is_acknowledgment: None,
            schedule: None,
            schedule_type: None,
            schedule_cron: None,
            domains: Vec::new(),
        };
    }

    // No lexical guessing fallback: rely on explicit model intent-gate fields.
    // Missing fields simply stay None.
    IntentGateDecision {
        can_answer_now: None,
        needs_tools: None,
        needs_clarification: None,
        clarifying_question: None,
        missing_info: Vec::new(),
        complexity: None,
        cancel_intent: None,
        cancel_scope: None,
        is_acknowledgment: None,
        schedule: None,
        schedule_type: None,
        schedule_cron: None,
        domains: Vec::new(),
    }
}

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

    let explicit_run_request = [
        "run the command",
        "run this command",
        "execute the command",
        "execute this command",
        "run command",
        "execute command",
        "run in terminal",
        "execute in terminal",
    ]
    .iter()
    .any(|kw| contains_keyword_as_words(&lower, kw));

    let local_scope_request = [
        "on this machine",
        "on this system",
        "on this host",
        "on this computer",
        "on my machine",
        "on my system",
        "on my host",
        "on my computer",
        "in this environment",
        "in my environment",
        "locally",
        "installed here",
    ]
    .iter()
    .any(|kw| contains_keyword_as_words(&lower, kw));

    let installed_wording = contains_keyword_as_words(&lower, "installed")
        || contains_keyword_as_words(&lower, "is installed");
    let installed_version_query = (contains_keyword_as_words(&lower, "what version of")
        || contains_keyword_as_words(&lower, "which version of"))
        && installed_wording
        && local_scope_request;

    let quoted_command_execution = lower.contains('`')
        && (contains_keyword_as_words(&lower, "run")
            || contains_keyword_as_words(&lower, "execute"));

    explicit_run_request
        || local_scope_request
        || installed_version_query
        || quoted_command_execution
}

fn contains_any_as_words(text: &str, keywords: &[&str]) -> bool {
    keywords
        .iter()
        .any(|kw| contains_keyword_as_words(text, kw))
}

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

    // Narrow deterministic trigger: only fire when the user explicitly asks
    // for a live check/verification of current capability or connection state.
    for phrase in [
        "check if you can",
        "check whether you can",
        "see if you can",
        "see whether you can",
        "verify you can",
        "verify if you can",
        "verify whether you can",
        "test if you can",
        "test whether you can",
        "confirm you can",
        "confirm whether you can",
        "make sure you can",
        "check if you have access",
        "check whether you have access",
        "verify you have access",
        "check if you're connected",
        "check whether you're connected",
        "verify you're connected",
        "see if you're connected",
        "see whether you're connected",
        "check if it is connected",
        "check whether it is connected",
        "check if this is connected",
        "check whether this is connected",
        "are you connected to",
        "are you hooked up to",
    ] {
        if contains_keyword_as_words(&lower, phrase) {
            return true;
        }
    }

    let has_validation_verb = ["check", "verify", "test", "confirm", "validate", "see"]
        .iter()
        .any(|kw| contains_keyword_as_words(&lower, kw));
    let asks_about_current_access_or_connection = [
        "do you have access",
        "you have access",
        "are you connected",
        "you're connected",
        "it is connected",
        "this is connected",
        "whether you can",
    ]
    .iter()
    .any(|kw| contains_keyword_as_words(&lower, kw));

    has_validation_verb && asks_about_current_access_or_connection
}

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

    const TARGET_MARKERS: &[&str] = &[
        "api",
        "apis",
        "oauth",
        "webhook",
        "connected account",
        "connected service",
        "service account",
        "integration",
        "github",
        "gitlab",
        "jira",
        "linear",
        "notion",
        "slack",
        "discord",
        "twitter",
        "bluesky",
        "mastodon",
        "linkedin",
        "reddit",
        "gmail",
        "email",
        "calendar",
        "google calendar",
        "google drive",
        "google docs",
        "google sheets",
        "drive",
        "stripe",
        "airtable",
        "salesforce",
        "hubspot",
        "shopify",
        "zendesk",
        "intercom",
        "confluence",
        "trello",
        "asana",
        "clickup",
        "dropbox",
        "youtube",
        "figma",
    ];

    contains_any_as_words(&lower, TARGET_MARKERS)
}

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

    const RESOURCE_MARKERS: &[&str] = &[
        "issue",
        "issues",
        "pull request",
        "pull requests",
        "pr",
        "prs",
        "ticket",
        "tickets",
        "task",
        "tasks",
        "comment",
        "comments",
        "reply",
        "replies",
        "message",
        "messages",
        "email",
        "emails",
        "draft",
        "drafts",
        "inbox",
        "calendar event",
        "calendar events",
        "event",
        "events",
        "page",
        "pages",
        "document",
        "documents",
        "doc",
        "docs",
        "sheet",
        "sheets",
        "record",
        "records",
        "row",
        "rows",
        "database",
        "file",
        "files",
        "folder",
        "folders",
        "repository",
        "repositories",
        "repo",
        "repos",
        "post",
        "posts",
        "tweet",
        "tweets",
        "thread",
        "threads",
        "invoice",
        "invoices",
        "customer",
        "customers",
        "contact",
        "contacts",
        "lead",
        "leads",
    ];

    contains_any_as_words(&lower, RESOURCE_MARKERS)
}

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

    contains_any_as_words(
        &lower,
        &[
            "my account",
            "our account",
            "your account",
            "their account",
            "his account",
            "her account",
            "for me",
            "for us",
            "for you",
            "for them",
            "for him",
            "for her",
            "from my",
            "from our",
            "from your",
            "from their",
            "from his",
            "from her",
            "to my",
            "to our",
            "to your",
            "to their",
            "to his",
            "to her",
            "in my",
            "in our",
            "in your",
            "in their",
            "in his",
            "in her",
            "on my",
            "on our",
            "on your",
            "on their",
            "on his",
            "on her",
            "with my",
            "with our",
            "with your",
            "with their",
            "with his",
            "with her",
            "using my",
            "using our",
            "using your",
            "using their",
            "using his",
            "using her",
        ],
    )
}

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

    contains_any_as_words(
        &lower,
        &[
            "this update",
            "this post",
            "this tweet",
            "this thread",
            "this email",
            "this message",
            "this reply",
            "this comment",
            "the following",
            "below",
            "above",
            "already drafted",
            "existing draft",
            "prepared draft",
        ],
    )
}

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

    contains_any_as_words(
        &lower,
        &[
            "post",
            "posts",
            "tweet",
            "tweets",
            "thread",
            "threads",
            "email",
            "emails",
            "message",
            "messages",
            "reply",
            "replies",
            "comment",
            "comments",
            "caption",
            "captions",
            "announcement",
            "announcements",
        ],
    )
}

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

    contains_any_as_words(
        &lower,
        &[
            "post",
            "publish",
            "send",
            "reply",
            "comment",
            "share",
            "upload",
            "submit",
            "retweet",
            "schedule",
            "tweet this",
            "email this",
            "message this",
            "dm this",
        ],
    )
}

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

    contains_any_as_words(
        &lower,
        &[
            "reply with",
            "reply only with",
            "respond with",
            "respond only with",
            "answer with",
            "answer only with",
        ],
    )
}

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

    mentions_connected_api_account_scope(&lower)
        || contains_any_as_words(
            &lower,
            &[
                "right now",
                "right away",
                "immediately",
                "send it",
                "post it",
                "publish it",
                "submit it",
                "upload it",
                "schedule it",
                "using the connected account",
                "using the connected service",
                "using my connected account",
                "using our connected account",
                "using your connected account",
                "using their connected account",
                "using his connected account",
                "using her connected account",
            ],
        )
        || contains_any_as_words(&lower, &["connected account", "connected service"])
}

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

    contains_any_as_words(
        &lower,
        &[
            "write",
            "draft",
            "compose",
            "brainstorm",
            "generate",
            "polish",
            "rewrite",
            "revise",
            "make",
            "create",
            "come up with",
            "help me write",
            "help me draft",
            "give me",
        ],
    )
}

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

    contains_any_as_words(
        &lower,
        &[
            "a post",
            "a tweet",
            "a thread",
            "an email",
            "a message",
            "a reply",
            "a comment",
            "a caption",
            "an announcement",
            "about",
        ],
    )
}

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

    contains_any_as_words(
        &lower,
        &[
            "engaging",
            "catchy",
            "compelling",
            "punchy",
            "polished",
            "professional",
            "friendly",
            "funny",
            "thoughtful",
            "make it",
            "so people",
            "want to comment",
            "call to action",
            "cta",
            "hook",
            "tone",
            "voice",
            "caption",
            "copy",
        ],
    )
}

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

    contains_any_as_words(
        &lower,
        &[
            "do not post",
            "don't post",
            "dont post",
            "do not send",
            "don't send",
            "dont send",
            "do not publish",
            "don't publish",
            "dont publish",
            "just draft",
            "draft only",
            "do not share",
            "don't share",
            "dont share",
        ],
    )
}

pub(super) fn classify_connected_content_mode(user_text: &str) -> ConnectedContentMode {
    let lower = user_text.trim().to_ascii_lowercase();
    if lower.is_empty() || !has_content_delivery_resource(&lower) {
        return ConnectedContentMode::None;
    }

    let has_connected_target = mentions_connected_api_target(&lower);
    let has_existing_payload = mentions_existing_connected_content_payload(&lower);
    let has_live_delivery_cue = has_content_live_delivery_cue(&lower);
    let has_delivery_verb = has_content_delivery_verb(&lower);
    let has_drafting_verb = has_content_drafting_verb(&lower);
    let has_new_payload_shape = has_content_new_payload_shape(&lower);
    let has_copywriting_cue = has_content_copywriting_cue(&lower);
    let needs_new_copy = has_drafting_verb || has_new_payload_shape || has_copywriting_cue;

    if !has_connected_target
        && !has_existing_payload
        && !has_live_delivery_cue
        && looks_like_direct_reply_format_instruction(&lower)
    {
        return ConnectedContentMode::None;
    }

    if has_explicit_do_not_deliver_cue(&lower) {
        return ConnectedContentMode::DraftOnly;
    }

    if has_existing_payload {
        return ConnectedContentMode::DeliverOnly;
    }

    if has_live_delivery_cue || has_delivery_verb {
        if needs_new_copy {
            return ConnectedContentMode::DraftThenDeliver;
        }
        return ConnectedContentMode::DeliverOnly;
    }

    if needs_new_copy {
        return ConnectedContentMode::DraftOnly;
    }

    ConnectedContentMode::None
}

#[cfg(test)]
pub(crate) fn content_authoring_request_is_text_only(user_text: &str) -> bool {
    classify_connected_content_mode(user_text).is_authoring_only()
}

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

    let has_auth_surface = mentions_connected_api_target(&lower)
        || contains_any_as_words(
            &lower,
            &[
                "oauth",
                "api key",
                "access token",
                "refresh token",
                "bearer token",
                "credentials",
                "credential",
                "auth profile",
                "webhook",
                "integration",
                "connected account",
                "connected service",
                "service account",
            ],
        );
    if !has_auth_surface {
        return false;
    }

    contains_any_as_words(
        &lower,
        &[
            "connect",
            "reconnect",
            "disconnect",
            "authorize",
            "reauthorize",
            "authenticate",
            "log in",
            "login",
            "sign in",
            "sign into",
            "grant access",
            "revoke access",
            "configure",
            "set up",
            "setup",
            "register",
            "repair connection",
            "refresh token",
            "verify connection",
            "manage oauth",
            "manage http auth",
            "manage api",
        ],
    )
}

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

    let content_mode = classify_connected_content_mode(&lower);
    let has_explicit_connected_target = mentions_connected_api_target(&lower);
    let has_live_content_delivery_intent = content_mode.expects_live_delivery();
    if !has_explicit_connected_target && !has_live_content_delivery_intent {
        return false;
    }

    if content_mode.is_authoring_only() {
        return false;
    }

    let has_strong_write_verb = contains_any_as_words(
        &lower,
        &[
            "post", "publish", "send", "reply", "comment", "share", "upload", "submit", "tweet",
            "retweet", "message", "dm",
        ],
    );
    // "open" is excluded — it conflicts with "open issues" (adjective meaning
    // "not-closed") and "open GitHub" (launch).  It rarely signals a write action.
    let has_scoped_write_verb = contains_any_as_words(
        &lower,
        &[
            "create", "update", "edit", "modify", "delete", "remove", "close", "reopen", "merge",
            "archive", "schedule", "cancel",
        ],
    ) && mentions_connected_api_resource(&lower);

    if has_content_delivery_resource(&lower) {
        return has_strong_write_verb && has_live_content_delivery_intent;
    }

    has_strong_write_verb || has_scoped_write_verb
}

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

    if !mentions_connected_api_target(&lower) {
        return false;
    }

    // "read" is excluded — it conflicts with "read the docs" (local reading)
    // and is rarely used for API fetching ("list my issues", not "read my issues").
    let has_read_verb = contains_any_as_words(
        &lower,
        &[
            "check", "list", "show", "fetch", "get", "view", "inspect", "look up", "lookup",
            "find", "search", "pull", "sync",
        ],
    );

    has_read_verb
        && (mentions_connected_api_account_scope(&lower) || mentions_connected_api_resource(&lower))
}

pub(super) fn classify_connected_api_intent(user_text: &str) -> Option<ConnectedApiIntent> {
    if user_text_requests_runtime_capability_validation(user_text) {
        Some(ConnectedApiIntent::RuntimeCapabilityValidation)
    } else if user_text_requests_connected_api_write_action(user_text) {
        Some(ConnectedApiIntent::WriteAction)
    } else if user_text_requests_connected_api_read_action(user_text) {
        Some(ConnectedApiIntent::ReadAction)
    } else {
        None
    }
}

/// Classify user intent complexity for orchestration routing.
///
/// Uses the LLM-provided `complexity` field from the `[INTENT_GATE]` JSON.
/// Falls back to `Simple` when the field is absent or unrecognized.
///
/// Guardrails override the LLM's "complex" classification for messages that
/// are clearly simple — the first-pass classifier over-classifies short
/// commands, acknowledgments, and single-action requests as complex.
pub(super) fn classify_intent_complexity(
    user_text: &str,
    intent_gate: &IntentGateDecision,
) -> (IntentComplexity, Vec<String>) {
    // Heuristic schedule extraction: if the model omitted schedule fields but the
    // user message contains a concrete schedule phrase, treat it as scheduled
    // rather than falling back into the tool loop (which can spiral).
    if ENABLE_SCHEDULE_HEURISTICS
        && intent_gate.schedule.is_none()
        && intent_gate.schedule_cron.is_none()
    {
        if let Some((schedule_raw, is_one_shot)) = detect_schedule_heuristic(user_text) {
            return (
                IntentComplexity::Scheduled {
                    schedule_raw,
                    schedule_cron: None,
                    is_one_shot,
                    schedule_type_explicit: false,
                },
                vec![],
            );
        }
    }

    // If user clearly wants recurring behavior but no timing could be extracted,
    // ask for schedule details instead of silently creating a non-recurring goal.
    if ENABLE_SCHEDULE_HEURISTICS
        && intent_gate.schedule.is_none()
        && intent_gate.schedule_cron.is_none()
        && looks_like_recurring_intent_without_timing(user_text)
    {
        return (IntentComplexity::ScheduledMissingTiming, vec![]);
    }

    // Schedule takes priority over all other classifications.
    if let Some(ref schedule_raw) = intent_gate.schedule {
        let schedule_type_explicit = intent_gate.schedule_type.is_some();
        let is_one_shot = intent_gate.schedule_type.as_deref() == Some("one_shot");
        return (
            IntentComplexity::Scheduled {
                schedule_raw: schedule_raw.clone(),
                schedule_cron: intent_gate.schedule_cron.clone(),
                is_one_shot,
                schedule_type_explicit,
            },
            vec![],
        );
    }
    if let Some(ref schedule_cron) = intent_gate.schedule_cron {
        let schedule_type_explicit = intent_gate.schedule_type.is_some();
        let is_one_shot = intent_gate.schedule_type.as_deref() == Some("one_shot");
        return (
            IntentComplexity::Scheduled {
                schedule_raw: schedule_cron.clone(),
                schedule_cron: Some(schedule_cron.clone()),
                is_one_shot,
                schedule_type_explicit,
            },
            vec![],
        );
    }

    if intent_gate.can_answer_now.unwrap_or(false) && !intent_gate.needs_tools.unwrap_or(false) {
        return (IntentComplexity::Knowledge, vec![]);
    }

    // Consultant-empty fallback: when the model omits `complexity`, promote
    // obviously cross-project / multi-question, multi-step requests to Complex.
    if intent_gate.complexity.is_none() && looks_like_complex_request_fallback(user_text) {
        return (IntentComplexity::Complex, vec![]);
    }

    // When can_answer_now=false, don't classify as Knowledge even if
    // complexity="knowledge" — the model can't answer, so we should
    // try tools (memory search, manage_people, etc.) as Simple.
    match intent_gate.complexity.as_deref() {
        Some("knowledge") => (IntentComplexity::Simple, vec![]),
        Some("read_only_investigation") => (IntentComplexity::Simple, vec![]),
        Some("scoped_modification") => (IntentComplexity::Simple, vec![]),
        Some("unscoped_modification") => (IntentComplexity::Complex, vec![]),
        Some("deployment_or_external_write") => (IntentComplexity::Complex, vec![]),
        Some("scheduled_action") => (IntentComplexity::Complex, vec![]),
        Some("complex") => (IntentComplexity::Complex, vec![]),
        _ => (IntentComplexity::Simple, vec![]),
    }
}

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

    let cross_project_scope = contains_keyword_as_words(&lower, "all my projects")
        || contains_keyword_as_words(&lower, "across all projects")
        || contains_keyword_as_words(&lower, "across my projects")
        || (contains_keyword_as_words(&lower, "all projects")
            && contains_keyword_as_words(&lower, "compare"));

    let action_markers = [
        "compare",
        "analyze",
        "analyse",
        "identify",
        "find",
        "count",
        "calculate",
        "summarize",
        "report",
        "list",
    ];
    let action_hits = action_markers
        .iter()
        .filter(|marker| contains_keyword_as_words(&lower, marker))
        .count();
    let multi_question = user_text.matches('?').count() >= 2;
    let compound_request =
        lower.contains(" and ") || lower.contains(" then ") || lower.contains(" also ");

    cross_project_scope
        || (action_hits >= 3 && (multi_question || compound_request))
        || (action_hits >= 2 && user_text.chars().count() >= 140)
}

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

    #[test]
    fn infer_intent_gate_does_not_force_tools_for_urls() {
        let d = infer_intent_gate("https://example.com/foo/bar", "");
        assert!(
            d.needs_tools.is_none(),
            "expected needs_tools=None for URL, got {:?}",
            d.needs_tools
        );
    }

    #[test]
    fn infer_intent_gate_does_not_force_tools_for_common_slash_shorthand() {
        for text in ["3/4", "2/14", "yes/no", "w/o"] {
            let d = infer_intent_gate(text, "");
            assert!(
                d.needs_tools.is_none(),
                "expected needs_tools=None for '{}', got {:?}",
                text,
                d.needs_tools
            );
        }
    }

    #[test]
    fn infer_intent_gate_forces_tools_for_unix_paths() {
        let d = infer_intent_gate("/Users/alice/project/file.txt", "");
        assert_eq!(d.needs_tools, Some(true));
    }

    #[test]
    fn infer_intent_gate_ignores_paths_in_channel_file_stub() {
        use crate::channels::attachments::format_file_stub;

        let stub = format_file_stub(
            "photo.jpg",
            221 * 1024,
            "image/jpeg",
            std::path::Path::new("/Users/alice/.aidaemon/files/inbox/photo.jpg"),
        );
        let d = infer_intent_gate(&stub, "");
        assert_ne!(
            d.needs_tools,
            Some(true),
            "channel file stub paths must not force tools, got {:?}",
            d.needs_tools
        );
    }

    #[test]
    fn infer_intent_gate_forces_tools_for_user_path_with_attachment_stub() {
        use std::path::PathBuf;

        use crate::channels::attachments::{build_inbound_text, message_attachment};

        let attachments = vec![message_attachment(
            PathBuf::from("/tmp/inbox/doc.pdf"),
            "doc.pdf".to_string(),
            "application/pdf".to_string(),
            100,
        )];
        let inbound = build_inbound_text("Please read ~/project/Cargo.toml", &attachments);
        let d = infer_intent_gate(&inbound, "");
        assert_eq!(d.needs_tools, Some(true));
    }

    #[test]
    fn infer_intent_gate_forces_tools_for_tilde_paths() {
        let d = infer_intent_gate("~/project/file.txt", "");
        assert_eq!(d.needs_tools, Some(true));
    }

    #[test]
    fn infer_intent_gate_forces_tools_for_windows_paths() {
        let d = infer_intent_gate(r"C:\Users\alice\file.txt", "");
        assert_eq!(d.needs_tools, Some(true));
    }

    #[test]
    fn infer_intent_gate_forces_tools_for_local_installed_version_query() {
        let d = infer_intent_gate("What version of rustc is installed on this machine?", "");
        assert_eq!(d.needs_tools, Some(true));
        assert_eq!(d.can_answer_now, Some(false));
    }

    #[test]
    fn infer_intent_gate_forces_tools_for_explicit_run_command_request() {
        let d = infer_intent_gate(
            "Run the command `cat /nonexistent/file.txt` and tell me what happens",
            "",
        );
        assert_eq!(d.needs_tools, Some(true));
        assert_eq!(d.can_answer_now, Some(false));
    }

    #[test]
    fn infer_intent_gate_does_not_force_tools_for_generic_version_question() {
        let d = infer_intent_gate("What version of rustc was released in 2021?", "");
        assert!(
            d.needs_tools.is_none(),
            "expected needs_tools=None for generic version question, got {:?}",
            d.needs_tools
        );
    }

    #[test]
    fn infer_intent_gate_does_not_force_tools_for_non_local_installed_question() {
        let d = infer_intent_gate("What version of Python is installed at the company?", "");
        assert!(
            d.needs_tools.is_none(),
            "expected needs_tools=None for non-local installed wording, got {:?}",
            d.needs_tools
        );
    }

    #[test]
    fn infer_intent_gate_forces_tools_for_runtime_capability_validation() {
        let d = infer_intent_gate("Can you check if you can post to Twitter right now?", "");
        assert_eq!(d.needs_tools, Some(true));
        assert_eq!(d.can_answer_now, Some(false));
    }

    #[test]
    fn infer_intent_gate_forces_tools_for_connection_state_validation() {
        let d = infer_intent_gate(
            "Please verify you're connected to GitHub before answering.",
            "",
        );
        assert_eq!(d.needs_tools, Some(true));
        assert_eq!(d.can_answer_now, Some(false));
    }

    #[test]
    fn infer_intent_gate_forces_tools_for_auth_management_request() {
        let d = infer_intent_gate("Connect my Twitter account so you can post for me.", "");
        assert_eq!(d.needs_tools, Some(true));
        assert_eq!(d.can_answer_now, Some(false));
    }

    #[test]
    fn infer_intent_gate_does_not_force_tools_for_generic_capability_question() {
        let d = infer_intent_gate("Can you help me write a tweet?", "");
        assert!(
            d.needs_tools.is_none(),
            "expected needs_tools=None for generic capability question, got {:?}",
            d.needs_tools
        );
    }

    #[test]
    fn infer_intent_gate_does_not_force_tools_for_simple_reply_format_request() {
        let d = infer_intent_gate(
            "Chrome smoke test 2026-03-30T02:27:33.908Z: what is 2+2? Reply with just 4.",
            "",
        );
        assert!(
            d.needs_tools.is_none(),
            "expected needs_tools=None for simple reply-format request, got {:?}",
            d.needs_tools
        );
    }

    #[test]
    fn classify_connected_content_mode_ignores_generic_reply_formatting() {
        assert_eq!(
            classify_connected_content_mode(
                "Chrome smoke test 2026-03-30T02:27:33.908Z: what is 2+2? Reply with just 4."
            ),
            ConnectedContentMode::None
        );
        assert_eq!(
            classify_connected_api_intent(
                "Chrome smoke test 2026-03-30T02:27:33.908Z: what is 2+2? Reply with just 4."
            ),
            None
        );
    }

    #[test]
    fn classify_connected_content_mode_keeps_explicit_live_reply_requests() {
        assert_eq!(
            classify_connected_content_mode("Reply to this tweet right now."),
            ConnectedContentMode::DeliverOnly
        );
        assert_eq!(
            classify_connected_api_intent("Reply to this tweet right now."),
            Some(ConnectedApiIntent::WriteAction)
        );
    }

    #[test]
    fn classify_connected_api_intent_detects_runtime_validation() {
        assert_eq!(
            classify_connected_api_intent(
                "Please check whether you're connected to GitHub before you answer."
            ),
            Some(ConnectedApiIntent::RuntimeCapabilityValidation)
        );
    }

    #[test]
    fn classify_connected_api_intent_detects_write_actions() {
        assert_eq!(
            classify_connected_api_intent("Create a GitHub issue for this regression."),
            Some(ConnectedApiIntent::WriteAction)
        );
        assert_eq!(
            classify_connected_api_intent("Post this update to LinkedIn."),
            Some(ConnectedApiIntent::WriteAction)
        );
        assert_eq!(
            classify_connected_api_intent("Draft the copy and post it to LinkedIn right now."),
            Some(ConnectedApiIntent::WriteAction)
        );
    }

    #[test]
    fn classify_connected_api_intent_treats_explicit_social_delivery_as_write_action() {
        assert_eq!(
            classify_connected_api_intent(
                "Can you post a tweet about your new stuff and make it engaging so people want to comment?"
            ),
            Some(ConnectedApiIntent::WriteAction)
        );
        assert_eq!(
            classify_connected_content_mode(
                "Can you post a tweet about your new stuff and make it engaging so people want to comment?"
            ),
            ConnectedContentMode::DraftThenDeliver
        );
        assert!(!content_authoring_request_is_text_only(
            "Can you post a tweet about your new stuff and make it engaging so people want to comment?"
        ));
    }

    #[test]
    fn classify_connected_content_mode_detects_draft_only_requests() {
        assert_eq!(
            classify_connected_content_mode("Can you help me write a tweet about our launch?"),
            ConnectedContentMode::DraftOnly
        );
        assert!(content_authoring_request_is_text_only(
            "Can you help me write a tweet about our launch?"
        ));
    }

    #[test]
    fn classify_connected_content_mode_detects_existing_payload_delivery() {
        assert_eq!(
            classify_connected_content_mode("Post this tweet right now."),
            ConnectedContentMode::DeliverOnly
        );
    }

    #[test]
    fn classify_connected_content_mode_respects_do_not_post_cues() {
        assert_eq!(
            classify_connected_content_mode("Draft a tweet about our launch but don't post it."),
            ConnectedContentMode::DraftOnly
        );
    }

    #[test]
    fn classify_connected_api_intent_detects_account_scoped_delivery_without_service_name() {
        assert_eq!(
            classify_connected_api_intent("Can you post a tweet on your account?"),
            Some(ConnectedApiIntent::WriteAction)
        );
        assert!(!content_authoring_request_is_text_only(
            "Can you post a tweet on your account?"
        ));
    }

    #[test]
    fn classify_connected_api_intent_detects_existing_payload_delivery_without_service_name() {
        assert_eq!(
            classify_connected_api_intent("Send this email for me right now."),
            Some(ConnectedApiIntent::WriteAction)
        );
        assert!(!content_authoring_request_is_text_only(
            "Send this email for me right now."
        ));
    }

    #[test]
    fn classify_connected_api_intent_detects_read_actions() {
        assert_eq!(
            classify_connected_api_intent("List my open GitHub issues."),
            Some(ConnectedApiIntent::ReadAction)
        );
        assert_eq!(
            classify_connected_api_intent("Check my calendar events for tomorrow."),
            Some(ConnectedApiIntent::ReadAction)
        );
    }

    #[test]
    fn classify_connected_api_intent_avoids_local_or_implementation_prompts() {
        assert_eq!(
            classify_connected_api_intent("Can you rewrite this paragraph?"),
            None
        );
        assert_eq!(
            classify_connected_api_intent("Write a GitHub Actions workflow for this repo."),
            None
        );
        assert_eq!(
            classify_connected_api_intent("Read the GitHub Actions docs and summarize them."),
            None
        );
        // "post" triggers both delivery resource and delivery verb detection,
        // resulting in DraftThenDeliver (live delivery), which qualifies as WriteAction.
        assert_eq!(
            classify_connected_api_intent("Draft a LinkedIn post about our launch."),
            Some(ConnectedApiIntent::WriteAction)
        );
    }

    #[test]
    fn auth_management_detection_matches_explicit_connection_requests() {
        assert!(user_text_requests_auth_or_integration_management(
            "Reconnect my GitHub OAuth integration."
        ));
        assert!(user_text_requests_auth_or_integration_management(
            "Set up the Twitter connected account."
        ));
        assert!(!user_text_requests_auth_or_integration_management(
            "Draft a tweet about our launch."
        ));
    }
}
