// SPDX-License-Identifier: Apache-2.0

//! Triage status detection for GitHub issues.
//!
//! This module provides utilities to check whether an issue has already been triaged,
//! either through labels or Aptu-generated comments.

use crate::ai::types::{IssueDetails, TriageResponse};
use crate::utils::is_priority_label;
use std::fmt::Write;
use tracing::debug;

/// Signature string used to identify Aptu-generated triage comments
pub const APTU_SIGNATURE: &str = "Generated by Aptu";

/// Status of whether an issue has already been triaged
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TriageStatus {
    /// Whether the issue has a type label (bug, enhancement, etc.)
    pub has_type_label: bool,
    /// Whether the issue has a priority label (p0-p4, priority: high/medium/low)
    pub has_priority_label: bool,
    /// Whether the issue has an Aptu-generated comment
    pub has_aptu_comment: bool,
    /// List of labels that indicate triage status
    pub label_names: Vec<String>,
}

impl TriageStatus {
    /// Create a new `TriageStatus`.
    #[must_use]
    pub fn new(
        has_type_label: bool,
        has_priority_label: bool,
        has_aptu_comment: bool,
        label_names: Vec<String>,
    ) -> Self {
        Self {
            has_type_label,
            has_priority_label,
            has_aptu_comment,
            label_names,
        }
    }

    /// Check if the issue has been triaged (has both type and priority labels, or Aptu comment).
    #[must_use]
    pub fn is_triaged(&self) -> bool {
        (self.has_type_label && self.has_priority_label) || self.has_aptu_comment
    }
}

/// Check if a label is a type label (bug, enhancement, documentation, etc.).
fn is_type_label(label: &str) -> bool {
    const TYPE_LABELS: &[&str] = &[
        "bug",
        "enhancement",
        "documentation",
        "question",
        "good first issue",
        "help wanted",
        "duplicate",
        "invalid",
        "wontfix",
        "triaged",
        "needs-triage",
        "status: triaged",
    ];
    TYPE_LABELS.contains(&label)
}

/// Renders a labeled list section in markdown format.
fn render_list_section_markdown(
    title: &str,
    items: &[String],
    empty_msg: &str,
    numbered: bool,
) -> String {
    let mut output = String::new();
    let _ = writeln!(output, "### {title}\n");
    if items.is_empty() {
        let _ = writeln!(output, "{empty_msg}");
    } else if numbered {
        for (i, item) in items.iter().enumerate() {
            let _ = writeln!(output, "{}. {}", i + 1, item);
        }
    } else {
        for item in items {
            let _ = writeln!(output, "- {item}");
        }
    }
    output.push('\n');
    output
}

