use std::collections::{HashMap, HashSet};

use rusqlite::Connection;

use crate::cli::render::{self, Icons, Theme};
use crate::core::error::Result;
use crate::core::path_resolver::build_path_query;
use crate::core::time::ms_to_iso;

use super::types::*;

pub(super) fn query_overlap(
    conn: &Connection,
    path: &str,
    project_id: Option<i64>,
    since_ms: i64,
    limit: usize,
) -> Result<OverlapResult> {
    let pq = build_path_query(conn, path, project_id)?;

    // Build SQL with 4 signal sources, matching the expert query expansion.
    // Each row produces (username, role, mr_id, mr_ref, seen_at) for Rust-side accumulation.
    let path_op = if pq.is_prefix {
        "LIKE ?1 ESCAPE '\\'"
    } else {
        "= ?1"
    };
    // Match both new_path and old_path to capture activity on renamed files.
    // INDEXED BY removed to allow OR across path columns; overlap runs once
    // per command so the minor plan difference is acceptable.
    let sql = format!(
        "SELECT username, role, touch_count, last_seen_at, mr_refs FROM (
            -- 1. DiffNote reviewer (matches both new_path and old_path)
            SELECT
                n.author_username AS username,
                'reviewer' AS role,
                COUNT(DISTINCT m.id) AS touch_count,
                MAX(n.created_at) AS last_seen_at,
                GROUP_CONCAT(DISTINCT (p.path_with_namespace || '!' || m.iid)) AS mr_refs
            FROM notes n
            JOIN discussions d ON n.discussion_id = d.id
            JOIN merge_requests m ON d.merge_request_id = m.id
            JOIN projects p ON m.project_id = p.id
            WHERE n.note_type = 'DiffNote'
              AND (n.position_new_path {path_op}
                   OR (n.position_old_path IS NOT NULL AND n.position_old_path {path_op}))
              AND n.is_system = 0
              AND n.author_username IS NOT NULL
              AND (m.author_username IS NULL OR n.author_username != m.author_username)
              AND m.state IN ('opened','merged','closed')
              AND n.created_at >= ?2
              AND (?3 IS NULL OR n.project_id = ?3)
            GROUP BY n.author_username

            UNION ALL

            -- 2. DiffNote MR author (matches both new_path and old_path)
            SELECT
                m.author_username AS username,
                'author' AS role,
                COUNT(DISTINCT m.id) AS touch_count,
                MAX(n.created_at) AS last_seen_at,
                GROUP_CONCAT(DISTINCT (p.path_with_namespace || '!' || m.iid)) AS mr_refs
            FROM notes n
            JOIN discussions d ON n.discussion_id = d.id
            JOIN merge_requests m ON d.merge_request_id = m.id
            JOIN projects p ON m.project_id = p.id
            WHERE n.note_type = 'DiffNote'
              AND (n.position_new_path {path_op}
                   OR (n.position_old_path IS NOT NULL AND n.position_old_path {path_op}))
              AND n.is_system = 0
              AND m.state IN ('opened','merged','closed')
              AND m.author_username IS NOT NULL
              AND n.created_at >= ?2
              AND (?3 IS NULL OR n.project_id = ?3)
            GROUP BY m.author_username

            UNION ALL

            -- 3. MR author via file changes (matches both new_path and old_path)
            SELECT
                m.author_username AS username,
                'author' AS role,
                COUNT(DISTINCT m.id) AS touch_count,
                MAX(m.updated_at) AS last_seen_at,
                GROUP_CONCAT(DISTINCT (p.path_with_namespace || '!' || m.iid)) AS mr_refs
            FROM mr_file_changes fc
            JOIN merge_requests m ON fc.merge_request_id = m.id
            JOIN projects p ON m.project_id = p.id
            WHERE m.author_username IS NOT NULL
              AND m.state IN ('opened','merged','closed')
              AND (fc.new_path {path_op}
                   OR (fc.old_path IS NOT NULL AND fc.old_path {path_op}))
              AND m.updated_at >= ?2
              AND (?3 IS NULL OR fc.project_id = ?3)
            GROUP BY m.author_username

            UNION ALL

            -- 4. MR reviewer via file changes + mr_reviewers (matches both new_path and old_path)
            SELECT
                r.username AS username,
                'reviewer' AS role,
                COUNT(DISTINCT m.id) AS touch_count,
                MAX(m.updated_at) AS last_seen_at,
                GROUP_CONCAT(DISTINCT (p.path_with_namespace || '!' || m.iid)) AS mr_refs
            FROM mr_file_changes fc
            JOIN merge_requests m ON fc.merge_request_id = m.id
            JOIN projects p ON m.project_id = p.id
            JOIN mr_reviewers r ON r.merge_request_id = m.id
            WHERE r.username IS NOT NULL
              AND (m.author_username IS NULL OR r.username != m.author_username)
              AND m.state IN ('opened','merged','closed')
              AND (fc.new_path {path_op}
                   OR (fc.old_path IS NOT NULL AND fc.old_path {path_op}))
              AND m.updated_at >= ?2
              AND (?3 IS NULL OR fc.project_id = ?3)
            GROUP BY r.username
        )"
    );

    let mut stmt = conn.prepare_cached(&sql)?;
    let rows: Vec<(String, String, u32, i64, Option<String>)> = stmt
        .query_map(rusqlite::params![pq.value, since_ms, project_id], |row| {
            Ok((
                row.get(0)?,
                row.get(1)?,
                row.get(2)?,
                row.get(3)?,
                row.get(4)?,
            ))
        })?
        .collect::<std::result::Result<Vec<_>, _>>()?;

    // Internal accumulator uses HashSet for MR refs from the start
    struct OverlapAcc {
        username: String,
        author_touch_count: u32,
        review_touch_count: u32,
        touch_count: u32,
        last_seen_at: i64,
        mr_refs: HashSet<String>,
    }

    let mut user_map: HashMap<String, OverlapAcc> = HashMap::new();
    for (username, role, count, last_seen, mr_refs_csv) in &rows {
        let mr_refs: Vec<String> = mr_refs_csv
            .as_deref()
            .map(|csv| csv.split(',').map(|s| s.trim().to_string()).collect())
            .unwrap_or_default();

        let entry = user_map
            .entry(username.clone())
            .or_insert_with(|| OverlapAcc {
                username: username.clone(),
                author_touch_count: 0,
                review_touch_count: 0,
                touch_count: 0,
                last_seen_at: 0,
                mr_refs: HashSet::new(),
            });
        entry.touch_count += count;
        if role == "author" {
            entry.author_touch_count += count;
        } else {
            entry.review_touch_count += count;
        }
        if *last_seen > entry.last_seen_at {
            entry.last_seen_at = *last_seen;
        }
        for r in mr_refs {
            entry.mr_refs.insert(r);
        }
    }

    // Convert accumulators to output structs
    let mut users: Vec<OverlapUser> = user_map
        .into_values()
        .map(|a| {
            let mut mr_refs: Vec<String> = a.mr_refs.into_iter().collect();
            mr_refs.sort();
            let mr_refs_total = mr_refs.len() as u32;
            let mr_refs_truncated = mr_refs.len() > MAX_MR_REFS_PER_USER;
            if mr_refs_truncated {
                mr_refs.truncate(MAX_MR_REFS_PER_USER);
            }
            OverlapUser {
                username: a.username,
                author_touch_count: a.author_touch_count,
                review_touch_count: a.review_touch_count,
                touch_count: a.touch_count,
                last_seen_at: a.last_seen_at,
                mr_refs,
                mr_refs_total,
                mr_refs_truncated,
            }
        })
        .collect();

    // Stable sort with full tie-breakers for deterministic output
    users.sort_by(|a, b| {
        b.touch_count
            .cmp(&a.touch_count)
            .then_with(|| b.last_seen_at.cmp(&a.last_seen_at))
            .then_with(|| a.username.cmp(&b.username))
    });

    let truncated = users.len() > limit;
    users.truncate(limit);

    Ok(OverlapResult {
        path_query: if pq.is_prefix {
            path.trim_end_matches('/').to_string()
        } else {
            pq.value.clone()
        },
        path_match: if pq.is_prefix { "prefix" } else { "exact" }.to_string(),
        users,
        truncated,
    })
}

