// ABOUTME: Bootstrap task classifier using heuristic keyword matching.
// ABOUTME: Classifies prompts into task types and selects relevant skills.

use super::types::{SkillRef, TaskClassification, TaskComplexity};

// =============================================================================
// Keyword Lists
// =============================================================================

const CODE_KEYWORDS: &[&str] = &[
    "function",
    "class",
    "implement",
    "write code",
    "script",
    "compile",
    "debug",
    "refactor",
    "bug",
    "stack trace",
    "variable",
    "method",
    "algorithm",
    "syntax",
    "module",
    "package",
    "dependency",
    "endpoint",
    "database",
    "query",
    "schema",
    "migration",
    "deploy",
    "lint",
    "type check",
    "async",
    "await",
    "promise",
    "callback",
    "struct",
    "enum",
    "trait",
    "interface",
    "generic",
    "template",
    "regex",
    "parse",
    "serialize",
    "deserialize",
    "docker",
    "kubernetes",
    "ci/cd",
    "pipeline",
    "git",
    "commit",
    "branch",
    "merge",
    "pull request",
    "code review",
    "code",
    "coding",
    "programming",
    "compiler",
    "runtime",
    "typescript",
    "javascript",
    "python",
    "rust",
];

const FILE_KEYWORDS: &[&str] = &[
    "read file",
    "read the file",
    "write file",
    "write to file",
    "write a file",
    "create file",
    "create a file",
    "delete file",
    "delete the file",
    "rename file",
    "rename the file",
    "move file",
    "move the file",
    "copy file",
    "copy the file",
    "list directory",
    "list the directory",
    "create directory",
    "create a directory",
    "file system",
    "file path",
    "file contents",
    "save to file",
    "open file",
    "open the file",
];

const RESEARCH_KEYWORDS: &[&str] = &[
    "search for",
    "search the",
    "look up",
    "find out",
    "tell me about",
    "what happened",
    "explain",
    "research",
    "summarize",
    "definition",
    "how does",
    "compare",
    "difference between",
    "pros and cons",
    "best practice",
    "documentation",
    "tutorial",
    "guide",
    "latest",
    "current",
    "recent",
    "news",
    "developments",
    "trends",
    "update on",
    "updates on",
    "what are the",
    "what is the latest",
];

/// Keywords that indicate the user explicitly wants to use publishers/tools.
/// Highest priority for tool routing — overrides general_chat classification.
/// Only intentional phrases — bare "publisher"/"publishers" would match
/// informational queries like "check the publishers" or "list publishers".
const PUBLISHER_KEYWORDS: &[&str] = &[
    "use publisher",
    "use publishers",
    "use the publisher",
    "use the publishers",
    "use any publisher",
    "use any of the publisher",
    "use any of the publishers",
    "with publisher",
    "with publishers",
    "using publisher",
    "using publishers",
    "via publisher",
    "via publishers",
];

const DOCUMENT_KEYWORDS: &[&str] = &[
    "draft",
    "document",
    "report",
    "essay",
    "email",
    "letter",
    "memo",
    "proposal",
    "presentation",
    "blog post",
    "article",
    "readme",
    "changelog",
    "release notes",
];

// =============================================================================
// Word Boundary Matching
// =============================================================================

/// Check if a keyword appears in text with word boundaries.
/// Multi-word keywords use simple substring matching.
/// Single-word keywords require word boundary characters on both sides.
fn contains_keyword(text: &str, keyword: &str) -> bool {
    if keyword.contains(' ') {
        // Multi-word: simple substring match
        return text.contains(keyword);
    }

    // Single-word: check word boundaries
    let keyword_bytes = keyword.as_bytes();
    let text_bytes = text.as_bytes();

    if keyword_bytes.len() > text_bytes.len() {
        return false;
    }

    let mut start = 0;
    while let Some(pos) = text[start..].find(keyword) {
        let abs_pos = start + pos;
        let end_pos = abs_pos + keyword.len();

        let boundary_before = abs_pos == 0 || !text_bytes[abs_pos - 1].is_ascii_alphanumeric();
        let boundary_after =
            end_pos >= text_bytes.len() || !text_bytes[end_pos].is_ascii_alphanumeric();

        if boundary_before && boundary_after {
            return true;
        }

        // Move past this match to avoid infinite loop
        start = abs_pos + 1;
        if start >= text.len() {
            break;
        }
    }

    false
}