/// Renders triage response as markdown for posting to GitHub.
///
/// Generates pure markdown without terminal colors. This is the core rendering
/// function used by both CLI and FFI layers.
///
/// # Arguments
///
/// * `triage` - The triage response to render
///
/// # Returns
///
/// A markdown string suitable for posting as a GitHub comment.
#[must_use]
pub fn render_triage_markdown(triage: &TriageResponse) -> String {
    let mut output = String::new();

    // Header
    output.push_str("## Triage Summary\n\n");
    output.push_str(&triage.summary);
    output.push_str("\n\n");

    // Labels (always show in markdown for maintainers)
    let labels: Vec<String> = triage
        .suggested_labels
        .iter()
        .map(|l| format!("`{l}`"))
        .collect();
    output.push_str(&render_list_section_markdown(
        "Suggested Labels",
        &labels,
        "None",
        false,
    ));

    // Suggested Milestone
    if let Some(milestone) = &triage.suggested_milestone
        && !milestone.is_empty()
    {
        output.push_str("### Suggested Milestone\n\n");
        output.push_str(milestone);
        output.push_str("\n\n");
    }

    // Questions
    output.push_str(&render_list_section_markdown(
        "Clarifying Questions",
        &triage.clarifying_questions,
        "None needed",
        true,
    ));

    // Duplicates
    output.push_str(&render_list_section_markdown(
        "Potential Duplicates",
        &triage.potential_duplicates,
        "None found",
        false,
    ));

    // Related issues with reason blockquote
    if !triage.related_issues.is_empty() {
        output.push_str("### Related Issues\n\n");
        for issue in &triage.related_issues {
            let _ = writeln!(output, "- **#{}** - {}", issue.number, issue.title);
            let _ = writeln!(output, "  > {}\n", issue.reason);
        }
    }

    // Status note (if present)
    if let Some(status_note) = &triage.status_note
        && !status_note.is_empty()
    {
        output.push_str("### Status\n\n");
        output.push_str(status_note);
        output.push_str("\n\n");
    }

    // Contributor guidance (if present)
    if let Some(guidance) = &triage.contributor_guidance {
        output.push_str("### Contributor Guidance\n\n");
        let beginner_label = if guidance.beginner_friendly {
            "**Beginner-friendly**"
        } else {
            "**Advanced**"
        };
        let _ = writeln!(output, "{beginner_label}\n");
        let _ = writeln!(output, "{}\n", guidance.reasoning);
    }

    // Implementation approach
    if let Some(approach) = &triage.implementation_approach
        && !approach.is_empty()
    {
        output.push_str("### Implementation Approach\n\n");
        for line in approach.lines() {
            let _ = writeln!(output, "  {line}");
        }
        output.push('\n');
    }

    // Signature
    output.push_str("---\n");
    output.push('*');
    output.push_str(APTU_SIGNATURE);
    output.push('*');
    output.push('\n');

    output
}

/// Render a `ReleaseNotesResponse` to markdown format.
///
/// Formats release notes with theme, narrative, and categorized sections
/// (highlights, features, fixes, improvements, documentation, maintenance, contributors).
///
/// # Arguments
///
/// * `response` - The release notes response to render
///
/// # Returns
///
/// A markdown string suitable for release notes or GitHub release descriptions.
#[must_use]
pub fn render_release_notes_markdown(response: &crate::ai::types::ReleaseNotesResponse) -> String {
    use std::fmt::Write;

    let mut body = String::new();

    // Add theme as header
    let _ = writeln!(body, "## {}\n", response.theme);

    // Add narrative
    if !response.narrative.is_empty() {
        let _ = writeln!(body, "{}\n", response.narrative);
    }

    // Add highlights
    if !response.highlights.is_empty() {
        body.push_str("### Highlights\n\n");
        for highlight in &response.highlights {
            let _ = writeln!(body, "- {highlight}");
        }
        body.push('\n');
    }

    // Add features
    if !response.features.is_empty() {
        body.push_str("### Features\n\n");
        for feature in &response.features {
            let _ = writeln!(body, "- {feature}");
        }
        body.push('\n');
    }

    // Add fixes
    if !response.fixes.is_empty() {
        body.push_str("### Fixes\n\n");
        for fix in &response.fixes {
            let _ = writeln!(body, "- {fix}");
        }
        body.push('\n');
    }

    // Add improvements
    if !response.improvements.is_empty() {
        body.push_str("### Improvements\n\n");
        for improvement in &response.improvements {
            let _ = writeln!(body, "- {improvement}");
        }
        body.push('\n');
    }

    // Add documentation
    if !response.documentation.is_empty() {
        body.push_str("### Documentation\n\n");
        for doc in &response.documentation {
            let _ = writeln!(body, "- {doc}");
        }
        body.push('\n');
    }

    // Add maintenance
    if !response.maintenance.is_empty() {
        body.push_str("### Maintenance\n\n");
        for maint in &response.maintenance {
            let _ = writeln!(body, "- {maint}");
        }
        body.push('\n');
    }

    // Add contributors
    if !response.contributors.is_empty() {
        body.push_str("### Contributors\n\n");
        for contributor in &response.contributors {
            let _ = writeln!(body, "- {contributor}");
        }
    }

    body
}

