use crate::cli::render::{self, Theme};
use rusqlite::Connection;
use serde::Serialize;

use crate::Config;
use crate::cli::robot::RobotMeta;
use crate::core::db::create_connection;
use crate::core::error::{LoreError, Result};
use crate::core::paths::get_db_path;
use crate::core::project::resolve_project;
use crate::core::time::ms_to_iso;

#[derive(Debug, Serialize)]
pub struct MrDetail {
    pub id: i64,
    pub iid: i64,
    pub title: String,
    pub description: Option<String>,
    pub state: String,
    pub draft: bool,
    pub author_username: String,
    pub source_branch: String,
    pub target_branch: String,
    pub created_at: i64,
    pub updated_at: i64,
    pub merged_at: Option<i64>,
    pub closed_at: Option<i64>,
    pub web_url: Option<String>,
    pub project_path: String,
    pub labels: Vec<String>,
    pub assignees: Vec<String>,
    pub reviewers: Vec<String>,
    pub discussions: Vec<MrDiscussionDetail>,
}

#[derive(Debug, Serialize)]
pub struct MrDiscussionDetail {
    pub notes: Vec<MrNoteDetail>,
    pub individual_note: bool,
}

#[derive(Debug, Serialize)]
pub struct MrNoteDetail {
    pub author_username: String,
    pub body: String,
    pub created_at: i64,
    pub is_system: bool,
    pub position: Option<DiffNotePosition>,
}

#[derive(Debug, Clone, Serialize)]
pub struct DiffNotePosition {
    pub old_path: Option<String>,
    pub new_path: Option<String>,
    pub old_line: Option<i64>,
    pub new_line: Option<i64>,
    pub position_type: Option<String>,
}