// =============================================================================
// Task Classification
// =============================================================================

/// Classify a user prompt into a task type using keyword heuristics.
///
/// Rules are applied in priority order:
/// 1. Code generation (code keywords or code fences)
/// 2. File operations (file keywords)
/// 3. Explicit publisher request (user mentions publishers)
/// 4. Research (search/explain/latest keywords)
/// 5. Document generation (writing keywords)
/// 6. General chat (default)
pub fn classify(prompt: &str, skills: &[SkillRef]) -> TaskClassification {
    let prompt_lower = prompt.to_lowercase();

    let relevant_skills = select_relevant_skills(prompt, skills);

    // Rule 0: Skill invocation — the frontend wraps invoked skills in
    // <skill-invocation> tags with the full SKILL.md content inlined.
    // This MUST be checked before any keyword rules because the SKILL.md
    // body contains code keywords, numbered lists, and other patterns
    // that would cause misclassification and unwanted decomposition.
    if prompt.contains("<skill-invocation") {
        return TaskClassification {
            task_type: "skill_execution".to_string(),
            requires_tools: true,
            requires_file_system: false,
            complexity: TaskComplexity::Simple,
            relevant_skills,
        };
    }

    // Rule 1: Code generation
    if contains_code_fence(prompt)
        || CODE_KEYWORDS
            .iter()
            .any(|kw| contains_keyword(&prompt_lower, kw))
    {
        return TaskClassification {
            task_type: "code_generation".to_string(),
            requires_tools: true,
            requires_file_system: true,
            complexity: TaskComplexity::Moderate,
            relevant_skills,
        };
    }

    // Rule 2: File operations
    if FILE_KEYWORDS
        .iter()
        .any(|kw| contains_keyword(&prompt_lower, kw))
    {
        return TaskClassification {
            task_type: "file_operations".to_string(),
            requires_tools: true,
            requires_file_system: true,
            complexity: TaskComplexity::Simple,
            relevant_skills,
        };
    }

    // Rule 3: Explicit publisher request (user wants MCP publisher tools)
    if PUBLISHER_KEYWORDS
        .iter()
        .any(|kw| contains_keyword(&prompt_lower, kw))
    {
        return TaskClassification {
            task_type: "research".to_string(),
            requires_tools: true,
            requires_file_system: false,
            complexity: TaskComplexity::Simple,
            relevant_skills,
        };
    }

    // Rule 4: Research
    if RESEARCH_KEYWORDS
        .iter()
        .any(|kw| contains_keyword(&prompt_lower, kw))
    {
        return TaskClassification {
            task_type: "research".to_string(),
            requires_tools: true,
            requires_file_system: false,
            complexity: TaskComplexity::Simple,
            relevant_skills,
        };
    }

    // Rule 5: Document generation
    if DOCUMENT_KEYWORDS
        .iter()
        .any(|kw| contains_keyword(&prompt_lower, kw))
    {
        return TaskClassification {
            task_type: "document_generation".to_string(),
            requires_tools: false,
            requires_file_system: false,
            complexity: TaskComplexity::Moderate,
            relevant_skills,
        };
    }

    // Rule 6: Default
    TaskClassification {
        task_type: "general_chat".to_string(),
        requires_tools: false,
        requires_file_system: false,
        complexity: TaskComplexity::Simple,
        relevant_skills,
    }
}

/// Check if the prompt contains a markdown code fence.
fn contains_code_fence(prompt: &str) -> bool {
    prompt.contains("```")
}

// =============================================================================
// Skill Selection
// =============================================================================