/// Check if an issue has already been triaged.
///
/// Returns `TriageStatus` indicating whether the issue has both type and priority labels,
/// or has an Aptu-generated comment.
pub fn check_already_triaged(issue: &IssueDetails) -> TriageStatus {
    let has_type_label = issue.labels.iter().any(|label| is_type_label(label));
    let has_priority_label = issue.labels.iter().any(|label| is_priority_label(label));

    let label_names: Vec<String> = issue
        .labels
        .iter()
        .filter(|label| is_type_label(label) || is_priority_label(label))
        .cloned()
        .collect();

    // Check for Aptu signature in comments
    let has_aptu_comment = issue
        .comments
        .iter()
        .any(|comment| comment.body.contains(APTU_SIGNATURE));

    if has_type_label || has_priority_label || has_aptu_comment {
        debug!(
            has_type_label = has_type_label,
            has_priority_label = has_priority_label,
            has_aptu_comment = has_aptu_comment,
            labels = ?label_names,
            "Issue triage status detected"
        );
    }

    TriageStatus::new(
        has_type_label,
        has_priority_label,
        has_aptu_comment,
        label_names,
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ai::types::IssueComment;

    fn create_test_issue(labels: Vec<String>, comments: Vec<IssueComment>) -> IssueDetails {
        IssueDetails::builder()
            .owner("test".to_string())
            .repo("repo".to_string())
            .number(1)
            .title("Test issue".to_string())
            .body("Test body".to_string())
            .labels(labels)
            .comments(comments)
            .url("https://github.com/test/repo/issues/1".to_string())
            .build()
    }

    #[test]
    fn test_no_triage() {
        let issue = create_test_issue(vec![], vec![]);
        let status = check_already_triaged(&issue);
        assert!(!status.is_triaged());
        assert!(!status.has_type_label);
        assert!(!status.has_priority_label);
        assert!(!status.has_aptu_comment);
        assert!(status.label_names.is_empty());
    }

    #[test]
    fn test_type_label_only() {
        let labels = vec!["bug".to_string()];
        let issue = create_test_issue(labels, vec![]);
        let status = check_already_triaged(&issue);
        assert!(!status.is_triaged());
        assert!(status.has_type_label);
        assert!(!status.has_priority_label);
        assert!(!status.has_aptu_comment);
        assert_eq!(status.label_names.len(), 1);
    }

    #[test]
    fn test_priority_label_only() {
        let labels = vec!["p1".to_string()];
        let issue = create_test_issue(labels, vec![]);
        let status = check_already_triaged(&issue);
        assert!(!status.is_triaged());
        assert!(!status.has_type_label);
        assert!(status.has_priority_label);
        assert!(!status.has_aptu_comment);
        assert_eq!(status.label_names.len(), 1);
    }

    #[test]
    fn test_type_and_priority_labels() {
        let labels = vec!["bug".to_string(), "p1".to_string()];
        let issue = create_test_issue(labels, vec![]);
        let status = check_already_triaged(&issue);
        assert!(status.is_triaged());
        assert!(status.has_type_label);
        assert!(status.has_priority_label);
        assert!(!status.has_aptu_comment);
        assert_eq!(status.label_names.len(), 2);
    }

    #[test]
    fn test_priority_prefix_labels() {
        // Test all priority: prefix variants (high, medium, low)
        for priority in ["priority: high", "priority: medium", "priority: low"] {
            let labels = vec!["bug".to_string(), priority.to_string()];
            let issue = create_test_issue(labels, vec![]);
            let status = check_already_triaged(&issue);
            assert!(status.is_triaged(), "Failed for {priority}");
            assert!(status.has_type_label, "Failed for {priority}");
            assert!(status.has_priority_label, "Failed for {priority}");
        }
    }

    #[test]
    fn test_aptu_comment_only() {
        let comments = vec![IssueComment {
            author: "aptu-bot".to_string(),
            body: "This looks good. Generated by Aptu".to_string(),
        }];
        let issue = create_test_issue(vec![], comments);
        let status = check_already_triaged(&issue);
        assert!(status.is_triaged());
        assert!(!status.has_type_label);
        assert!(!status.has_priority_label);
        assert!(status.has_aptu_comment);
        assert!(status.label_names.is_empty());
    }

    #[test]
    fn test_type_label_with_aptu_comment() {
        let labels = vec!["bug".to_string()];
        let comments = vec![IssueComment {
            author: "aptu-bot".to_string(),
            body: "Generated by Aptu".to_string(),
        }];
        let issue = create_test_issue(labels, comments);
        let status = check_already_triaged(&issue);
        assert!(status.is_triaged());
        assert!(status.has_type_label);
        assert!(!status.has_priority_label);
        assert!(status.has_aptu_comment);
    }

    #[test]
    fn test_partial_signature_no_match() {
        let comments = vec![IssueComment {
            author: "other-bot".to_string(),
            body: "Generated by AnotherTool".to_string(),
        }];
        let issue = create_test_issue(vec![], comments);
        let status = check_already_triaged(&issue);
        assert!(!status.is_triaged());
        assert!(!status.has_aptu_comment);
    }

    #[test]
    fn test_irrelevant_labels() {
        let labels = vec!["component: ui".to_string(), "needs-review".to_string()];
        let issue = create_test_issue(labels, vec![]);
        let status = check_already_triaged(&issue);
        assert!(!status.is_triaged());
        assert!(!status.has_type_label);
        assert!(!status.has_priority_label);
        assert!(status.label_names.is_empty());
    }

    #[test]
    fn test_priority_label_case_insensitive() {
        let labels = vec!["bug".to_string(), "P2".to_string()];
        let issue = create_test_issue(labels, vec![]);
        let status = check_already_triaged(&issue);
        assert!(status.is_triaged());
        assert!(status.has_priority_label);
    }

    #[test]
    fn test_priority_prefix_case_insensitive() {
        let labels = vec!["enhancement".to_string(), "Priority: HIGH".to_string()];
        let issue = create_test_issue(labels, vec![]);
        let status = check_already_triaged(&issue);
        assert!(status.is_triaged());
        assert!(status.has_priority_label);
    }

    #[test]
    fn test_render_triage_markdown_basic() {
        let triage = TriageResponse {
            summary: "This is a test summary".to_string(),
            implementation_approach: None,
            clarifying_questions: vec!["Question 1?".to_string()],
            potential_duplicates: vec![],
            related_issues: vec![],
            suggested_labels: vec!["bug".to_string()],
            suggested_milestone: None,
            status_note: None,
            contributor_guidance: None,
        };

        let markdown = render_triage_markdown(&triage);
        assert!(markdown.contains("## Triage Summary"));
        assert!(markdown.contains("This is a test summary"));
        assert!(markdown.contains("### Clarifying Questions"));
        assert!(markdown.contains("1. Question 1?"));
        assert!(markdown.contains(APTU_SIGNATURE));
    }

    #[test]
    fn test_render_triage_markdown_with_labels() {
        let triage = TriageResponse {
            summary: "Summary".to_string(),
            implementation_approach: None,
            clarifying_questions: vec![],
            potential_duplicates: vec![],
            related_issues: vec![],
            suggested_labels: vec!["bug".to_string(), "p1".to_string()],
            suggested_milestone: None,
            status_note: None,
            contributor_guidance: None,
        };

        let markdown = render_triage_markdown(&triage);
        assert!(markdown.contains("### Suggested Labels"));
        assert!(markdown.contains("`bug`"));
        assert!(markdown.contains("`p1`"));
    }

    #[test]
    fn test_render_triage_markdown_multiline_approach() {
        let triage = TriageResponse {
            summary: "Summary".to_string(),
            implementation_approach: Some("Line 1\nLine 2\nLine 3".to_string()),
            clarifying_questions: vec![],
            potential_duplicates: vec![],
            related_issues: vec![],
            suggested_labels: vec![],
            suggested_milestone: None,
            status_note: None,
            contributor_guidance: None,
        };

        let markdown = render_triage_markdown(&triage);
        assert!(markdown.contains("### Implementation Approach"));
        assert!(markdown.contains("Line 1"));
        assert!(markdown.contains("Line 2"));
        assert!(markdown.contains("Line 3"));
    }
}
