use agentic_tools_core::fmt::TextFormat;
use agentic_tools_core::fmt::TextOptions;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::fmt::Write as _;

/// Truncate a string to at most `max` characters (UTF-8 safe).
fn truncate_chars(s: &str, max: usize) -> String {
    s.chars().take(max).collect()
}

// ============================================================================
// Nested Ref types for structured JSON output
// ============================================================================

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct UserRef {
    pub id: String,
    pub name: String,
    pub email: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TeamRef {
    pub id: String,
    pub key: String,
    pub name: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct WorkflowStateRef {
    pub id: String,
    pub name: String,
    pub state_type: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ProjectRef {
    pub id: String,
    pub name: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ParentIssueRef {
    pub id: String,
    pub identifier: String,
}

// ============================================================================
// Issue models
// ============================================================================

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct IssueSummary {
    pub id: String,
    pub identifier: String,
    pub title: String,
    pub url: String,

    pub team: TeamRef,
    pub state: Option<WorkflowStateRef>,
    pub assignee: Option<UserRef>,
    pub creator: Option<UserRef>,
    pub project: Option<ProjectRef>,

    pub priority: i32,
    pub priority_label: String,

    pub label_ids: Vec<String>,
    pub due_date: Option<String>,

    pub created_at: String,
    pub updated_at: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SearchResult {
    pub issues: Vec<IssueSummary>,
    pub has_next_page: bool,
    pub end_cursor: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct IssueDetails {
    pub issue: IssueSummary,
    pub description: Option<String>,

    pub estimate: Option<f64>,
    pub parent: Option<ParentIssueRef>,
    pub started_at: Option<String>,
    pub completed_at: Option<String>,
    pub canceled_at: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct CreateIssueResult {
    pub success: bool,
    pub issue: Option<IssueSummary>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct IssueResult {
    pub issue: IssueSummary,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct CommentResult {
    pub success: bool,
    pub comment_id: Option<String>,
    pub body: Option<String>,
    pub created_at: Option<String>,
}

// ============================================================================
// Get issue comments models
// ============================================================================

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct CommentSummary {
    pub id: String,
    pub body: String,
    pub url: String,
    pub created_at: String,
    pub updated_at: String,
    pub parent_id: Option<String>,
    pub author_name: Option<String>,
    pub author_email: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct CommentsResult {
    pub issue_identifier: String,
    pub comments: Vec<CommentSummary>,
    pub shown_comments: usize,
    pub total_comments: usize,
    pub has_more: bool,
}

// ============================================================================
// Text formatting
// ============================================================================

#[derive(Debug, Clone)]
pub struct FormatOptions {
    pub show_ids: bool,
    pub show_urls: bool,
    pub show_dates: bool,
    pub show_assignee: bool,
    pub show_state: bool,
    pub show_team: bool,
    pub show_priority: bool,
}

impl Default for FormatOptions {
    fn default() -> Self {
        Self {
            show_ids: false,
            show_urls: false,
            show_dates: false,
            show_assignee: true,
            show_state: true,
            show_team: false,
            show_priority: true,
        }
    }
}

impl FormatOptions {
    pub fn from_env() -> Self {
        Self::from_csv(&std::env::var("LINEAR_TOOLS_EXTRAS").unwrap_or_default())
    }

    pub fn from_csv(csv: &str) -> Self {
        let mut o = Self::default();
        for f in csv
            .split(',')
            .map(|s| s.trim().to_lowercase())
            .filter(|s| !s.is_empty())
        {
            match f.as_str() {
                "id" | "ids" => o.show_ids = true,
                "url" | "urls" => o.show_urls = true,
                "date" | "dates" => o.show_dates = true,
                "assignee" | "assignees" => o.show_assignee = true,
                "state" | "states" => o.show_state = true,
                "team" | "teams" => o.show_team = true,
                "priority" | "priorities" => o.show_priority = true,
                _ => {}
            }
        }
        o
    }
}

impl TextFormat for SearchResult {
    fn fmt_text(&self, _opts: &TextOptions) -> String {
        if self.issues.is_empty() {
            return "Issues: <none>".into();
        }
        let o = FormatOptions::from_env();
        let mut out = String::new();
        let _ = writeln!(out, "Issues:");
        for i in &self.issues {
            let mut line = format!("{} - {}", i.identifier, i.title);
            if o.show_state
                && let Some(s) = &i.state
            {
                let _ = write!(line, " [{}]", s.name);
            }
            if o.show_assignee
                && let Some(u) = &i.assignee
            {
                let _ = write!(line, " (by {})", u.name);
            }
            if o.show_priority {
                let _ = write!(line, " P{} ({})", i.priority, i.priority_label);
            }
            if o.show_team {
                let _ = write!(line, " [{}]", i.team.key);
            }
            if o.show_urls {
                let _ = write!(line, " {}", i.url);
            }
            if o.show_ids {
                let _ = write!(line, " #{}", i.id);
            }
            if o.show_dates {
                let _ = write!(line, " @{}", i.updated_at);
            }
            let _ = writeln!(out, "  {line}");
        }
        if self.has_next_page
            && let Some(cursor) = &self.end_cursor
        {
            let _ = writeln!(out, "\n[More results available, cursor: {cursor}]");
        }
        out
    }
}

impl TextFormat for IssueDetails {
    fn fmt_text(&self, _opts: &TextOptions) -> String {
        let o = FormatOptions::from_env();
        let i = &self.issue;
        let mut out = String::new();

        // Header line
        let _ = writeln!(out, "{}: {}", i.identifier, i.title);

        // Metadata line
        let mut meta = Vec::new();
        if let Some(s) = &i.state {
            meta.push(format!("Status: {}", s.name));
        }
        if o.show_priority {
            meta.push(format!("Priority: P{} ({})", i.priority, i.priority_label));
        }
        if o.show_assignee
            && let Some(u) = &i.assignee
        {
            meta.push(format!("Assignee: {}", u.name));
        }
        if o.show_team {
            meta.push(format!("Team: {}", i.team.key));
        }
        if let Some(p) = &i.project {
            meta.push(format!("Project: {}", p.name));
        }
        if !meta.is_empty() {
            let _ = writeln!(out, "{}", meta.join(" | "));
        }

        if o.show_urls {
            let _ = writeln!(out, "URL: {}", i.url);
        }
        if o.show_dates {
            let _ = writeln!(out, "Created: {} | Updated: {}", i.created_at, i.updated_at);
        }

        // Description
        if let Some(description) = self.description.as_ref().filter(|d| !d.trim().is_empty()) {
            let _ = writeln!(out, "\n{description}");
        }

        out
    }
}

impl TextFormat for CreateIssueResult {
    fn fmt_text(&self, _opts: &TextOptions) -> String {
        if !self.success {
            return "Failed to create issue".into();
        }
        match &self.issue {
            Some(i) => format!(
                "Created issue: {} - {}\nURL: {}",
                i.identifier, i.title, i.url
            ),
            None => "Issue created (no details returned)".into(),
        }
    }
}

impl TextFormat for IssueResult {
    fn fmt_text(&self, _opts: &TextOptions) -> String {
        format!(
            "Updated issue: {} - {}\nURL: {}",
            self.issue.identifier, self.issue.title, self.issue.url
        )
    }
}

impl TextFormat for CommentResult {
    fn fmt_text(&self, _opts: &TextOptions) -> String {
        if !self.success {
            return "Failed to add comment".into();
        }
        match (&self.comment_id, &self.body) {
            (Some(id), Some(body)) => {
                // 80 total, reserve 3 for "..."
                let preview = if body.chars().count() > 80 {
                    format!("{}...", truncate_chars(body, 77))
                } else {
                    body.clone()
                };
                format!("Comment added ({id}): {preview}")
            }
            _ => "Comment added".into(),
        }
    }
}

impl TextFormat for CommentsResult {
    fn fmt_text(&self, _opts: &TextOptions) -> String {
        if self.comments.is_empty() && self.total_comments == 0 {
            return format!("No comments on {}", self.issue_identifier);
        }

        let mut out = String::new();
        let start = self.shown_comments.saturating_sub(self.comments.len()) + 1;
        let _ = writeln!(
            out,
            "Comments for {} (showing {}-{} of {}):",
            self.issue_identifier, start, self.shown_comments, self.total_comments
        );

        for c in &self.comments {
            let author = c.author_name.as_deref().unwrap_or("Unknown");
            let timestamp = if c.created_at.len() >= 16 {
                &c.created_at[..16]
            } else {
                &c.created_at
            };

            // Indent replies with ↳
            let prefix = if c.parent_id.is_some() {
                "  ↳ "
            } else {
                "  "
            };

            let _ = writeln!(out, "{prefix}[{timestamp}] {author}:");
            // Indent body lines
            for line in c.body.lines() {
                let _ = writeln!(out, "{prefix}  {line}");
            }
            let _ = writeln!(out);
        }

        if self.has_more {
            let _ = writeln!(
                out,
                "(more comments available - call linear_get_issue_comments again)"
            );
        } else if self.total_comments > 0 {
            let _ = writeln!(out, "(complete - another call restarts from beginning)");
        }

        out
    }
}

// ============================================================================
// Archive + Metadata models
// ============================================================================

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ArchiveIssueResult {
    pub success: bool,
}

/// Result of a `set_relation` operation
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SetRelationResult {
    pub success: bool,
    /// Action taken: `"created"`, `"removed"`, or `"no_change"`
    pub action: String,
}

impl TextFormat for ArchiveIssueResult {
    fn fmt_text(&self, _opts: &TextOptions) -> String {
        if self.success {
            "Issue archived successfully".into()
        } else {
            "Failed to archive issue".into()
        }
    }
}

impl TextFormat for SetRelationResult {
    fn fmt_text(&self, _opts: &TextOptions) -> String {
        match (self.success, self.action.as_str()) {
            (true, "created") => "Relation created successfully".into(),
            (true, "removed") => "Relation removed successfully".into(),
            (true, "no_change") => "No relation change needed".into(),
            (false, _) => "Failed to modify relation".into(),
            _ => format!("Relation operation: {}", self.action),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum MetadataKind {
    Users,
    Teams,
    Projects,
    WorkflowStates,
    Labels,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MetadataItem {
    pub id: String,
    pub name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub key: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub email: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub state_type: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub team_id: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct GetMetadataResult {
    pub kind: MetadataKind,
    pub items: Vec<MetadataItem>,
    pub has_next_page: bool,
    pub end_cursor: Option<String>,
}

impl TextFormat for GetMetadataResult {
    fn fmt_text(&self, _opts: &TextOptions) -> String {
        if self.items.is_empty() {
            return format!("{:?}: <none>", self.kind);
        }
        let mut out = String::new();
        for item in &self.items {
            let mut line = if let Some(ref key) = item.key {
                format!("{} [{}] ({})", item.name, key, item.id)
            } else {
                format!("{} ({})", item.name, item.id)
            };
            if let Some(ref email) = item.email {
                let _ = write!(line, " <{email}>");
            }
            if let Some(ref st) = item.state_type {
                let _ = write!(line, " [{st}]");
            }
            let _ = writeln!(out, "  {line}");
        }
        if self.has_next_page
            && let Some(ref cursor) = self.end_cursor
        {
            let _ = writeln!(out, "  (more results: after={cursor})");
        }
        out
    }
}

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

    #[test]
    fn truncates_ascii_safely() {
        let s = "abcdefghijklmnopqrstuvwxyz";
        assert_eq!(truncate_chars(s, 5), "abcde");
    }

    #[test]
    fn truncates_utf8_safely() {
        let s = "hello 😀😃😄😁"; // multi-byte
        let truncated = truncate_chars(s, 8);
        assert_eq!(truncated.chars().count(), 8);
        assert_eq!(truncated, "hello 😀😃");
    }

    #[test]
    fn handles_short_strings() {
        assert_eq!(truncate_chars("hi", 10), "hi");
    }

    #[test]
    fn format_options_default_shows_state_assignee_priority() {
        let opts = FormatOptions::default();
        assert!(opts.show_state);
        assert!(opts.show_assignee);
        assert!(opts.show_priority);
        assert!(!opts.show_ids);
        assert!(!opts.show_urls);
        assert!(!opts.show_dates);
        assert!(!opts.show_team);
    }

    #[test]
    fn format_options_csv_adds_to_defaults() {
        let opts = FormatOptions::from_csv("id,url");
        assert!(opts.show_ids);
        assert!(opts.show_urls);
        // defaults still true
        assert!(opts.show_state);
        assert!(opts.show_assignee);
        assert!(opts.show_priority);
    }

    #[test]
    fn create_issue_result_includes_url() {
        let result = CreateIssueResult {
            success: true,
            issue: Some(IssueSummary {
                id: "id".into(),
                identifier: "ENG-123".into(),
                title: "Test Issue".into(),
                url: "https://linear.app/team/issue/ENG-123".into(),
                team: TeamRef {
                    id: "team-id".into(),
                    key: "ENG".into(),
                    name: "Engineering".into(),
                },
                state: None,
                assignee: None,
                creator: None,
                project: None,
                priority: 3,
                priority_label: "Normal".into(),
                label_ids: vec![],
                due_date: None,
                created_at: "2024-03-27T10:00:00Z".into(),
                updated_at: "2024-03-27T10:00:00Z".into(),
            }),
        };
        let text = result.fmt_text(&TextOptions::default());
        assert!(text.contains("ENG-123"));
        assert!(text.contains("URL: https://linear.app/team/issue/ENG-123"));
    }

    #[test]
    fn issue_result_includes_url() {
        let result = IssueResult {
            issue: IssueSummary {
                id: "id".into(),
                identifier: "ENG-456".into(),
                title: "Updated Issue".into(),
                url: "https://linear.app/team/issue/ENG-456".into(),
                team: TeamRef {
                    id: "team-id".into(),
                    key: "ENG".into(),
                    name: "Engineering".into(),
                },
                state: None,
                assignee: None,
                creator: None,
                project: None,
                priority: 3,
                priority_label: "Normal".into(),
                label_ids: vec![],
                due_date: None,
                created_at: "2024-03-27T10:00:00Z".into(),
                updated_at: "2024-03-27T10:00:00Z".into(),
            },
        };
        let text = result.fmt_text(&TextOptions::default());
        assert!(text.contains("ENG-456"));
        assert!(text.contains("URL: https://linear.app/team/issue/ENG-456"));
    }

    #[test]
    fn comments_result_formats_empty() {
        let result = CommentsResult {
            issue_identifier: "ENG-123".into(),
            comments: vec![],
            shown_comments: 0,
            total_comments: 0,
            has_more: false,
        };
        let text = result.fmt_text(&TextOptions::default());
        assert!(text.contains("No comments on ENG-123"));
    }

    #[test]
    fn comments_result_formats_with_reply_indent() {
        let result = CommentsResult {
            issue_identifier: "ENG-123".into(),
            comments: vec![
                CommentSummary {
                    id: "c1".into(),
                    body: "Parent comment".into(),
                    url: "https://linear.app/...".into(),
                    created_at: "2024-03-27T10:00:00Z".into(),
                    updated_at: "2024-03-27T10:00:00Z".into(),
                    parent_id: None,
                    author_name: Some("Alice".into()),
                    author_email: Some("alice@example.com".into()),
                },
                CommentSummary {
                    id: "c2".into(),
                    body: "Reply comment".into(),
                    url: "https://linear.app/...".into(),
                    created_at: "2024-03-27T10:15:00Z".into(),
                    updated_at: "2024-03-27T10:15:00Z".into(),
                    parent_id: Some("c1".into()),
                    author_name: Some("Bob".into()),
                    author_email: Some("bob@example.com".into()),
                },
            ],
            shown_comments: 2,
            total_comments: 2,
            has_more: false,
        };
        let text = result.fmt_text(&TextOptions::default());
        assert!(text.contains("Alice"));
        assert!(text.contains("↳")); // Reply has indent
        assert!(text.contains("Bob"));
    }

    #[test]
    fn comments_result_shows_pagination_message() {
        let result = CommentsResult {
            issue_identifier: "ENG-123".into(),
            comments: vec![CommentSummary {
                id: "c1".into(),
                body: "Test comment".into(),
                url: "https://linear.app/...".into(),
                created_at: "2024-03-27T10:00:00Z".into(),
                updated_at: "2024-03-27T10:00:00Z".into(),
                parent_id: None,
                author_name: Some("Alice".into()),
                author_email: None,
            }],
            shown_comments: 10,
            total_comments: 15,
            has_more: true,
        };
        let text = result.fmt_text(&TextOptions::default());
        assert!(text.contains("more comments available"));
    }
}