#[derive(Debug, Clone, Serialize)]
pub struct ClosingMrRef {
    pub iid: i64,
    pub title: String,
    pub state: String,
    pub web_url: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct IssueDetail {
    pub id: i64,
    pub iid: i64,
    pub title: String,
    pub description: Option<String>,
    pub state: String,
    pub author_username: String,
    pub created_at: i64,
    pub updated_at: i64,
    pub closed_at: Option<String>,
    pub confidential: bool,
    pub web_url: Option<String>,
    pub project_path: String,
    pub references_full: String,
    pub labels: Vec<String>,
    pub assignees: Vec<String>,
    pub due_date: Option<String>,
    pub milestone: Option<String>,
    pub user_notes_count: i64,
    pub merge_requests_count: usize,
    pub closing_merge_requests: Vec<ClosingMrRef>,
    pub discussions: Vec<DiscussionDetail>,
    pub status_name: Option<String>,
    pub status_category: Option<String>,
    pub status_color: Option<String>,
    pub status_icon_name: Option<String>,
    pub status_synced_at: Option<i64>,
}

#[derive(Debug, Serialize)]
pub struct DiscussionDetail {
    pub notes: Vec<NoteDetail>,
    pub individual_note: bool,
}

#[derive(Debug, Serialize)]
pub struct NoteDetail {
    pub author_username: String,
    pub body: String,
    pub created_at: i64,
    pub is_system: bool,
}

pub fn run_show_issue(
    config: &Config,
    iid: i64,
    project_filter: Option<&str>,
) -> Result<IssueDetail> {
    let db_path = get_db_path(config.storage.db_path.as_deref());
    let conn = create_connection(&db_path)?;

    let issue = find_issue(&conn, iid, project_filter)?;

    let labels = get_issue_labels(&conn, issue.id)?;

    let assignees = get_issue_assignees(&conn, issue.id)?;

    let closing_mrs = get_closing_mrs(&conn, issue.id)?;

    let discussions = get_issue_discussions(&conn, issue.id)?;

    let references_full = format!("{}#{}", issue.project_path, issue.iid);
    let merge_requests_count = closing_mrs.len();

    Ok(IssueDetail {
        id: issue.id,
        iid: issue.iid,
        title: issue.title,
        description: issue.description,
        state: issue.state,
        author_username: issue.author_username,
        created_at: issue.created_at,
        updated_at: issue.updated_at,
        closed_at: issue.closed_at,
        confidential: issue.confidential,
        web_url: issue.web_url,
        project_path: issue.project_path,
        references_full,
        labels,
        assignees,
        due_date: issue.due_date,
        milestone: issue.milestone_title,
        user_notes_count: issue.user_notes_count,
        merge_requests_count,
        closing_merge_requests: closing_mrs,
        discussions,
        status_name: issue.status_name,
        status_category: issue.status_category,
        status_color: issue.status_color,
        status_icon_name: issue.status_icon_name,
        status_synced_at: issue.status_synced_at,
    })
}

#[derive(Debug)]
struct IssueRow {
    id: i64,
    iid: i64,
    title: String,
    description: Option<String>,
    state: String,
    author_username: String,
    created_at: i64,
    updated_at: i64,
    closed_at: Option<String>,
    confidential: bool,
    web_url: Option<String>,
    project_path: String,
    due_date: Option<String>,
    milestone_title: Option<String>,
    user_notes_count: i64,
    status_name: Option<String>,
    status_category: Option<String>,
    status_color: Option<String>,
    status_icon_name: Option<String>,
    status_synced_at: Option<i64>,
}

fn find_issue(conn: &Connection, iid: i64, project_filter: Option<&str>) -> Result<IssueRow> {
    let (sql, params): (&str, Vec<Box<dyn rusqlite::ToSql>>) = match project_filter {
        Some(project) => {
            let project_id = resolve_project(conn, project)?;
            (
                "SELECT i.id, i.iid, i.title, i.description, i.state, i.author_username,
                        i.created_at, i.updated_at, i.closed_at, i.confidential,
                        i.web_url, p.path_with_namespace,
                        i.due_date, i.milestone_title,
                        (SELECT COUNT(*) FROM notes n
                         JOIN discussions d ON n.discussion_id = d.id
                         WHERE d.noteable_type = 'Issue' AND d.issue_id = i.id AND n.is_system = 0) AS user_notes_count,
                        i.status_name, i.status_category, i.status_color,
                        i.status_icon_name, i.status_synced_at
                 FROM issues i
                 JOIN projects p ON i.project_id = p.id
                 WHERE i.iid = ? AND i.project_id = ?",
                vec![Box::new(iid), Box::new(project_id)],
            )
        }
        None => (
            "SELECT i.id, i.iid, i.title, i.description, i.state, i.author_username,
                    i.created_at, i.updated_at, i.closed_at, i.confidential,
                    i.web_url, p.path_with_namespace,
                    i.due_date, i.milestone_title,
                    (SELECT COUNT(*) FROM notes n
                     JOIN discussions d ON n.discussion_id = d.id
                     WHERE d.noteable_type = 'Issue' AND d.issue_id = i.id AND n.is_system = 0) AS user_notes_count,
                    i.status_name, i.status_category, i.status_color,
                    i.status_icon_name, i.status_synced_at
             FROM issues i
             JOIN projects p ON i.project_id = p.id
             WHERE i.iid = ?",
            vec![Box::new(iid)],
        ),
    };

    let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect();

    let mut stmt = conn.prepare(sql)?;
    let issues: Vec<IssueRow> = stmt
        .query_map(param_refs.as_slice(), |row| {
            let confidential_val: i64 = row.get(9)?;
            Ok(IssueRow {
                id: row.get(0)?,
                iid: row.get(1)?,
                title: row.get(2)?,
                description: row.get(3)?,
                state: row.get(4)?,
                author_username: row.get(5)?,
                created_at: row.get(6)?,
                updated_at: row.get(7)?,
                closed_at: row.get(8)?,
                confidential: confidential_val != 0,
                web_url: row.get(10)?,
                project_path: row.get(11)?,
                due_date: row.get(12)?,
                milestone_title: row.get(13)?,
                user_notes_count: row.get(14)?,
                status_name: row.get(15)?,
                status_category: row.get(16)?,
                status_color: row.get(17)?,
                status_icon_name: row.get(18)?,
                status_synced_at: row.get(19)?,
            })
        })?
        .collect::<std::result::Result<Vec<_>, _>>()?;

    match issues.len() {
        0 => Err(LoreError::NotFound(format!("Issue #{} not found", iid))),
        1 => Ok(issues.into_iter().next().unwrap()),
        _ => {
            let projects: Vec<String> = issues.iter().map(|i| i.project_path.clone()).collect();
            Err(LoreError::Ambiguous(format!(
                "Issue #{} exists in multiple projects: {}. Use --project to specify.",
                iid,
                projects.join(", ")
            )))
        }
    }
}

fn get_issue_labels(conn: &Connection, issue_id: i64) -> Result<Vec<String>> {
    let mut stmt = conn.prepare(
        "SELECT l.name FROM labels l
         JOIN issue_labels il ON l.id = il.label_id
         WHERE il.issue_id = ?
         ORDER BY l.name",
    )?;

    let labels: Vec<String> = stmt
        .query_map([issue_id], |row| row.get(0))?
        .collect::<std::result::Result<Vec<_>, _>>()?;

    Ok(labels)
}

fn get_issue_assignees(conn: &Connection, issue_id: i64) -> Result<Vec<String>> {
    let mut stmt = conn.prepare(
        "SELECT username FROM issue_assignees
         WHERE issue_id = ?
         ORDER BY username",
    )?;

    let assignees: Vec<String> = stmt
        .query_map([issue_id], |row| row.get(0))?
        .collect::<std::result::Result<Vec<_>, _>>()?;

    Ok(assignees)
}

fn get_closing_mrs(conn: &Connection, issue_id: i64) -> Result<Vec<ClosingMrRef>> {
    let mut stmt = conn.prepare(
        "SELECT mr.iid, mr.title, mr.state, mr.web_url
         FROM entity_references er
         JOIN merge_requests mr ON mr.id = er.source_entity_id
         WHERE er.target_entity_type = 'issue'
           AND er.target_entity_id = ?
           AND er.source_entity_type = 'merge_request'
           AND er.reference_type = 'closes'
         ORDER BY mr.iid",
    )?;

    let mrs: Vec<ClosingMrRef> = stmt
        .query_map([issue_id], |row| {
            Ok(ClosingMrRef {
                iid: row.get(0)?,
                title: row.get(1)?,
                state: row.get(2)?,
                web_url: row.get(3)?,
            })
        })?
        .collect::<std::result::Result<Vec<_>, _>>()?;

    Ok(mrs)
}

fn get_issue_discussions(conn: &Connection, issue_id: i64) -> Result<Vec<DiscussionDetail>> {
    let mut disc_stmt = conn.prepare(
        "SELECT id, individual_note FROM discussions
         WHERE issue_id = ?
         ORDER BY first_note_at",
    )?;

    let disc_rows: Vec<(i64, bool)> = disc_stmt
        .query_map([issue_id], |row| {
            let individual: i64 = row.get(1)?;
            Ok((row.get(0)?, individual == 1))
        })?
        .collect::<std::result::Result<Vec<_>, _>>()?;

    let mut note_stmt = conn.prepare(
        "SELECT author_username, body, created_at, is_system
         FROM notes
         WHERE discussion_id = ?
         ORDER BY position",
    )?;

    let mut discussions = Vec::new();
    for (disc_id, individual_note) in disc_rows {
        let notes: Vec<NoteDetail> = note_stmt
            .query_map([disc_id], |row| {
                let is_system: i64 = row.get(3)?;
                Ok(NoteDetail {
                    author_username: row.get(0)?,
                    body: row.get(1)?,
                    created_at: row.get(2)?,
                    is_system: is_system == 1,
                })
            })?
            .collect::<std::result::Result<Vec<_>, _>>()?;

        let has_user_notes = notes.iter().any(|n| !n.is_system);
        if has_user_notes || notes.is_empty() {
            discussions.push(DiscussionDetail {
                notes,
                individual_note,
            });
        }
    }

    Ok(discussions)
}

pub fn run_show_mr(config: &Config, iid: i64, project_filter: Option<&str>) -> Result<MrDetail> {
    let db_path = get_db_path(config.storage.db_path.as_deref());
    let conn = create_connection(&db_path)?;

    let mr = find_mr(&conn, iid, project_filter)?;

    let labels = get_mr_labels(&conn, mr.id)?;

    let assignees = get_mr_assignees(&conn, mr.id)?;

    let reviewers = get_mr_reviewers(&conn, mr.id)?;

    let discussions = get_mr_discussions(&conn, mr.id)?;

    Ok(MrDetail {
        id: mr.id,
        iid: mr.iid,
        title: mr.title,
        description: mr.description,
        state: mr.state,
        draft: mr.draft,
        author_username: mr.author_username,
        source_branch: mr.source_branch,
        target_branch: mr.target_branch,
        created_at: mr.created_at,
        updated_at: mr.updated_at,
        merged_at: mr.merged_at,
        closed_at: mr.closed_at,
        web_url: mr.web_url,
        project_path: mr.project_path,
        labels,
        assignees,
        reviewers,
        discussions,
    })
}

struct MrRow {
    id: i64,
    iid: i64,
    title: String,
    description: Option<String>,
    state: String,
    draft: bool,
    author_username: String,
    source_branch: String,
    target_branch: String,
    created_at: i64,
    updated_at: i64,
    merged_at: Option<i64>,
    closed_at: Option<i64>,
    web_url: Option<String>,
    project_path: String,
}

fn find_mr(conn: &Connection, iid: i64, project_filter: Option<&str>) -> Result<MrRow> {
    let (sql, params): (&str, Vec<Box<dyn rusqlite::ToSql>>) = match project_filter {
        Some(project) => {
            let project_id = resolve_project(conn, project)?;
            (
                "SELECT m.id, m.iid, m.title, m.description, m.state, m.draft,
                        m.author_username, m.source_branch, m.target_branch,
                        m.created_at, m.updated_at, m.merged_at, m.closed_at,
                        m.web_url, p.path_with_namespace
                 FROM merge_requests m
                 JOIN projects p ON m.project_id = p.id
                 WHERE m.iid = ? AND m.project_id = ?",
                vec![Box::new(iid), Box::new(project_id)],
            )
        }
        None => (
            "SELECT m.id, m.iid, m.title, m.description, m.state, m.draft,
                    m.author_username, m.source_branch, m.target_branch,
                    m.created_at, m.updated_at, m.merged_at, m.closed_at,
                    m.web_url, p.path_with_namespace
             FROM merge_requests m
             JOIN projects p ON m.project_id = p.id
             WHERE m.iid = ?",
            vec![Box::new(iid)],
        ),
    };

    let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect();

    let mut stmt = conn.prepare(sql)?;
    let mrs: Vec<MrRow> = stmt
        .query_map(param_refs.as_slice(), |row| {
            let draft_val: i64 = row.get(5)?;
            Ok(MrRow {
                id: row.get(0)?,
                iid: row.get(1)?,
                title: row.get(2)?,
                description: row.get(3)?,
                state: row.get(4)?,
                draft: draft_val == 1,
                author_username: row.get(6)?,
                source_branch: row.get(7)?,
                target_branch: row.get(8)?,
                created_at: row.get(9)?,
                updated_at: row.get(10)?,
                merged_at: row.get(11)?,
                closed_at: row.get(12)?,
                web_url: row.get(13)?,
                project_path: row.get(14)?,
            })
        })?
        .collect::<std::result::Result<Vec<_>, _>>()?;

    match mrs.len() {
        0 => Err(LoreError::NotFound(format!("MR !{} not found", iid))),
        1 => Ok(mrs.into_iter().next().unwrap()),
        _ => {
            let projects: Vec<String> = mrs.iter().map(|m| m.project_path.clone()).collect();
            Err(LoreError::Ambiguous(format!(
                "MR !{} exists in multiple projects: {}. Use --project to specify.",
                iid,
                projects.join(", ")
            )))
        }
    }
}

fn get_mr_labels(conn: &Connection, mr_id: i64) -> Result<Vec<String>> {
    let mut stmt = conn.prepare(
        "SELECT l.name FROM labels l
         JOIN mr_labels ml ON l.id = ml.label_id
         WHERE ml.merge_request_id = ?
         ORDER BY l.name",
    )?;

    let labels: Vec<String> = stmt
        .query_map([mr_id], |row| row.get(0))?
        .collect::<std::result::Result<Vec<_>, _>>()?;

    Ok(labels)
}

fn get_mr_assignees(conn: &Connection, mr_id: i64) -> Result<Vec<String>> {
    let mut stmt = conn.prepare(
        "SELECT username FROM mr_assignees
         WHERE merge_request_id = ?
         ORDER BY username",
    )?;

    let assignees: Vec<String> = stmt
        .query_map([mr_id], |row| row.get(0))?
        .collect::<std::result::Result<Vec<_>, _>>()?;

    Ok(assignees)
}

fn get_mr_reviewers(conn: &Connection, mr_id: i64) -> Result<Vec<String>> {
    let mut stmt = conn.prepare(
        "SELECT username FROM mr_reviewers
         WHERE merge_request_id = ?
         ORDER BY username",
    )?;

    let reviewers: Vec<String> = stmt
        .query_map([mr_id], |row| row.get(0))?
        .collect::<std::result::Result<Vec<_>, _>>()?;

    Ok(reviewers)
}

fn get_mr_discussions(conn: &Connection, mr_id: i64) -> Result<Vec<MrDiscussionDetail>> {
    let mut disc_stmt = conn.prepare(
        "SELECT id, individual_note FROM discussions
         WHERE merge_request_id = ?
         ORDER BY first_note_at",
    )?;

    let disc_rows: Vec<(i64, bool)> = disc_stmt
        .query_map([mr_id], |row| {
            let individual: i64 = row.get(1)?;
            Ok((row.get(0)?, individual == 1))
        })?
        .collect::<std::result::Result<Vec<_>, _>>()?;

    let mut note_stmt = conn.prepare(
        "SELECT author_username, body, created_at, is_system,
                position_old_path, position_new_path, position_old_line,
                position_new_line, position_type
         FROM notes
         WHERE discussion_id = ?
         ORDER BY position",
    )?;

    let mut discussions = Vec::new();
    for (disc_id, individual_note) in disc_rows {
        let notes: Vec<MrNoteDetail> = note_stmt
            .query_map([disc_id], |row| {
                let is_system: i64 = row.get(3)?;
                let old_path: Option<String> = row.get(4)?;
                let new_path: Option<String> = row.get(5)?;
                let old_line: Option<i64> = row.get(6)?;
                let new_line: Option<i64> = row.get(7)?;
                let position_type: Option<String> = row.get(8)?;

                let position = if old_path.is_some()
                    || new_path.is_some()
                    || old_line.is_some()
                    || new_line.is_some()
                {
                    Some(DiffNotePosition {
                        old_path,
                        new_path,
                        old_line,
                        new_line,
                        position_type,
                    })
                } else {
                    None
                };

                Ok(MrNoteDetail {
                    author_username: row.get(0)?,
                    body: row.get(1)?,
                    created_at: row.get(2)?,
                    is_system: is_system == 1,
                    position,
                })
            })?
            .collect::<std::result::Result<Vec<_>, _>>()?;

        let has_user_notes = notes.iter().any(|n| !n.is_system);
        if has_user_notes || notes.is_empty() {
            discussions.push(MrDiscussionDetail {
                notes,
                individual_note,
            });
        }
    }

    Ok(discussions)
}

fn format_date(ms: i64) -> String {
    render::format_date(ms)
}

fn wrap_text(text: &str, width: usize, indent: &str) -> String {
    render::wrap_indent(text, width, indent)
}

pub fn print_show_issue(issue: &IssueDetail) {
    let header = format!("Issue #{}: {}", issue.iid, issue.title);
    println!("{}", Theme::bold().render(&header));
    println!("{}", "\u{2501}".repeat(header.len().min(80)));
    println!();

    println!("Ref:      {}", Theme::dim().render(&issue.references_full));
    println!("Project:  {}", Theme::info().render(&issue.project_path));

    let state_styled = if issue.state == "opened" {
        Theme::success().render(&issue.state)
    } else {
        Theme::dim().render(&issue.state)
    };
    println!("State:    {}", state_styled);

    if issue.confidential {
        println!("          {}", Theme::error().bold().render("CONFIDENTIAL"));
    }

    if let Some(status) = &issue.status_name {
        println!(
            "Status:   {}",
            render::style_with_hex(status, issue.status_color.as_deref())
        );
    }

    println!("Author:   @{}", issue.author_username);

    if !issue.assignees.is_empty() {
        let label = if issue.assignees.len() > 1 {
            "Assignees"
        } else {
            "Assignee"
        };
        println!(
            "{}:{} {}",
            label,
            " ".repeat(10 - label.len()),
            issue
                .assignees
                .iter()
                .map(|a| format!("@{}", a))
                .collect::<Vec<_>>()
                .join(", ")
        );
    }

    println!("Created:  {}", format_date(issue.created_at));
    println!("Updated:  {}", format_date(issue.updated_at));

    if let Some(closed_at) = &issue.closed_at {
        println!("Closed:   {}", closed_at);
    }

    if let Some(due) = &issue.due_date {
        println!("Due:      {}", due);
    }

    if let Some(ms) = &issue.milestone {
        println!("Milestone: {}", ms);
    }

    if issue.labels.is_empty() {
        println!("Labels:   {}", Theme::dim().render("(none)"));
    } else {
        println!("Labels:   {}", issue.labels.join(", "));
    }

    if !issue.closing_merge_requests.is_empty() {
        println!();
        println!("{}", Theme::bold().render("Development:"));
        for mr in &issue.closing_merge_requests {
            let state_indicator = match mr.state.as_str() {
                "merged" => Theme::success().render(&mr.state),
                "opened" => Theme::info().render(&mr.state),
                "closed" => Theme::error().render(&mr.state),
                _ => Theme::dim().render(&mr.state),
            };
            println!("  !{} {} ({})", mr.iid, mr.title, state_indicator);
        }
    }

    if let Some(url) = &issue.web_url {
        println!("URL:      {}", Theme::dim().render(url));
    }

    println!();

    println!("{}", Theme::bold().render("Description:"));
    if let Some(desc) = &issue.description {
        let wrapped = wrap_text(desc, 76, "  ");
        println!("  {}", wrapped);
    } else {
        println!("  {}", Theme::dim().render("(no description)"));
    }

    println!();

    let user_discussions: Vec<&DiscussionDetail> = issue
        .discussions
        .iter()
        .filter(|d| d.notes.iter().any(|n| !n.is_system))
        .collect();

    if user_discussions.is_empty() {
        println!("{}", Theme::dim().render("Discussions: (none)"));
    } else {
        println!(
            "{}",
            Theme::bold().render(&format!("Discussions ({}):", user_discussions.len()))
        );
        println!();

        for discussion in user_discussions {
            let user_notes: Vec<&NoteDetail> =
                discussion.notes.iter().filter(|n| !n.is_system).collect();

            if let Some(first_note) = user_notes.first() {
                println!(
                    "  {} ({}):",
                    Theme::info().render(&format!("@{}", first_note.author_username)),
                    format_date(first_note.created_at)
                );
                let wrapped = wrap_text(&first_note.body, 72, "    ");
                println!("    {}", wrapped);
                println!();

                for reply in user_notes.iter().skip(1) {
                    println!(
                        "      {} ({}):",
                        Theme::info().render(&format!("@{}", reply.author_username)),
                        format_date(reply.created_at)
                    );
                    let wrapped = wrap_text(&reply.body, 68, "        ");
                    println!("        {}", wrapped);
                    println!();
                }
            }
        }
    }
}

pub fn print_show_mr(mr: &MrDetail) {
    let draft_prefix = if mr.draft { "[Draft] " } else { "" };
    let header = format!("MR !{}: {}{}", mr.iid, draft_prefix, mr.title);
    println!("{}", Theme::bold().render(&header));
    println!("{}", "\u{2501}".repeat(header.len().min(80)));
    println!();

    println!("Project:  {}", Theme::info().render(&mr.project_path));

    let state_styled = match mr.state.as_str() {
        "opened" => Theme::success().render(&mr.state),
        "merged" => Theme::accent().render(&mr.state),
        "closed" => Theme::error().render(&mr.state),
        _ => Theme::dim().render(&mr.state),
    };
    println!("State:    {}", state_styled);

    println!(
        "Branches: {} -> {}",
        Theme::info().render(&mr.source_branch),
        Theme::warning().render(&mr.target_branch)
    );

    println!("Author:   @{}", mr.author_username);

    if !mr.assignees.is_empty() {
        println!(
            "Assignees: {}",
            mr.assignees
                .iter()
                .map(|a| format!("@{}", a))
                .collect::<Vec<_>>()
                .join(", ")
        );
    }

    if !mr.reviewers.is_empty() {
        println!(
            "Reviewers: {}",
            mr.reviewers
                .iter()
                .map(|r| format!("@{}", r))
                .collect::<Vec<_>>()
                .join(", ")
        );
    }

    println!("Created:  {}", format_date(mr.created_at));
    println!("Updated:  {}", format_date(mr.updated_at));

    if let Some(merged_at) = mr.merged_at {
        println!("Merged:   {}", format_date(merged_at));
    }

    if let Some(closed_at) = mr.closed_at {
        println!("Closed:   {}", format_date(closed_at));
    }

    if mr.labels.is_empty() {
        println!("Labels:   {}", Theme::dim().render("(none)"));
    } else {
        println!("Labels:   {}", mr.labels.join(", "));
    }

    if let Some(url) = &mr.web_url {
        println!("URL:      {}", Theme::dim().render(url));
    }

    println!();

    println!("{}", Theme::bold().render("Description:"));
    if let Some(desc) = &mr.description {
        let wrapped = wrap_text(desc, 76, "  ");
        println!("  {}", wrapped);
    } else {
        println!("  {}", Theme::dim().render("(no description)"));
    }

    println!();

    let user_discussions: Vec<&MrDiscussionDetail> = mr
        .discussions
        .iter()
        .filter(|d| d.notes.iter().any(|n| !n.is_system))
        .collect();

    if user_discussions.is_empty() {
        println!("{}", Theme::dim().render("Discussions: (none)"));
    } else {
        println!(
            "{}",
            Theme::bold().render(&format!("Discussions ({}):", user_discussions.len()))
        );
        println!();

        for discussion in user_discussions {
            let user_notes: Vec<&MrNoteDetail> =
                discussion.notes.iter().filter(|n| !n.is_system).collect();

            if let Some(first_note) = user_notes.first() {
                if let Some(pos) = &first_note.position {
                    print_diff_position(pos);
                }

                println!(
                    "  {} ({}):",
                    Theme::info().render(&format!("@{}", first_note.author_username)),
                    format_date(first_note.created_at)
                );
                let wrapped = wrap_text(&first_note.body, 72, "    ");
                println!("    {}", wrapped);
                println!();

                for reply in user_notes.iter().skip(1) {
                    println!(
                        "      {} ({}):",
                        Theme::info().render(&format!("@{}", reply.author_username)),
                        format_date(reply.created_at)
                    );
                    let wrapped = wrap_text(&reply.body, 68, "        ");
                    println!("        {}", wrapped);
                    println!();
                }
            }
        }
    }
}

fn print_diff_position(pos: &DiffNotePosition) {
    let file = pos.new_path.as_ref().or(pos.old_path.as_ref());

    if let Some(file_path) = file {
        let line_str = match (pos.old_line, pos.new_line) {
            (Some(old), Some(new)) if old == new => format!(":{}", new),
            (Some(old), Some(new)) => format!(":{}→{}", old, new),
            (None, Some(new)) => format!(":+{}", new),
            (Some(old), None) => format!(":-{}", old),
            (None, None) => String::new(),
        };

        println!(
            "  {} {}{}",
            Theme::dim().render("\u{1f4cd}"),
            Theme::warning().render(file_path),
            Theme::dim().render(&line_str)
        );
    }
}

#[derive(Serialize)]
pub struct IssueDetailJson {
    pub id: i64,
    pub iid: i64,
    pub title: String,
    pub description: Option<String>,
    pub state: String,
    pub author_username: String,
    pub created_at: String,
    pub updated_at: String,
    pub closed_at: Option<String>,
    pub confidential: bool,
    pub web_url: Option<String>,
    pub project_path: String,
    pub references_full: String,
    pub labels: Vec<String>,
    pub assignees: Vec<String>,
    pub due_date: Option<String>,
    pub milestone: Option<String>,
    pub user_notes_count: i64,
    pub merge_requests_count: usize,
    pub closing_merge_requests: Vec<ClosingMrRefJson>,
    pub discussions: Vec<DiscussionDetailJson>,
    pub status_name: Option<String>,
    #[serde(skip_serializing)]
    pub status_category: Option<String>,
    pub status_color: Option<String>,
    pub status_icon_name: Option<String>,
    pub status_synced_at: Option<String>,
}

#[derive(Serialize)]
pub struct ClosingMrRefJson {
    pub iid: i64,
    pub title: String,
    pub state: String,
    pub web_url: Option<String>,
}

#[derive(Serialize)]
pub struct DiscussionDetailJson {
    pub notes: Vec<NoteDetailJson>,
    pub individual_note: bool,
}

#[derive(Serialize)]
pub struct NoteDetailJson {
    pub author_username: String,
    pub body: String,
    pub created_at: String,
    pub is_system: bool,
}

impl From<&IssueDetail> for IssueDetailJson {
    fn from(issue: &IssueDetail) -> Self {
        Self {
            id: issue.id,
            iid: issue.iid,
            title: issue.title.clone(),
            description: issue.description.clone(),
            state: issue.state.clone(),
            author_username: issue.author_username.clone(),
            created_at: ms_to_iso(issue.created_at),
            updated_at: ms_to_iso(issue.updated_at),
            closed_at: issue.closed_at.clone(),
            confidential: issue.confidential,
            web_url: issue.web_url.clone(),
            project_path: issue.project_path.clone(),
            references_full: issue.references_full.clone(),
            labels: issue.labels.clone(),
            assignees: issue.assignees.clone(),
            due_date: issue.due_date.clone(),
            milestone: issue.milestone.clone(),
            user_notes_count: issue.user_notes_count,
            merge_requests_count: issue.merge_requests_count,
            closing_merge_requests: issue
                .closing_merge_requests
                .iter()
                .map(|mr| ClosingMrRefJson {
                    iid: mr.iid,
                    title: mr.title.clone(),
                    state: mr.state.clone(),
                    web_url: mr.web_url.clone(),
                })
                .collect(),
            discussions: issue.discussions.iter().map(|d| d.into()).collect(),
            status_name: issue.status_name.clone(),
            status_category: issue.status_category.clone(),
            status_color: issue.status_color.clone(),
            status_icon_name: issue.status_icon_name.clone(),
            status_synced_at: issue.status_synced_at.map(ms_to_iso),
        }
    }
}

impl From<&DiscussionDetail> for DiscussionDetailJson {
    fn from(disc: &DiscussionDetail) -> Self {
        Self {
            notes: disc.notes.iter().map(|n| n.into()).collect(),
            individual_note: disc.individual_note,
        }
    }
}

impl From<&NoteDetail> for NoteDetailJson {
    fn from(note: &NoteDetail) -> Self {
        Self {
            author_username: note.author_username.clone(),
            body: note.body.clone(),
            created_at: ms_to_iso(note.created_at),
            is_system: note.is_system,
        }
    }
}

#[derive(Serialize)]
pub struct MrDetailJson {
    pub id: i64,
    pub iid: i64,
    pub title: String,
    pub description: Option<String>,
    pub state: String,
    pub draft: bool,
    pub author_username: String,
    pub source_branch: String,
    pub target_branch: String,
    pub created_at: String,
    pub updated_at: String,
    pub merged_at: Option<String>,
    pub closed_at: Option<String>,
    pub web_url: Option<String>,
    pub project_path: String,
    pub labels: Vec<String>,
    pub assignees: Vec<String>,
    pub reviewers: Vec<String>,
    pub discussions: Vec<MrDiscussionDetailJson>,
}

#[derive(Serialize)]
pub struct MrDiscussionDetailJson {
    pub notes: Vec<MrNoteDetailJson>,
    pub individual_note: bool,
}

#[derive(Serialize)]
pub struct MrNoteDetailJson {
    pub author_username: String,
    pub body: String,
    pub created_at: String,
    pub is_system: bool,
    pub position: Option<DiffNotePosition>,
}

impl From<&MrDetail> for MrDetailJson {
    fn from(mr: &MrDetail) -> Self {
        Self {
            id: mr.id,
            iid: mr.iid,
            title: mr.title.clone(),
            description: mr.description.clone(),
            state: mr.state.clone(),
            draft: mr.draft,
            author_username: mr.author_username.clone(),
            source_branch: mr.source_branch.clone(),
            target_branch: mr.target_branch.clone(),
            created_at: ms_to_iso(mr.created_at),
            updated_at: ms_to_iso(mr.updated_at),
            merged_at: mr.merged_at.map(ms_to_iso),
            closed_at: mr.closed_at.map(ms_to_iso),
            web_url: mr.web_url.clone(),
            project_path: mr.project_path.clone(),
            labels: mr.labels.clone(),
            assignees: mr.assignees.clone(),
            reviewers: mr.reviewers.clone(),
            discussions: mr.discussions.iter().map(|d| d.into()).collect(),
        }
    }
}

impl From<&MrDiscussionDetail> for MrDiscussionDetailJson {
    fn from(disc: &MrDiscussionDetail) -> Self {
        Self {
            notes: disc.notes.iter().map(|n| n.into()).collect(),
            individual_note: disc.individual_note,
        }
    }
}

impl From<&MrNoteDetail> for MrNoteDetailJson {
    fn from(note: &MrNoteDetail) -> Self {
        Self {
            author_username: note.author_username.clone(),
            body: note.body.clone(),
            created_at: ms_to_iso(note.created_at),
            is_system: note.is_system,
            position: note.position.clone(),
        }
    }
}

pub fn print_show_issue_json(issue: &IssueDetail, elapsed_ms: u64) {
    let json_result = IssueDetailJson::from(issue);
    let meta = RobotMeta { elapsed_ms };
    let output = serde_json::json!({
        "ok": true,
        "data": json_result,
        "meta": meta,
    });
    match serde_json::to_string(&output) {
        Ok(json) => println!("{json}"),
        Err(e) => eprintln!("Error serializing to JSON: {e}"),
    }
}

pub fn print_show_mr_json(mr: &MrDetail, elapsed_ms: u64) {
    let json_result = MrDetailJson::from(mr);
    let meta = RobotMeta { elapsed_ms };
    let output = serde_json::json!({
        "ok": true,
        "data": json_result,
        "meta": meta,
    });
    match serde_json::to_string(&output) {
        Ok(json) => println!("{json}"),
        Err(e) => eprintln!("Error serializing to JSON: {e}"),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::db::run_migrations;
    use std::path::Path;

    fn setup_test_db() -> Connection {
        let conn = create_connection(Path::new(":memory:")).unwrap();
        run_migrations(&conn).unwrap();
        conn
    }

    fn seed_project(conn: &Connection) {
        conn.execute(
            "INSERT INTO projects (id, gitlab_project_id, path_with_namespace, web_url, created_at, updated_at)
             VALUES (1, 100, 'group/repo', 'https://gitlab.example.com', 1000, 2000)",
            [],
        )
        .unwrap();
    }

    fn seed_issue(conn: &Connection) {
        seed_project(conn);
        conn.execute(
            "INSERT INTO issues (id, gitlab_id, iid, project_id, title, state, author_username,
             created_at, updated_at, last_seen_at)
             VALUES (1, 200, 10, 1, 'Test issue', 'opened', 'author', 1000, 2000, 2000)",
            [],
        )
        .unwrap();
    }

    fn seed_second_project(conn: &Connection) {
        conn.execute(
            "INSERT INTO projects (id, gitlab_project_id, path_with_namespace, web_url, created_at, updated_at)
             VALUES (2, 101, 'other/repo', 'https://gitlab.example.com/other', 1000, 2000)",
            [],
        )
        .unwrap();
    }

    fn seed_discussion_with_notes(
        conn: &Connection,
        issue_id: i64,
        project_id: i64,
        user_notes: usize,
        system_notes: usize,
    ) {
        let disc_id: i64 = conn
            .query_row(
                "SELECT COALESCE(MAX(id), 0) + 1 FROM discussions",
                [],
                |r| r.get(0),
            )
            .unwrap();
        conn.execute(
            "INSERT INTO discussions (id, gitlab_discussion_id, project_id, issue_id, noteable_type, first_note_at, last_note_at, last_seen_at)
             VALUES (?1, ?2, ?3, ?4, 'Issue', 1000, 2000, 2000)",
            rusqlite::params![disc_id, format!("disc-{}", disc_id), project_id, issue_id],
        )
        .unwrap();
        for i in 0..user_notes {
            conn.execute(
                "INSERT INTO notes (gitlab_id, discussion_id, project_id, author_username, body, created_at, updated_at, last_seen_at, is_system, position)
                 VALUES (?1, ?2, ?3, 'user1', 'comment', 1000, 2000, 2000, 0, ?4)",
                rusqlite::params![1000 + disc_id * 100 + i as i64, disc_id, project_id, i as i64],
            )
            .unwrap();
        }
        for i in 0..system_notes {
            conn.execute(
                "INSERT INTO notes (gitlab_id, discussion_id, project_id, author_username, body, created_at, updated_at, last_seen_at, is_system, position)
                 VALUES (?1, ?2, ?3, 'system', 'status changed', 1000, 2000, 2000, 1, ?4)",
                rusqlite::params![2000 + disc_id * 100 + i as i64, disc_id, project_id, (user_notes + i) as i64],
            )
            .unwrap();
        }
    }

    // --- find_issue tests ---

    #[test]
    fn test_find_issue_basic() {
        let conn = setup_test_db();
        seed_issue(&conn);
        let row = find_issue(&conn, 10, None).unwrap();
        assert_eq!(row.iid, 10);
        assert_eq!(row.title, "Test issue");
        assert_eq!(row.state, "opened");
        assert_eq!(row.author_username, "author");
        assert_eq!(row.project_path, "group/repo");
    }

    #[test]
    fn test_find_issue_with_project_filter() {
        let conn = setup_test_db();
        seed_issue(&conn);
        let row = find_issue(&conn, 10, Some("group/repo")).unwrap();
        assert_eq!(row.iid, 10);
        assert_eq!(row.project_path, "group/repo");
    }

    #[test]
    fn test_find_issue_not_found() {
        let conn = setup_test_db();
        seed_issue(&conn);
        let err = find_issue(&conn, 999, None).unwrap_err();
        assert!(matches!(err, LoreError::NotFound(_)));
    }

    #[test]
    fn test_find_issue_wrong_project_filter() {
        let conn = setup_test_db();
        seed_issue(&conn);
        seed_second_project(&conn);
        // Issue 10 only exists in project 1, not project 2
        let err = find_issue(&conn, 10, Some("other/repo")).unwrap_err();
        assert!(matches!(err, LoreError::NotFound(_)));
    }

    #[test]
    fn test_find_issue_ambiguous_without_project() {
        let conn = setup_test_db();
        seed_issue(&conn); // issue iid=10 in project 1
        seed_second_project(&conn);
        conn.execute(
            "INSERT INTO issues (id, gitlab_id, iid, project_id, title, state, author_username,
             created_at, updated_at, last_seen_at)
             VALUES (2, 201, 10, 2, 'Same iid different project', 'opened', 'author', 1000, 2000, 2000)",
            [],
        )
        .unwrap();
        let err = find_issue(&conn, 10, None).unwrap_err();
        assert!(matches!(err, LoreError::Ambiguous(_)));
    }