/// Format overlap role for display: "A", "R", or "A+R".
pub(super) fn format_overlap_role(user: &OverlapUser) -> &'static str {
    match (user.author_touch_count > 0, user.review_touch_count > 0) {
        (true, true) => "A+R",
        (true, false) => "A",
        (false, true) => "R",
        (false, false) => "-",
    }
}

pub(super) fn print_overlap_human(r: &OverlapResult, project_path: Option<&str>) {
    println!();
    println!(
        "{}",
        Theme::bold().render(&format!("Overlap for {}", r.path_query))
    );
    println!("{}", "\u{2500}".repeat(60));
    println!(
        "  {}",
        Theme::dim().render(&format!(
            "(matching {} {})",
            r.path_match,
            if r.path_match == "exact" {
                "file"
            } else {
                "directory prefix"
            }
        ))
    );
    super::print_scope_hint(project_path);
    println!();

    if r.users.is_empty() {
        println!(
            "  {}",
            Theme::dim().render("No overlapping users found for this path.")
        );
        println!();
        return;
    }

    println!(
        "  {:<16} {:<6} {:>7}  {:<12}  {}",
        Theme::bold().render("Username"),
        Theme::bold().render("Role"),
        Theme::bold().render("MRs"),
        Theme::bold().render("Last Seen"),
        Theme::bold().render("MR Refs"),
    );

    for user in &r.users {
        let mr_str = user
            .mr_refs
            .iter()
            .take(5)
            .cloned()
            .collect::<Vec<_>>()
            .join(", ");
        let overflow = if user.mr_refs.len() > 5 {
            format!(" +{}", user.mr_refs.len() - 5)
        } else {
            String::new()
        };

        println!(
            "  {:<16} {:<6} {:>7}  {:<12}  {}{}",
            Theme::info().render(&format!("{} {}", Icons::user(), user.username)),
            format_overlap_role(user),
            user.touch_count,
            render::format_relative_time(user.last_seen_at),
            mr_str,
            overflow,
        );
    }
    if r.truncated {
        println!(
            "  {}",
            Theme::dim().render("(showing first -n; rerun with a higher --limit)")
        );
    }
    println!();
}

pub(super) fn overlap_to_json(r: &OverlapResult) -> serde_json::Value {
    serde_json::json!({
        "path_query": r.path_query,
        "path_match": r.path_match,
        "truncated": r.truncated,
        "users": r.users.iter().map(|u| serde_json::json!({
            "username": u.username,
            "role": format_overlap_role(u),
            "author_touch_count": u.author_touch_count,
            "review_touch_count": u.review_touch_count,
            "touch_count": u.touch_count,
            "last_seen_at": ms_to_iso(u.last_seen_at),
            "mr_refs": u.mr_refs,
            "mr_refs_total": u.mr_refs_total,
            "mr_refs_truncated": u.mr_refs_truncated,
        })).collect::<Vec<_>>(),
    })
}