/// Select skills relevant to a prompt by matching against skill metadata.
///
/// Matches by:
/// - Skill name appearing in the prompt (word boundary)
/// - Any skill tag appearing in the prompt (word boundary)
/// - Skill slug appearing in the prompt (word boundary)
pub fn select_relevant_skills(prompt: &str, skills: &[SkillRef]) -> Vec<String> {
    let prompt_lower = prompt.to_lowercase();

    skills
        .iter()
        .filter(|skill| {
            contains_keyword(&prompt_lower, &skill.name.to_lowercase())
                || skill
                    .tags
                    .iter()
                    .any(|tag| contains_keyword(&prompt_lower, &tag.to_lowercase()))
                || contains_keyword(&prompt_lower, &skill.slug.to_lowercase())
        })
        .map(|skill| skill.slug.clone())
        .collect()
}

/// Select installed skills that explicitly declare meeting handling.
///
/// Meeting Mode calls this at call-end, where relying on transcript text would
/// make routing accidental. The tag must come from the skill metadata that was
/// parsed from SKILL.md frontmatter by the frontend.
#[allow(dead_code)]
pub fn select_meeting_skills(skills: &[SkillRef]) -> Vec<String> {
    skills
        .iter()
        .filter(|skill| {
            skill.tags.iter().any(|tag| {
                let normalized = tag.trim().to_lowercase();
                matches!(
                    normalized.as_str(),
                    "meeting" | "meetings" | "meeting-mode" | "meeting_mode"
                )
            })
        })
        .map(|skill| skill.slug.clone())
        .collect()
}

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

    fn make_skill(slug: &str, name: &str, tags: &[&str]) -> SkillRef {
        SkillRef {
            slug: slug.to_string(),
            name: name.to_string(),
            description: String::new(),
            tags: tags.iter().map(|t| t.to_string()).collect(),
            path: format!("/skills/{}/SKILL.md", slug),
        }
    }

    // =========================================================================
    // Word Boundary Tests
    // =========================================================================

    #[test]
    fn word_boundary_rejects_substring_match() {
        // "capital" should NOT match "api"
        assert!(!contains_keyword("capital", "api"));
        // "latest" should NOT match "test"
        assert!(!contains_keyword("latest", "test"));
        // "testing" should NOT match "test" (no boundary after)
        assert!(!contains_keyword("testing", "test"));
    }

    #[test]
    fn word_boundary_accepts_whole_word() {
        assert!(contains_keyword("run a test", "test"));
        assert!(contains_keyword("test this", "test"));
        assert!(contains_keyword("test", "test"));
        assert!(contains_keyword("the api is broken", "api"));
    }

    #[test]
    fn multi_word_keyword_uses_substring_match() {
        assert!(contains_keyword("search for the news", "search for"));
        assert!(contains_keyword("please write code here", "write code"));
    }

    // =========================================================================
    // Task Classification Tests
    // =========================================================================

    #[test]
    fn classifies_code_generation_from_keywords() {
        let result = classify("Write a Python function that sorts a list", &[]);
        assert_eq!(result.task_type, "code_generation");
        assert!(result.requires_tools);
        assert!(result.requires_file_system);
        assert_eq!(result.complexity, TaskComplexity::Moderate);
    }

    #[test]
    fn classifies_code_generation_from_code_fence() {
        let prompt = "Fix this code:\n```python\ndef foo():\n    pass\n```";
        let result = classify(prompt, &[]);
        assert_eq!(result.task_type, "code_generation");
    }

    #[test]
    fn classifies_general_chat_for_simple_question() {
        let result = classify("What is the capital of France?", &[]);
        assert_eq!(result.task_type, "general_chat");
        assert!(!result.requires_tools);
        assert!(!result.requires_file_system);
        assert_eq!(result.complexity, TaskComplexity::Simple);
    }

    #[test]
    fn classifies_research_for_search_prompt() {
        let result = classify("Search for the latest news about AI", &[]);
        assert_eq!(result.task_type, "research");
        assert!(result.requires_tools);
        assert!(!result.requires_file_system);
    }

    #[test]
    fn classifies_document_generation() {
        let result = classify("Draft an email to my team about the Q3 results", &[]);
        assert_eq!(result.task_type, "document_generation");
        assert!(!result.requires_tools);
        assert_eq!(result.complexity, TaskComplexity::Moderate);
    }

    #[test]
    fn classifies_file_operations() {
        let result = classify("Read the file at /tmp/foo.txt", &[]);
        assert_eq!(result.task_type, "file_operations");
        assert!(result.requires_tools);
        assert!(result.requires_file_system);
        assert_eq!(result.complexity, TaskComplexity::Simple);
    }

    #[test]
    fn classifies_empty_string_as_general_chat() {
        let result = classify("", &[]);
        assert_eq!(result.task_type, "general_chat");
    }

    #[test]
    fn code_takes_priority_over_research() {
        // "explain" is research, but "function" is code — code wins
        let result = classify("Explain how this function works", &[]);
        assert_eq!(result.task_type, "code_generation");
    }

    #[test]
    fn case_insensitive_matching() {
        let result = classify("IMPLEMENT a CLASS in TypeScript", &[]);
        assert_eq!(result.task_type, "code_generation");
    }

    // =========================================================================
    // Skill Selection Tests
    // =========================================================================

    #[test]
    fn selects_skill_by_name() {
        let skills = vec![make_skill("prose", "Prose", &["writing", "ai"])];
        let result = select_relevant_skills("Use Prose to help me write", &skills);
        assert_eq!(result, vec!["prose"]);
    }

    #[test]
    fn selects_skill_by_tag() {
        let skills = vec![make_skill(
            "git-commit",
            "Git Commit",
            &["git", "version-control"],
        )];
        let result = select_relevant_skills("help me with git", &skills);
        assert_eq!(result, vec!["git-commit"]);
    }

    #[test]
    fn selects_skill_by_slug() {
        let skills = vec![make_skill(
            "prose",
            "Prose Helper",
            &["ai", "orchestration"],
        )];
        let result = select_relevant_skills("prose run my_workflow", &skills);
        assert_eq!(result, vec!["prose"]);
    }

    #[test]
    fn returns_empty_when_no_skills_match() {
        let skills = vec![
            make_skill("prose", "Prose", &["writing"]),
            make_skill("git-commit", "Git Commit", &["git"]),
        ];
        let result = select_relevant_skills("What's the weather?", &skills);
        assert!(result.is_empty());
    }

    #[test]
    fn selects_multiple_matching_skills() {
        let skills = vec![
            make_skill("prose", "Prose", &["writing"]),
            make_skill("docs", "Documentation", &["writing", "markdown"]),
        ];
        let result = select_relevant_skills("Help me with writing documentation", &skills);
        assert_eq!(result.len(), 2);
        assert!(result.contains(&"prose".to_string()));
        assert!(result.contains(&"docs".to_string()));
    }

    #[test]
    fn skill_selection_is_case_insensitive() {
        let skills = vec![make_skill("prose", "Prose", &["Writing"])];
        let result = select_relevant_skills("I need PROSE help", &skills);
        assert_eq!(result, vec!["prose"]);
    }

    #[test]
    fn classify_includes_relevant_skills() {
        let skills = vec![make_skill(
            "git-commit",
            "Git Commit",
            &["git", "version-control"],
        )];
        let result = classify("Help me git commit my code changes", &skills);
        assert_eq!(result.task_type, "code_generation");
        assert_eq!(result.relevant_skills, vec!["git-commit"]);
    }

    #[test]
    fn meeting_skill_selection_uses_declared_tags_only() {
        let skills = vec![
            make_skill("sales-notes", "Sales Notes", &["meeting"]),
            make_skill("crm-writer", "CRM Writer", &["meeting-mode"]),
            make_skill("recap", "Meeting Recap", &["document"]),
        ];

        let result = select_meeting_skills(&skills);

        assert_eq!(
            result,
            vec!["sales-notes".to_string(), "crm-writer".to_string()]
        );
    }

    #[test]
    fn meeting_skill_selection_returns_empty_without_tag() {
        let skills = vec![make_skill("recap", "Meeting Recap", &["document"])];

        let result = select_meeting_skills(&skills);

        assert!(result.is_empty());
    }

    #[test]
    fn meeting_skill_selection_returns_single_tagged_skill() {
        let skills = vec![
            make_skill("crm-notes", "CRM Notes", &["meetings"]),
            make_skill("recap", "Meeting Recap", &["document"]),
        ];

        let result = select_meeting_skills(&skills);

        assert_eq!(result, vec!["crm-notes".to_string()]);
    }

    // =========================================================================
    // Publisher & Expanded Research Tests
    // =========================================================================

    #[test]
    fn classifies_explicit_publisher_request_as_research() {
        let result = classify(
            "What is the latest FASB digital asset developments in 2026. Use any of the publishers to help with this question.",
            &[],
        );
        assert_eq!(result.task_type, "research");
        assert!(result.requires_tools);
    }

    #[test]
    fn classifies_use_publishers_as_research() {
        let result = classify("Use publishers to find the answer", &[]);
        assert_eq!(result.task_type, "research");
        assert!(result.requires_tools);
    }

    #[test]
    fn classifies_latest_keyword_as_research() {
        let result = classify("What is the latest news about Bitcoin?", &[]);
        assert_eq!(result.task_type, "research");
        assert!(result.requires_tools);
    }

    #[test]
    fn classifies_current_keyword_as_research() {
        let result = classify("What is the current price of gold?", &[]);
        assert_eq!(result.task_type, "research");
        assert!(result.requires_tools);
    }

    #[test]
    fn classifies_developments_keyword_as_research() {
        let result = classify("Tell me about recent developments in AI regulation", &[]);
        assert_eq!(result.task_type, "research");
        assert!(result.requires_tools);
    }

    #[test]
    fn classifies_find_out_as_research() {
        let result = classify("Find out what happened at the conference", &[]);
        assert_eq!(result.task_type, "research");
        assert!(result.requires_tools);
    }

    #[test]
    fn publisher_does_not_match_substring() {
        // "published" should NOT match "publisher" because of word boundary check
        let result = classify("The book was published last year", &[]);
        assert_eq!(result.task_type, "general_chat");
    }

    #[test]
    fn bare_publishers_does_not_trigger_publisher_routing() {
        // Asking ABOUT publishers, not asking to USE them
        let result = classify("Check the seren mcp publishers", &[]);
        assert_eq!(result.task_type, "general_chat");

        let result2 = classify("List all available publishers", &[]);
        assert_eq!(result2.task_type, "general_chat");
    }

    #[test]
    fn latest_does_not_match_in_code_context() {
        // Code keywords take priority over research keywords
        let result = classify("Get the latest version of the function from git", &[]);
        assert_eq!(result.task_type, "code_generation");
    }

    // =========================================================================
    // Skill Invocation Classification
    // =========================================================================

    #[test]
    fn skill_invocation_overrides_code_keywords() {
        // The SKILL.md body contains "python", "script", code fences, etc.
        // Skill invocation must win over code_generation.
        let prompt = r#"<skill-invocation name="polymarket-bot">
The user has invoked the /polymarket-bot skill. Execute it by following the skill instructions below.

## Scanning for Opportunities
```bash
cd ~/.config/seren/skills/polymarket-bot && python3 scripts/agent.py --config config.json --dry-run
```

1. Check prerequisites
2. Run the scan
3. Report results
</skill-invocation>"#;
        let result = classify(prompt, &[]);
        assert_eq!(result.task_type, "skill_execution");
        assert_eq!(result.complexity, TaskComplexity::Simple);
        assert!(result.requires_tools);
        assert!(!result.requires_file_system);
    }

    #[test]
    fn non_skill_prompt_with_code_keywords_still_classifies_as_code() {
        let result = classify("Write a python function to sort a list", &[]);
        assert_eq!(result.task_type, "code_generation");
    }
}