    #[test]
    fn test_find_issue_ambiguous_resolved_with_project() {
        let conn = setup_test_db();
        seed_issue(&conn);
        seed_second_project(&conn);
        conn.execute(
            "INSERT INTO issues (id, gitlab_id, iid, project_id, title, state, author_username,
             created_at, updated_at, last_seen_at)
             VALUES (2, 201, 10, 2, 'Same iid different project', 'opened', 'author', 1000, 2000, 2000)",
            [],
        )
        .unwrap();
        let row = find_issue(&conn, 10, Some("other/repo")).unwrap();
        assert_eq!(row.title, "Same iid different project");
    }

    #[test]
    fn test_find_issue_user_notes_count_zero() {
        let conn = setup_test_db();
        seed_issue(&conn);
        let row = find_issue(&conn, 10, None).unwrap();
        assert_eq!(row.user_notes_count, 0);
    }

    #[test]
    fn test_find_issue_user_notes_count_excludes_system() {
        let conn = setup_test_db();
        seed_issue(&conn);
        // 2 user notes + 3 system notes = should count only 2
        seed_discussion_with_notes(&conn, 1, 1, 2, 3);
        let row = find_issue(&conn, 10, None).unwrap();
        assert_eq!(row.user_notes_count, 2);
    }

    #[test]
    fn test_find_issue_user_notes_count_across_discussions() {
        let conn = setup_test_db();
        seed_issue(&conn);
        seed_discussion_with_notes(&conn, 1, 1, 3, 0); // 3 user notes
        seed_discussion_with_notes(&conn, 1, 1, 1, 2); // 1 user note + 2 system
        let row = find_issue(&conn, 10, None).unwrap();
        assert_eq!(row.user_notes_count, 4);
    }

    #[test]
    fn test_find_issue_notes_count_ignores_other_issues() {
        let conn = setup_test_db();
        seed_issue(&conn);
        // Add a second issue
        conn.execute(
            "INSERT INTO issues (id, gitlab_id, iid, project_id, title, state, author_username,
             created_at, updated_at, last_seen_at)
             VALUES (2, 201, 20, 1, 'Other issue', 'opened', 'author', 1000, 2000, 2000)",
            [],
        )
        .unwrap();
        // Notes on issue 2, not issue 1
        seed_discussion_with_notes(&conn, 2, 1, 5, 0);
        let row = find_issue(&conn, 10, None).unwrap();
        assert_eq!(row.user_notes_count, 0); // Issue 10 has no notes
    }

    #[test]
    fn test_ansi256_from_rgb() {
        // Moved to render.rs — keeping basic hex sanity check
        let result = render::style_with_hex("test", Some("#ff0000"));
        assert!(!result.is_empty());
    }

    #[test]
    fn test_get_issue_assignees_empty() {
        let conn = setup_test_db();
        seed_issue(&conn);
        let result = get_issue_assignees(&conn, 1).unwrap();
        assert!(result.is_empty());
    }

    #[test]
    fn test_get_issue_assignees_single() {
        let conn = setup_test_db();
        seed_issue(&conn);
        conn.execute(
            "INSERT INTO issue_assignees (issue_id, username) VALUES (1, 'charlie')",
            [],
        )
        .unwrap();
        let result = get_issue_assignees(&conn, 1).unwrap();
        assert_eq!(result, vec!["charlie"]);
    }

    #[test]
    fn test_get_issue_assignees_multiple_sorted() {
        let conn = setup_test_db();
        seed_issue(&conn);
        conn.execute(
            "INSERT INTO issue_assignees (issue_id, username) VALUES (1, 'bob')",
            [],
        )
        .unwrap();
        conn.execute(
            "INSERT INTO issue_assignees (issue_id, username) VALUES (1, 'alice')",
            [],
        )
        .unwrap();
        let result = get_issue_assignees(&conn, 1).unwrap();
        assert_eq!(result, vec!["alice", "bob"]); // alphabetical
    }

    #[test]
    fn test_get_closing_mrs_empty() {
        let conn = setup_test_db();
        seed_issue(&conn);
        let result = get_closing_mrs(&conn, 1).unwrap();
        assert!(result.is_empty());
    }

    #[test]
    fn test_get_closing_mrs_single() {
        let conn = setup_test_db();
        seed_issue(&conn);
        conn.execute(
            "INSERT INTO merge_requests (id, gitlab_id, iid, project_id, title, state, author_username,
             source_branch, target_branch, created_at, updated_at, last_seen_at)
             VALUES (1, 300, 5, 1, 'Fix the bug', 'merged', 'dev', 'fix', 'main', 1000, 2000, 2000)",
            [],
        )
        .unwrap();
        conn.execute(
            "INSERT INTO entity_references (project_id, source_entity_type, source_entity_id,
             target_entity_type, target_entity_id, reference_type, source_method, created_at)
             VALUES (1, 'merge_request', 1, 'issue', 1, 'closes', 'api', 3000)",
            [],
        )
        .unwrap();
        let result = get_closing_mrs(&conn, 1).unwrap();
        assert_eq!(result.len(), 1);
        assert_eq!(result[0].iid, 5);
        assert_eq!(result[0].title, "Fix the bug");
        assert_eq!(result[0].state, "merged");
    }

    #[test]
    fn test_get_closing_mrs_ignores_mentioned() {
        let conn = setup_test_db();
        seed_issue(&conn);
        // Add a 'mentioned' reference that should be ignored
        conn.execute(
            "INSERT INTO merge_requests (id, gitlab_id, iid, project_id, title, state, author_username,
             source_branch, target_branch, created_at, updated_at, last_seen_at)
             VALUES (1, 300, 5, 1, 'Some MR', 'opened', 'dev', 'feat', 'main', 1000, 2000, 2000)",
            [],
        )
        .unwrap();
        conn.execute(
            "INSERT INTO entity_references (project_id, source_entity_type, source_entity_id,
             target_entity_type, target_entity_id, reference_type, source_method, created_at)
             VALUES (1, 'merge_request', 1, 'issue', 1, 'mentioned', 'note_parse', 3000)",
            [],
        )
        .unwrap();
        let result = get_closing_mrs(&conn, 1).unwrap();
        assert!(result.is_empty()); // 'mentioned' refs not included
    }

    #[test]
    fn test_get_closing_mrs_multiple_sorted() {
        let conn = setup_test_db();
        seed_issue(&conn);
        conn.execute(
            "INSERT INTO merge_requests (id, gitlab_id, iid, project_id, title, state, author_username,
             source_branch, target_branch, created_at, updated_at, last_seen_at)
             VALUES (1, 300, 8, 1, 'Second fix', 'opened', 'dev', 'fix2', 'main', 1000, 2000, 2000)",
            [],
        )
        .unwrap();
        conn.execute(
            "INSERT INTO merge_requests (id, gitlab_id, iid, project_id, title, state, author_username,
             source_branch, target_branch, created_at, updated_at, last_seen_at)
             VALUES (2, 301, 5, 1, 'First fix', 'merged', 'dev', 'fix1', 'main', 1000, 2000, 2000)",
            [],
        )
        .unwrap();
        conn.execute(
            "INSERT INTO entity_references (project_id, source_entity_type, source_entity_id,
             target_entity_type, target_entity_id, reference_type, source_method, created_at)
             VALUES (1, 'merge_request', 1, 'issue', 1, 'closes', 'api', 3000)",
            [],
        )
        .unwrap();
        conn.execute(
            "INSERT INTO entity_references (project_id, source_entity_type, source_entity_id,
             target_entity_type, target_entity_id, reference_type, source_method, created_at)
             VALUES (1, 'merge_request', 2, 'issue', 1, 'closes', 'api', 3000)",
            [],
        )
        .unwrap();
        let result = get_closing_mrs(&conn, 1).unwrap();
        assert_eq!(result.len(), 2);
        assert_eq!(result[0].iid, 5); // Lower iid first
        assert_eq!(result[1].iid, 8);
    }

    #[test]
    fn wrap_text_single_line() {
        assert_eq!(wrap_text("hello world", 80, "  "), "hello world");
    }

    #[test]
    fn wrap_text_multiple_lines() {
        let result = wrap_text("one two three four five", 10, "  ");
        assert!(result.contains('\n'));
    }

    #[test]
    fn format_date_extracts_date_part() {
        let ms = 1705276800000;
        let date = format_date(ms);
        assert!(date.starts_with("2024-01-15"));
    }
}
